Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): fold the ADR-0010 lock gate's type key at its producer - #9161
Conversation
…cer (#9009) `getEffectiveLock` handed `type` to its two limbs verbatim and they did not read it the same way: the artifact limb folded, the overlay limb queried `sys_metadata` with the raw spelling. A miss there falls through to `lock: 'none'` — the verdict "the author declared no protection" — so an ADR-0010 `_lock` was addressable around from the wire. Fold once with `canonicalMetaType` and use that key for both limbs. No reachable caller changes behaviour; a future unfolded one is now refused instead of admitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
…asured (#9009) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
📓 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:
|
os-zhuang
commented
Aug 16, 2026
Docs-drift advisory — read independently, not inheritedRead all three pages myself at Result: no drift from this PR, and nothing to attach to #9157. The hypothesis survives an attempt to falsify it.
No page edited; Generated by Claude Code Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9009
Route 1 of the ruling — file the fold, at the producer.
getEffectiveLocknow folds its own type key once withcanonicalMetaTypeand uses that one key for both limbs.The seam
getEffectiveLockhandedtypeto two limbs that did not read it the same way:lookupArtifactItem→PLURAL_TO_SINGULAR[type] ?? type, then the raw spelling as a second lookup_lockresolved under a plural all alongengine.findOne('sys_metadata', { where: { type, … } })SysMetadataRepository.whereForemits the canonical spelling with no at-rest fallback, so the stored active row lives under atypethis query never asked forA miss on that query falls through to
lock: 'none', which is not a neutral placeholder: it is the verdict "the author declared no protection" (#5706), andevaluateLockForWrite/evaluateLockForDeleteturn it straight into "allow". #8769 closed that forpublishMetaItemand #8819 forrollbackMetaItem, each by folding its own request — and #8819's comment said so, then promised a follow-up card for the producer that was never filed. This is that fold.Reachability, measured before the fix was written
The card's own framing said reachability was not measured. It is now, and it holds — this is a Task, not a Bug.
getEffectiveLockhas exactly two callers, and the whole call graph above them is five sites, every one of which folds first:So
canonical === typeat every live site, both limbs already agreed, and no behaviour on any reachable path changes. What the fold removes is the unstated "callers must fold" invariant that lived in no type, no signature and no assertion — the shape that has now cost this family several cards.The limbs can disagree, which is why this is worth closing: hand the gate
viewsand the artifact limb finds the packaged lock while the overlay limb asks for atypeno row carries.Two deliberate non-changes
canonicalMetaType, notPLURAL_TO_SINGULAR. The manifest-collection map omitsfield,seed,external_catalogandtranslation— a fold written against it would canonicalize the types that never needed it and leave the four that do, which is the "tolerant AND INCOMPLETE" shaperecordMetadataAudit's header rejects.recordMetadataAudit'sAUDIT_TYPE_NOT_CANONICALassert (A draft row stored under a manifest-absent plural (fields,seeds,external_catalogs,translations) is promotable by publishPackageDrafts and lands ACTIVE in the second namespace #8908) and fails loudly at 500 rather than silently allowing the write. That is the fail-closed direction and the assert doing its job; folding the ledger key here too would be re-opening A draft row stored under a manifest-absent plural (fields,seeds,external_catalogs,translations) is promotable by publishPackageDrafts and lands ACTIVE in the second namespace #8908's ruling by accident.#9111 is NOT absorbed
Measured, because the dispatch asked for it plainly either way:
hydrateOverlayIntoRegistryis not ongetEffectiveLock's call graph in either direction — it is reached fromapplyRegistryWriteThroughand the read-side hydration paths, and it mints registry entries rather than reading a lock. This fold does not touch it. #9111 still needs its own card.Tests
New:
packages/metadata-protocol/src/protocol.lock-gate-canonical-type.test.ts(9 cases). They address the gate directly rather than through a route, and the file's header says why: every wire path folds at the boundary, and a plural is refused there bymetaUrlSpellingRefusal(400) long before it reaches the gate — so driving this through a route would measure the boundary fold a second time and say nothing about the producer.Reverse verification, direction predicted before running. Ordinary red. The fix was committed first, then ablated (both raw
typereads restored), then restored withgit checkout claude/issue-9009-geteffectivelock-raw-type -- packages/metadata-protocol/src/protocol.ts.Predicted 5 red / 4 green; measured 5 red / 4 green, each red for its predicted reason rather than merely in the predicted count:
The four greens under the ablation are green for reasons, not by vacuum: the canonical spelling is unchanged, a genuine miss still answers
none(so "fail closed" is not satisfiable by locking everything), a canonical locked delete is refused exactly as before, and the artifact limb answers the same in both directions — it already folded, so it was never the hole, and a fix that moved it would be moving something already right.Green runs, all at
fd564e494(the head this PR is opened on; the union was run after the final commit):pnpm --filter '@objectstack/metadata-protocol' test—Test Files 113 passed (113) · Tests 1576 passed (1576)dist/, thenpackages/objectql's four protocol suites that resolve it through thatdist—Test Files 4 passed (4) · Tests 49 passed (49)(protocol-publish-canonical-fold,protocol-registry-shadow,protocol-delete-object-registry-heal,protocol-publish-package-drafts).@objectstack/metadata-protocolhas notypecheckscript — it is one of the 13 packages in the type-check DEBT ledger, andcheck:type-check-coverageis green over that state.scripts/pm/dispatch-gates.mjs, all OK atfd564e494:check:nul-bytes,check:where-matcher,check:engine-double-contract,check:query-options-erasure,check:cross-package-test-inputs,check:type-check-coverage,check:changeset-gate-self-tests,check:durability-log-level,check:filter-alias-parity,check:objectui-changeset.Hot-file coordination
protocol.tsalso carries #9066 this round. Changed line ranges, both taken from the real diffs rather than from a symbol's address:10720-10744,13440-13446,16340-1634515632-15635,15652,15668-15671No overlap.
Out of scope, filed
#9157 —
historyMetaItem,auditMetaItemandfindReferencesToMetaare three live/meta/:type/:nameroutes that never reachcanonicalizeMetaRequestType; they still derive their key from the manifest map. Found while measuring this card's reachability. None of the three reachesgetEffectiveLock, so it does not affect anything here — but it does falsify, as literally stated, the assumption that every/metaentry point folds. #9157 is not addressed here. #9111 remains open.Backlinks: #8862 · #8819 · #8769 · #8908 · #5706.
Generated by Claude Code