Repository navigation
feat(metadata-protocol)!: the save door refuses a tenant's copy of a packaged validation rule that changes the rule's condition, type or any other key the package owns - #28478
Conversation
…redicate Claude-Session: https://claude.ai/code/session_01L9qibx3A7AeHSnSr39oxaV Co-authored-by: Claude <noreply@anthropic.com>
…packaged validation rule that changes what the package owns The runtime authoring gate asks the registry's own predicate, SchemaRegistry.tenantRuleCopyRefusal, the one the load report reads, and folds its verdict into the gate's one 422 as the gate-local rule validation-rule-copy-changed, located at the copy's first changed key, with the ruling's prescription. A reworded or switched-off copy and a rule under a new name still save. Claude-Session: https://claude.ai/code/session_01L9qibx3A7AeHSnSr39oxaV Co-authored-by: Claude <noreply@anthropic.com>
…d bound, and the engine-double ledger records it Claude-Session: https://claude.ai/code/session_01L9qibx3A7AeHSnSr39oxaV Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check9 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 10 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 cb12714955734b07797d0c389cc7779dc6d83282 && git checkout cb12714955734b07797d0c389cc7779dc6d83282
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 119931d82832e7f923a4c0a4a8251f9e7ad54a1b 5b370a280976465a0b2087338d42c618854bb698 && git checkout -B drift-repro 119931d82832e7f923a4c0a4a8251f9e7ad54a1b && git merge --no-ff 5b370a280976465a0b2087338d42c618854bb698
node scripts/docs-audit/affected-docs.mjs --json 119931d82832e7f923a4c0a4a8251f9e7ad54a1b |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: ① Ruling → code
② Declarations
③ Pins and ablations
④ Traps and should-fix
Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-tesla · session_01L9qibz3ZUbZ8Qcn1rDw9yc 维护者速读
席位意见:契约复审 PASS, 席位核对ACCEPT · seat
|
Requested by Tesla · project thread
Fixes #28448
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Ruling-ref: 6545641964 (letter B: the loud refusal sits at the save door), under 6524229156 (letter C′:
messageandactiveare the tenant's, everything else is the package's). The load half landed in #28445 (50015d84).维护者速读
object元数据时,若租户覆盖层里有一条与包内(或扩展包)校验规则同名的副本,且改了message、active以外的任何键(条件、类型等),保存门现在用422 INVALID_METADATA拒绝,并给出裁定的处方:"To change the condition, add a rule under a new name, or change it in the package."。判定直接调用 fix(objectql)!: the registry fold merges validation rules by name; a tenant's same-named copy carries only its message and active, and a changed condition is reported at load, never run #28445 留下的SchemaRegistry.tenantRuleCopyRefusal,所以加载报告和保存拒绝是同一个判断。active: false、新名字的规则照常保存;草稿保存不受影响(发布时才判定);已存数据照旧加载,不需迁移。回滚即 revert 本 PR。packages/metadata-protocol/src/runtime-authoring-gate.ts的findPackagedRuleCopyGaps,以及protocol-authoring-gate.ts的packagedRuleCopyRefusalOf。protocol.ts未改动(feat(objectql,metadata-protocol,rest,spec): the object read names the objects a delete can cascade into #28451 占用中)。pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/protocol-validation-rule-copy-save-refusal.test.ts,预期 5 个用例全绿(需先构建@objectstack/metadata-protocol)。What changes
Where the refusal sits. The save door's own refusal set (
protocol-save-door-refusals.ts) is reached only through calls written inprotocol.ts, and seat 2's #28451 holds that file. So the refusal joins the runtime authoring gate, whichprotocol.tsalready calls on every door this needs: the active save, publish, batch publish, the draft-publish verdict and the restore door. Noprotocol.tsline changes.The host fact (
packages/metadata-protocol/src/protocol-authoring-gate.ts).judgeRuntimeAuthoringRulesgathers host facts impurely and hands them to the pure gate. For anobjectit now also askspackagedRuleCopyRefusalOf(engine, name, body), which calls the registry'stenantRuleCopyRefusal(name, body)when the engine's registry has one and reduces the answer to{ ruleName, changedKeys, message }. A host whose registry has no such predicate (a test double, a non-ObjectQL engine) answersundefined, and the rule is not run. This keeps metadata-protocol free of an objectql import, and the judgement is the registry's own, the same one the load report reads.The gate rule (
packages/metadata-protocol/src/runtime-authoring-gate.ts). A new gate-local rule,validation-rule-copy-changed(VALIDATION_RULE_COPY_CHANGED), withfindPackagedRuleCopyGaps. A refusing verdict becomes one error issue that is:validations[1].condition, orvalidations[1].type(the predicate liststypefirst);It folds into the gate's one
422 INVALID_METADATAenvelope withissues[], andrulesRunnames the rule whenever the host answered (clean or not). Nothing is written on a refusal.What still saves: a copy that changes only
message(a reword) or onlyactive(active: falseswitches the rule off), a rule under a new name, and a draft save (D1: drafts are not gated; publishing the draft is).Changeset
.changeset/28448-save-door-validation-rule-copy.md:@objectstack/metadata-protocolminor(ruling A:Metadata-protocol: narrowedmoves at least one package tominor), with the BREAKING paragraph and the ADR-0087not-required (no-migration-prescription)disposition. No key of any schema changes, and stored rows keep loading.Tests
packages/metadata-protocol/src/runtime-authoring-gate.validation-rule-copy.test.ts(4 cases, pure gate): a refusing verdict is one located issue with the hint and the rule inrulesRun; a clean verdict refuses nothing; no verdict judges nothing; a draft is not gated.packages/objectql/src/protocol-validation-rule-copy-save-refusal.test.ts(5 cases, a realSchemaRegistrybehind the realObjectStackProtocolImplementationsave door): the tenant's own object, extended by a code package with a rule throughregisterObject(..., 'extend'), then PUT back with an edited copy of the served body.validations[i].conditionwith the prescription, the stored body unchanged.scripttocross_field, which still parses) is refused,typenamed.active: falsesaves.meta-object-*-roundtripsiblings, because objectql depends on metadata-protocol and never the reverse.scripts/engine-double-contract.pinned.jsonrecords the new test's engine double (--write, 3 rows added, 0 lost).pnpm --filter @objectstack/metadata-protocol exec vitest run: Test Files 287 passed / 4 skipped, Tests 21879 passed / 26 skipped.@objectstack/metadata-protocoland@objectstack/objectqltypecheck: exit 0.4 ablations, one per refusal leg (each through scripts/ablation-replace.mjs, restored, final tree clean)
Run:
pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/protocol-validation-rule-copy-save-refusal.test.ts(5 cases), underos-verify-lock. The objectql test reads metadata-protocol from its dist, so B1 and B2 rebuilt it and proved the marker withscripts/ablation-dist-preflight.mjs(present after mutate, absent after restore and rebuild).packagedRuleCopyRefusalOfnever asks the registry (protocol-authoring-gate.ts, dist markerablation-b1)findPackagedRuleCopyGapsdrops the finding (runtime-authoring-gate.ts, dist markerablation-b2)typenot package-owned in the registry predicate (registry.ts, source)conditionnot package-owned in the registry predicate (registry.ts, source)The three saving pins stay green under every mutation, so they pin the allowed side rather than the wiring.
Acceptance notes
protocol.tsis untouched (serial constraint, feat(objectql,metadata-protocol,rest,spec): the object read names the objects a delete can cascade into #28451). The dispatch named the save-door refusal set; reaching it needs aprotocol.tscall, so the runtime authoring gate, already called from every door, carries the rule instead.package-authorchannel returns before the gate, as it does for every gate rule, so a package author's own save is not judged here.OS_ALLOW_UNLINTED_METADATA_WRITES=1, the gate's migration hatch, turns this refusal into its loud log like every other gate rule. A copy saved under it still runs the package's rule at load and is reported there (fix(objectql)!: the registry fold merges validation rules by name; a tenant's same-named copy carries only its message and active, and a changed condition is reported at load, never run #28445).getMetaItemreturns the registry's own rule objects; the save-door test clones the served body before editing it (a wire client always holds a copy).Verification
Derived gates and dogfood
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon5b370a2809derived 69 commands over 6 paths (474 changed lines). Each ran with stdin from/dev/null, its exit captured by redirect before any pipe. All 69 exit 0.dispatch-gates --ran:69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED, exit 0.On the previous head,
check:engine-double-contractandcheck:objectql-double-limitwere red on the new test's engine double (an unrecorded ledger row; afindthat ignored the caller'slimit). The fix is the last commit: the double applies the bound after the filter, and the ledger records it.Scoped lint, using the root
lintscript's command overpackages/metadata-protocoland the new objectql test: exit 0.Full dogfood (owed by the
(narrowing)arm), on5b370a2809after a dts build of@objectstack/metadata-protocol, underos-verify-lock:NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter @objectstack/dogfood test: Test Files 362 passed / 3 skipped (365), Tests 2922 passed / 28 skipped (2950), exit 0.No Tier H path (
check-governed-merges --testexits 0 on every changed path). It stays a draft until the contract review is on record; the seat lands it.Generated by Claude Code