feat(spec): refuse undeclared keys on address and location values — AddressSchema / LocationValueSchema strict (#13802) - #14335
Conversation
…3802) Maintainer ruling 2026-09-01 (option A): LocationValueSchema and AddressSchema (= AddressValueSchema) were all-optional stripping z.objects, so a value with a wrong key set parsed green and the wrong keys vanished — the showcase seed's postal_code (#13388) was accepted, dropped and rendered as an empty ZIP box, and a stored-value scan over the class could only report a clean count. Both are strictObject now; FileValueSchema stays the one deliberate looseObject. The refusal names the key and the rename (postal_code/zipCode -> postalCode, latitude/longitude -> lat/lng). Ordered census first: every in-repo corpus that writes address/location values (8459 files, 196,098 leaf literals, 57 shaped literals) carries zero keys outside the declared sets other than batch D's own tolerance pin, which is repinned here — the repair commit the ruling ordered is empty by measurement (#13388's seed fix landed at #14090). Where the refusal bites is ADR-0104's unchanged evidence gate: defaultValue literals and action params reject at authoring; record writes reject only on a deployment that attested adr-0104-value-shapes (or the env opt-in) and stay warn-first elsewhere; os migrate value-shapes now counts the key; no read path parses these shapes. Strictness-ledger triage row re-verdicted open -> authorable with the census and the migration note, the strip-map row dropped (reverse pin), finding 21 added, counts regenerated; D3 semantic entry address-location-value-unknown-keys-refused registered under 18. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
check:doc-authoring refused the two strictObject `history` strings for carrying `#13802` / `#13388` — customer-facing refusal text has no tracker to resolve them (maintainer ruling 2026-08-12). The sentences are repaired around the ids, the anchors stay in the JSDoc, and the zod-level pins gain the negative assertion the gate asks for (the message must not match an issue id). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 38e9c5714335e72fad2f17d51fd6b62d9409ab51 && git checkout 38e9c5714335e72fad2f17d51fd6b62d9409ab51
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f645d6f8879f5e868b8c0aac978dc0db92552739 bbf14f4ba79738362bd495e40c3fe8c7079bee7a && git checkout -B drift-repro f645d6f8879f5e868b8c0aac978dc0db92552739 && git merge --no-ff bbf14f4ba79738362bd495e40c3fe8c7079bee7a
node scripts/docs-audit/affected-docs.mjs --json f645d6f8879f5e868b8c0aac978dc0db92552739
|
Fixes #13802
Ruling of record: issue comment 5494663475 (director seat; maintainer verbatim 「同意」, 2026-09-01) — option A:
LocationValueSchemaandAddressSchemago.strict();FileValueSchemastaysz.looseObject(⛔ untouched); implementation order = corpus census + violation repair first, then strict + strictness-ledger entry + migration note in one stroke; Clause ② YES ⇒needs:contract-review; changeset marks the behaviour change; rejection tests assert the envelope; ⛔ no consumer-side alias.Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21 (subagent of the
domain:specPM seat, same session id).What changed
packages/spec/src/data/field-value.zod.ts— both value contracts arestrictObjectnow (the ledger's standard wiring;.strict()underneath). Every member of both shapes is optional, so under zod's default.stripa value with a completely wrong key set parsed green and the wrong keys vanished — the showcase seed'spostal_code(finding(examples): the showcase seed writesf_address.postal_code, a key the value contract does not declare — accepted-and-stripped by the spec, and the ZIP box renders empty #13388) was accepted, dropped, and rendered as an empty ZIP box, and a stored-value scan over the class could only ever report a clean count. The refusal now names the surface, the key, and a rename:postal_code/zipCode/zip/postcode→postalCode;latitude→lat,longitude→lng. Thosealiasesare did-you-mean renames in the refusal text, not tolerance —postal_codeis refused, never read.AddressValueSchema = AddressSchemafollows automatically.FileValueSchemauntouched (pinned as the positive control).field-value.zod.tsnow carries a value import into the existingfield.zod ↔ suggestions.zod ↔ strict-objectevaluation cycle. Verified safe in both entry orders, including withfield-value.zod.tsas the first module imported underOS_EAGER_SCHEMAS=1(everythingstrictObject(…)touches at construction is a hoisted function; the suggester builds on the first issue).docs/audits/2026-07-unknown-key-strictness-ledger.md):data/triage row re-verdictedopen→authorablewith the census and the migration note; the remaining-strip-map row removed (the reverse pin — the file has 0 strip sites now); finding 21; the data-waves roll-call and the closing record carry a dated note. Counts regenerated (gen:strictness-ledger): global strip 124 → 122 in 22 → 21 files;data/81 → 79.address-location-value-unknown-keys-refusedunder protocol 18 (gen:migration-registryregeneratedregistry.ts;spec-changes.json/ upgrade guide verified current bycheck:generated).@objectstack/specminor,**BREAKING**accept-set narrowing with the FROM → TO and the four-point "where it bites" note;<!-- adr-0087: registered … -->(the gate reads it:[BREAKING] registered address-location-value-unknown-keys-refused (new here)).content/docs/protocol/objectql/types.mdx(address,location) andcontent/docs/data-modeling/field-types.mdx.success === false): specfield-value.test.ts(unrecognized_keys+keysnamingpostal_code/heading,speed+ the rename text + the negative pin that the refusal carries no issue id +FileValueSchemastill loose),field-default-value.test.ts(the authored door: address/locationdefaultValueliterals),analytics-strictness-batchd.test.ts(batch D's tolerance pin repinned to the closure, with why); objectqlrecord-validator.test.ts(warn-first admits + reports to the sink naming the key; strict rejectsinvalid_typenaming the key; declared keys write in both modes) andscan-value-shapes.test.ts(an undeclared key is now a finding the scan counts — one predicate with strict); restrest-data-create-address-unknown-key.test.ts(the ADR-0112 door:POST /api/v1/data/:objectanswers 400VALIDATION_FAILED+fields[0].code = 'invalid_type'namingpostal_codeon a strict deployment; on an unattested deployment the same write is admitted and reads back verbatim — no read path narrowed). Door-test pattern copied fromimport-integration.test.ts'smax_scalecase.Census (the ruling's step 1) — empty repair by measurement
At
a39b02a6, every in-repo corpus that writes address/location VALUES (examples/**,packages/apps/**,packages/qa/**,packages/**/srcfixtures/tests,content/docs/**,skills/**,.changeset/**,docs/**;.ts/.tsx/.js/.mjs/.cjs/.json/.yaml/.yml/.md/.mdx; generatedreferences/,releases/, CHANGELOGs excluded): a brace-matched scan of 8459 files / 196,098 leaf object literals found 57 address- or location-shaped literals and 0 carrying a key outside the declared sets, other than batch D's own tolerance pin (repinned here). Spelling grepgit grep -n -E "postal_code|zip_code|zipCode|postcode|latitude|longitude|heading *:|speed *:|district *:" -- 'examples/**' 'packages/apps/**' 'packages/qa/**' 'packages/spec/**' 'content/docs/**' 'skills/**'hits only the retired-form docs,ListMapConfig's field-name keys, SCIM (SCIMAddressSchema, a different contract) and pins; YAML-syntax grep over*.yaml|*.yml|*.mdx|*.md→ only a blog post'slatitude:zod example. #13388's seed fix had already landed at #14090. So the "census + repair" commit the ruling ordered first is empty: nothing to repair.Migration note — where the refusal bites (by call site; read paths untouched)
git grep -n "valueSchemaFor(" -- packages(non-test): ① authoring, hard reject, unconditional —defaultValueliterals (default-value-shape.ts→FieldSchema, #7127) and action params (ui/action-params.zod.ts, D2 strict by default since 17.0); ② record writes, per deployment — objectqlrecord-validatorvalidateOne(insert + update) rejects only whenvalueShapeStrictEffectiveholds (deployment attestedadr-0104-value-shapes, orOS_DATA_VALUE_SHAPE_STRICT_ENABLED=1;OS_ALLOW_LAX_VALUE_SHAPES=1re-opens), else warn-first + admitted-violation sink — unchanged; ③os migrate value-shapes(valueShapeViolation, same predicate) now counts an undeclared key, so a deployment holding such values cannot attest until cleaned — the mechanism that keeps ② from stranding stored data; ④ read paths: none — no consumer callsvalueSchemaFor(def, 'expanded')for these types outsidepackages/spec(git grep "'expanded')" -- 'packages/**/src/**' ':!packages/spec/**'→ 0), drivers return stored JSON verbatim. Customer-database inventory is the confidence gap this repo cannot see; recorded in the ledger row, not glossed.Sibling (objectui) — no pin-gate break, one test pin to flip later
At the pin
d8ec8d6d…,LocationField.tsxsafeParses only a widget-built{ lat, lng, altitude?, accuracy? }candidate, so its runtime verdicts do not move;AddressFieldparses nothing at runtime. The Console Pin Gate builds objectui (no tests), so nothing breaks there.LocationField.optionalKeys.test.tsxpins the OLD strip behaviour by name and flips the day objectui takes a spec carrying this: filed as objectstack-ai/objectui#7267 withBlocked-by:.Gates (run in the worktree; readings are the gates' own verdict lines, exit captured before any pipe)
Final union at
bbf14f4ba(0 dirty), underscripts/pm/os-verify-lock.sh:pnpm --filter @objectstack/spec build(0 TS errors) → spec targeted suite 9 files / 406 tests passed (field-value,field-default-value,analytics-strictness-batchd,alias-integrity,strict-object,field,action-params,scripts/strictness-ledger,scripts/strictness-ledger-doc) → objectql 7 / 151 passed (record-validator,scan-value-shapes,adr0104-attestation-evidence,adr0104-lax-deviation-marker,validate-only,engine-cel-default-temporal-shape,value-shape-scan-advisory) → rest 3 / 47 passed (rest-data-create-address-unknown-key,import-dryrun-parity,import-integration) → dogfood 1 / 45 passed (field-zoo-value-shape) →check:generated"✓ All 15 generated artifacts are up to date" →check:doc-authoring✓. Consumer direction: every package that callsvalueSchemaFor(objectql, rest, spec itself, qa/dogfood — the downstream set), each run by name; typecheckspec/objectql/rest0 TS errors (spec'stsconfig.test.json --listFileslists all three edited spec test files).Dispatch list derived by
node scripts/pm/dispatch-gates.mjs(41 by path ata39b02a6, asserted; 74 re-derived with no paths from the real changeset at809cbc9e— the 33 additions all run). Green by their own verdict lines: api-surface ("public API surface + factory signatures unchanged ✓"), authorable-surface ("Successfully generated 1603 schemas" — anchor unchanged), docs ("229 generated files in sync"), strictness-ledger ("437 site(s) measured, 1 authorable strip site(s) left"), liveness, llms-txt, yaml-examples, skill-refs, spec-changes / upgrade-guide ("up to date"), migration-registry, adr-0087-registration, changeset-no-major, empty-changeset, merge-driver, nul-bytes ("7815 text file(s) … no raw ASCII control bytes"), doc-authoring (was red on the first commit — issue ids in customer-facinghistorystrings; repaired inbbf14f4b, gate ✓, negative pins added), skill-examples (✓ once spec dist was rebuilt at the final head), engine-split-ratio (✓ aftergit fetch --shallow-since=2026-05-28 origin main, as the gate prescribed), and the rest of the 74.NOT MEASURED locally, by the gates' own text — CI owns them (declared narrowing):
check-dev-prereqs/check:dual-build-cjs-loads/check:type-check-debt(each: "PREREQUISITE NOT MET — 42 of 67 workspace packages have no dist/"; whole-workspace build is a CI run),check-test-completeness(needs the turbo test log CI tees; exit 3 = NOT MEASURED, "⛔ It is not a red").Not done here, on purpose
skills/**untouched (no skill diff → no before/after line counts owed).packages/qastored-value scan reporting (qa stored-value scans fold strip-mode value classes into a violation count of zero they cannot earn — report them unmeasurable instead #14060) untouched — other lane.FileValueSchemauntouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
Generated by Claude Code