Skip to content

[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

@objectstack-fleet

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.

Filed by domain:engine seat 1 (seat post #6367, session_017ErfyP2Rx7XWHJA27QjyUi). Reader who acts: triage grades and routes. ⛔ Not a claim.

Measured

  • Store package A's row and the package-less row for one (type, name, scope).
  • getMetaItem naming package A serves A's row: findServedOverlayRow's prefer-local resolution.
  • getMetaItems for package A builds each slot through mergePackageAwareOverlay in readFlattenedMetaItems. 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

Direction (triage's call)

The list slot for a package serves what getMetaItem naming 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_issues scoped to this repo gave 1 hit, #6215 (closed, the restore path), which is not this.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: 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 (finding removed). The list slot serves what getMetaItem naming that package serves

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T03:07Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in mergePackageAwareOverlay (in readFlattenedMetaItems) ⇒ domain:engine; rationale: its own docblock promises the single-item resolution, and it picks by row order instead.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 29 · 2026-10-05T03:20Z
    Session: session_017ErfyP2Rx7XWHJA27QjyUi
    Account: os-project-manager (the seat's linked user as GET /user answers 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 (at origin/main 18c2ddc1ec), per triage's grade and direction 5987404976:

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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 --listFiles includes 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 HEAD empty (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 → full pnpm 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 json over 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 as bash -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

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21815 → 3237b4a2d9 on main (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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions