Repository navigation
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. These are the family's remaining positions, on the artifact-layer axis.
- reach: the real
ObjectStackProtocolImplementationreads (getMetaItem,getMetaItemLayered, which servesGET /api/v1/meta/:type/:name/layers) and the_lockgate, over an engine double atc43a8ae612. Measured by [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's dev (PR fix(metadata-protocol)!: the ADR-0010 _lock gate reads the row the read serves for the request's organization (#21716) #21737,out_of_scope_findings[0]and[1]). Not measured over HTTP.
Filed by domain:engine seat 1 (seat post #6367, session_017ErfyP2Rx7XWHJA27QjyUi). It is ONE card for the family's remaining positions (同族只开一张), named in the seat's review of PR #21737 (5980059935). Reader who acts: triage grades and routes. ⛔ Not a claim.
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: the layered read reported lock
nonewhile the doors refused (PR fix(metadata-protocol): the read envelope's lock / editable / deletable report the write doors' locked-base verdict #21693). - 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: the topology axis (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). - [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: the organization axis, the closing card for the axes triage enumerated. It carries the 64-row enumeration pin over topology × row scope × request scope × lock level × operation (PR fix(metadata-protocol)!: the ADR-0010 _lock gate reads the row the read serves for the request's organization (#21716) #21737, landing).
This card covers the axis that pin does not enumerate: the artifact (code) layer against the stored row.
Measured, position 1: an explicit artifact 'none'
- A packaged view's artifact declares
_lock: 'none'explicitly (protection.lock: 'none'throughapplyProtection). A stored env-wide row declares_lock: 'full'. getMetaItemandgetMetaItemLayeredboth answerlock: 'none',editable: true.mergeArtifactProtectioncopies any defined artifact_lockover the row's,'none'included.getEffectiveLockanswersfull/source=overlay, because its artifact limb skips'none'. So the save door refuses withITEM_LOCKED.- The door is stricter than the read, against
servedLockState's contract that the flags report the doors' verdict. - Seam:
MetadataProtectionFields._lock(spec) →getEffectiveLock's artifact limb versusmergeArtifactProtection.
Measured, position 2: the layered read's lock source
- A packaged view declares no
_lock. A stored env-wide row declares_lock: 'full'. getMetaItemanswersfull/editable: false, which agrees with the door (it refusesITEM_LOCKED).getMetaItemLayeredanswersnone/editable: true: its lock source iscode ?? overlay, which takes the code layer whenever one exists. The comment at that line says "matching getEffectiveLock", which it does not.- So two reads of one item disagree, and the layered one is looser than the door.
Position 3: the spelling residue (option C from PR #21737)
- Since PR fix(metadata-protocol)!: the ADR-0010 _lock gate reads the row the read serves for the request's organization (#21716) #21737, the
_lockgate addresses the canonical type spelling only (meta overlays: unnormalized type segment creates phantom rows that shadow the code-authored listing and cannot be deleted #4432's write-side rule). - Both reads still fall back to a row stored under the type's other (plural) spelling when there is no canonical row in that scope.
- For pre-meta overlays: unnormalized type segment creates phantom rows that shadow the code-authored listing and cannot be deleted #4432 at-rest residue, the read can report a lock the door does not see. No live writer mints such a row.
- Contract-first direction: retire the reads' tolerant other-spelling fallback, after which the read and the door resolve identically.
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 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 / [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: the read envelope reports the doors' verdict, from one resolution. - Position 1 needs a judgment on what an explicit artifact
'none'means against a stored row's lock (ADR-0010 §3.3), applied to the read and the door alike. That may make it a fork. - Extend the enumeration pin with the artifact-layer axis (no artifact, artifact without
_lock, artifact with an explicit lock, including'none') × the stored row's lock, for both reads and the door.
Related
#21670 · #21694 · #21716 · PR #21693 · PR #21715 · PR #21737 · ADR-0010 §3.3 · #4432.
Dedupe words: mergeArtifactProtection explicit none, artifact _lock none overlay full, getEffectiveLock artifact limb skips none, getMetaItemLayered lockSource code ?? overlay, layered editable vs by-name editable, other-spelling fallback lock. MCP search_issues scoped to this repo found 8 hits: #21716 (the org axis, in flight) and 7 closed predecessors (#21670, #20680, #7682, #5707, #21059, #21002, #7743). None names these positions.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 4, 2026
Triage: first grade —
bug·priority:p3·domain:engine·area:records·pm:blocked(findingremoved). This becomes the lock family's closing card, by construction: one lock resolution, which both reads and the door callBlocked-by: #21716
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T12:52Z. ⛔ Not a claim, ⛔ not a dispatch.This amends my
5979228992(#21716's grade)._lock: fullreads locked in an org-scoped read, but an org-scoped save of it is admitted —getEffectiveLock's overlay limb matchesorganization_idexactly #21716 the family's closing card, and its enumeration named five axes: topology, row scope, request scope, lock level and operation. It left out the artifact layer against the stored row, and it left out the second read (getMetaItemLayered). This card measures both.Direction: one resolution, three callers. The family rule is already ruled (#21670
5976009143, carried by #21694 and #21716): the read envelope reports the doors' verdict, from one resolution.getMetaItem,getMetaItemLayeredand the_lockdoor take the item's lock from one resolver: today'sgetEffectiveLock, or its successor.mergeArtifactProtection's lock copy (protocol.tsabout:1723) as a lock source;code ?? overlay ?? {}(about:10277–:10278, whose comment claims a match it does not have).servedLockStatekeeps derivingeditable/deletablefrom that one answer. ⛔ No third derivation.Position 1 (explicit artifact
'none'against a stored'full'): not a fork.'none', so the stored row's lock binds, and the read follows the door. That is the family rule.'none'declares no package lock. It is not a grant over an administrator's stored lock.'none'override a stored lock would widen the door. That would be a separate decision for the maintainer, and ⛔ not this card.Position 2 (the layered read's source): resolved by the single resolver above.
Position 3 (the other-spelling residue):
Acceptance: a pin whose rows are generated from the resolver's inputs.
'none'included), and the spelling. Each row asserts thatgetMetaItemequalsgetMetaItemLayeredequals the door, for save and delete.Why p3. It matches the family: no shipped producer is measured.
protection.lockinplatform-objectssits onobject, and a packaged view with an explicit'none'has 0 producers in-repo. Position 2 is a read looser than the door, not a door looser than the declared lock.Why blocked. PR #21737 (#21716, draft, head
f508c21ce3) is reworking the same resolver region ofpackages/metadata-protocol/src/protocol.ts. The unlock scan fires when #21716 closes.Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
Claim: PM loop round 27 · 2026-10-04T13:59Z
Session:
session_017ErfyP2Rx7XWHJA27QjyUiAccount:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-21738-lock-one-resolverWorktree:
objectstack-issue-21738Domain:
domain:engineSeat:
domain:engine#1Unlock:
Blocked-by: #21716is closedcompleted. PR #21737 →cf60dbc8b3landedfindServedOverlayRowat 2026-10-04T13:57Z, and the record is 5980781584. Re-derived atorigin/mainff29410ed2: no new blocker, since no open PR touchesprotocol.ts.File surface, per triage's grade and direction 5980131521:
packages/metadata-protocol/src/protocol.ts: ONE lock resolver (today'sgetEffectiveLock, or its successor), from whichgetMetaItem,getMetaItemLayeredand the_lockdoor take the item's lock.mergeArtifactProtection's lock copy as a lock source (about:1723).code ?? overlay ?? {}lock source (about:10277).servedLockStatekeeps derivingeditable/deletablefrom that one answer. ⛔ No third derivation.'none'declares no package lock and is no grant over a stored lock), and the read follows the door.'none'included, spelling), with a completeness check that names a missing input. PR fix(metadata-protocol)!: the ADR-0010 _lock gate reads the row the read serves for the request's organization (#21716) #21737's 64-row pin folds into it..changeset/21738-*.md.Container & model:
S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).Clause-②: no
Thread-read: 5980131521
Serial constraints cleared: at 2026-10-04T13:59Z:
packages/metadata-protocol/src/protocol.ts.os-verify-lock: the lock is free and the queue empty.metadata-coreand the packages door, not this file.fix(metadata-protocol): one item-lock resolution for the _lock gate, …
test(metadata-protocol): the lock family's acceptance pin, generated …
chore(changeset): metadata-protocol patch for the one item-lock resol…
chore(scripts): record the acceptance pin's findOne double in the eng…
chore(changeset): name the served-body half of the prose change (#21738)
objectstack-fleet commentedon Oct 4, 2026
os-dev-report
{
"issue": 21738,
"status": "done",
"branch": "claude/issue-21738-lock-one-resolver",
"pr": "#21759",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi",
"premise_still_valid": true,
"summary": "One item-lock resolution now answers the _lock door, both reads' envelopes, the served body's lock fields and the getMetaDiagnostics locked count. It is resolveItemLock in the new packages/metadata-protocol/src/item-lock.ts: the first layer in ITEM_LOCK_LAYERS order (artifact, overlay) whose declared _lock is not 'none' binds. getEffectiveLock reads its layers through resolveItemLockLazily, so it still makes no store read when the artifact binds. getMetaItem and getMetaItemLayered pass in the artifact they already looked up and the served row's stored body, with no extra store read. mergeArtifactProtection copies the artifact's _lock family only when the artifact binds. code ?? overlay is no longer a lock source, and servedLockState derives editable / deletable from the resolver's answer. Positions 1 and 2 are closed: a 216-cell census found 36 split cells before and 0 after, the door columns are identical in all 216 cells, and every non-lock field is byte-identical before and after. The acceptance pin has 1632 rows generated from the resolver's layers and a completeness check; PR #21737's 64 rows are folded in as a checked subset. Position 3: residue can exist on a supported upgrade path (16.1.0 wrote raw plural spellings, the floor is 16, and the stored-metadata migration never rewrites a type spelling), so the reads' fallback stays declared and no retirement card follows from the census.",
"census": {
"measured_at": "the real ObjectStackProtocolImplementation over an engine double, view type, both kernel topologies; base = ff29410 code (merge base ea7ff39 for the H3 probe), after = this branch",
"H1_derivations_at_ff29410ed2": [
"getEffectiveLock | the _lock gate (save, publish, rollback, delete, publishPackageDrafts promotion) | artifact: lookupArtifactItem(canonical, name), no package | stored: findServedOverlayRow(canonical, no package, org-gated, otherSpelling false) | artifact unless 'none', else row, else 'none'",
"mergeArtifactProtection | getMetaItem body, getMetaItems list, hydrateOverlayIntoRegistry, expandRuntimeViewContainer | the caller's artifact | the item's own _lock | artifact _lock whenever defined ('none' included), else the item's",
"getMetaItem envelope | servedLockState(document) | via the merge (package-scoped lookup) | the served row or a registry / MetadataService item | the merged document's _lock",
"getMetaItemLayered envelope | servedLockState(code ?? overlay ?? {}) | the code layer (MetadataService copy, else artifact, else runtime-only registry item) | only when no code layer | the code layer's _lock",
"getMetaDiagnostics locked count | servedLockState(list item) | via the list's merge | the list item | the merged list body's _lock",
"servedLockState | the three reads above | none of its own | none of its own | joins the given document's lock with packagedBaseRefusal",
"packages/rest/src and packages/runtime/src | pass the protocol envelope through | none | none | no derivation (git grep of resolveLockState, extractProtection, _lock, editable / deletable)"
],
"split_census": "216 cells = 2 topologies x 6 artifact states (absent, no _lock, each of 4 levels) x 9 stored states (none, env-wide or org-scoped row at each level) x 2 request scopes. Before: 36 split cells (per topology 9 position 1 + 9 position 2). After: 0. Door columns byte-identical in all 216 (after trailing-space normalisation). getMetaItem changed in 18 cells, getMetaItemLayered in 36.",
"H3_other_fields": "same 216 cells at base and head: getMetaItem envelope + served body, getMetaItemLayered envelope + code / overlay / effective / overlayScope / _diagnostics, getMetaItems list item, with the lock family masked: all 216 byte-identical. Lock family differs in 54 cells: 18 position 1, 18 position 2 (layered only), and 18 explicit-'none'-artifact cells where nothing binds (prose no longer reported; the artifact's _lock family no longer copied onto a stored row's body).",
"H4_residue": "@objectstack/metadata-protocol@16.1.0 SysMetadataRepository.whereFor keys type: ref.type raw, so a plural request wrote a plural row. The fold 63b33e6 (#4432) first ships in 17.0.0-rc.2: merge-base --is-ancestor of 63b33e6 against the 16.1.0 tag gives exit 1, the control 16.0.0 gives exit 0, and the repo is not shallow. MIGRATION_SUPPORT_FLOOR = 16. migrateStoredMetadata rewrites bodies only: the six URL-only plurals are reported skipped (#8957), and a manifest-plural row stays at rest. Verdict: residue can exist on a supported path, so the fallback stays declared.",
"edges_after": "package axis (one package-less row and one package A row, read with ?package=A): 2 of 3 arrangements split read from door, as before this PR (row selection, unchanged). Residue (a plural row declaring full only): both reads say full and the door admits, the declared difference; with a canonical row also present, all three agree."
},
"tests": "All at 60232c3 (after merging origin/main ea7ff39). @objectstack/metadata-protocol vitest run: Test Files 213 passed | 3 skipped (216), Tests 5249 passed | 19 skipped (5268). That is base 3675 + 1640 new - 66 moved. tsc --noEmit is green, and --listFiles includes item-lock.ts and protocol.lock-one-resolution.test.ts. The new pin alone: 1640 passed in 1.43s test time. @objectstack/objectql against the rebuilt metadata-protocol dist (built from ae121ea; protocol.ts and item-lock.ts unchanged since): --project local 372 files / 7458 tests passed; --project repo 1 file / 5 tests passed. Narrowed lint: eslint --no-inline-config --format json over the 5 changed TS files gives 5 results, 0 errors, 0 warnings. Population from eslint's own config: isPathIgnored false for all 5, computed configs carry 5 or 6 rules. Invariance: no parserOptions.project or projectService in any computed config, and eslint.config.mjs never enables type-aware linting. Reverse verification ran from committed HEAD (protocol.ts blob db49dc4d2fab) through scripts/ablation-replace.mjs in wrap mode, inside a script with an absolute-path trap restore from HEAD. The subject resolves from source (./protocol.js), so no rebuild was in the path; the direction was declared first. Leg A (mergeArtifactProtection as a lock source: the copy unconditional and getMetaItem's lock taken from the merged document): anchors 1 -> 0, markers 0 -> 1 (grep -c 2), blob db49dc4d2fab -> cb753a719d8b -> decca0d87278. Predicted 146 red / 1494 green; measured Tests 146 failed | 1494 passed: pin 2 x2 + 144 pin-1 rows, all with an explicit 'none' artifact; pin 3 green. Leg B (layered code ?? overlay ?? {} restored): anchor 1 -> 0, marker 1, blob db49dc4d2fab -> bdd63140c660. Predicted 292 / 1348; measured 292 failed | 1348 passed: pin 3 x2, pin 2 x2 and 288 pin-1 rows, all with an artifact declaring no _lock or 'none'. Restore after each leg: blob == HEAD blob db49dc4d2fab, git diff HEAD empty and git status --porcelain empty (tool and trap). NOT MEASURED: HTTP (reach on doubles); the rest / runtime / plugin-security / plugin-email / client suites (consumers with no export or wire-shape change; their lock-field assertions were grepped and use fakes); CI-owned Dogfood, Temporal Conformance, the type-check lanes and the full pnpm lint.",
"gates": "At 60232c3, exit codes captured before any pipe. node scripts/pm/dispatch-gates.mjs --commands (no paths) derived 72 families: 72 run, 72 exit 0. --ran reports '72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED'. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET) and exited 0 after a full turbo build (72 tasks). Artifact-roster block: 54 run, 51 exit 0. The 3 PR-context guards exited 2 (NOT WIRED) before the PR existed; with PR 21759's context (PR_NUMBER, PR_HEAD_REF, the live body) check-closing-target-claim, check-partof-closing-keyword and check-single-claim-paths all exit 0. The four symbol-anchor sweeps exit 0: adr-symbol-anchors 2167 anchors / 140 records; scripts-symbol-anchors 3760 / 282 scripts; spec-docblock-symbol-anchors 4950 / 1868 sources; adr-anchors OK. check:engine-double-contract asked for the new pin's findOne double; its own --write recorded it (+1 row, 0 lost) and it was committed. check:durability-log-level reports 70 read seams at both the merge base and head, and the --depth-cost census differs only in line numbers and one enclosing function name. 130 commands in total, 0 non-zero at the end.",
"line_budget": "n/a",
"files_changed": [
"packages/metadata-protocol/src/item-lock.ts (new: ITEM_LOCK_LAYERS, resolveItemLock, resolveItemLockLazily; not exported from the package entry)",
"packages/metadata-protocol/src/protocol.ts (getEffectiveLock + readLockGateOverlayLayer, getMetaItem, getMetaItemLayered, servedLockState, mergeArtifactProtection, getMetaDiagnostics count, storedRowDocument; docblocks)",
"packages/metadata-protocol/src/protocol.lock-one-resolution.test.ts (new, 1640 cases: 1632 generated rows, completeness, 64-row fold check, 2 lit controls, positions 1 and 2 named)",
"packages/metadata-protocol/src/protocol.lock-org-axis-agree.test.ts (its 64-row pin 1 and lit control moved out; pins 2 to 4 kept)",
"packages/metadata-protocol/src/protocol.metadata-store-outage.test.ts (#5840 healthy leg declares its locked artifact in the registry)",
"scripts/engine-double-contract.pinned.json (+1 row, by the gate's --write)",
".changeset/21738-lock-one-resolution.md (patch @objectstack/metadata-protocol, Clause-②: no)"
],
"deviations": [
"Rows stored under the other spelling (pre-#4432 residue) are in the generated table, but they assert the declared difference instead of getMetaItem = getMetaItemLayered = door. This is per the seat's answer A on PR #21737 and triage's 'not folded in here'; they flip by name when the fallback retires.",
"The completeness check is keyed on the resolver's layers (ITEM_LOCK_LAYERS: artifact, overlay). Triage's row scope, request scope and spelling are the overlay layer's axes; topology, request spelling and operation are listed as caller axes. resolveItemLock.length === 1 is asserted too. The ADR-0048 package axis is not a table axis: it is row selection, and it was measured separately (see findings).",
"Reverse verification: leg A turned pin 2 red alone (pin 3 green). Leg B turned pin 3 red and pin 2 as well, because the layered half of position 1 was the same code ?? overlay derivation. Both legs were predicted as stated before they ran.",
"Read answers change beyond positions 1 and 2, and the changeset states all of them: (a) lockReason / lockSource / lockDocsUrl come from the binding layer only, and are absent when nothing binds; (b) a _lock that only a copy the doors never read declares (a MetadataService copy reloaded by the dev watcher, a package-less runtime registration) no longer reads locked. Because of (b), the #5840 outage test's healthy leg had to declare its locked artifact in the registry, as a loader does: disposition 'complete the declaration', since the old fixture's lock was never visible to the door.",
"git push: 7 pushes to the one branch (the empty-branch probe, then each committed step, per the WIP-push rule, including one merge of origin/main), all fast-forward, no force.",
"The PR carries labels documentation, size/l, tests and tooling from the path labeler, not from this run. This run wrote only the PR assignee."
],
"mcp_calls": "0",
"api_writes": "3 REST writes, each through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft PR 21759; 14937 bytes sent and stored, identical on read-back); (2) label-write --assign os-project-manager -> POST /repos//issues/21759/assignees (read back matches); (3) this os-dev-report comment -> POST /repos//issues/21738/comments. Plus git push (not a REST write).",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: named producer: SysMetadataRepository.put keys a row by (organization, type, name, package_id) under ADR-0048, reached from saveMetaItem with a packageId (REST ?package=); measured on the real protocol reads and _lock gate over an engine double at 60232c3, not over HTTP · evidence: one env-wide package-less view row and one package A row of the same name. getMetaItem / getMetaItemLayered with packageId A serve and report A's row, while the gate's overlay read (readLockGateOverlayLayer -> findServedOverlayRow with no packageId) takes whichever row findOne returns first. With the package-less row first and A's row declaring _lock 'full', both reads say full / editable false, yet a save naming package A is admitted. With A's row declaring none and the package-less row 'full', the reads say none / editable true while the door refuses. 2 of 3 arrangements split; this PR leaves row selection unchanged. Contract text: ADR-0010 §3.3 'full' = overlay writes rejected; servedLockState's contract that the flags report the doors' verdict. Seam: spec:MetadataProtectionFields._lock → runtime:ObjectStackProtocolImplementation.getEffectiveLock (package-agnostic overlay read) vs getMetaItem package-scoped findServedOverlayRow · dedupe words: lock gate packageId, findServedOverlayRow package_id prefer-local lock, package-scoped row lock door, ?package= save locked row",
"carrier: 承接者:无 · noted, not filed (in PR 21759 Acceptance notes): a _lock that only a MetadataService copy reloaded by the dev watcher after boot declares (the registry's boot-time artifact is stale until restart) was reported locked by the reads while the doors admitted the write; it now reads as the doors answer. This is dev-watcher staleness of the registry the doors read, with no production producer measured."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 5, 2026
Landed: PR #21759 →
b7a13c762fonmain(merged 2026-10-04T17:03Z through the merge queue, entered 2026-10-04T16:31Z), verified at 2026-10-05T00:00Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.origin/mainas a single-parent commit. Its diffstat is the reviewed one: 7 files, +828/-122.main.packages/metadata-protocol/src/item-lock.tsdefinesITEM_LOCK_LAYERSandresolveItemLock.protocol.tscalls it from the_lockgate, both item reads,servedLockState,mergeArtifactProtectionand the diagnostics count.Fixes #21738closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body._lock: 'none'under a stored locked row reads locked on both reads (position 1).GET /api/v1/meta/:type/:name/layersreports the stored row's lock when the code layer declares none (position 2).Generated by Claude Code
fix(metadata-protocol): one item-lock resolution for the _lock gate, …
fix(metadata-protocol)!: an item's lock is the strictest lock among t…