Skip to content

feat(spec): refuse undeclared keys on address and location values — AddressSchema / LocationValueSchema strict (#13802) - #14335

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-13802-address-location-strict
Sep 2, 2026
Merged

feat(spec): refuse undeclared keys on address and location values — AddressSchema / LocationValueSchema strict (#13802)#14335
os-zhuang merged 2 commits into
mainfrom
claude/issue-13802-address-location-strict

Conversation

@claude

@claude claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #13802

Ruling of record: issue comment 5494663475 (director seat; maintainer verbatim 「同意」, 2026-09-01) — option A: LocationValueSchema and AddressSchema go .strict(); FileValueSchema stays z.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:spec PM seat, same session id).

What changed

  • packages/spec/src/data/field-value.zod.ts — both value contracts are strictObject now (the ledger's standard wiring; .strict() underneath). Every member of both shapes is optional, so under zod's default .strip a value with a completely wrong key set parsed green and the wrong keys vanished — the showcase seed's postal_code (finding(examples): the showcase seed writes f_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 / postcodepostalCode; latitudelat, longitudelng. Those aliases are did-you-mean renames in the refusal text, not tolerance — postal_code is refused, never read. AddressValueSchema = AddressSchema follows automatically. FileValueSchema untouched (pinned as the positive control).
  • Import note: field-value.zod.ts now carries a value import into the existing field.zod ↔ suggestions.zod ↔ strict-object evaluation cycle. Verified safe in both entry orders, including with field-value.zod.ts as the first module imported under OS_EAGER_SCHEMAS=1 (everything strictObject(…) touches at construction is a hoisted function; the suggester builds on the first issue).
  • Strictness ledger (docs/audits/2026-07-unknown-key-strictness-ledger.md): data/ triage row re-verdicted openauthorable with 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.
  • ADR-0087 D3 semantic entry address-location-value-unknown-keys-refused under protocol 18 (gen:migration-registry regenerated registry.ts; spec-changes.json / upgrade guide verified current by check:generated).
  • Changeset @objectstack/spec minor, **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)).
  • Hand docs: one sentence each on content/docs/protocol/objectql/types.mdx (address, location) and content/docs/data-modeling/field-types.mdx.
  • Tests (all assert the envelope, never a bare success === false): spec field-value.test.ts (unrecognized_keys + keys naming postal_code / heading,speed + the rename text + the negative pin that the refusal carries no issue id + FileValueSchema still loose), field-default-value.test.ts (the authored door: address/location defaultValue literals), analytics-strictness-batchd.test.ts (batch D's tolerance pin repinned to the closure, with why); objectql record-validator.test.ts (warn-first admits + reports to the sink naming the key; strict rejects invalid_type naming the key; declared keys write in both modes) and scan-value-shapes.test.ts (an undeclared key is now a finding the scan counts — one predicate with strict); rest rest-data-create-address-unknown-key.test.ts (the ADR-0112 door: POST /api/v1/data/:object answers 400 VALIDATION_FAILED + fields[0].code = 'invalid_type' naming postal_code on a strict deployment; on an unattested deployment the same write is admitted and reads back verbatim — no read path narrowed). Door-test pattern copied from import-integration.test.ts's max_scale case.

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/**/src fixtures/tests, content/docs/**, skills/**, .changeset/**, docs/**; .ts/.tsx/.js/.mjs/.cjs/.json/.yaml/.yml/.md/.mdx; generated references/, 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 grep git 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's latitude: 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, unconditionaldefaultValue literals (default-value-shape.tsFieldSchema, #7127) and action params (ui/action-params.zod.ts, D2 strict by default since 17.0); ② record writes, per deployment — objectql record-validator validateOne (insert + update) rejects only when valueShapeStrictEffective holds (deployment attested adr-0104-value-shapes, or OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1; OS_ALLOW_LAX_VALUE_SHAPES=1 re-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 calls valueSchemaFor(def, 'expanded') for these types outside packages/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.tsx safeParses only a widget-built { lat, lng, altitude?, accuracy? } candidate, so its runtime verdicts do not move; AddressField parses nothing at runtime. The Console Pin Gate builds objectui (no tests), so nothing breaks there. LocationField.optionalKeys.test.tsx pins the OLD strip behaviour by name and flips the day objectui takes a spec carrying this: filed as objectstack-ai/objectui#7267 with Blocked-by:.

Gates (run in the worktree; readings are the gates' own verdict lines, exit captured before any pipe)

Final union at bbf14f4ba (0 dirty), under scripts/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 calls valueSchemaFor (objectql, rest, spec itself, qa/dogfood — the downstream set), each run by name; typecheck spec / objectql / rest 0 TS errors (spec's tsconfig.test.json --listFiles lists all three edited spec test files).

Dispatch list derived by node scripts/pm/dispatch-gates.mjs (41 by path at a39b02a6, asserted; 74 re-derived with no paths from the real changeset at 809cbc9e — 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-facing history strings; repaired in bbf14f4b, gate ✓, negative pins added), skill-examples (✓ once spec dist was rebuilt at the final head), engine-split-ratio (✓ after git 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

🤖 Generated with Claude Code

https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21


Generated by Claude Code

…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
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 4 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/field-types.mdx (via LocationValueSchema (symbol, a top-level const), postalCode (literal, a string literal in AddressSchema))
  • content/docs/data-modeling/validation-rules.mdx (via LocationValueSchema (symbol, a top-level const), postalCode (literal, a string literal in AddressSchema))
  • content/docs/protocol/objectql/types.mdx (via AddressSchema (symbol, a top-level const), LocationValueSchema (symbol, a top-level const), postalCode (literal, a string literal in AddressSchema))
What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f645d6f8879f5e868b8c0aac978dc0db92552739packageMentionDocs.

Which tree this was computed on

This run read content/docs from 38e9c5714335e72fad2f17d51fd6b62d9409ab51 — the merge of head bbf14f4ba79738362bd495e40c3fe8c7079bee7a into base f645d6f8879f5e868b8c0aac978dc0db92552739, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f645d6f8879f5e868b8c0aac978dc0db92552739 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-zhuang
os-zhuang marked this pull request as ready for review September 2, 2026 02:48
@os-zhuang
os-zhuang enabled auto-merge September 2, 2026 02:48
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit d62f990 Sep 2, 2026
43 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-13802-address-location-strict branch September 2, 2026 03:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

2 participants