Repository navigation
feat(spec)!: key an object extension's fields by the field map's own key grammar - #29774
Conversation
…key grammar ObjectExtensionSchema.fields was keyed by a bare string, so an extension could add a field under a key (`Region Code`, `region-code`) the target's own field map refuses. It now reads FieldNameKeySchema, with an invalid_key prescription at fields.KEY, an ADR-0087 semantic entry, and a REFUSED_STORED_SHAPES detector that lists such a stored row. Claude-Session: https://claude.ai/code/session_01WGufzf5kvTFdbWYDbe8bHE Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzf5kvTFdbWYDbe8bHE Co-Authored-By: Claude <noreply@anthropic.com>
…describe Claude-Session: https://claude.ai/code/session_01WGufzf5kvTFdbWYDbe8bHE Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzf5kvTFdbWYDbe8bHE Co-Authored-By: Claude <noreply@anthropic.com>
|
domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz Contract reviewServed-tier: Inputs: card #29523 (body, triage 6584774842, claim 6593404228, os-dev-report 6595183097), PR #29774 (body, 8-file list, net diff of ① Derived judgments
② Semver level
Should-fix (changeset prose, not the level): the "at every door that parses an object extension" list names "the marketplace install"; the install does not parse an extension with the schema (it judges ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
…-fskrbl # Conflicts: # packages/spec/src/conversions/refused-shapes.ts
… object extension Claude-Session: https://claude.ai/code/session_01WGufzf5kvTFdbWYDbe8bHE Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
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 6923c61d2d2a3b3c145991a9d3ff30c11ff2e050 && git checkout 6923c61d2d2a3b3c145991a9d3ff30c11ff2e050
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c91a55fc46c19979aba499fbdeea2022d784f35 649d47eabbc85188b8c8f9576dc7758266ed402e && git checkout -B drift-repro 2c91a55fc46c19979aba499fbdeea2022d784f35 && git merge --no-ff 649d47eabbc85188b8c8f9576dc7758266ed402e
node scripts/docs-audit/affected-docs.mjs --json 2c91a55fc46c19979aba499fbdeea2022d784f35
|
|
domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz Contract reviewServed-tier: This is a DELTA record. It judges ① Derived judgments
② Semver levelUnchanged and still matching the diff: ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:spec#2 · os-support-ai · session_01WGufzhJG3tKnksHVa1krBz 席位核对 — ACCEPT · domain:spec#2 · head
|
Requested by Yibo Wang · project thread
Fixes #29523
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: updated here —
content/docs/data-modeling/object-extensions.mdx("Naming the fields you add") gains the key grammar.skills/objectstack-data/SKILL.mdand its evals teach extension fields only asregion__c, so every taught line stays true and no skill line changes.维护者速读
fields(扩展新增的字段)的键改用与对象fields、扩展fieldPatches同一个键 schema(小写 snake_case),Region Code、region-code这类对象自己的字段表不可能容纳的键在解析时就被拒,报一个带示例的invalid_key。defineStack、os validate都放行;在.custom包里Region Code__c被保存门保存并发布、出现在每个对象读取里、启动重读照样注册,还按这个名字建列存了值。os migrate meta --stored会列出存量坏键行)。仓内 examples/skills/docs/模板无一处写过这种键;已存储的带坏键的扩展行升级后整行不再注册(其fieldPatches一并),再保存被 422 拒,直到改名。回滚即 revert 本 PR。packages/spec/src/data/object.zod.ts(ObjectExtensionSchema.fields的键 schema 与invalid_key文案)、packages/spec/src/conversions/refused-shapes.ts(新检测器)、packages/spec/src/migrations/entries/semantic/18.object-extension-field-key-grammar-refused.ts。pnpm --filter @objectstack/spec exec vitest run src/data/field-customization.test.ts src/conversions/refused-shapes.test.ts,预期 2 个文件全绿(192 条)。What changes
packages/spec/src/data/object.zod.ts:ObjectExtensionSchema.fieldsis keyed byFieldNameKeySchema, the one key schemaObjectSchema.fieldsandObjectExtensionSchema.fieldPatchesalready read (not exported; its doc comment now lists all three readers). The record's error map answersinvalid_keywith a prescription: the key is the name of a field the extension adds, lowercase snake_case such asregion_code(region_code__cin a customization package), never its label. The issue sits atfields.KEY, as thefieldPatchesone sits atfieldPatches.KEY. Thefieldsdescribe names the grammar.__proto__stays refused on the raw input byrefuseRecordProtoKey, unchanged.packages/spec/src/conversions/refused-shapes.ts: one detector,object-extension-field-key-grammar-refused, appended after thefieldPatchesone. Its predicate is the refusing door's own:ObjectExtensionSchema.safeParse, keeping eachinvalid_keyissue atfields.KEY(thefieldPatchesreader and this one now share one helper,refusedExtensionRecordKeys(extension, slot)). It lists, never rewrites, names the target, shows only the key and the field'stypeinfrom, and closes each reason with the parse's own message.packages/spec/src/migrations/entries/semantic/18.object-extension-field-key-grammar-refused.ts: the ADR-0087 semantic entry (refused, with a prescription; no conversion, because the key is the name a field is stored and read under, and a respelling names a different column or lands on a field the target already declares)..changeset/29523-object-extension-field-key-grammar.md: specminor, BREAKING,adr-0087: registered object-extension-field-key-grammar-refused, with the FROM → TO line.content/docs/data-modeling/object-extensions.mdx: one paragraph in "Naming the fields you add".content/docs/references/data/object.mdx: regenerated (thefieldsdescribe moved).fieldskey inexamples/**,skills/**,content/docs/**, templates and test fixtures is already snake_case.Why — the measurement that chose the declaration
Measured on
origin/main1c1a5d40(spec built from main'sobject.zod.ts), then on this branch:mainObjectExtensionSchema/defineObjectExtension(fields: { 'Region Code': … })invalid_keyatfields.Region CodedefineStack({ objectExtensions }), target outside the stackobjectExtensions.0.fields.Region CodedefineStack, target inside the stackos validate(CLI on a scratchdefineStackconfig)security-owd-unseton the scratch object)object_extensionsave + publish door,.custompair, keyRegion Code__cEMEAunderRegion Code__c; a boot re-read (loadMetaFromDb) registered it again with 0 errorsINVALID_METADATA, issueinvalid_keyatfields.Region Code__c; no row stored; insert refusedINVALID_FIELDloadMetaFromDbcounts 1 error, the extension is not registered (no extender), and its re-save is refused 422So every authoring door accepted such a key end to end, which makes this
Clause-②: no (narrowing)·Metadata-protocol: narrowed, BREAKING, with the semantic entry and theREFUSED_STORED_SHAPESdetector the card asks for (the stored pass reachesobject_extensionrows since #29589).Tests
packages/spec/src/data/field-customization.test.tsgains the describe "extensionfieldskeys — the field map's own key grammar" (8 cases):Region Code,RegionCode,region-code,2nd_phone,Region Code__ceach refused with exactly oneinvalid_keyatfields.KEY(message names the grammar and an example, no tracker id); CONTROLregion_codeandregion_code__cparse; CONTROL both authored examples of the object-extension guide parse whole; and an agreement test that the extensionfieldsrecord refuses at the key exactly the keysObjectSchema.fieldsrefuses at the key, over an 11-key sample that holds keys on both sides (guarded against vacuity).packages/spec/src/conversions/refused-shapes.test.ts: the registered id list gains the new id last; one TODO per refused key atobjectExtensions[0].fields.KEY, the row unchanged; the listed keys equal the parse'sinvalid_keyissues and each reason ends with the parse's message; snake_case keys (__cincluded) are not listed and parse; and a row with both a badfieldskey and a badfieldPatcheskey is listed once by each detector, never crossed.Acceptance notes
ObjectQL.registerApp, theobjectExtensionsbranch inpackages/objectql/src/engine.ts) copiesext.fieldsas given and parses nothing; measured on this branch,registerAppwithfields: { 'Region Code': … }still registers the field and stores a value. Every authoring path that produces a manifest (defineStack,os validate,os build, the save door) now refuses the key first, so only an artifact built before the upgrade reaches it. That is the fold's trust model for every collection, not this card's; noted, not filed.ObjectSchema.fieldsrefusesconstructorandprototypeby record refines that sit outside the key schema, soObjectExtensionSchema.fields: { constructor: … }still parses (asfieldPatches: { constructor: … }does, noted on feat(spec)!: key an object extension's fieldPatches by the field map's own key grammar #29513). Noted, not filed.Verification
All readings on objectstack-ai/objectstack, head
bd50ef25(merge oforigin/main87956dcc; the delta against main is the eight files listed above).Door probes (scratch, not committed)
A throwaway copy of the
packages/runtime/src/domains/meta-object-extension-item.test.tsrig (better-sqlite3:memory:, realObjectQL, real protocol), run once against a spec dist built from main'sobject.zod.tsand once against this branch's; andos validateon a scratchdefineStackconfig inpackages/cli. Readings are the table above. The probe files were deleted before commit.Ablation: the new tests can fail
node scripts/ablation-replace.mjsreplacedz.record(FieldNameKeySchema, FieldSchema, {withz.record(z.string(), FieldSchema, {inobject.zod.ts(anchor 1 → 0, blob06e35bf5→09e91a78) and ransrc/data/field-customization.test.ts src/conversions/refused-shapes.test.ts: 9 failed | 183 passed (192). The five refusal cases, the agreement test and the three new listing tests went red; every control stayed green. Restored: blob == HEAD,git diff HEADempty. The spec suite imports./object.zodfrom source, so no build sits between the mutation and the run. Taken on61876fc4; the tested lines did not change in the merge.Spec and consumer packages (downstream of the contract)
Under
scripts/pm/os-verify-lock.sh.On the final head
bd50ef25, after a fullturbo run build --concurrency=2(117/117 tasks):@objectstack/speccheck:generated: exit 0.test: 803/803 files, 23807 passed | 1 todo.@objectstack/organizationsobject-extension-managed-target.wall.test.ts: 1/1, 5 tests.@objectstack/plugin-authidentity-objects-plugin.test.ts: 1/1, 15.@objectstack/plugin-securityupgrade-impact-report.wiring.test.ts: 1/1, 5.@objectstack/downstream-contracttest: 3/3, 31.@objectstack/example-showcasetest: 45/45, 541.On
61876fc4(before the main merge; spec built from this branch):@objectstack/spectest: 803/803 files, 23798 passed | 1 todo.typecheck: exit 0.test:repo: 89/89, 1522.@objectstack/metadata-core(full): 23/23, 904.@objectstack/objectql(full): 544/544, 10606.@objectstack/metadata-protocol(full): 308 passed | 4 skipped (312), 22262 passed | 26 skipped.packages/specthat namesobjectExtensions,defineObjectExtension,object_extensionorObjectExtensionSchema:@objectstack/cli9/9 (108),@objectstack/cloud-connection3/3 (19),@objectstack/lint7/7 (535),@objectstack/metadata2/2 (17),@objectstack/platform-objects2/2 (73),@objectstack/rest5/5 (81),@objectstack/runtime4/4 (48).ui/packages/app-shellfiles that name an extension (ResourceListPage.objectExtensionName-29569,DataPillar.baseObjectExtension-28575,DataPillar.draftUnreadable-29148). Their extension bodies are mock-server fixtures keyedcustom_note__c,budget_code__c,region__c,tier__c, and none parses them with the spec; Test UI runs them. Studio's add-field derives the name throughtoFieldNameLooseandextensionFieldName, so it writes snake_case keys.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon the final headbd50ef25: 104 commands, all exit 0;--ranreconciliation: 104 derived, 104 run, 0 NOT-MEASURED. They ran after the full build, so the built-output gates (check:skill-examples,check:dual-build-cjs-loads) measured, where on61876fc4they had exited 3 for want of that build.check-adr-0087-registration(registered, new here) andcheck-changeset-no-majorare among the 104.Dogfood (narrowing arm, final head)
pnpm --filter @objectstack/dogfood testunder the lock onbd50ef25, after the full build in the same locked run: 402 passed | 3 skipped (405) files; 3317 passed | 28 skipped (3345) tests; exit 0, lock VERDICT command-exit 0.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_01WGufzf5kvTFdbWYDbe8bHE
Generated by Claude Code