Repository navigation
feat(metadata-protocol,cli,cloud-connection)!: the override write gate — only the base's paired .custom declares an override (ADR-0146 D2) - #28554
Conversation
…t publish Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
…nly the base's paired .custom declares an override Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
…red same name refused last; export the judge The undeclared-same-name refusal throws just before the store at the save and promote doors, after every refusal those doors already answered, so no existing code or status changes. Only a read-only (installed) package is a base the gate guards. The sealed sentence's base id is read through packagedArtifactOwner. judgeOverrideWrite is exported for os validate and the marketplace install door. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
…d the marketplace managed install os validate walks each package of the artifact under the publish door's judge (judgeOverrideWrite) against its closure inside the artifact and exits 1 on a refusal. The managed install judges every delivered package's items before any write (403 NOT_OVERRIDABLE, one entry per item) and lists, on a base install, the names installed dependents already hold undeclared (overrideCollisions). The item walk (overrideWriteItemsOf) lives beside the judge in metadata-protocol so both doors name items one way. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
…or refuses; record the build gap The flow and action not-yet-available refusals are one rule (declared_type_not_yet_available): they differ only by type. The parity ledger records findOverrideDeclarationRefusals as validate-only: the card named publish, os validate and install as the gate points. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
… refusal once, last before the store The early declared-refusal throw at the promotion was redundant with the late one (an ablation of it read green): nothing between them answers a declared draft first. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
… the dispatch asserts and holds the find bound Pinned in the engine-double ledger (check:engine-double-contract --write). Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 181 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 eb727a472f4f176429b60d26fde2fea20bfce2f9 && git checkout eb727a472f4f176429b60d26fde2fea20bfce2f9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d77dc07a5e5a4b5478763fb3a2126913c92f8c01 4a25192b91f97cc0c96a3d713abcadc3763daf3b && git checkout -B drift-repro d77dc07a5e5a4b5478763fb3a2126913c92f8c01 && git merge --no-ff 4a25192b91f97cc0c96a3d713abcadc3763daf3b
node scripts/docs-audit/affected-docs.mjs --json d77dc07a5e5a4b5478763fb3a2126913c92f8c01
|
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC Contract-review-claim: session_01R1q2xdtYgU6FKrZ7LgjPCC · head b902331 · 2026-11-01T05:56Z Face: |
… of NOT_OVERRIDABLE The error-code provenance guard reds on a registered code stamped by a package with no ledger row; the override write gate stamps NOT_OVERRIDABLE from @objectstack/cli and @objectstack/cloud-connection. Provenance only. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC Contract reviewServed-tier: ① Derived judgmentsAccept-set changes the diff implies, each judged against the spec on
② Semver levelChangeset ③ Boundary flags
④ Design judgmentThe narrowing makes the spec's own refusal real; it takes no declared capability away at the consumer. On Tried to break it:
Findings:
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…re sealed advice; malformed overrides fall to the schema gate - The #28409 / #28238 read tests in objectql and rest seeded dependent and undeclared same-named rows through the save door, which ruling A on #28390 now refuses. Each now asserts that refusal, then seeds the row below the gate; the read assertions are unchanged. The package-less save onto a dependent's same-named row is refused on purpose and pinned so. - The sealed write's advice reads the writer: the base's paired .custom, or a write naming no package, is told to declare or rename; any other package is told to rename, since only the pair may declare. - An `overrides` value OverrideDeclarationSchema refuses is not judged as a declaration: the save door's schema gate answers it (422 INVALID_METADATA) in place of 403 declared_outside_pair. os validate's walker reads a declared base the same way. - The rest sealed-sentence pin and the changeset are updated. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
…ge its address gives The save door judged the override write gate before a save naming no package resolved its address, so a package-less save landing in a dependent's same-named row was "not concerned" and wrote the very row the same save naming the dependent is refused. The last refusal before the store now re-asks the gate for the inherited package (the publish address, or the draft address's package). Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC Seat note on contract-review-claim Generated by Claude Code |
Brings in #28595 (#28411, GET /packages/:id/customizations), whose test seeds two rows the override write gate refuses; fixed in the next commit. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
…s down below the gate #28595's GET /packages/:id/customizations test seeded a non-pair declaration and an undeclared pair row through the save door, which the override write gate refuses (ruling A on #28390). Each now asserts that refusal and its sentence, then lays the row down with the gate answering not_concerned for that save alone; the read assertions are unchanged. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-hold: On the seat's question ( |
…r's 422 INVALID_METADATA The schema gate a malformed `overrides` falls through to answers 422 INVALID_METADATA at the protocol door, as the pin asserts; the docblock said 400 VALIDATION_ERROR. Comment only. Claude-Session: https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: The hold |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: The hold |
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC Seat note: CI is red on Generated by Claude Code |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: ① Derived judgmentsThe FAIL record
② Semver levelChangeset ③ Boundary flags
④ Design judgmentThe narrowing delivers the spec's declared capability and takes nothing sanctioned away. Before this head, N-08's schemas accepted Tried to break it on this head:
Findings: no must-fix, no should-fix on this head (the one should-fix of the lane's read on
Implemented-by: VERDICT: PASS Posted by triage#1 ( Generated by Claude Code |
|
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #28410. 20 files, +1956 / −34; labels documentation, size/xl, tests, tooling. The only packages/spec/** edit is the error-code provenance ledger append (NOT_OVERRIDABLE under @objectstack/cli and @objectstack/cloud-connection), which the review grades as the spec lane's pre-approved append class. The lane red line was raised by the seat's FAIL record 6554280633 and answered in 6554284418 (card cut under domain:server as epic child N-09, implementing #28390 ruling A on N-08's schema already on main); the review carries that answer at CONTRACT_REVIEW_TIER as the compensating control. |
| Diff check (seat) | Since the seat's FAIL record on b9023312, the heads moved by: the must-fix ledger append (9ead6858); the writer-aware sealed sentence and the safeParse reading of overrides (96f6f99e); the package-less re-judgement at the resolved address (15969ace); two merges of origin/main, changing one context line of the generated engine-double pin and bringing #28595's packages-customizations.test.ts, re-seeded below the gate (cd852e2a, each case asserting the door's 403 NOT_OVERRIDABLE first); and the declarationOf docblock alone (4a25192b). Each pin re-seeded below the gate asserts the refusal first and keeps every read assertion unchanged. The PR body's Verification section carries the cd852e2a re-run. |
| Evidence | CI on 4a25192b: 58 success, 13 skipped, 0 failed, mergeable_state clean, and the required Test Core, TypeScript Type Check, Lint & Repo Gates and Build Core contexts read success in the latest reading. Governed check: NOT governed, 0 of 20 paths on the register, 1990 changed lines (under the 3000 human-merge threshold). The cd852e2a re-run gives @objectstack/runtime 505/505 files (9106 tests), packages-customizations.test.ts 13/13, and 98 of 98 derived gate families run, 0 NOT-MEASURED; red first is CI Test Core (3/8) job 124795448012 on 21bc1b51. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip. The review's two notes (the os build wiring of findOverrideDeclarationRefusals, and the rollbackMetaItem intent / legacy overrides without basedOn reading) are filed by the seat as #28643 and #28644, not fixed here. |
Generated by Claude Code
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Requested by Leehom · project thread
Fixes #28410
Clause-②: yes (narrowing)
New refusals at the save and publish doors, at
os validateand at the marketplace managed install narrow the accepted set; the base's paired.customdeclaring an override is admitted beside them. The onepackages/spec/**file touched is the error-code provenance ledger: two rows namingos validateand the marketplace install as emitters of the already-registeredNOT_OVERRIDABLE; no schema, type or runtime interface moves.Metadata-protocol: narrowed
Skills-docs: none — no file under
skills/**teaches theoverridesdeclaration, a<base>.custompackage or the sealed-write sentence; thecontent/docs/references/**pages that showoverridesare generated from the spec and do not move.维护者速读
<base>.customdeclares an override; a dependent package adds under its own names (ruled A, withdraws rider 3) #28390 裁决 A)落在三个门点:保存/发布门、os validate、市场托管安装。只有基础包配对的<base>.custom能声明overrides;依赖包里与闭包内只读包同名的条目被拒(改名),五类拒绝各有一句(未声明同名、非配对包声明、基础包不在闭包或不含该条目、排除类型、object),flow/action 声明答「尚不可用」。403 NOT_OVERRIDABLE),未声明同名的拒绝排在各门已有拒绝之后。回滚即 revert,无数据迁移。packages/metadata-protocol/src/protocol-packaged-base-refusal.ts(judgeOverrideWrite、overrideWriteItemsOf),protocol.ts的保存门与promoteDraftForPublish两处接线,cloud-connection安装门的步骤 2d。pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.packaged-base-refusal.test.ts,预期Tests 63 passed (63)。installPackage移到市场托管安装(席位已批);另有一个开放问题:os build要不要也拒(见报告)。What changes
@objectstack/metadata-protocol,protocol-packaged-base-refusal.ts).judgeOverrideWrite(subject, closure)is a pure verdict:admit, one namedrefuse, ornot_concerned. It reads N-08's surface from@objectstack/spec/kernel(OVERRIDE_TYPE_CLASSIFICATION,isCustomizationPairOf,customizationPackageId,customizationBasePackageId) and copies none of it.overrideWriteItemsOf(body)is the item walk the two outside doors share (an action keyed<objectName>:<name>, aviews:container expanded by the loader's ownexpandViewContainer). Both are exported from the package index in one export line.403 NOT_OVERRIDABLE, each with its own sentence:undeclared_same_name: a dependent's item under the name a read-only package in its closure ships, on an overridable type. In<base>.custom: "Declare it … or rename the item". Elsewhere: "Rename the item".declared_outside_pair:overrideson a package that is not the base's pair.declared_package_outside_closure("Declare a dependency on '' …") anddeclared_package_ships_no_such_item.declared_type_not_overridable: an excluded type, pointing to a new item under a new name.declared_object: pointing toobjectExtensions.declared_type_not_yet_available: a declaredflow("flow customization is not yet available") oraction.runtime-onlywrite (the pair's own row), at the save and at the promotion.protocol.ts). The save door judges every save; a refused declaration throws before the flow-only regime check (so a declared flow reads its own sentence, not the clone sentence), and an undeclared same name throws last, just before the store, so every refusal the door already answered keeps its code and status (a view container's name collision keeps its 400, a lock itsITEM_LOCKED). The promotion (publishMetaItem,publishPackageDrafts) judges the draft body and throws once, last before the store; a batch publish reports it infailed.isWritablePackageanswers false for is a base the gate guards: an override replaces an installed item, and a writable package's item is edited in place (ADR-0070). Every artifact a kernel serves is registered throughregisterApp, which puts it inmanifests, so this keeps no real base out.packagedArtifactOwner, so no newlookupArtifactItemcall site.overridesis not a declaration. A valueOverrideDeclarationSchemarefuses is not judged as a declaration: the save door's schema gate answers it (422 INVALID_METADATA), andos validate's schema step reports it.os validate(@objectstack/cli,utils/override-declarations.ts, step 3a-iii). Each package of the artifact (or the stack as one package) is judged against its closure, followed through the artifact's packages. A same name is found where both packages are in the artifact; a declaration is judged whole wherever its base lives. Exit 1, the rows onerrorswithpath,code,rule,packageId,message.@objectstack/cloud-connection, step 2d, before any write). Every delivered package (apackages[]artifact judged per entry) is judged against its closure (registry dependencies plus the artifact's own) with shippers read from the protocol's per-package listing. A refusal answers403 NOT_OVERRIDABLEwithdetails.refusals; nothing registers and no ledger entry is written. On an install of a base,overrideCollisionson the200answer lists each name an installed dependent already holds undeclared (the judge's sentence per row); the key is omitted when empty.Tests
protocol.packaged-base-refusal.test.ts: the gate describe block, 63 tests in the file, including the package-less save re-judged for its address's package (the one write behaviour refused on purpose). Red first on main (b24adb3a): 20 failed, 35 passed.protocol.sealed-remedy-kind.test.ts: the one-sentence pin reads the declare-or-rename tail from the classification, never transcribed.packages/cli/src/utils/override-declarations.test.ts(7, unit tier) andpackages/cli/test/validate-override-declarations.test.ts(2, the command, exit code).packages/cloud-connection/src/marketplace-install-local-override-gate.test.ts(6): refusals, nothing written, the pair's admit, the control, the collision list.protocol.org-scoped-write-refused.test.tsis green and unchanged (git diff 23d1303d -- <file>empty)..customdeclared override, and a withdrawn public form stays withdrawn #28409 and metadata-protocol: a same-named item in a dependent package replaces the base package's item for package-less reads and in the base app #28238 read pins in the rest and objectql suites seed their same-named rows before the gate (pre-gate seeding), so they keep pinning the read side the gate now keeps from being written.scripts/ablation-replace.mjswith its restore proved clean (git diff HEADempty); each reads red: (a) 7 failed, (b) 4, (c) closure 1, (c) ships 1, (d) 1, (e) 1, not-yet 2, sealed tail 5, read-only base filter 127 (the org-scoped file), save door declared throw 4, save door undeclared throw 5, promotion throw 3, admitted runtime-only intent 1, install refusal 3, install collisions 1, validate wiring 1. Round 2 adds A18 (writer-aware tail, 1 failed), A19 (malformedoverridesis not a declaration, 5 failed) and A20 (package-less re-judge, 1 failed). Two first attempts were no-ops (the anchor's replacement count did not rise) and were redone; one (an early declared throw at the promotion) read green and was removed as redundant.Acceptance notes
installPackagegate point moved to the marketplace managed install (seat 1 amendment, posted on metadata-protocol: the override write gate — only the base's paired.custommay declare; undeclared same names and a dependent'soverridesare refused at publish,os validateand install #28410).installPackagetakes a manifest and no items (PackageInstallManifestSchema), and the unmanaged install copies through the save door, which is gated; the managed install is where a package's items arrive unjudged.os builddoes not run the gate. The parity ledger recordsfindOverrideDeclarationRefusalsinVALIDATE_ONLY_GATESwith the reason; whether the build refuses too is an open question on the card.protocol.packaged-base-refusal.test.tsas the order put it: metadata-protocol cannot import cli or cloud-connection (they depend on it).rollbackMetaItem) of a declared override row still computes theoverride-artifactintent fromisArtifactBackedalone. Rolling back the pair's row is N-10/N-16 territory and not a door this card names.getMetaItems({ type, packageId })), which serves code-registered and stored items alike, so a.custombuilt in Studio is covered.Verification
Run on
21bc1b51(a merge oforigin/main) underos-verify-lock(VERDICT command-exit 0).scripts/engine-double-contract.pinned.json. Main's pin was taken and regenerated withcheck-engine-double-contract.mjs --write; it differs from main only by this branch's three rows (the gate test'sdelete,findOneandupdate).check:engine-double-contractexit 0.pnpm install --frozen-lockfileexit 0; closure rebuild exit 0. Typecheck metadata-protocol, cli and cloud-connection: exit 0.test/validate-override-declarationsandtest/validate-build-gate-parity: Test Files 290 passed | 1 skipped, Tests 3730 passed | 8 skipped.dispatch-gates.mjs --commands, reconciled with--ran):✓ dispatch-gates --ran: 98 derived famil(ies) accounted for — 98 run, 0 NOT-MEASURED, includingcheck:error-code-provenanceand the i18n and dual-build families with their prerequisites built first.Re-run on
cd852e2a(a merge oforigin/mainbringing in #28595, then the re-seed ofpackages-customizations.test.ts) underos-verify-lock(VERDICT command-exit 0).21bc1b51(the twopackages-customizations.test.tscases seeding throughsaveMetaItem, which the gate now refuses).NOT_OVERRIDABLEwith its own sentence ("carriesoverrides" for the non-pair declaration, "declares no override" for the undeclared pair row), then lays the row down below the gate through the same draft save and publish with the gate shadowed tonot_concernedfor that save alone. The read assertions are unchanged.packages-customizations.test.ts: 13/13.@objectstack/runtimefull suite: Test Files 505 passed (505), Tests 9106 passed | 19 skipped.✓ dispatch-gates --ran: 98 derived famil(ies) accounted for — 98 run, 0 NOT-MEASURED.4a25192bchanges only thedeclarationOfdocblock (it now names the door's 422INVALID_METADATA); comment only in a non-exported function, eslint exit 0.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oYcVD24TCK4yt5e1i