Repository navigation
fix(metadata-protocol)!: a save, publish or rollback naming a read-only base itself onto its view, dashboard, report, translation or email template is refused, naming the .custom pair - #29270
Conversation
…sed onto its pair An authoring save (draft or active), a publish (per-item and the package batch) or a rollback that names the read-only code package itself (`?package=<base>`) onto an item of one of the five overlay types it ships landed a row bound to the base with no declaration: served at the base's address, outside the pair ADR-0146 D1 puts every customization in, and not seen by `os migrate packageless-overlays`. Each is now refused `403 NOT_OVERRIDABLE` with the package-less close's fix (the pair, its `?package=` address, the declaration), one spelling of it, under the 500-character bound. Closed only where that fix can be taken; a delete naming the base stays open (the one way to clear a row stored before). Claude-Session: https://claude.ai/code/session_01ADHkmTBQhj1HBQsdAopiJ8 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmTBQhj1HBQsdAopiJ8 Co-authored-by: Claude <noreply@anthropic.com>
… the pinned ledger Claude-Session: https://claude.ai/code/session_01ADHkmTBQhj1HBQsdAopiJ8 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
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 a4286ca261e3280262c72dc39581a53e1f69fc09 && git checkout a4286ca261e3280262c72dc39581a53e1f69fc09
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin db6b4084699f81d63fdb2373720f6973622c9dfe 29a58bb6fd89d046f877c8dbe7c07a4b082db677 && git checkout -B drift-repro db6b4084699f81d63fdb2373720f6973622c9dfe && git merge --no-ff 29a58bb6fd89d046f877c8dbe7c07a4b082db677
node scripts/docs-audit/affected-docs.mjs --json db6b4084699f81d63fdb2373720f6973622c9dfe
|
…binding The package-batch member cases drafted into the read-only package itself, which the save door now refuses; they draft into its pair, declared, and assert the same member semantics. Claude-Session: https://claude.ai/code/session_01ADHkmTBQhj1HBQsdAopiJ8 Co-authored-by: Claude <noreply@anthropic.com>
The control drafted into the read-only showcase itself, which the save door now refuses naming the pair. It asserts that refusal, then writes the same customization into the pair, declared, and pins its commit's revert through the pair's own path and through no other. Claude-Session: https://claude.ai/code/session_01ADHkmTBQhj1HBQsdAopiJ8 Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 Contract-review-claim: session_01ADHkmQKwKFCp1m6Pb6Q8c1 · head 29a58bb · 2026-11-02T12:47Z The narrowing face ( |
|
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 Contract reviewServed-tier: Face: narrowing. The PR body and the changeset both carry ① Derived judgments
② Semver level
③ Boundary flagsAuthor: the PR Dev deviations:
Open questions:
Out-of-scope findings:
④ Design judgmentAgainst the published spec on The doors. Read in the live tree at this head:
Message and status. Findings.
Implemented-by: VERDICT: PASS |
|
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 维护者速读
席位意见:与 diff 相符。保存、发布、回滚三道门在写库之前就拒绝,删除与草稿丢弃有意保持开放,作为旧行唯一的出口。达档契约复审 PASS(6577747588),无 must-fix。复审提的 should-fix 由本席在本 PR 合并后立卡跟进(见核对第 5 项)。 席位核对ACCEPT ·
|
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Requested by Steve · project thread
Fixes #28905
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: none needed —
skills/objectstack-platform/SKILL.md's ADR-0146 paragraph already places every customization of an installed base in its paired.custom, declaringoverrides, and names?package=BASEonly as a read that serves it; nothing it says becomes false.维护者速读
?package=只读基础包到它自带的视图、仪表盘、报表、翻译或邮件模板,现在一律 403NOT_OVERRIDABLE,提示改写进它的.custom配对包并声明overrides——与 metadata-protocol: close the package-less write onto a packaged item (PUT/DELETE, rollback, source save, reset) naming <base>.custom; Studio drops its overlay link #28576 对“不写包名”的拒绝同一句修复。删除保持开放。os migrate packageless-overlays也看不见它。os migrate organization-ownership --apply把公开表单撤回带到代码包视图时,现会停下报阻塞并给出配对包写法。packages/metadata-protocol/src/protocol-packaged-base-refusal.ts(namedBaseCloseSubject的放行条件与句子)、protocol.ts中promoteDraftForPublish的新判定(批量发布整批中止)、protocol.named-base-overlay-write.test.ts。cd packages/metadata-protocol && npx vitest run src/protocol.named-base-overlay-write.test.ts,应见 17 个用例全部通过。What changes
@objectstack/metadata-protocol— an authoring write that names the read-only code package itself (?package=BASE) onto an item of one of the five overlay types that package ships is now refused403 NOT_OVERRIDABLE, writes nothing, and names the fix the package-less close (#28576) names, in the same words: write it intoBASE.custom(?package=BASE.custom), declaringoverrides: { package: 'BASE', basedOn }.publishPackageDrafts(both throughpromoteDraftForPublish, judged on the package the active row is written under; the batch aborts all-or-nothing andfailed[]names the item), and the rollback of a row bound to the base.namedBaseCloseSubject, which reads "packaged" exactly as the package-less close does (overlayBaseShippers, read-only shippers of the five types), with the named package among the shippers, and then checks the same steps: a pair id derives, the ensure door would not refuse the pair, the base records a version, and the override write gate admits the declared body into the pair. Anything else keeps the write it has onmain.422 WRITABLE_PACKAGE_REQUIRED); a body carryingoverrides(the D2 gate'sdeclared_outside_pairsentence, unchanged); a base whose pair cannot be ensured or that has no version; anemail_templatelocale the base does not ship; and the server-stated rewrites (sourceset,package-duplicate).closedOverlayWriteClausesnow builds the item / base / pair / declaration / fix clauses for both closes.packagelessOverlayWriteRefusal's output is byte-identical (its pins inprotocol.packageless-save-address.test.ts,protocol.packaged-base-refusal.test.tsand objectql's five-types file are unchanged and green). Only the rule clause after the fix differs.The sentence, save of
view/task.crm_listnamingcom.acme.crm(358 characters, against 382 for the package-less save of the same item; a 30-character base id with a 40-character name stays under 500 on every verb, pinned):Measured before the change (origin/main 14208f2, stub engine, five types)
?package=BASE?package=BASEin place of BASE's documentpublishPackageDrafts(BASE)outcome: published)outcome: refused,failed[0].codeNOT_OVERRIDABLElistPackagelessOverlays)A pair row declaring the override beside a row bound to BASE is served over it (read naming no package,
?package=BASE, layeredeffective), so the refusal's fix works for rows stored before.Tests
packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts(17 cases): the save naming the base, active and draft, for each of the five types; the per-item publish;publishPackageDrafts; the rollback; the 500-character bound at a long base id and name; and the controls: delete and draft discard naming the base stay open; the pair's declared save is accepted and served over a row bound to the base; amigrate-storedrewrite still writes; a writable package's same name; a name the base does not ship keepsWRITABLE_PACKAGE_REQUIRED; a base whose pair cannot be ensured keeps the write; a body carryingoverrideskeeps the D2 sentence.packages/objectql/src/environment-overlay-five-types.test.ts: one real-engine case per type and topology (10): the save naming the read-only package, active and draft, is refused naming its pair and stores nothing.packages/metadata-protocol/src/protocol.packageless-save-address.test.ts› "an item a CODE package ships…" › "with an overlay row bound to the code package: the package-less save is refused, and that row is untouched". It seeded that row withsaveMetaItem({ packageId: CODE_PACKAGE }). It now asserts that save is refused (403, names the pair, nothing written), then seeds the row directly (seedRow) as a build before this change left it. The rest of the case is unchanged.packages/objectql/src/environment-overlay-email-locale-restart.test.ts› "the package-batch doors and the cluster fan-out carry the member ([finding] email_template: a Studio save of the same template in a second locale replaces the first locale's overlay at the next boot (sys_metadata keeps one overlay row per type and name) #22877, stage 2)": four cases draftedemail_templatemembers into the read-onlycom.acme.mailitself and then askedpublishPackageDrafts/revertCommit/discardPackageDrafts/listDrafts/listCommitsof it. They now draft into its pair, declaring the override, and ask the same doors of the pair. Every member assertion is unchanged (35/35).packages/qa/dogfood/test/package-commit-revert-path-package.dogfood.test.ts(landed with fix(runtime)!: package commit revert and rollback act only on a commit of the package their path names #29090 today) › its last control drafted a showcase view intocom.example.showcaseitself to give the read-only package a commit. It now asserts that draft is refused naming the pair. It then writes the same customization intocom.example.showcase.custom, declared, and pins the commit's revert through the pair's own path, and through no other (404 through B, 200 andremovedthrough the pair). Its header says why: a commit recorded before keeps its revert, since the commit doors still take no read-only guard, but a fresh boot can no longer make one.Ablations (each red, restored, `git diff HEAD` empty)
if (namedBase) throw namedBase;after the save's close): 6 red — the five per-type save cases and the forced pin (Error: expected the call to be refused, but it resolved; the report case got422from a later gate instead).expected undefined to be an instance of Error) andpublishPackageDrafts(expected { success: true, …(7) } to match object { success: false, …(3) }).overridesexclusion removed: 1 red — the D2 control (expected "'view/task.crm_list' ships in 'com…" to contain "only 'com.acme.crm.custom' may decl…").ablation-dist-preflightsaw the marker in 2 built files: 10 red / 26 green of 36 (publish: expected undefined to match object { Object (code, status) }). Restored, dist rebuilt, preflight--absentsaw the marker in none of 24 built files, tree clean.{"success":true,…,"message":"Saved customization overlay (env-wide, package=com.example.showcase, state=draft) …"}: expected 200 to be 403). Restored, rebuilt, marker absent, tree clean.Acceptance notes
os migrate packageless-overlayslists package-less rows only and never sees one.sys_metadatawhose type is one of the five and whosepackage_idis a read-only code package that ships the name. I could not take a census of a real deployment from here. Not widened into the ceremony (migrate: os migrate packageless-overlays fails on an email template row of a locale the base does not ship, so after #28576 that row can be neither written nor migrated #28904 is in that file).os migrate organization-ownership --apply(read from code, not run):carryFormWithdrawalssaves the withdrawal onto a code-shipped view naming its served package, which is the base. That save is now refused, so the ceremony reports a fail-closed blocker quoting this refusal. The operator savessharing.enabled: falseinto the pair, declaring the override, then re-runs. Its cli tests use a stub door, so they stay green. The changeset says so too.namedBaseCloseSubjectrepeats the pair, ensure, version and gate steps ofpackagelessCloseSubject. That function lives inprotocol-packageless-overlay-migration.ts, which this card stays out of while migrate: os migrate packageless-overlays fails on an email template row of a locale the base does not ship, so after #28576 that row can be neither written nor migrated #28904 is open. Folding the two predicates into one is a follow-up for after migrate: os migrate packageless-overlays fails on an email template row of a locale the base does not ship, so after #28576 that row can be neither written nor migrated #28904 lands.email_templateat a locale the base does not ship stays open here, as it does for the package-less close (the gate refuses the declared body in the pair; migrate: os migrate packageless-overlays fails on an email template row of a locale the base does not ship, so after #28576 that row can be neither written nor migrated #28904 covers that move).not-required (already-registered packageless-overlay-write-closed). That entry has not been released and ships in the same release. Its replacement — write the customization intoBASE.customwith the declaration — is this change's TO. Its surface text names the package-less write only, and widening it is apackages/specedit outside this lane.package-lesssentence (metadata-protocol: close the package-less write onto a packaged item (PUT/DELETE, rollback, source save, reset) naming <base>.custom; Studio drops its overlay link #28576, unchanged here) passes 500 characters at a 30-character base id with a 40-character name (510 on save). Noted, not changed.Verification
All on head
29a58bb6(origin/main45e9b2f1merged in), underos-verify-lock:dispatch-gates --commands: 74 derived, 74 run, every exit 0;--ranreconciles (0 NOT-MEASURED, a derived zero). The first round reddenedcheck:engine-double-contracton the new suite's fake engine; its pinned ledger now records the three doubles (scripts/engine-double-contract.pinned.json).packages/metadata-protocoland the touched test files: exit 0.pnpm --filter @objectstack/metadata-protocol typecheck0 ·test301 files / 22117 tests passed.pnpm --filter @objectstack/objectql typecheck0 ·test527 / 10150 passed.pnpm --filter @objectstack/runtime test515 / 8329 passed ·pnpm --filter @objectstack/rest test367 / 6963 passed.(narrowing)arm), full: 389 files passed, 3 skipped; 3215 tests passed, 28 skipped. Lock VERDICT command-exit 0.check-changeset-no-major --eventon this body: level axis green (yes (narrowing)+narrowed,minor).check-adr-0087-registration: the disposition is accepted.Human Minimum
packages/specholds no runtime logic. (No spec change.)pnpm lintpasses (eslint overpackages/metadata-protocoland the touched objectql test: exit 0).main, one pull request.packages/spectouched: n/a, not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.