Found while measuring #14060 (the re-scoped corpus-coverage question). Not a blocker for that card — the value is flagged and the gate does stay shut. What is wrong is the operator-facing prescription on the one legacy shape the scan's own header names.
The defect
valueShapeViolation (packages/objectql/src/validation/record-validator.ts:879) returns parsed.error.issues[0]?.message, and ValueShapeFinding.detail documents that string as "The first parse issue seen — the prescription an author acts on."
For a location value stored as {latitude, longitude} — the exact case scan-value-shapes.ts's header names as one the scan exists to find — zod emits three issues, and the actionable one is not first:
issues[0] code=invalid_type path=["lat"] msg=Invalid input: expected number, received undefined
issues[1] code=invalid_type path=["lng"] msg=Invalid input: expected number, received undefined
issues[2] code=unrecognized_keys path=[] msg=Unrecognized key(s) on this location value: `latitude`, `longitude`.
Did you mean `latitude` -> `lat`, `longitude` -> `lng`? ...
The retired spelling is a rename, not a typo — edit distance cannot reach latitude from lat, which is precisely why LocationValueSchema declares the aliases map. That hint is built, and then discarded.
Measured end-to-end through the real exported scan
Driving scanValueShapes (out of tree, origin/main @ a06faebbe4, @objectstack/spec and @objectstack/objectql freshly built):
A. legacy {latitude,longitude}
scannedRecords=1 blocking=1 passed=false
finding contact.geo (location) count=1
detail -> Invalid input: expected number, received undefined
names the rename hint? false
| contact.geo (location) - 1 record(s): Invalid input: expected number, received undefined
B. #13388 seed {postal_code} <- the address path, for contrast
detail -> Unrecognized key(s) on this address value: `postal_code`.
Did you mean `postal_code` -> `postalCode`? ...
names the rename hint? true
C. CONTROL all-clean values -> blocking=0 passed=true, "No malformed values found."
So an operator running os migrate value-shapes against real legacy location data is told expected number, received undefined and left to work out the rename themselves, while the identically-shaped address case gets the full prescription. The asymmetry is invisible from inside: address happens to declare every member optional, so no invalid_type issue can sort ahead of the unrecognized-key one; location requires the lat/lng pair, so two always do.
Why no gate catches it
packages/objectql/src/validation/scan-value-shapes.test.ts uses this exact value ({ geo: { latitude: 1, longitude: 2 } }) but asserts only report.blocking === 1 — never detail.
packages/spec/src/data/field-value.test.ts:313 does assert the rename text, and it reaches it by searching the issue list for the unrecognized_keys issue rather than reading issues[0]. The spec layer already knows the ordering; the consumer that formats the operator's report does not.
Suggested direction (not a decision)
Prefer the unrecognized_keys issue when one is present, rather than taking issues[0] positionally — an undeclared key is the more actionable diagnosis whenever both are reported, and it is the one carrying the alias map. Worth confirming that the same positional read does not mislead the other structured-JSON classes before changing it.
Provenance: #14060 (the measurement that surfaced this), #13802 (which closed both shapes), #13388 (the live drift instance), #3438 (the scan).
Generated by Claude Code
Found while measuring #14060 (the re-scoped corpus-coverage question). Not a blocker for that card — the value is flagged and the gate does stay shut. What is wrong is the operator-facing prescription on the one legacy shape the scan's own header names.
The defect
valueShapeViolation(packages/objectql/src/validation/record-validator.ts:879) returnsparsed.error.issues[0]?.message, andValueShapeFinding.detaildocuments that string as "The first parse issue seen — the prescription an author acts on."For a
locationvalue stored as{latitude, longitude}— the exact casescan-value-shapes.ts's header names as one the scan exists to find — zod emits three issues, and the actionable one is not first:The retired spelling is a rename, not a typo — edit distance cannot reach
latitudefromlat, which is precisely whyLocationValueSchemadeclares thealiasesmap. That hint is built, and then discarded.Measured end-to-end through the real exported scan
Driving
scanValueShapes(out of tree,origin/main@a06faebbe4,@objectstack/specand@objectstack/objectqlfreshly built):So an operator running
os migrate value-shapesagainst real legacy location data is toldexpected number, received undefinedand left to work out the rename themselves, while the identically-shaped address case gets the full prescription. The asymmetry is invisible from inside:addresshappens to declare every member optional, so noinvalid_typeissue can sort ahead of the unrecognized-key one;locationrequires thelat/lngpair, so two always do.Why no gate catches it
packages/objectql/src/validation/scan-value-shapes.test.tsuses this exact value ({ geo: { latitude: 1, longitude: 2 } }) but asserts onlyreport.blocking === 1— neverdetail.packages/spec/src/data/field-value.test.ts:313does assert the rename text, and it reaches it by searching the issue list for theunrecognized_keysissue rather than readingissues[0]. The spec layer already knows the ordering; the consumer that formats the operator's report does not.Suggested direction (not a decision)
Prefer the
unrecognized_keysissue when one is present, rather than takingissues[0]positionally — an undeclared key is the more actionable diagnosis whenever both are reported, and it is the one carrying the alias map. Worth confirming that the same positional read does not mislead the other structured-JSON classes before changing it.Provenance: #14060 (the measurement that surfaced this), #13802 (which closed both shapes), #13388 (the live drift instance), #3438 (the scan).
Generated by Claude Code