Repository navigation
feat(spec): a per-field display patch slot on the object extension (fieldPatches), keys derived from BASE_FIELD_DISPLAY_WHITELIST - #28650
Conversation
…erived from the display whitelist ObjectExtensionSchema gains fieldPatches: a map from a base field name to a closed patch whose keys are read from BASE_FIELD_DISPLAY_WHITELIST (plus an options map keyed by stored value, its entries' keys read from the option whitelist). Every other FieldSchema or option key is refused by name with the ADR-0146 D6 prescription, a field list under options is refused as membership, and an __c key is refused with "declare it in fields". The slot is marked not enforced until the registry merge carries it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…hangeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
📓 Docs Drift CheckThis PR changes 1 package(s): 6 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 ac8da4fdc6c78d7bffe4a99c9149d413eb1761af && git checkout ac8da4fdc6c78d7bffe4a99c9149d413eb1761af
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f23e051232c21d104fef019191ff504faaf700f1 8b8a7949d1592b511858f7b6924b88d32ba39e34 && git checkout -B drift-repro f23e051232c21d104fef019191ff504faaf700f1 && git merge --no-ff 8b8a7949d1592b511858f7b6924b88d32ba39e34
node scripts/docs-audit/affected-docs.mjs --json f23e051232c21d104fef019191ff504faaf700f1
|
…lot's comment Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V 维护者速读(席位终稿 · head
|
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken under Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Read-only shape held. The diff was read as ① Derived judgmentsEach line names one change the diff implies to the accept set or the public surface, and judges it.
② Semver level
One gap on the declaration set, should-fix: SKILL.md:313 says a kind other than ③ Boundary flagsAuthor: the PR's Dev deviations (os-dev-report 6557468193), each answered:
Out-of-scope finding ( Experimental slot, ADR-0049 and the liveness ledger: ADR-0049 allows a declared property in exactly three states; state 2 is marked and documented as not enforced. The slot's First consumer: #28565 (N-23d) reads the slot as a record of field name to patch, applies whitelisted keys onto the base field and option labels onto the option whose stored value matches the map key, after the additive merges, with D9.2a yield and priority order. The shape here carries exactly that: a per-field map, option patches keyed by stored value, no Check-runs on Hand-off line: the seat's ④ Design judgmentJudged on the two
Break attempts, each with its result:
Findings. None is a must-fix.
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V Landing stroke · 2026-11-01T12:10Z · head
|
🤖 Generated with Claude Code
https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Fixes #28560
Clause-②: yes (widening)
Metadata-protocol: added
维护者速读
objectExtensions[])新增一个可选槽fieldPatches,按字段名给厂商字段打"显示补丁":只能改label、description、inlineHelpText、placeholder和选项的label;键集合直接从BASE_FIELD_DISPLAY_WHITELIST读出,白名单放宽槽就跟着放宽。[EXPERIMENTAL — not enforced],引擎注册扩展时还不搬运它,N-23d(objectql: an object extension's display patch is carried and applied by mergeObjectDefinitions; a patch naming a missing field or option is refused at install and validate #28565)接上合并之前没有任何运行时读它。回滚 = revert 本 PR。packages/spec/src/data/object.zod.ts的FieldDisplayPatchSchema/FieldOptionDisplayPatchSchema(键从白名单派生、结构键按名拒收);② 同文件ObjectExtensionSchema根superRefine里的refuseExtensionFieldPatchKeys(__c拒收);③packages/spec/src/data/field-patches-derived.test.ts(放宽白名单即放宽槽的证明)。pnpm --filter @objectstack/spec exec vitest run src/data/field-customization.test.ts src/data/field-patches-derived.test.ts,预期 2 个文件、71 个测试全绿。What changes
ADR-0146 D10, second rule: a base field changes only within the display whitelist, and the patch is a per-field map in the extension body.
ObjectExtensionSchemagains one optional key,fieldPatches:Record(fieldName, closed patch). The patch's keys areBASE_FIELD_DISPLAY_WHITELIST.field, each value schema taken by key fromFieldSchemaand made optional, plusoptions:Record(storedValue, closed option patch)whose keys areBASE_FIELD_DISPLAY_WHITELIST.option, value schemas taken fromSelectOptionSchema. Nothing restates the list. Construction throws if the whitelist names a key the source shape no longer declares.FieldSchemakey on a patch is refused by name (unrecognized_keys) through aguidanceSetsentry whose member set is derived (FieldSchemakeys minus the whitelist minusoptions). The prescription names the whitelist, lists its members read from the constant, and states that everything else stays the vendor's (ADR-0146 D6).valuehas its own guidance: the option is addressed by its stored value, and an extension cannot rename it. Every other option key (color,default, …) gets the D6 prescription, from a derived set.FieldSchemaarray form) underoptionsis refused as membership, with a prescription showing the map form.fieldPatcheskey spelled as an extension field (isExtensionFieldName, e.g.region__c) is refused atfieldPatches.region__cwith "declare it infields; it is your own field". This check is in the extension's existing rootsuperRefine. The published JSON Schema already lists that root as a dropped-refinement site (data/ObjectExtension''), so thedropped-refinementsledger does not change..describe()reads[EXPERIMENTAL — not enforced]and names rollout card N-23d.objectql/src/engine.ts's extension registration (now at:8857-8869; the card measured:8820-8829on an older tree) copies the declared keys by name, so the slot is dropped there until N-23d. The engine is not changed here. The guidance comment onObjectExtensionSchemathat said "copies EXACTLY the declared keys" now namesfieldPatchesas the one declared key not carried yet.field-customization.ts's header now points at the slot instead of saying the shape is "not declared here".The settled name:
fieldPatchesNo existing spelling of this slot exists. A repo-wide search for
fieldPatches,fieldOverrides,fieldOverrideandfield_patchesfinds only unrelated hits: a permission-matrix UI stat and a lint test helper. The name follows ADR-0146's own words: D10 says the extension may patch a base field's display properties and calls the result "a per-field map in the extension body". The Salesforce-style "override" is not used because in ADR-0146 an override replaces the whole item (D8/D9's declared override, ADR-0131 D6's O regime), andfieldson the same extension already does "add/override", which replaces a same-named field whole (mergeObjectDefinitions). A patch keeps the vendor's field and changes named display properties of it, so "patch" is the accurate word, andfieldPatches(plural, keyed by field) matches the siblingfieldsmap.Measured assumptions (PM)
check:livenessdoes not walkObjectExtensionSchema. Two legs on this branch: with the marker removed from thefieldPatchesdescribe (an ablation, restored and checked byte-identical toHEAD),check-liveness.mts --jsonexits 0 withunclassified: []. The positive control, a marker-less key added toObjectSchema, exits 1 withunclassified: ['object/probeKeyNoLedger']. The walk is rooted on the metadata-type registry plusSPEC_ONLY_SCHEMAS.ManifestSchemahas noobjectExtensions, and the stack schema is not a governed root. So no ledger row is added./meta/typescount pins are not reached. They count top-level keys of registered metadata types (objectisObjectSchema). An extension is a stack collection entry, not a metadata type.@objectstack/metadata-protocol's whole suite is green on this head (291 files passed, 4 skipped).object-strictness-batch20.test.tsand the extension fixtures parse unchanged. Batch 20 is green, as is the frozenpackages/qa/downstream-contractDcObjectExtension(optional key; nothing narrowed). A test pins that an extension without the slot parses to the same output as before.ui/packages/typesmirror ofObjectExtensionexists (grepfinds none), so there is nothing to sync.Tests
packages/spec/src/data/field-customization.test.tsgains afieldPatchesblock next toSTRUCTURAL_FIELD_KEYS:STRUCTURAL_FIELD_KEYSkey is refused by name (codeunrecognized_keys,pathfieldPatches.industry,keys, and the message namesBASE_FIELD_DISPLAY_WHITELISTand ADR-0146 D6);optionsmembership (the list form) is refused asinvalid_typeatfieldPatches.industry.options;FieldSchemakey outside the whitelist is refused, and the refused set is derived from the shape;labelonly:valueis refused with its own guidance, every other option key with D6;__ckey is refused (customatfieldPatches.region__c, namingfields);[EXPERIMENTAL — not enforced]and names N-23d.packages/spec/src/data/field-patches-derived.test.tswidens the constant itself (a file-scopedvi.mockaddingrowsand an option'scolor) and parses through the realObjectExtensionSchema. The widened keys are accepted, a widened key keeps itsFieldSchemavalue type, and the refusal prose lists the widened members.Ablation (one-time, not a permanent test). With the fix committed,
scripts/ablation-replace.mjsreplaced the derived field keys with the restated list['label', 'description', 'inlineHelpText', 'placeholder']. Two tests infield-patches-derived.test.tswent red (2 failed, 69 passed). The restore was checked againstHEAD: blobb44d0197cc1d,git diff HEADempty.Acceptance notes
check:doc-authoringrule 3 bans#NNNNin.describe()prose: it projects intocontent/docs/references/**, where a reader cannot resolve it. The id#28565is in the code comments beside the slot.experimental(the marker grammar has noplannedspelling that also says "not enforced"). N-23d flips the marker.ObjectExtensionSchema.fields(and nowfieldPatches) is a barez.recordwithout therefuseRecordProtoKeyguard thatObjectSchema.fieldscarries. A__proto__own key (asJSON.parseproduces) is dropped silently from the parse output. The new slot keeps parity with its sibling and does not take that on.Verification
All readings are on head
09f996f719(after mergingorigin/mainonce).Local runs
pnpm --filter @objectstack/spec build: exit 0.pnpm --filter @objectstack/spec check:generated -- --fix: regeneratedcontent/docs/references/**(gen:docs) and the strictness-ledger counts (gen:strictness-ledger). The build wroteauthorable-surface/data.json(data/ObjectExtension:fieldPatches). The re-check was green, andcheck:generatedwas green on the final head.pnpm --filter @objectstack/spec test: 783 files passed, 23173 tests passed, 1 todo. Exit 0.pnpm --filter @objectstack/spec typecheck(includescheck:scripts-typecheckandcheck:test-typecheck): exit 0.check:test-typecheckrun alone: exit 0.pnpm --filter @objectstack/metadata-protocol test: 291 files passed, 4 skipped. 21912 tests passed, 26 skipped. Exit 0.Derived gates (dispatch-gates --commands, reconciled with --ran)
check:skill-examplesandcheck:lean-entry-closurefirst exited 3 (prerequisite: noclient/client-react/objectqldist). They were re-run green after building those closures.check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: about 75 packages withoutdist/). That is NOT MEASURED locally, a declared narrowing, and CI'sLint & Repo Gatesruns it.--ranreconciliation: "103 derived famil(ies) accounted for — 102 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)". Exit 0.Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.
Generated by Claude Code