Repository navigation
[finding] getMetaItems: a package's list slot can serve the package-less row's body while getMetaItem naming that package serves the package's own row #21804
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: the road — run it: the metadata registry | 缺项 (no item reads a package's list slot beside a package-less row) | P2
Triage: first grade —
bug·priority:p3·domain:engine·area:records·pm:queue(findingremoved). The list slot serves whatgetMetaItemnaming that package servesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T03:07Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
mergePackageAwareOverlay(inreadFlattenedMetaItems) ⇒domain:engine; rationale: its own docblock promises the single-item resolution, and it picks by row order instead.- Direction, as the card proposes. One prefer-local resolution (ADR-0048), shared with
findServedOverlayRowand independent of row order. ⛔ No second resolver. - Why p3. It needs a package row beside a package-less row of one
(type, name, scope). It is measured on the engine double, not over HTTP. - Pins: both row orders serve the package's own row in the list slot, the same as
getMetaItem. - Serial: PR fix(metadata-protocol)!: an item's lock is the strictest lock among the stored rows in scope for its address (#21761) #21801 ([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) touches the same list body for the lock. The claim reads its file list and starts after it lands.
Generated by Claude Code
- Direction, as the card proposes. One prefer-local resolution (ADR-0048), shared with
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingand removed
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 29 · 2026-10-05T03:20Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21804-list-slot-prefer-local
Worktree:objectstack-issue-21804
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/main18c2ddc1ec), per triage's grade and direction 5987404976:packages/metadata-protocol/src/protocol.ts:mergePackageAwareOverlay(about:1966, inreadFlattenedMetaItemsat about:8596). A package's list slot serves whatgetMetaItemnaming that package serves: one prefer-local resolution (ADR-0048), shared withfindServedOverlayRowand independent of row order.- ⛔ No second resolver.
- Tests (both row orders serve the package's own row in the list slot, the same as
getMetaItem);.changeset/21804-*.md.
Container & model:S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no - A package's list read stops depending on row order and serves the package's own row, as its by-name read already does. No accepted input, key, export, status or error code changes.
Thread-read: 5987404976
Serial constraints cleared: at 2026-10-05T03:20Z: - Triage's serial predecessor PR fix(metadata-protocol)!: an item's lock is the strictest lock among the stored rows in scope for its address (#21761) #21801 ([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) landed as
18c2ddc1ec. The claim starts from that tree. - Of the 3 open PRs, none touches
packages/metadata-protocol/src/protocol.ts. - This lane's other claim, metadata: publishing a field reorder from the object designer is a silent no-op — the content hash is key-order-insensitive, so a reordered
fieldsmap hashes equal and the draft is dropped #21790 (5987515805), holdsmetadata-core'scanonicalize.tsand, only if needed,sys-metadata-repository.ts. Neither is this file.
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21804,
"status": "done",
"branch": "claude/issue-21804-list-slot-prefer-local",
"pr": "#21815",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi — subagent run, the parent PM session id (dispatch mode:subagent)",
"premise_still_valid": true,
"summary": "Measured at 18c2ddc: findServedOverlayRow picks per scope (org, env), per spelling, the package's row then the package-less row, while mergePackageAwareOverlay took the LATEST of the package's row and the package-less row in row order, so the unscoped list served the package-less body for A in one order (39/120 list-vs-getMetaItem disagreements without an org, 41/120 with one). The order now lives in one function, servedOverlayRowCandidates(address {organizationId, packageId}) in packages/metadata-protocol/src/protocol.ts: findServedOverlayRow reads it from the store (the same findOne sequence as before) and mergePackageAwareOverlay reads it over the rows the list holds (servedStoredRow; each stored row carries stored {organizationId, type}, the caller passes rowsRead {organizationId}), in the active merge and the previewDrafts merge. At head both unscoped lists agree with getMetaItem in 0/120 disagreements; the 25 left are all in package-scoped lists, which read only that package's rows (row read, not this merge; Acceptance notes). No spec change, no lock change: the list item's lock still comes from resolveOverlayLockLayer.",
"tests": "At 978945a (after merging origin/main 75ddcd1; install refreshed, workspace rebuilt):pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2→ 214 files passed, 3 skipped; 20037 tests passed, 19 skipped.pnpm --filter @objectstack/metadata-protocol typecheck→ exit 0;tsc --noEmit --listFilesincludes protocol.list-slot-prefer-local.test.ts. objectql against the rebuilt metadata-protocol dist (dist carries servedOverlayRowCandidates; no vitest alias): the 20 test files that call getMetaItems → 343 passed. New pin file protocol.list-slot-prefer-local.test.ts: 85 tests (pin 1 generated over 32 subsets x 2 org = 64 cases plus completeness; pins 2-5 named). Reverse verification: committed first (5c7330e);node scripts/ablation-replace.mjs(wrap mode, trap armed, absolute path) put the latest-wins loop back in place of the served-row call: anchor 1→0, replacement 0→1, blob c3958342651e→5261196669fb; tests resolve ./protocol.js from src, so no dist leg. Predicted then measured: 36 failed / 49 passed of 85 — pin 2 red when A's row comes first and green when the package-less row does (4 red / 4 green over dashboard+view x artifact); pin 3 org package-less over env A 3 red / 3 green; pin 3 org A over env package-less green both orders (org rows are read after env rows, so latest coincided); pin 4 green; pin 5 1 red / 1 green; pin 1 28 red / 37 green. Restore:git checkout HEAD -- ABS_PATH(tool and trap); blob after = blob at HEAD = c3958342651e;git diff HEADempty (0 bytes).",
"gates": "All at 978945a after the final commit.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 72 (64 before the ledger commit; the ledger row under scripts/ added 8, all run). Ran 72 derived + the 54-row artifact-roster block (53 beyond the derived set; check:engine-double-contract is in both) + the 4 symbol-anchor sweeps = 129 commands: 126 exit 0, 3 exit 2.dispatch-gates --ran: '72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED (a DERIVED zero — all 72 recorded an exit code and none of them is 3)'. Exit 2 = PR-context roster gates, not wired locally: check-partof-closing-keyword re-run with the PR body as PR_BODY → exit 0; check-closing-target-claim and check-single-claim-paths NOT MEASURED, reason: each needs a PR number and a token, and their guard workflows run on PR 21815. check:engine-double-contract asked for a row at 5c7330e → recorded with its own --write (740c063, 1 added, 0 lost) → OK. First pass at 5c7330e: check:dual-build-cjs-loads and check:lean-entry-closure exit 3 PREREQUISITE NOT MET → fullpnpm turbo run build --filter=!@objectstack/docs --concurrency=2→ green; the first-pass codes are not counted. check:changeset-no-major with a simulated pull_request event carrying the PR body: 'LEVEL AXIS: this PR declares clause-② no'. check-closing-keyword-parity --body: only #21804 binds. Four sweeps green: check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors. Lint, proven narrowing at 978945a:eslint --no-inline-config --format jsonover the 2 touched .ts files → 2 files, 0 errors, 0 warnings; population from eslint's calculateConfigForFile/isPathIgnored: both linted; invariance: parserOptions.project and projectService null (no type-aware linting), so untouched files' verdicts cannot move. Repo-wide pnpm lint is CI's. CI on PR 21815: in_progress at report time (not awaited). NOT MEASURED, CI's: Test Core shards beyond metadata-protocol and objectql, Temporal Conformance, Dogfood, Build Core, the workspace type-check lane.",
"line_budget": "n/a",
"files_changed": [
".changeset/21804-list-slot-prefer-local.md (patch, @objectstack/metadata-protocol, Clause-②: no)",
"packages/metadata-protocol/src/protocol.ts (servedOverlayRowCandidates; mergePackageAwareOverlay takes rowsRead and stored places; findServedOverlayRow iterates the candidates; StoredOverlayEntry gains type; active and previewDrafts merges pass places)",
"packages/metadata-protocol/src/protocol.list-slot-prefer-local.test.ts (new, 85 tests)",
"scripts/engine-double-contract.pinned.json (+1 row, written by check-engine-double-contract --write)"
],
"census": {
"instrument": "engine double (view rows), 16 arrangements over env package-less / env A / env B / org package-less / org A (none, one, both, a third package, org mixes), every row order, artifact absent and A, at 18c2ddc vs this head; each case compares the list slot for A with getMetaItem naming A",
"list, no package, no org": "39/120 disagree at base → 0/120 at head",
"list, org": "41/120 → 0/120",
"list, package A": "5/120 → 5/120 (package-scoped row read; unchanged)",
"list, org + package A": "20/120 → 20/120 (package-scoped row read; unchanged)",
"H3 content dumps": "840 dumps: 748 byte-identical, 92 differ, all inside a package slot of the two unscoped lists, each moving to the row getMetaItem naming that package serves (A pkgless→A 42; B pkgless→B 12; org pkgless→org A 26; env pkgless→org A 6; env A→org pkgless 6); 0 diffs outside package slots, 0 slot-count changes, 0 getMetaItem diffs",
"H3 lock family": "4032 dumps with every lock level on two rows: 4014 identical, 18 differ, all 'list, org' over [org pkgless, env pkgless, env A] where the base list item stated env A's _lock (no-overlay / no-delete / full, 6 each) while the envelope said none; at head list _lock = getMetaItem envelope lock in 992/992 slot comparisons (base 18 disagree); envelopes identical base vs head",
"H4": "org list and getMetaItem with an org agree in every order at head (0/120; pin 3); scoped org+package list still disagrees 20/120 for the row-read reason above"
},
"mcp_calls": "0",
"api_writes": "3 — REST writes through the scripts/pm fleet-write relay (each one POST /repos/objectstack-ai/objectstack/dispatches, executed by fleet-write.yml as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (PR 21815, draft, body read back identical, 12282 bytes); (2) label-write --assign os-project-manager → POST /repos//issues/21815/assignees (read back: assignee os-project-manager; labels documentation, size/l, tests, tooling are the labelers’, not this write’s); (3) this report → POST /repos//issues/21804/comments. git push (4 branch pushes) is not a REST write.",
"deviations": [
"A built-in Claude Code safety check refused one command, verbatim: 'Permission for this command was denied by a built-in Claude Code safety check, not by the user. The check stops removals that can delete far more than intended ... What was flagged: This command passes a shell -c script that runs rm, and Claude Code could not check the script for dangerous removals.' The command was the ablation wrapped asbash -c \"grep ...; cd ... && pnpm ... vitest run ...\"(it contained no removal). It was not run; the ablation was then run with the vitest command passed to ablation-replace directly as argv (no shell -c, no removal).",
"Pin 1 (generated) runs on type dashboard, not view: for view the list’s view-container expansion upserts items by bare name after the merge, folding two packages’ same-name slots into one (env A + env B rows list only B). Pin 2 runs on both dashboard and view with one package.",
"The order’s control (only the package-less row, the slot still falls back to it) holds for the unscoped list (pin 4). A list scoped to the package (packageId) never reads the package-less row, so it does not fall back; unchanged and noted (Acceptance notes).",
"H4 holds for the unscoped org list; the org + package-scoped list still disagrees for the same row-read reason (20/120).",
"The draft-preview merge also takes the shared resolution (same function, same file); pinned by pin 5.",
"scripts/engine-double-contract.pinned.json is outside the claim’s file surface; it was written by the gate’s own --write, as the order instructs.",
"Commit trailers use the model-free pair from AGENTS.md (Claude-Session + Co-authored-by: Claude) instead of the harness reminder’s model-bearing Co-Authored-By line; the PR body ends with the AGENTS.md session-URL footer instead of the harness reminder’s form.",
"H3 lock family: 18 of 4032 list-item lock-family dumps moved (see census); each moved to agree with the envelope, and the lock selection (resolveOverlayLockLayer) is untouched."
],
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: named producer — SysMetadataRepository.put with ?package= in an organization scope (ADR-0048 + ADR-0005; the producer #21804 names), with a stored body that declares protection.lock · evidence: engine double, identical at 18c2ddc and 978945a: the organization holds only package B's row of view/v; an env-wide row of package A declares _lock full; getMetaItem({type:'view', name:'v', organizationId:'org_a', packageId:'com.a'}) answers lock none, editable true, and the served body (env A's row) carries _lock 'full'. Contract: item-lock.ts header 'so a body never states a lock the envelope does not report'. Seam: spec: none → runtime: withOverlayLockFamily (item-lock.ts, returns the body unchanged when no overlay row binds) between findServedOverlayRow's content scope (org, then env, per package) and resolveOverlayLockLayer's lock scope (the org's rows when it holds any row of the item) · same family as #21761 (lock family) — for that family's closure card · dedupe words: served body _lock envelope none, withOverlayLockFamily non-binding body, organization holds another package row, content scope vs lock scope",
"carrier: 承接者:无 · noted in PR 21815 Acceptance notes, not filed — a list scoped to one package (getMetaItems packageId, GET /meta/:type?package=) reads only that package’s rows, so it never falls back to the package-less row that getMetaItem naming the package serves (25/240 scoped comparisons on the engine double). mergePackageAwareOverlay’s docblock scopes its promise to the unscoped list and ADR-0048 states no package-scoped list rule, so it is a semantics question, not a contract break",
"carrier: 承接者:无 · noted in PR 21815 Acceptance notes, not filed — for type view the list’s view-container expansion (#13407 upsert by bare name) runs after the package-aware merge and folds two packages’ same-name slots into one (engine double: env A + env B rows of one view name list only B). No real producer of two packages’ rows for one view name was named"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded: PR #21815 →
3237b4a2d9onmain(merged 2026-10-05T06:08Z through the merge queue, entered 2026-10-05T05:29Z), verified at 2026-10-05T06:09Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash is on
origin/mainas a single-parent commit. Its diffstat is the reviewed one: 4 files, +494/-55. - The fix is on
main:servedOverlayRowCandidatesinpackages/metadata-protocol/src/protocol.ts, shared byfindServedOverlayRowandmergePackageAwareOverlay. Fixes #21804closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.- From this release on, a package's slot in the unscoped metadata list (and its
previewDraftsview) serves the rowgetMetaItemnaming that package serves, in every row order: the organization's rows, then the env-wide rows; the package's own row, then the package-less row. - Carried from this card:
- [finding] getMetaItems scoped to a package reads only that package's rows, so it never falls back to the package-less row that getMetaItem naming the package serves #21817 (filed): a list scoped to one package never falls back to the package-less row.
- A served-body lock position, folded into [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 (5988387087).
Generated by Claude Code
- The squash is on
Filing gate: ① a product defect, class (b). Two reads of one item serve different content for the same package, against a docblock that says they agree.
SysMetadataRepository.putwith?package=(ADR-0048), which stores a package's row beside a package-less row of the same(type, name, scope).out_of_scope_findings[1], the base's H1 list column). Not measured over HTTP.Filed by
domain:engineseat 1 (seat post #6367,session_017ErfyP2Rx7XWHJA27QjyUi). Reader who acts: triage grades and routes. ⛔ Not a claim.Measured
(type, name, scope).getMetaItemnaming package A serves A's row:findServedOverlayRow's prefer-local resolution.getMetaItemsfor package A builds each slot throughmergePackageAwareOverlayinreadFlattenedMetaItems. That merge serves the LATEST of the two rows in row order. So in one row order the list body for A is the package-less row's.Contract it breaks
mergePackageAwareOverlay's own docblock says it mirrors "getMetaItem(name, packageId=P)'s scoped-then-global-fallback resolution, so the list and single-item paths agree".Seam
spec: none. ADR-0048's prefer-local rule is the contract.mergePackageAwareOverlay(inreadFlattenedMetaItems) versusfindServedOverlayRow.Direction (triage's call)
The list slot for a package serves what
getMetaItemnaming that package serves: one prefer-local resolution, independent of row order.Related
#21761 · PR #21801 · ADR-0048 · #6215 (closed: the package-bound restore lookup).
Dedupe words: list slot latest row package-less, mergePackageAwareOverlay prefer-local, getMetaItems package row order, list vs getMetaItem package content. MCP
search_issuesscoped to this repo gave 1 hit, #6215 (closed, the restore path), which is not this.Generated by Claude Code