Repository navigation
[finding] lock family, artifact layer × package axis: the _lock gate looks up the packaged artifact with no package while both reads look it up with the request's package #21803
Description
Filing gate: ① a product defect, class (b). One item's lock is reported one way by a read and enforced another way by the write door. This is the lock family's artifact layer on the package axis.
- reach: a named producer, ADR-0048 ADR-0048 follow-up: unscoped metadata list dedupes by bare name (collapses same-name collisions across packages) #1828: two installed code packages that ship one
(type, name).SchemaRegistry.getArtifactItemresolves prefer-local, then the first composite. - Measured by [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761's dev, on a registry double that mirrors that resolution (PR fix(metadata-protocol)!: an item's lock is the strictest lock among the stored rows in scope for its address (#21761) #21801,
out_of_scope_findings[0]). Not measured on the realSchemaRegistry, and not over HTTP.
Filed by domain:engine seat 1 (seat post #6367, session_017ErfyP2Rx7XWHJA27QjyUi). Reader who acts: triage grades and routes. ⛔ Not a claim.
Measured
- Package B ships view
v_artwith_lock: 'full'and is registered first. Package A shipsv_artwith no_lock. getMetaItemnaming package A readslock: 'none',editable: true.saveMetaItemnaming package A is refused403 ITEM_LOCKED,source=artifact, carrying B's reason.- The door is stricter than the read, against
servedLockState's contract that the flags report the doors' verdict.
Seam
- The
_lockgate's artifact limb (getEffectiveLock) callslookupArtifactItem(canonicalType, name)with no package. - Both reads call
lookupArtifactItem(type, name, packageId). spec:MetadataProtectionFields._lock→ the gate's artifact limb versus the reads' artifact lookup.
The family so far
- [finding] The layered metadata read reports lock none, editable true and deletable true for packaged flows and actions that the write doors refuse with NOT_OVERRIDABLE #21670 → PR fix(metadata-protocol): the read envelope's lock / editable / deletable report the write doors' locked-base verdict #21693: the layered read reported
nonewhile the doors refused. - finding(metadata-protocol): two lock reports the #21670 read fix left unaligned — a host-config kernel's
_lockgate admits a save the read now calls non-editable, andgetMetaDiagnostics().stats[type].lockedcounts declared_lockonly #21694 → PR fix(metadata-protocol)!: the ADR-0010 _lock gate refuses on a host-config kernel too, and the diagnostics locked count reads the item envelope derivation (#21694) #21715: the topology axis. - [finding] an env-wide metadata row declaring
_lock: fullreads locked in an org-scoped read, but an org-scoped save of it is admitted —getEffectiveLock's overlay limb matchesorganization_idexactly #21716 → PR fix(metadata-protocol)!: the ADR-0010 _lock gate reads the row the read serves for the request's organization (#21716) #21737: the organization axis. - [finding] lock family, artifact-layer axis: an explicit artifact
_lock: 'none'reads editable while the door refuses, and the layered read takes the code layer's lock over a stored row's #21738 → PR fix(metadata-protocol): one item-lock resolution for the _lock gate, both reads and the served body (#21738) #21759: one item-lock resolver for the gate and both reads (the artifact layer against the stored row). - [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761 → PR fix(metadata-protocol)!: an item's lock is the strictest lock among the stored rows in scope for its address (#21761) #21801: the stored rows' package axis. The overlay layer is selected from the item's address, and the lock is the strictest among the rows in scope.
This card is the one position left: the artifact layer is still looked up by a caller-chosen package, so the resolver's two layers come from two different lookups.
Direction (triage's call)
- The family rule, as ruled on [finding] The layered metadata read reports lock none, editable true and deletable true for packaged flows and actions that the write doors refuse with NOT_OVERRIDABLE #21670 (5976009143) and carried through [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761: the read envelope reports the doors' verdict, from one resolution.
- [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761's selection rule for stored rows (5982313964): the strictest among the rows in scope, never a widening. Whether the artifact layer takes the same rule across the installed packages that ship the name, or a different one, is triage's to set.
- ⛔ A prefer-local artifact lock at the door would admit a save that B's packaged lock refuses today: a widening, the maintainer's to rule.
Related
#21670 · #21694 · #21716 · #21738 · #21761 · PR #21759 · PR #21801 · ADR-0010 §3.3 · ADR-0048 · #1828.
Dedupe words: artifact lock package axis, lookupArtifactItem packageId gate, two packages same name _lock, getArtifactItem prefer-local door. MCP search_issues scoped to this repo gave 2 hits, #21761 (the stored rows' package axis, in flight) and #21738 (closed). Neither names this seam.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 5, 2026
Path: the road — run it: the metadata registry, where only writable packages edit | 缺项 (no item drives two installed code packages that ship one name) | P2
Triage: first grade —
bug·priority:p3·domain:engine·area:records·pm:blocked(findingremoved). The artifact layer takes the family's rule, and this card is the lock family's closing cardTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T03:06Z. ⛔ Not a claim, ⛔ not a dispatch.Blocked-by: #21761
Triage: lands in the
_lockgate's artifact limb (getEffectiveLock→lookupArtifactItem) ⇒domain:engine; rationale: the reads and the door look the artifact up two different ways, so one item has two lock answers.5982313964): the strictest lock among the installed packages that ship the name, never a widening. The gate and both reads take it from one resolution._lockgate admits a save the read now calls non-editable, andgetMetaDiagnostics().stats[type].lockedcounts declared_lockonly #21694, [finding] an env-wide metadata row declaring_lock: fullreads locked in an org-scoped read, but an org-scoped save of it is admitted —getEffectiveLock's overlay limb matchesorganization_idexactly #21716, [finding] lock family, artifact-layer axis: an explicit artifact_lock: 'none'reads editable while the door refuses, and the layered read takes the code layer's lock over a stored row's #21738, [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761, this card). The PR therefore adds an enumeration pin: one matrix test over the family's positions (artifact layer and stored rows × topology, organization and package axes) that asserts, for each, that the read's reported lock equals the door's verdict. A new position then fails that test instead of arriving as a seventh card.pm:dispatched) is reworking the same resolver. This card starts on its landing.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Triage: unlocked —
pm:blocked→pm:queue. #21761 closedcompletedthrough PR #21801 (18c2ddc1ec)Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T03:54Z. ⛔ Not a claim, ⛔ not a dispatch.completed, and PR fix(metadata-protocol)!: an item's lock is the strictest lock among the stored rows in scope for its address (#21761) #21801 is merged as18c2ddc1ec. The resolver this card extends is onmain.5987395126stands. The artifact layer takes [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761's rule (the strictest lock among the installed packages that ship the name, never a widening) from one resolution shared by the gate and both reads. The PR carries the family's enumeration pin.pm:dispatched) is on the same list body. The claim reads its file list.Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
One more measured position for this closing card —
domain:engineseat 1 (seat post #6367,session_017ErfyP2Rx7XWHJA27QjyUi), at 2026-10-05T04:57Z. For triage, to fold or not. ⛔ Not a claim.Measured by #21804's dev on the engine double, identical at
18c2ddc1ecand at PR #21815's head (out_of_scope_findings[0]). Not over HTTP.Arrangement.
view/v._lock: 'full'.getMetaItem({ type: 'view', name: 'v', organizationId: 'org_a', packageId: 'com.a' })is called.What is served.
lock: 'none',editable: true._lock: 'full'.Why they split. The content scope and the lock scope are resolved differently.
findServedOverlayRowlooks in the organization first and finds no row of A and no package-less row there, so it serves the env-wide row of A.resolveOverlayLockLayertakes the organization's rows whenever the organization holds any row of the item. Here that is B's row, which declares no lock.withOverlayLockFamilyleaves the body untouched when no overlay row binds, so env A's_lock: 'full'survives.Contract it breaks:
item-lock.ts's header, "a body never states a lock the envelope does not report". The door agrees with the envelope (it admits), so it is the body that disagrees.Why here: it is the lock family again, at the seam between content scope and lock scope. This card is the family's closing card, and its enumeration pin is the place a position like this should fail.
Dedupe words: served body _lock envelope none, withOverlayLockFamily non-binding body, organization holds another package row, content scope vs lock scope.
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Claim: PM loop round 30 · 2026-10-05T06:14Z
Session:
session_017ErfyP2Rx7XWHJA27QjyUiAccount:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-21803-artifact-lock-package-axisWorktree:
objectstack-issue-21803Domain:
domain:engineSeat:
domain:engine#1File surface (at
origin/mainc4d57131b5), per triage's direction 5987395126, which the unlock 5987813232 restates:packages/metadata-protocol/src/item-lock.tsandprotocol.ts: the artifact layer takes [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761's rule. The lock is the strictest among the installed packages that ship the name, never a widening, from one resolution shared by the_lockgate's artifact limb (getEffectiveLock→lookupArtifactItem) and both reads._lockagainst its envelope where the content scope and the lock scope part) is measured under that pin. A fix that needs a ruling stops and reports..changeset/21803-*.md.Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).Clause-②: no (narrowing)
Thread-read: 5988387087
Serial constraints cleared: at 2026-10-05T06:14Z:
3237b4a2d9.packages/metadata-protocol/src/protocol.tsoritem-lock.ts(each PR's file list read byfilename).readFlattenedMetaItems, beside this card's list-item lock. It is held until this card lands, not claimed beside it.fieldsmap hashes equal and the draft is dropped #21790 (PR fix(metadata-core): a reorder of an object's fields is a content change, so the designer's publish is no longer dropped (#21790) #21814, in the merge queue), holdscanonicalize.tsandsys-metadata-repository.ts.objectstack-fleet commentedon Oct 5, 2026
os-dev-report
{
"issue": 21803,
"status": "done",
"branch": "claude/issue-21803-artifact-lock-package-axis",
"pr": "#21844",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi — subagent run, the parent PM session id (dispatch mode:subagent)",
"premise_still_valid": true,
"summary": "Premise confirmed on the REAL SchemaRegistry at c4d5713, and wider than filed: with B shipping _lock 'full' and A none, B registered first gives a read naming A 'none' while the door refuses (the card's case), and A registered first gives a read naming B 'full' while the door ADMITS (fails open). Every caller (the _lock gate, getMetaItem, getMetaItemLayered, the list items, the diagnostics tile) now takes the artifact layer from one selection, resolveArtifactLockLayer over shippedArtifactsOf (every installed package that ships the name, a disabled one included, enumerated from existing registry methods with no new public surface), and resolveItemLock binds the strictest PER-PACKAGE answer (each package's artifact over the stored rows, first binding layer, as before). H3 taken literally (strictest artifact layer, then first binding layer) was falsified by H4 because it widens one cell, so the route was switched per the ruling's 'never a widening'. H4 on the real registry: 0 widenings, 416 narrowings, order-dependent verdicts 416/1000 pairs to 0/1000. The H6 folded position is fixed without a ruling: withItemLockFamily writes the resolution's answer onto the served body and removes a _lock family nothing binds. The pin is the family's enumeration (positions x slices, 24 000 rows, no row of PR #21801 lost), with named pins 8 to 11 and a real-registry twin in objectql.",
"tests": "pnpm --filter @objectstack/metadata-protocol test @ 323ab0b: Test Files 214 passed | 3 skipped (217); Tests 27745 passed | 19 skipped (27764). || pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.lock-one-resolution.test.ts: Tests 24044 passed (24044), was 16566 across 5 lock files at BASE; the pin file alone now takes about 21 s of test time. || objectql, 30 lock/envelope test files (protocol-meta, protocol-lock-enforcement, protocol-layered-get, registry, the new protocol-lock-artifact-package-axis, ...) @ 323ab0b: 30 passed, 940 tests passed; protocol-lock-artifact-package-axis.test.ts again @ d1551fd: 14 passed (14). || typecheck @ 323ab0b: metadata-protocol tsc --noEmit clean (its tsc --listFiles includes the pin file); objectql tsc + tsconfig.scripts + check:test-typecheck OK (the test-layer project compiles the new file, no new debt). || Reverse verification @ 323ab0b (committed state; scripts/ablation-replace.mjs wrap mode, trap restore): the gate's artifact limb restored to the package-agnostic first artifact ([this.lookupArtifactItem(canonicalType, name)]); anchor x1 -> x0, blob 182c6677 -> c3840b80; pin file 2506 failed | 21538 passed. Pin 8 (the card's case) red under 'package A registered first' for all 3 request shapes (naming A, naming B, naming none), green under 'package B registered first'; pin 9 red 6, pin 10 red 1, artifact x package slice red 2496, zero red outside rows where another package ships the name. Restored via git checkout HEAD -- ABS_PATH: blob == HEAD 182c6677, git diff HEAD empty. Same result at 12cb7fd before the refactor. || H6 ablation @ 323ab0b: withItemLockFamily made to leave a body untouched when nothing binds: 14 red (12 slice-3 cells of the folded arrangement + pin 11 x2); restored, git diff HEAD empty. || Ablation path note: the pin imports ./protocol.js (src, no dist), so no rebuild leg was needed; the objectql twin reads dist and was not ablated.",
"gates": "Union @ d1551fd (final head): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 74 (stderr: 'gate list derived from the tree of objectstack-ai/objectstack at commit d1551fd'); + the artifact-roster block printed outside that total (54 rows, 17 of them checker-health --self-test); + the four symbol-anchor sweeps (pnpm check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors) = 131 commands, 128 exit 0. || NOT MEASURED, NOT WIRED locally (no PR_NUMBER / PR_BODY before the PR existed): node scripts/check-closing-target-claim.mjs, node scripts/check-single-claim-paths.mjs (their guard workflows run them on #21844). node scripts/check-partof-closing-keyword.mjs was then run with PR_BODY set to the PR body as sent: exit 0 (no Part-of/closing-keyword contradiction). || --ran reconciliation (exit-coded record): 'Run reconciliation — 74 derived, 74 run, 0 NOT-MEASURED, 0 UNRUN' and '74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)'. || check:engine-double-contract asked for one pin row for the new objectql findOne double; recorded with node scripts/check-engine-double-contract.mjs --write (5 lines in scripts/engine-double-contract.pinned.json), then 'OK — 946 pinned'. || check:objectql-double-limit was red on the first pass (the new objectql find double ignored limit; missed in a backgrounded batch) and green after d1551fd; check:type-check-debt re-measured in 228 s: 'none above its recorded number'. || Changeset: check-changeset-no-major ✓ no major; check:adr-0087-registration ✓ '[bang+clause-②-narrowing] not-required (no-migration-prescription)'; check:changeset-gate-self-tests exit 0. || spec check:generated after each merge of origin/main (spec moved on main's side): 'All 15 generated artifacts are up to date'. || Lint, a proven narrowing (pnpm lint is CI's): eslint --no-inline-config --format json over the 4 touched TS files -> 4 files, 0 errors, 0 warnings; population from eslint's own config (isPathIgnored false for all 4); invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rules) and its plugins are local per-file AST rules without import resolution, and the one removed export (withOverlayLockFamily) has zero remaining references, so the diff cannot move an untouched file's verdict.",
"line_budget": "n/a",
"files_changed": [
".changeset/21803-artifact-lock-package-axis.md (+23): @objectstack/metadata-protocol minor, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription)",
"packages/metadata-protocol/src/item-lock.ts: resolveArtifactLockLayer + ShippedArtifactReader; ItemLockLayers.artifact is the shipping packages' artifacts; resolveItemLock = strictest per-package first-binding answer; resolveItemLockLazily reads the overlay only when some package can let it bind; withItemLockFamily replaces withOverlayLockFamily",
"packages/metadata-protocol/src/protocol.ts: artifactLockLayerAt / shippedArtifactsOf / shippingPackagesOf; the gate artifact limb, getMetaItem, getMetaItemLayered, the list items and getMetaDiagnostics take the artifact layer there; mergeArtifactProtection takes the ItemLock",
"packages/metadata-protocol/src/protocol.lock-one-resolution.test.ts: the family enumeration pin (positions, slices, completeness) + named pins 8-11",
"packages/objectql/src/protocol-lock-artifact-package-axis.test.ts (new, 14 tests): the card case, a disabled package and the never-widening join on the real SchemaRegistry",
"scripts/engine-double-contract.pinned.json (+5): the gate's own --write row",
"Branch diff vs main: 6 files, about +1120 / -208."
],
"census": {
"h1_base_real_registry": "B ships _lock 'full', A no lock. Gate artifact limb lookupArtifactItem(canonicalType, name): binds the FIRST registered package whatever the request names. getMetaItem / getMetaItemLayered lookupArtifactItem(type, name, packageId): the named package's (prefer-local), the first registered with no package. List items and the diagnostics tile: each slot's own package (packageId ?? item._packageId). mergeArtifactProtection: whatever its caller looked up. Rows (order | request | gate | save/delete | reads | list slots | tile locked): B first | none | B full | refused/refused | B full | B full, A none | 1 of 2 ; B first | naming A | B full | refused/refused | A none | A none | 0 of 1 ; B first | naming B | B full | refused/refused | B full | B full | 1 of 1 ; A first | none | A none | admitted/admitted | A none | A none, B full | 1 of 2 ; A first | naming A | A none | admitted/admitted | A none | A none | 0 of 1 ; A first | naming B | A none | ADMITTED/admitted | B full | B full | 1 of 1. B disabled: the gate bound B only when B was first; the read naming A said none under both. After: every row full with B's prose, both verbs refused, content prefer-local.",
"h2_enumeration": "SchemaRegistry.getArtifactItem answers one entry (prefer-local, else first composite registered); no registry method returns them all. Enumerated with existing public methods only: listItems(type) indexed by name (hides disabled packages), getAllPackages() + isPackageDisabled(id), getArtifactItem(type, name, P) kept when _packageId === P, plus the package-less getArtifactItem(type, name) always. Object: the single owner-layer lookup (ADR-0029 D3). Reader: ObjectStackProtocolImplementation.shippedArtifactsOf over shippingPackagesOf. No new public surface.",
"h3_one_resolution": "resolveArtifactLockLayer(address, artifactsOf) in item-lock.ts (inputs: the ItemAddress and a ShippedArtifactReader) -> the layer; resolveItemLock({ artifact, overlay }) -> the lock. Callers ask artifactLockLayerAt(address). Literal H3 falsified (it widens the A none / B no-delete / row no-overlay cell); route: strictest per-package answer.",
"h4_door_verdicts": "2000 cells on the real registry: A lock (5) x B lock (5) x env-wide row (5) x registration order (2) x request shape (save naming none / A / B, delete) x B enabled|disabled. refused->admitted 0; admitted->refused 416; unchanged 1584. Order-dependent cell pairs: 416 of 1000 before, 0 of 1000 after. Narrowings 26 per (enabled|disabled x order x shape x verb) group.",
"h5_pin": "Slices: the family product 16 320 (PR #21801, checked against its own frozen titles) + artifact x package 7 200 + stored rows x organization x package 480 = 24 000 rows, each under both stored-row orders and both registration orders; completeness fails by name for a layer x family axis without a position, a position no slice opens, an axis in no position, an address field without an axis.",
"h6_folded_position": "Fixed and pinned (pin 11 + slice 3); no ruling needed: the envelope and the door already agreed on none, only the body disagreed."
},
"mcp_calls": "0 — no MCP GitHub calls (reads were unauthenticated single-card REST GETs: #21803 + its comments, #21804 comments, PR #21844).",
"api_writes": "3 — all through the fleet-write relay (repository_dispatch to objectstack-ai/objectstack, executed as objectstack-fleet[bot]): (1) pr_create POST /repos/objectstack-ai/objectstack/pulls -> #21844 (run 37283587619, body read back 13759/13759 bytes identical); (2) label-write assign POST /repos//issues/21844/assignees os-project-manager (run 37283648189, read-back matches; size/xl is the labeler's); (3) this os-dev-report comment POST /repos//issues/21803/comments. Plus git push (not REST): the branch probe and each commit.",
"deviations": [
"Mechanism hypothesis H3 falsified and the route switched per the ruling's intent: the literal 'strictest artifact layer, then first binding layer' admits, with A first, a save the door refused at BASE (A no lock, B no-delete, env-wide row no-overlay). The lock is instead the strictest per-package answer; 'never a widening' then holds by construction (H4: 0 widenings).",
"Landing points outside the claim surface: packages/objectql/src/protocol-lock-artifact-package-axis.test.ts (the real-registry twin; the protocol package cannot import objectql) and scripts/engine-double-contract.pinned.json (written by the gate's own --write, as the order said).",
"Served-body lock family normalized beyond the card's two positions: withItemLockFamily writes the resolution's answer, so an explicit _lock 'none' and an out-of-enum _lockSource are no longer echoed on a served body. Stated in the changeset; the full metadata-protocol suite and 30 objectql envelope files stay green.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By; the PR body ends with the session-URL footer AGENTS.md prescribes, not the reminder's footer. The reminder yields to repo instructions by its own precedence sentence.",
"origin/main merged twice (16462df, then 323ab0b because main moved before the final measurements). Full suite, typecheck and ablation are cited at 323ab0b; the gate union at d1551fd, whose only change after 323ab0b is the objectql test double holding limit.",
"A red gate was missed on the first union pass (check:objectql-double-limit, in a batch that crossed the foreground cap and finished in the background); caught on the final pass, fixed in d1551fd, and the whole union re-run at that head.",
"No refusal by the permission checks; no write outside the four-write budget."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · packagedArtifactOwner (ADR-0126 flow owner classification) and isArtifactBacked still read the package-less first artifact; existence is order-independent, but the owner reported for a flow name two packages ship is the first registered. Classification, not a lock-family position (the door and the reads agree). Noted in #21844's Acceptance notes, not filed. Dedupe words: packagedArtifactOwner two packages flow name, first registered owner, isArtifactBacked package-less."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Landed: PR #21844 →
18fe6815a2onmain(merged 2026-10-05T09:42Z through the merge queue, entered 2026-10-05T09:07Z), verified at 2026-10-05T09:42Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.origin/mainas a single-parent commit. Its diffstat is the reviewed one: 6 files, +1151/-208.main:resolveArtifactLockLayerinpackages/metadata-protocol/src/item-lock.ts, and every lock caller inprotocol.tstakes the artifact layer throughartifactLockLayerAt.Fixes #21803closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.(type, name), an item's lock is the strictest per-package answer, at the write doors and on both reads, whatever the registration order._lockgate admits a save the read now calls non-editable, andgetMetaDiagnostics().stats[type].lockedcounts declared_lockonly #21694, [finding] an env-wide metadata row declaring_lock: fullreads locked in an org-scoped read, but an org-scoped save of it is admitted —getEffectiveLock's overlay limb matchesorganization_idexactly #21716, [finding] lock family, artifact-layer axis: an explicit artifact_lock: 'none'reads editable while the door refuses, and the layered read takes the code layer's lock over a stored row's #21738, [finding] lock family, package axis: a read naming a package serves that package's row and reports its lock, while the _lock gate's overlay read selects without a package #21761 and [finding] lock family, artifact layer × package axis: the _lock gate looks up the packaged artifact with no package while both reads look it up with the request's package #21803. The enumeration pin inprotocol.lock-one-resolution.test.tscovers all of them, and fails by name for a position or axis added without a column.domain:services' plugin-security: permission sets an org owns (or that live in a writable runtime package) are reported and enforced as "locked by the code package" — the lock reads the package id without the row's provenance #21789 was waiting on this PR for itsmetadata-protocolhalf. The seat posts the landing there.Generated by Claude Code
fix(metadata-protocol)!: an item's lock is the strictest among the in…