Repository navigation
feat(spec)!: key an object extension's fieldPatches by the field map's own key grammar - #29513
Conversation
…s own key grammar `ObjectExtensionSchema.fieldPatches` was `z.record(z.string(), …)`, so a key no field name can be (`Industry`, `Annual Revenue`) parsed and reached only the downstream target judge. The `ObjectSchema.fields` key schema is hoisted to one module constant, `FieldNameKeySchema`, and both records read it; a key outside the grammar gets one `invalid_key` at `fieldPatches.KEY` with a prescription. Claude-Session: https://claude.ai/code/session_01ADHkmSCC3J91hA9anBJ81g Co-authored-by: Claude <noreply@anthropic.com>
…7 semantic entry, changeset, guide) Claude-Session: https://claude.ai/code/session_01ADHkmSCC3J91hA9anBJ81g Co-authored-by: Claude <noreply@anthropic.com>
…s key grammar Claude-Session: https://claude.ai/code/session_01ADHkmSCC3J91hA9anBJ81g Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: What this run could not see
Coarse fallback — 168 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 0063314334246c25fd0a28d2ef95b4b4e4bcf4fa && git checkout 0063314334246c25fd0a28d2ef95b4b4e4bcf4fa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 399df5bba888ff7cf7531d01d8dcb61d7690438a fb1c5bc9636b4de1e297d66f4a7f167acbb60f66 && git checkout -B drift-repro 399df5bba888ff7cf7531d01d8dcb61d7690438a && git merge --no-ff fb1c5bc9636b4de1e297d66f4a7f167acbb60f66
node scripts/docs-audit/affected-docs.mjs --json 399df5bba888ff7cf7531d01d8dcb61d7690438a
|
|
domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut Contract-review-claim: session_01ADHkmSduNGC2SZXszyrNut · head 6a70094 · 2026-11-02T17:19Z The lane seat reviews this head itself at |
|
domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut Contract reviewServed-tier: Inputs: card #29366 (body, triage 6580329038, claim 6581882084, os-dev-report 6583545776), PR #29513 body and file list, the net diff of ① Derived judgments
② Semver levelChangeset ③ Boundary flags
④ Design judgment
Verdict reasoning. Both Implemented-by: VERDICT: PASS |
…ith a non-grammar fieldPatches key `applyConversionsToStoredItem` maps no `object_extension` row onto a stack collection, so it returns the row untouched and no REFUSED_STORED_SHAPES detector could reach it. The semantic entry and the changeset now say so and name the check an operator runs instead. Claude-Session: https://claude.ai/code/session_01ADHkmSCC3J91hA9anBJ81g Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmSCC3J91hA9anBJ81g Co-authored-by: Claude <noreply@anthropic.com>
|
domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut Contract reviewServed-tier: This is a DELTA record for the pushes after the PASS record 6583861239 (head
Check-runs on Implemented-by: VERDICT: PASS |
|
domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut Head-sha: 维护者速读
席位意见:可落地。契约复审 PASS(6583861239 @ 席位核对 — ACCEPT ·
|
Requested by Steve · project thread
Fixes #29366
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: updated here —
content/docs/data-modeling/object-extensions.mdx("A patch must name something real") gains the key grammar.skills/objectstack-data/SKILL.mdkeys itsfieldPatchesexample byindustry, so every taught line stays true and no skill line changes.维护者速读
fieldPatches的键改用与对象fields完全相同的键 schema(小写 snake_case),Industry、Annual Revenue这类不可能是字段名的键在解析时就被拒,报一个带示例的invalid_key。defineStack、保存门、启动时都被接受,启动只在日志里点名、什么也不改;作者以为改了标签,界面却没变。packages/spec/src/data/object.zod.ts(FieldNameKeySchema与fieldPatches的invalid_key文案)、packages/spec/src/data/field-customization.test.ts末尾新一节、packages/spec/src/migrations/entries/semantic/18.object-extension-field-patch-key-grammar-refused.ts。pnpm --filter @objectstack/spec exec vitest run src/data/field-customization.test.ts,预期 1 个文件全绿(93 条)。What changes
packages/spec/src/data/object.zod.ts: the key schemaObjectSchema.fieldsused inline (the snake_case regex and its message) is hoisted, unchanged, to one module constantFieldNameKeySchema(not exported), declared aboveObjectSchemaBase.ObjectSchema.fieldsandObjectExtensionSchema.fieldPatchesboth read it, so the grammar is written once and the two cannot drift.fieldPatches' record error map answersinvalid_keywith a prescription: the key is a field name the extended object declares, lowercase snake_case such asindustryorannual_revenue, never a label. The issue sits atfieldPatches.KEY, as the option-key refusal sits atfieldPatches.FIELD.options.KEY. ThefieldPatchesdescribe and its doc comment name the grammar.packages/spec/src/migrations/entries/semantic/18.object-extension-field-patch-key-grammar-refused.ts: the ADR-0087 semantic entry (refused, with a prescription; no conversion, because a respelling can land on another field or on none)..changeset/29366-field-patches-key-grammar.md: specminor, BREAKING,adr-0087: registered object-extension-field-patch-key-grammar-refused, with the FROM → TO line.content/docs/data-modeling/object-extensions.mdx: one sentence in "A patch must name something real".content/docs/references/data/object.mdx: regenerated (the describe moved).check:api-surfaceunchanged). No forced test edit.ui/packageshas no zod twin offieldPatches(git grephits only the Studio controls that write keys read from the target's own field names).Why — the measurement that chose the declaration
The target judge (
@objectstack/metadata-corefieldPatchTargetMisses) does NOT reach every door, so a non-grammar key was accepted end to end before:defineStack/defineObjectExtension: pure schema, no target judge; the key parsed./api/v1/metaobject_extensionsave door:OBJECT_EXTENSION_ITEM_SCHEMAisObjectExtensionSchema(packages/metadata-protocol/src/protocol-object-extension-item.ts); the save door's refusal (objectExtensionItemRefusal) runs the__csuffix judge, not the target judge, so the row was stored.packages/objectql/src/registry.ts, thefieldPatchTargetMissescall in the extension fold): never refuses; the patch is not applied and named once in the log.os validate: refuses only when the target is in the composed stack (packages/cli/src/utils/field-patch-targets.ts); a target outside it is left to the install door.So this is
Clause-②: no (narrowing)·Metadata-protocol: narrowed, BREAKING, with the semantic migration entry the delta prescribes for that branch.Authored forms: every
fieldPatcheskey inexamples/**,skills/**,content/docs/**(references excluded),packages/**/templates, dogfood and test fixtures is snake_case (industry×20+,name,sector,notes,region,state,anything,no_such_field, plus the deliberateregion__cand__proto__refusal fixtures), so nothing authored in this repository newly refuses.One consequence to read: a stored
object_extensionrow that carries such a key no longer parses, so the sync that registers stored extensions (registrableObjectExtension→INVALID_METADATA) names the row and registers none of it, itsfieldsincluded. This is the uniform effect of anyObjectExtensionSchemanarrowing (PR #29331 had the same), stated in the changeset and the migration entry.No stored-shape detector, measured (round 2). The contract review asked for a
REFUSED_STORED_SHAPESdetector soos migrate meta --storedlists such a row before the upgrade refuses it. None can be reached:applyConversionsToStoredItemwraps a row in the stack collection its type maps to, andobject_extensionmaps to none (it is in neitherSINGULAR_TO_PLURALnor the stored-only map inconversions/stored.ts), so the stored pass returns the row untouched. Probe on this branch:applyConversionsToStoredItem('object_extension', { extend, fieldPatches: { Industry: … }, fields: { shift_at: { type: 'time', defaultValue: '12:00Z' } } }, { onTodo, onNotice })returns the same reference with 0 TODOs and 0 notices, while the controlapplyConversionsToStoredItem('object', …)rewrites the time default (1 notice). A detector walkingobjectExtensionswould therefore be dead code, so none is added; the semantic entry and the changeset say the stored pass does not list such a row, and name the check an operator runs instead.Tests
packages/spec/src/data/field-customization.test.tsgainsfieldPatches keys — the field map's own key grammar(8 cases):Industry,Annual Revenue,annual-revenue,2nd_phoneeach refused with exactly oneinvalid_keyatfieldPatches.KEY(message names the grammar and an example, no tracker id); CONTROL a snake_case key parses and a__ckey stays the__crule's singlecustomissue; CONTROL the guide's authored example (object-extensions.mdx, "Relabelling a vendor field") parses whole; and an agreement test thatfieldPatchesrefuses at the key exactly the keysObjectSchema.fieldsrefuses, over an 11-key sample that holds keys on both sides (guarded against vacuity).Verification
Round 2 readings on head
fb1c5bc9(objectstack-ai/objectstack):origin/mainmerged withscripts/pm/os-regen-merge.sh(merge base399df5bb),content/docs/references/data/object.mdxregenerated from the merged tree; the branch's delta against main is still the six files listed above. The ablation below ran in round 1 on6a70094d; the tested lines did not change since.Ablation: the new tests can fail
node scripts/ablation-replace.mjsreplacedz.record(FieldNameKeySchema, FieldDisplayPatchSchema, {withz.record(z.string(), FieldDisplayPatchSchema, {inobject.zod.ts(anchor 1 → 0, blob f24cd618 → c7a8c925), ransrc/data/field-customization.test.ts: 5 failed | 88 passed (93), the four refusal cases and the agreement test red, every control green. Restored: blob == HEAD,git diff HEADempty. The spec suite imports./object.zodfrom source, so no build sits between the mutation and the run.Consumer packages (downstream of the contract)
@objectstack/spectest: 792/792 files, 23529 passed | 1 todo.typecheck: exit 0.@objectstack/metadata-core(the target judge): 23/23 files, 904 tests.@objectstack/objectql(the fold that appliesfieldPatches): 535/535 files, 10396 tests.@objectstack/metadata-protocol(theobject_extensionsave door): 307 passed | 4 skipped (311), 22229 passed | 26 skipped.@objectstack/clifield-patch files (src/utils/field-patch-targets.test.ts,test/validate-build-gate-parity.test.ts,test/validate-field-patch-targets.test.ts): 3/3, 41 tests.@objectstack/cloud-connectionmarketplace-install-local-field-patch-targets.test.ts: 1/1, 8 tests.@objectstack/restmeta-object-extension-property-classes.test.ts: 1/1, 19 tests.uiapp-shellDataPillar.baseObjectExtension-28575.test.tsx+StudioCustomizeControls.objectExtension-28575.test.ts: 2/2, 19 tests.fieldPatchesin a test outsidepackages/specis in this list; the full suites ran for the three packages the delta names.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackonfb1c5bc9: 105 commands, all exit 0;--ranreconciliation: 105 derived, 105 run, 0 NOT-MEASURED.check:type-check-debtfirst returned exit 3 (its closure build queued behind the dogfood run on the verify lock,queue-timeout), and was re-run once the lock was free: exit 0.check:generated,check:api-surface(no export change),check-adr-0087-registration(registered, new here) andcheck-changeset-no-majorexit 0.Dogfood (narrowing arm, final head)
pnpm --filter @objectstack/dogfood testunder the lock onfb1c5bc9: 397 passed | 3 skipped (400) files; 3277 passed | 28 skipped (3305) tests; VERDICT command-exit 0. (Round 1 on6a70094dshowed 4 reds inflow-runasanddeclarative-endpoint-anonymous-guestthat also failed with the merge-base spec; main fixed them before this merge.)Acceptance notes
ObjectExtensionSchema.fieldsis still keyed by a barez.string(). Measured at the schema:{ extend: 'crm_account', fields: { 'Region Code': { type: 'text', label: 'Region' } } }PARSES, so an extension can add a field whose name the target's ownfieldswould refuse. Reported to the seat for a card.ObjectSchema.fieldsrefusesconstructor/prototypeby record refines, which are not part of the key schema;fieldPatches: { constructor: … }still parses at the schema (it is a valid snake_case word) and is refused by the target judge at install /os validatelike any unknown field. Noted, not filed.objectExtensions[]replays over a storedobject_extensionrow either, for the same reason (no stack collection for the type). Reported to the seat.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses (CI).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmSCC3J91hA9anBJ81g
Generated by Claude Code