Repository navigation
feat(spec)!: object extension fieldPatches refuse a field the extension replaces, key option patches by value, refuse __proto__, carry display renames - #29331
Conversation
…key options by value, carry display aliases Claude-Session: https://claude.ai/code/session_01ADHkmT6ynacYWXqz9Yredm Co-authored-by: Claude <noreply@anthropic.com>
… derived description Claude-Session: https://claude.ai/code/session_01ADHkmT6ynacYWXqz9Yredm Co-authored-by: Claude <noreply@anthropic.com>
… changeset and the patch-or-take-over rule Claude-Session: https://claude.ai/code/session_01ADHkmT6ynacYWXqz9Yredm Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmT6ynacYWXqz9Yredm Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 77 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 17 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 ec41b2be516ea3244a9947b137535dacfc87187b && git checkout ec41b2be516ea3244a9947b137535dacfc87187b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 738971d2eabaaa677b894ae6030cac5b887f9bc6 5f8d7494c93b397f54abc4deeea5780025d4335d && git checkout -B drift-repro 738971d2eabaaa677b894ae6030cac5b887f9bc6 && git merge --no-ff 5f8d7494c93b397f54abc4deeea5780025d4335d
node scripts/docs-audit/affected-docs.mjs --json 738971d2eabaaa677b894ae6030cac5b887f9bc6
|
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Head-sha: 维护者速读
席位意见:可落地,等契约复审。 席位核对 — ACCEPT ·
|
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract-review-claim: session_01ADHkmPi25XXKx92Xmg4viS · head 5f8d749 · 2026-11-02T12:57Z |
|
domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS Contract reviewServed-tier: Face: schema ( Gate verdicts on ① Derived judgmentsEach judgment is read off the tree at the head, never off the PR body.
② Semver level
③ Boundary flagsThe dev report names six deviations; each answered here.
④ Design judgmentJudged on the tree, file by file, against the code each hunk cites.
Cited code, read to judge: Must-fix: none. Should-fix: one, named above ( Implemented-by: VERDICT: PASS |
Requested by Steve · project thread
Fixes #28655
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: updated here —
content/docs/data-modeling/object-extensions.mdxgains the "Patch it or take it over, not both" bullet (the replaced-field refusal and the option-key grammar).skills/objectstack-data/SKILL.mdalready teaches "Relabel a field the owner declares withfieldPatches— never by redeclaring it infields" and its example keys the option bytechnology, so every taught line stays true and no skill line changes.维护者速读
fieldPatches多拒三种输入——同一扩展既在fields里整字段替换又在fieldPatches里打补丁的字段、不可能是选项值的选项补丁键(如Tech Sector)、以及__proto__键;补丁上写helpText/title这类别名时给出改名提示;描述里的白名单成员改为从常量生成。fieldPatches现在已被合并真实应用,原来这些写法"能保存、不生效":补丁落在扩展自己的替换字段上,或永远匹配不到选项,__proto__键在保存门被存下却在注册时丢失。packages/spec/src/data/object.zod.ts(refuseFieldPatchOfReplacedField、options的键 schema、whitelistedAliases)、packages/spec/src/data/field-key-aliases.ts(别名表搬出的唯一副本)、packages/spec/src/data/field-customization.test.ts新增一节。pnpm --filter @objectstack/spec exec vitest run src/data/field-customization.test.ts src/data/field-patches-derived.test.ts,预期 2 个文件全绿(91 条)。What changes
All in
ObjectExtensionSchema(packages/spec/src/data/object.zod.ts), the four should-fixes A–D of the contract review on PR #28650:superRefinerefuses a field name present in bothfieldsandfieldPatchesof one extension (refuseFieldPatchOfReplacedField):customatfieldPatches.NAME, prescribing "patch the vendor's field or declare your own, not both". A__cname in both is left to the existing__crule, so it is refused once.optionsmap is keyed bySelectOptionSchema.shape.value(read off the shape, not restated), soTech Sectoris refused withinvalid_keyatfieldPatches.industry.options.Tech Sector.refuseRecordProtoKeynow guardsfields,fieldPatchesand theoptionsmap: the measurement holds at the door (below), not only at schema level.whitelistedAliases). Field:title,displayNameontolabel;help,helpText,hint,tooltipontoinlineHelpText. Option:text,name,titleontolabel. Carriage follows theui/action-target-aliases.tsprecedent: the two tables moved out offield.zod.tsinto a relative-import-only leaf,packages/spec/src/data/field-key-aliases.ts, whichFieldSchema,SelectOptionSchemaand the patches all read. No second mechanism and no new public export (check:api-surfaceunchanged).fieldPatches.describe()lists its members fromBASE_FIELD_DISPLAY_WHITELIST(fieldPatchesDescription).Generated and ledger files that ride along:
content/docs/references/data/object.mdx(regenerated description);packages/spec/dropped-refinements.baseline.json: the fivedata/ObjectExtensionfields.valueType…sites moved tofields.out.valueType…, because the__proto__guard wraps the record in a preprocess. The same sites, one level deeper, and no new site;content/docs/data-modeling/object-extensions.mdxgains one bullet;.changeset/28655-field-patches-refuse-replaced-field.md.Forced test edits: none. The new pins are added to
field-customization.test.tsandfield-patches-derived.test.ts. No existing assertion changed.Why
The card's "reach: none yet" premise has expired:
mergeObjectDefinitions(packages/objectql/src/registry.ts) appliesfieldPatcheson main (#28565). Each input A and B refuse parsed and then did nothing visible. A patch landed on the extension's own whole-field replacement, and an option key no value can be never matched an option.__proto__reach, measured at the door on main47ee57febefore the change. This is the/api/v1/metaobject_extensionsave door's own schema (OBJECT_EXTENSION_ITEM_SCHEMA) and its registration seam (registrableObjectExtension), over a JSON-parsed body:success: true, withfields: {}andfieldPatchesmissing the key;fieldPatches.__proto__andfields.__proto__. The stored row and the served extension disagree.Tests
The new pins, each giving zod code, path and named subject beside a control through the same parse:
fieldPatches.industry, with a control (a patch beside afieldsentry of another name parses), and the__ccase refused once.invalid_keyat the option key, with a control (the key verdict equalsSelectOptionSchema.shape.value.safeParsefor four keys). There are three__proto__cases (fields,fieldPatches,options) over JSON documents, plus a control.colourcarries none under the shipped whitelist). The derived proof is infield-patches-derived.test.ts: once the mocked whitelist admitscolor, the option'scolourrename reaches the patch.Ablation, in a side worktree at
5f8d7494, throughscripts/ablation-replace.mjs(each anchor 1 → 0, every leg restored to blob67228dde3fe9,git diff HEADempty). Spec tests importsrc, so no build is in the path:refuseFieldPatchOfReplacedFieldcallz.string()key instead ofoptionShape.valuefieldPatchesguardfieldPatches__proto__case)aliasesaliasesAcceptance notes
title,help,helpText,hint,tooltip. The filter also yieldsdisplayNameontolabel, whichFieldSchema's table carries. It is pinned.optionsmap's__proto__guard goes beyond the card's "fieldPatchesandfields". It is the same defect class and the same mechanism, in the same schema, inside the same gate family. Without it B's key grammar could not see that key, because zod skips__proto__before the key schema.object.zod.ts. C also touchesfield.zod.ts(the two tables now read from the leaf, byte-identical rows) and addsfield-key-aliases.ts. This is declared as a deviation for the seat to amend.fieldPatches(the docs page and the skill) key options bytechnology.refuseUnlessPlatformWrittenandmergeObjectDefinitionsare untouched (spec(currencyConfig.homeAmount): a home or rate field with both readonly: false and an expression is told only about readonly, so the author meets the second fault on the next parse #28205 follows in the same file).Verification
Consumer packages (downstream of spec), at 5f8d749
@objectstack/spectest: Test Files 790 passed, Tests 23390 passed, 1 todo. typecheck: exit 0.@objectstack/objectqltest: 527 files, 10140 tests passed.@objectstack/metadata-protocoltest: 300 passed, 4 skipped files; 22100 tests passed.@objectstack/metadata-coretest: 22 files, 885 tests passed.@objectstack/linttest: 189 files, 7830 tests passed.@objectstack/cloud-connectiontest: 67 files, 813 tests passed.@objectstack/resttest: 367 files, 6963 passed, 332 skipped.@objectstack/cli--project unit: 370 files passed. Two files reported "cli is not built" (published-subpath-*.pin.test.ts), a prerequisite, rerun after the full build: 2 files, 29 tests passed.--project integration test/validate-field-patch-targets.test.ts: 2/2 passed.Gates and dogfood
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat5f8d7494: 103 derived commands, each run with stdin closed. 102 exited 0 on the first pass.check-plugin-teardown-shape --self-testexited 3 (PREREQUISITE NOT MET: its pinned fixture commit is outside the shallow clone); aftergit fetch --depth=1 origin 621a4876…it exited 0 (48 cases).--ranreconciliation: "103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN" (a DERIVED zero). Offline before the PR:check-changeset-no-major --eventon this body's two lines: LEVEL AXIS accepted (no (narrowing)+narrowed, specminor);check:adr-0087-registration: 1 declared-breaking changeset with a disposition. eslint on the five changed.tsfiles: exit 0.pnpm --filter @objectstack/spec check:generated: 1 stale (check:docs), fixed with--fixand re-checked green; the other 13 were up to date.Dogfood (title carries
!and the(narrowing)arm) at5f8d7494, afterturbo run build --filter=./packages/**(78/78):os-verify-lock: VERDICT command-exit 0· held 1829 s ·pnpm --filter @objectstack/dogfood test: Test Files 389 passed, 3 skipped (392); Tests 3215 passed, 28 skipped (3243).Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code