Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): ADR-0070 D1 delegates its refusal to the repository's create-side emitter, so the hatch clause reaches saveMetaItem (#8361) - #8630
Conversation
…sitory's create-side emitter (#8361) `saveMetaItem`'s D1 gate spelled its own `WRITABLE_PACKAGE_REQUIRED` sentence, which shadowed #8146's hatch-aware create-side emitter on every kernel: D1 refuses on a strictly wider predicate, so every write that would have reached `SysMetadataRepository.readOnlyBaseCreateError` had already been thrown by D1 — and D1's sentence carried no hatch clause. An operator with `OS_METADATA_WRITABLE` set was never told the hatch does not reach package writability. D1 now calls that emitter (the create-side mirror of #8184's override-side delegation), computing `hatchOpen` from this class's own memoised reader — the one the admission limb used. Both named reasons the one-emitter default might not apply were measured and did not hold: the emitter is static (no repository instance needed) and both sites already carried the identical ADR-0070 `docs` pointer. The emitter gained an optional trailing `name` so the delegated sentence keeps naming the item; omitted, its output is byte-identical for the direct `put` callers (promoteDraft / restoreVersion / revertCommit), which were the only callers reaching the hatch clause at all. D1's predicate is untouched — no create is newly refused or newly admitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8361
The defect, reproduced on the merged ref
OS_METADATA_WRITABLEunlocks a metadata type, never a package's writability. #8146 wrote that sentence into both package-door emitters inSysMetadataRepository. The override one is reachable, and #8184 made it reachable on scoped kernels too. The create one —readOnlyBaseCreateError, 422WRITABLE_PACKAGE_REQUIRED— was reachable from nowhere an author writes from:saveMetaItem's ADR-0070 D1 gate refuses on a strictly wider predicate (no "did the caller name a base" limb, no registry limbs above it), so it threw first on every kernel, with its own sentence, which carried no hatch clause.Probed on
ffb090e6f7withOS_METADATA_WRITABLE=permissionset and a runtime-only create aimed at a read-only package:The operator is told the base is read-only (true) and never told that the variable they set a moment ago cannot make it writable.
The architecture fork — what I measured, not what is tidier
The card declined to pick between teach D1 the same
hatchOpenselection and let D1 delegate to the repository emitter, naming the lane's one-emitter precedent as the default and D1's fires-before-the-repository constraint as the reason it might not apply. I took the delegation reading. Both reasons the default might not apply were measured here, and neither holds:ffb090e6f7getOverlayRepois below D1) and irrelevant to the emitter: it isstatic,SysMetadataRepositoryis imported atprotocol.ts:23, and the override-side site inside this very method has been calling a static sibling since #8184docspointer"docs/adr/0070-package-first-authoring.md, because both implement D1. (This is precisely where the create side differs from the override side, whose repository emitter points at ADR-0010 — there the fork was real)The one difference that was real: D1's sentence names the item (
type/name) and the emitter's named only the type. I closed that by widening the emitter with an optional trailingnamerather than forking the sentence — the same class of widening #8184 made when it addedhatchOpenand droppedprivate. Omitted, the emitter's output is byte-identical to what #8146 shipped, which is what the directputcallers still see (pinned).So the fork resolved by measurement, not by taste — and the reading that survived is the lane's own default. Had the
docspointers diverged, or had the emitter needed an instance, this would have gone back asneeds_decision.Secondary deliverable — where the create-side clause actually was live
Verified by enumeration on the merged ref rather than restated.
assertAllowedreachesreadOnlyBaseCreateErroronly fromput, withintent: 'runtime-only'and a non-emptypackageId(assertDeleteAllowedpasses no base, so the delete side can never reach it). Productionputcallers carrying both facts:saveMetaItem— shadowed: D1's predicate is strictly wider, so every write that would reach the door was already thrownSysMetadataRepository.promoteDraft— frompublishMetaDraftSysMetadataRepository.restoreVersion— fromrevertCommitSysMetadataRepository.restoreVersion— fromrestoreMetaVersionThe last three pass the row's own binding, never a caller-named base, and none passes through D1 — so the clause was exercised only by republish/repair traffic, exactly as the card said.
LayeredRepositoryis not a fourth route: nothing in the repo composes aSysMetadataRepositoryinto one outside a TSDoc example. Recorded in the emitter's TSDoc, in the new test block's docblock, and pinned bythe direct put callers keep their sentence verbatim.What changed
D1 now calls the repository's emitter instead of spelling a second sentence for one condition.
hatchOpenis computed at D1 — unlike the override site, which passes a literalfalsebecause reaching it proves the hatch is shut. Both directions are live at D1:permission(allowRuntimeCreate: true) arrives with the hatch shut, andjob(neither channel) arrives only because the hatch openedisOverlayAllowed. It is read through this class's own memoised reader — the one the admission limb used — sohatchOpencannot disagree with the limb that let the write through.Scope — no acceptance decision moves
D1's predicate is byte-for-byte unchanged; only the error object it throws changed. Same
code,status,packageId,docs. Pinned in both directions: a create into a writable base and a package-less create still land; the read-only create is still refused. This card changes what the operator is told, not what is refused.Verification
Ablation, both directions, run against the committed fix (restored byte-identically afterwards —
git hash-objectmatches the committed blob):jobcase and the two-kernel case go red. The hatch-SHUT case stays green, which is exactly why (a) alone cannot separate this deliverable from a vacuous pin.hatchOpenpassed as a literaltrue) — 2 red / 37 green:with the hatch CLOSED the same request does NOT mention itandthe sentence is the repository's, not a copy of it. Every hatch-OPEN case stays green, so the OPEN cases alone would not have caught "reports the repository message everywhere".The anti-fork pin compares
saveMetaItem's message against the emitter's own output rather than a literal, so a re-spelling at D1 goes red even if it is word-perfect on the day it lands. No pin asserts a list length or delta; the row assertions are membership (toContainEqual) and emptiness.Suites (dependency closure built first;
metadata-protocolrebuilt before the downstream runs, per the dist-resolution trap):@objectstack/metadata-protocol— 87 files, 1282 tests, all pass (39 in the touched file, 10 of them new)@objectstack/objectqlprotocol-save-meta-repo-path.test.ts— 13 pass@objectstack/runtimepackages-readonly-gate+package-duplicate-adopt-org-scope— 27 pass@objectstack/dogfoodpackage-first-authoring.dogfood.test.ts— 5 pass, against a real booted stacktsc --noEmiton the package: 63 errors, all pre-existing test-layerliberrors; none in the three edited filesGates — the derived union (
scripts/pm/dispatch-gates.mjsre-run against the real changed paths), all green:check:nul-bytes,check:changeset-gate-self-tests,check:cross-package-test-inputs,check:durability-log-level,check:filter-alias-parity,check:objectui-changeset,check:query-options-erasure,check:type-check-coverage,check-adr-0087-registration,check-changeset-no-major,check-cross-package-test-inputs,check-empty-changeset.Note for review
scripts/adr-anchors/packages__metadata-protocol__src__protocol.ts.jsonwas left alone. Anchors are one file per path and that file already carries another author's ADR-0029/ADR-0119 invariant; the D1 region names ADR-0070 inline and both sites carry thedocspointer, which is Prime Directive #13's core requirement. Extending someone else's anchor on this hot file seemed the wrong call to make unasked.Generated by Claude Code