Found while fixing #15490 (which retired the same positional read on the stored-value path). Not touched there: it is a different function, on a different authoring surface, so it is filed rather than ridden along.
The defect
checkLiteralDefaultValue (packages/spec/src/data/default-value-shape.ts:120) reads a value-contract rejection positionally:
export function checkLiteralDefaultValue(def: ValueShapeFieldDef, dv: unknown): LiteralDefaultValueVerdict {
const result = valueSchemaFor(def, 'stored').safeParse(dv);
if (result.success) return { ok: true };
return { ok: false, detail: result.error.issues[0]?.message ?? 'invalid value' };
}
LiteralDefaultValueVerdict.detail documents itself as "First issue message from the value contract — the 'why' a refusal carries verbatim." zod sorts per-member issues ahead of the object-level unrecognized_keys one, so when an author writes a defaultValue with a RENAMED key, the message that names the rename is discarded and the author is handed a missing-member type error instead.
Measured on origin/main at c463d03e0, against the built @objectstack/spec
checkLiteralDefaultValue({type:'location'}, {latitude:1, longitude:2})
detail -> "Invalid input: expected number, received undefined"
names the rename? false
checkLiteralDefaultValue({type:'address'}, {street:5, postal_code:'98101'})
detail -> "Invalid input: expected string, received number"
names the rename? false
checkLiteralDefaultValue({type:'address'}, {postal_code:'98101'}) <- no type error to sort ahead
detail -> "Unrecognized key(s) on this address value: `postal_code`. Did you mean `postal_code` -> `postalCode`? ..."
names the rename? true
Same asymmetry #15490 documented: the diagnosis quality depends on whether some unrelated member happened to also be wrong, which nobody chose and nobody can see.
Why it is a separate card
This is the authoring-time gate for defaultValue — the shared core of the action-param gate and the field gate — not the stored-value scan. It is a different surface with a different audience (a metadata author writing a declaration, not an operator running a migration), and LiteralDefaultValueVerdict.detail is its own documented contract. #15490's fix is scoped to record-validator.ts and deliberately did not widen into packages/spec.
Suggested direction (not a decision)
The same shape #15490 landed: prefer the unrecognized_keys issue when the rejection carries one. The sweep done for #15490 measured all 16 classes reachable through valueSchemaFor and found only location and address able to emit unrecognized_keys at all, so the preference is a no-op for every other class — that sweep should carry over here, since this function calls the same valueSchemaFor(def, 'stored'). Worth confirming that no caller of checkLiteralDefaultValue pins the current text.
Provenance: #15490 (the stored-value half, and the sweep).
Generated by Claude Code
Found while fixing #15490 (which retired the same positional read on the stored-value path). Not touched there: it is a different function, on a different authoring surface, so it is filed rather than ridden along.
The defect
checkLiteralDefaultValue(packages/spec/src/data/default-value-shape.ts:120) reads a value-contract rejection positionally:LiteralDefaultValueVerdict.detaildocuments itself as "First issue message from the value contract — the 'why' a refusal carries verbatim." zod sorts per-member issues ahead of the object-levelunrecognized_keysone, so when an author writes adefaultValuewith a RENAMED key, the message that names the rename is discarded and the author is handed a missing-member type error instead.Measured on
origin/mainatc463d03e0, against the built@objectstack/specSame asymmetry #15490 documented: the diagnosis quality depends on whether some unrelated member happened to also be wrong, which nobody chose and nobody can see.
Why it is a separate card
This is the authoring-time gate for
defaultValue— the shared core of the action-param gate and the field gate — not the stored-value scan. It is a different surface with a different audience (a metadata author writing a declaration, not an operator running a migration), andLiteralDefaultValueVerdict.detailis its own documented contract. #15490's fix is scoped torecord-validator.tsand deliberately did not widen intopackages/spec.Suggested direction (not a decision)
The same shape #15490 landed: prefer the
unrecognized_keysissue when the rejection carries one. The sweep done for #15490 measured all 16 classes reachable throughvalueSchemaForand found onlylocationandaddressable to emitunrecognized_keysat all, so the preference is a no-op for every other class — that sweep should carry over here, since this function calls the samevalueSchemaFor(def, 'stored'). Worth confirming that no caller ofcheckLiteralDefaultValuepins the current text.Provenance: #15490 (the stored-value half, and the sweep).
Generated by Claude Code