Yep, it indeed turns out that just about every possible aspect of my lighting calculations was wrong. I had the light direction backwards, I was incorrectly transforming the normals when constructing the object, and I was using the wrong coordinate space for sending the light direction to the shader. Other than that everything was perfect. Seems to be better now, at any rate.
It’s kind of embarrassing, but then I suppose as a game programmer one has to get used to embarrassment.