Repository navigation
fix(metadata-protocol)!: the metadata save and publish doors refuse a select field bound to a picklist no loaded package declares - #29956
Conversation
…t at the save and publish doors Claude-Session: https://claude.ai/code/session_01WGufzis5HWU3u6jXfEfM17 Co-authored-by: Claude <noreply@anthropic.com>
…t at the save and publish doors Claude-Session: https://claude.ai/code/session_01WGufzis5HWU3u6jXfEfM17 Co-authored-by: Claude <noreply@anthropic.com>
…t at the save and publish doors Claude-Session: https://claude.ai/code/session_01WGufzis5HWU3u6jXfEfM17 Co-authored-by: Claude <noreply@anthropic.com>
…t at the save and publish doors Claude-Session: https://claude.ai/code/session_01WGufzis5HWU3u6jXfEfM17 Co-authored-by: Claude <noreply@anthropic.com>
…ine double in the pinned ledger Claude-Session: https://claude.ai/code/session_01WGufzis5HWU3u6jXfEfM17 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 179 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 06f9ba5c8775f7237c03d65d3e8a644146220eaa && git checkout 06f9ba5c8775f7237c03d65d3e8a644146220eaa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 40fa433539cba010343b02ede4e66cccfab47d15 8d7a02d5a906dd2c108f015c92d9cab68cc25c14 && git checkout -B drift-repro 40fa433539cba010343b02ede4e66cccfab47d15 && git merge --no-ff 8d7a02d5a906dd2c108f015c92d9cab68cc25c14
node scripts/docs-audit/affected-docs.mjs --json 40fa433539cba010343b02ede4e66cccfab47d15
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver level
③ Boundary flags
④ Narrowing face
Implemented-by: VERDICT: PASS |
…klist judges' comments (#29898) The os validate picklist judge walks the parsed artifact with its own reader; only the boot audit shares collectPicklistReferences with the save-door judge. Both comments now say so, and the changeset carries the Skills-docs declaration line. Claude-Session: https://claude.ai/code/session_01WGufzis5HWU3u6jXfEfM17 Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: narrowing ( ① Derived judgments
② Semver levelUnchanged: ③ Boundary flags
④ Narrowing face
Implemented-by: VERDICT: PASS |
Requested by Yibo Wang · project thread
Fixes #29898
Clause-②: no (narrowing) — the metadata save door (draft and live), the per-item and package publish, and the restore doors refuse an
objectorobject_extensionwhose field names a picklist no loaded package declares, a body they accepted before. Nothing is widened.Metadata-protocol: narrowed
Skills-docs: no move —
skills/**teaches no picklist REFERENCE verdict (the two mentions are "a picklist-bound field's options are never patchable" and the stack-key list), andcontent/docs/**carries no severity table for this reference; the prescription ships in the refusal text, the changeset's FROM → TO and the new upgrade-guide entry.维护者速读
os validate/os build也拒绝栈内字段,只有运行时写入这道门放行,作者(或经 API 写元数据的 agent)拼错一个清单名就得到一张永远存不进数据的字段,且_diagnostics.valid: true什么都不提示。migrate-stored自愈不判;此前存下的草稿会在发布时被拒(它本来就让该字段写入全部失败)。回滚即 revert 本 PR,无数据迁移。packages/metadata-protocol/src/protocol-save-door-refusals.ts(新判定unknownPicklistBindingSaveRefusal,"已持有绑定"豁免是风险 hunk)、packages/metadata-protocol/src/protocol.ts(保存门与发布晋升的接入点、方法文档)、packages/cli/src/utils/picklist-references.boot-parity.test.ts(两道门判定一致的 pin)。pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-picklist-binding-door.test.ts,预期 5 项全过:未声明清单在对象与.custom扩展上各被拒一次,依赖包的清单、.custom基包的清单、同环境另一个已安装包的清单各自保存+发布成功并被服务上选项。packages/spec的迁移条目,按席位安排走CONTRACT_REVIEW_TIER复核)。What changes
The dangling-reference refusal family at the metadata save door gains its picklist member.
unknownPicklistBindingSaveRefusalinpackages/metadata-protocol/src/protocol-save-door-refusals.ts, besideunknownGrantObjectSaveRefusal(metadata: a runtime permission-set save accepts a grant on an object that does not exist (200) — the build refuses the same grant (#18202), so a typo silently grants nothing #28990) andunknownDatasourceSaveRefusal(metadata: an object bound to a datasource that does not exist publishes successfully, and is then silently unregistered (data calls 404 "not registered"); Studio still lists it #26724), whose shape it follows. It judges anobjectand anobject_extensionbody (an extension's fields join the object it extends, so the object it names isextend), reads which picklists the body's fields name through the boot audit's own walk (collectPicklistReferences,@objectstack/metadata/picklist-references; theos validatedoor walks the parsed artifact with its own reader and agrees with this door by pin, not by sharing the walk), and refuses422 INVALID_METADATAwith one finding per unknown binding atfields.FIELD.picklist, naming the field and the list.resolvePicklistOptions, the read that SERVES a bound field's options. So the package's own list, a dependency's, the base package's of a.custompair and any other package installed here all resolve, and the judge cannot offer a field options the write door then refuses. A registry that cannot answer the question refuses nothing (the postureunknownGrantObjectSaveRefusaltakes forgetObject).saveMetaItemafter the schema gate, in draft and in publish mode (amigrate-storedre-save excepted, the platform healing its own storage);promoteDraftForPublishon the draft body, so a draft stored before this rule, or one whose list went between its save and its publish, is refused and a batch publish aborts on it; and the restore doors throughrefuseRestoredBodyTheSaveDoorRefuses.picklistis not runtime-creatable (allowRuntimeCreate: false), so no draft of a batch can declare one.packages/spec— one ADR-0087 semantic upgrade-guide entry,picklist-binding-unknown-refused. ⛔ No schema shape underpackages/spec/src/**is touched (the order's stop condition):FieldSchema.picklistis unchanged, and the refusal is a door judgement against the registry, not a Zod refinement — the verdict depends on which packages are installed, which a schema cannot see.packages/cli— one source comment in theos validate/os buildpicklist judge. It said the runtime metadata write path "is not judged here" as a stated bound; it now names the door that judges it against the engine's registry.The measured surface shift (the dispatch's route was lint; the door is metadata-protocol)
The order and the claim named "the author-time dangling-reference gate in
packages/lint". Measured at21510bba, that is not where the precedent lives, and the seat accepted the shift before this PR was opened:validateObjectReferences, which refuses a field's danglingreference) declaresruntimeTypes: ['flow', 'dataset'], so it never judges anobjectwrite at the save door — a crossing is aruntimeTypesdecision of its own, and an advisory-tier fence sits over that door ([P2] 运行时授权门扩到 object 写入 + 全量 runtime-safe 规则快照(#4463 P1 之后) #4716);packages/cli/src/utils/picklist-references.tssays why in its header (the verdict depends on WHICH package declares the reference, and a registry rule is handed one flattened stack), and it names the runtime write path as its unjudged bound;protocol-save-door-refusals.ts. fix(metadata-protocol)!: an object saved back from a translated /meta read stores its source labels, and one the registry would refuse is refused at save and at publish #29790'sobjectRegistrationRefusalis the registry pre-flight at the same door, which picklist resolution is not one of.So the fix joins that family, and no
packages/lintfile is touched (no overlap with #29926, which editspackages/lint/src/validate-expressions.ts).Tests
Every reading below is from
objectstack-ai/objectstackon this branch, each heavy command underscripts/pm/os-verify-lock.sh(slotdev-29898). The suites ran atf04c956c. Since thend1809dd5adds onlyscripts/engine-double-contract.pinned.json(the ledger row for the new test's engine double, written by the gate's own--write), and8d7a02d5corrects two source comments and adds the changeset'sSkills-docs:line; no package or dogfood test reads either.Test Files 2 passed (2),Tests 63 passed (63), lockVERDICT command-exit 0@objectstack/metadata-protocolwhole packageSchemaRegistrydoor test (@objectstack/runtime)Test Files 1 passed (1),Tests 5 passed (5)@objectstack/cli,unit)Test Files 1 passed (1),Tests 4 passed (4)@objectstack/cliunitlayer@objectstack/metadata-protocoland@objectstack/clitsc --noEmitclean (lockVERDICT command-exit 0)f04c956c(narrowing arm)New and changed pins:
packages/metadata-protocol/src/protocol.unknown-picklist-binding-refusal.test.ts(new) — throughsaveMetaItem(draft into a package, draft naming none, a publish save, a publish save underforce),publishMetaItemandpublishPackageDrafts: the envelope (code,status), theissues[].pathset and the first sentence of each finding, nothing stored and nothing promoted; amultiselectjudged as aselect; anobject_extensionjudged under the object it extends; a list that goes between a draft's save and its publish; themigrate-storedexemption; and the controls — a resolved list, no binding at all, a held binding on a list gone since (with the two re-binding cases that ARE refused), and a registry that cannot resolve a list.packages/metadata-protocol/src/protocol.restore-doors-save-refusal-family.test.ts— one more member of the family it exists for, at both restore doors (rollbackMetaItem,revertCommit) with their controls.packages/runtime/src/domains/meta-picklist-binding-door.test.ts(new) — the realSchemaRegistrybehind a realObjectQL+better-sqlite3, a code base package, its.custompair, a second installed list package and a runtime package: the two refusals, and the three "declared" controls each saved, published AND served with the list's options.packages/cli/src/utils/picklist-references.boot-parity.test.ts— a second top-leveldescribein the harness that already pins this door against the boot (AGENTS.md: a new pin for a component with a shared harness file goes into that file). It answers the seat's ask directly: the two judges cannot share one judgement (this door reads the parsed artifact and weighs outside dependencies; the save door asks the live registry), so the agreement is pinned — on a list a SIBLING package of the same artifact owns, where they could disagree, and on a name nothing declares, where both refuse the same field (the paths are rooted in their own subjects, so the artifact-rooted path ends with the body-rooted one).Ablation — each door's negative pins really fail without the fix (three legs)
Leg 1, the save door. One leg, through
node scripts/ablation-replace.mjsin wrap mode: the save door's call site insaveMetaItem(the four lines that askunknownPicklistBindingSaveRefusaland throw) replaced by a marker, anchorx1 -> x0, blobbecf5c5fcc0e -> 19085994bfa4. The suite resolves./protocol.jsfrom source, so no build sits between the mutation and the run.The 7 that stay green are the publish-door pins (their own call site in
promoteDraftForPublishis untouched) and the controls — the direction expected. Restore:blob after restore becf5c5fcc0e == blob at HEAD,git diff HEADempty.Leg 2, the publish door. The
throwof the call site inpromoteDraftForPublishreplaced by a marker, anchorx1 -> x0, blobbecf5c5fcc0e -> f15546e7b85a, same suite:Exactly the three publish-door pins and the
migrate-stored"publish is still refused" assertion; the save-door pins and the controls stay green. Restore:blob after restore becf5c5fcc0e == blob at HEAD,git diff HEADempty.Leg 3, the restore doors. The
throwinrefuseRestoredBodyTheSaveDoorRefuses(protocol-authoring-gate.ts) replaced by a marker, anchorx1 -> x0, bloba114a4c2f1ff -> e7d365211ff2,protocol.restore-doors-save-refusal-family.test.ts:Exactly the two picklist refusal cases of
pinBothDoors; every other family member and the controls stay green. Restore:blob after restore a114a4c2f1ff == blob at HEAD,git diff HEADempty.Acceptance notes
describeUnresolvedPicklistReferences) is unchanged and still the authority for a package's own objects at load; this door is the authoring half it named as missing.os validatecannot see the environment, so it judges the stack alone, as it did before this PR. The new door never refuses a list the registry serves, so it cannot be stricter than the serve path; the runtime test pins that case as a control.duplicatePackageand the unmanaged import copy items throughsaveMetaItemin publish mode, and the copy target holds nothing yet, so a source object holding a binding on a list that is gone since is now refused when copied (the import rolls back). That is the same propertyunknownGrantObjectSaveRefusal(metadata: a runtime permission-set save accepts a grant on an object that does not exist (200) — the build refuses the same grant (#18202), so a typo silently grants nothing #28990) already gives those two paths, and it is right on the merits: the copy would be served with no options.Verification
PUT /api/v1/meta/object/x_probe?mode=draft&package=com.qa.xwithfields.t = { type: 'select', picklist: 'nope_list' }→422 INVALID_METADATA, finding atfields.t.picklistnaming the field andnope_list; nothing stored, soPOST /api/v1/packages/com.qa.x/publish-draftshas nothing of it to publish (the card's steps 2–5).200,publish-drafts→published,GET /api/v1/meta/object/x_probeservestwith that list's options, andPOST /api/v1/data/x_probewith one of its values →200.Gate sweep —
node scripts/pm/dispatch-gates.mjs --ran: 95 derived families accounted for, 94 run, 1 NOT-MEASUREDEvery family
dispatch-gates.mjs --commandsderives for this diff was run locally and recorded;--rananswers✓ 95 derived famil(ies) accounted for — 94 run, 1 NOT-MEASURED.0. Five of them were green only on a second run, each for a reason outside the diff, with the fix noted:check:engine-double-contract(the new test's engine double had no ledger row; added by the gate's own--write, commitd1809dd5),check-plugin-teardown-shape --self-test(needs full history; green after unshallowing the clone),check:dts-closure(specdistwas rebuilt at the same moment by the dogfood build; green on rerun, 225/225),check:i18n-coverageandcheck:type-check-debt(build prerequisites and the shared lock; green on rerun,426 baselined, none newand0 raw tsc error(s)).check:dual-build-cjs-loadsexits3, the prerequisite refusal, because packages this diff does not touch have no localdist. CI's build lane runs it.pnpm lint: exit0, run locally after the PR opened.@objectstack/cliintegrationvitest layer (no integration-tier file is touched).🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzis5HWU3u6jXfEfM17