Repository navigation
[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
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: the road — run it: the metadata registry | 缺项 (no item reads a package-scoped list beside a package-less customization) | P2
Triage: first grade —
bug·priority:p3·domain:engine·area:records·pm:queue(findingremoved). A package-scoped list slot serves whatgetMetaItemnaming that package servesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T05:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
readFlattenedMetaItems's package-scoped read ⇒domain:engine; rationale: it reads only the package's own rows, while the single-item read falls back to the package-less row.- Direction (triage's call, as the card asks): align, don't document a second meaning.
- A slot in
GET /api/v1/meta/:type?package=resolves through the same candidate orderservedOverlayRowCandidatesthe unscoped list andfindServedOverlayRowshare: the package's row, else the package-less row. - A package-less customization of an item the package ships is part of how that item is served. Leaving it out shows the uncustomized body in one read and the customized one in the other.
- ⛔ The scoped list's membership does not grow: it still lists only the items the package ships. Only each slot's content resolves the way
getMetaItemdoes.
- A slot in
- Why p3. It is measured on the engine double (25 of 240 comparisons), not over HTTP. It is the scoped twin of [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, which is also p3.
- Pins: the scoped slot equals
getMetaItemnaming the package, with and without an organization, for both row orders. - Serial: PR fix(metadata-protocol): a package's list slot serves the stored row getMetaItem naming that package serves, in every row order (#21804) #21815 ([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) introduces the shared candidate order. This card starts after it lands.
Generated by Claude Code
- Direction (triage's call, as the card asks): align, don't document a second meaning.
- 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 32 · 2026-10-05T09:44Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21817-scoped-list-fallback
Worktree:objectstack-issue-21817
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/main18fe6815a2), per triage's grade and direction 5988911029:packages/metadata-protocol/src/protocol.ts:readFlattenedMetaItems' package-scoped read (about:8709–:8874,readActiveOverlayRowsat about:9310). A slot inGET /api/v1/meta/:type?package=resolves through the same candidate order the unscoped list andfindServedOverlayRowshare (servedOverlayRowCandidates): the package's row, else the package-less row.- ⛔ The scoped list's membership does not grow: it still lists only the items the package ships. Only each slot's content resolves the way
getMetaItemdoes.
- ⛔ The scoped list's membership does not grow: it still lists only the items the package ships. Only each slot's content resolves the way
- Tests;
.changeset/21817-*.md.
Container & model:S,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no - A package-scoped list slot serves the package-less customization
getMetaItemnaming the package already serves. No accepted input, key, export, status or error code changes, and the list's membership is unchanged.
Thread-read: 5988911029
Serial constraints cleared: at 2026-10-05T09:44Z: - Triage's serial predecessor [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 landed as PR fix(metadata-protocol): a package's list slot serves the stored row getMetaItem naming that package serves, in every row order (#21804) #21815 →
3237b4a2d9. The seat's own hold behind [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 cleared when PR fix(metadata-protocol)!: an item's lock is the strictest among the installed packages that ship its name (#21803) #21844 landed as18fe6815a2. - Of the 11 open PRs, none touches
packages/metadata-protocol/src/protocol.tsoritem-lock.ts(each file list read byfilename, paged). - This lane's other claims hold other packages: identity: the "Link Social Account" action is dead on every boot — it navigates to a GET of the POST-only social sign-in route (404) and offers a fixed provider list regardless of configuration #21849 (
platform-objects'sys-account.object.ts) and [finding] DatabaseLoader.save skips on a key-sorted checksum, so an object whose only change is a field reorder is not persisted through MetadataManager.register #21828 (PR fix(metadata): DatabaseLoader stamps and compares hashSpec(body, type), so a field-reorder-only register is persisted (#21828) #21852,packages/metadata, in the merge queue).
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21817,
"status": "done",
"branch": "claude/issue-21817-scoped-list-fallback",
"pr": "#21871",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi — subagent run, the parent PM session id (dispatch mode:subagent); RESUME of the run a container restart killed",
"premise_still_valid": true,
"summary": "A list scoped to one package (getMetaItems with packageId, GET /api/v1/meta/:type?package=, getMetaItemsForExecution) now serves, in each slot the package ships, the row getMetaItem naming that package serves: the package's own row, else the package-less row, the organization's rows before the env-wide ones, through the one candidate order servedOverlayRowCandidates (H2: no second resolver; servedStoredRow walks it). The package-less rows enter mergePackageAwareOverlay as stand-ins: active rows come from the package-agnostic lock-row read the scoped path already made, filtered back to package_id null (no extra read); drafts from one package-agnostic draft read per scope, only with previewDrafts. A stand-in never seats a slot and stand-ins no layer seated are dropped after the last merge, so membership is unchanged (H3) and the lock is untouched (H4). This resume kept the killed run's commits as they were (e0589b0, ab41401, b029b18, 1d226f1), merged origin/main 9f9510f as b5492dc, re-measured H1-H4, the reverse verification, the package suite and the full gate union at b5492dc, and opened draft PR 21871.",
"tests": "All at b5492dc. Dependency closure (12 packages, spec through metadata) built under os-verify-lock: VERDICT command-exit 0. pnpm --filter @objectstack/metadata-protocol build (check-dts-emitted: 2/2 declared declaration file(s) present) && typecheck (exit 0; tsc --listFiles compiles 218 of the package's test files, both pin files in it) && exec vitest run --maxWorkers=2: 'Test Files 215 passed | 3 skipped (218)', 'Tests 27900 passed | 19 skipped (27919)'; VERDICT command-exit 0. Pin file protocol.scoped-list-fallback.test.ts: 155 tests (pins 1-7). Reverse verification on committed HEAD b5492dc, every restore git checkout HEAD -- PATH proven by blob == HEAD (c96ce0d942) and empty git diff HEAD: (i) base protocol.ts restored whole (blob 182c66778c, the blob at 18fe681 and 9f9510f; on-disk grep -c standInRows 0 vs HEAD 4): 48/155 red (pin1 32, pin2 6, pin4 2, pin5 1, pin6 1, pin7 6), membership pin 3 3/3 green; (ii) leg A, active stand-in read off via scripts/ablation-replace.mjs (anchor 1->0, blob c96ce0d942->1935e0b66f): 47/155 red (pin1 32, pin2 6, pin4 2, pin6 1, pin7 6), pin 3 3/3 green, pin 5 4/4 green; (iii) leg B, draft stand-in read off (blob ->798b96b4a4): 1/155 red (pin 5, A's artifact and a package-less draft), pin 3 and pin 5's membership case green; after restore both pin files 240/240 green. Resolution path: the pin file imports ./protocol.js (relative, vitest resolves src/), no dist on the path, so no rebuild was owed between legs. Narrowed lint: pnpm exec eslint --no-inline-config --format json over the 3 changed TS files: 3 files linted, 0 errors, 0 warnings, none ignored (population: eslint.config.mjs's all-TS/JS block minus NEVER_LINTED); invariance: the config enables no type-aware linting (no parserOptions.project), so the diff cannot move an untouched file's verdict.",
"gates": {
"head": "b5492dce3e",
"derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 5 paths vs merge base 9f9510f, 72 commands; 72 run, 72 exit 0",
"ran": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list: '✓ 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).'",
"artifact_roster": "54 families printed outside the total (37 + 17 self-test only; check:engine-double-contract also derived): all exit 0 except 3 PR-context gates that judged nothing without a PR (exit 2: check-closing-target-claim NOT MEASURED, check-partof-closing-keyword NOT WIRED, check-single-claim-paths NOT WIRED)",
"pr_context_rerun": "with PR 21871's context, all exit 0: '✓ check:closing-target-claim: PR #21871 closes #21817, and each carries a Claim: whose Branch: line names claude/issue-21817-scoped-list-fallback'; '✓ check:partof-closing-keyword: PR #21871 carries no Part-of/closing-keyword contradiction'; '✓ check:single-claim-paths: PR #21871 modifies none of the 1 declared at-most-one-writer path(s)'",
"symbol_anchor_sweeps": "pnpm check:adr-symbol-anchors, pnpm check:scripts-symbol-anchors, pnpm check:spec-docblock-symbol-anchors, pnpm check:adr-anchors: all exit 0",
"new_since_dispatch_list": "derived now from the whole change set, not at dispatch from protocol.ts alone, all exit 0: check-adr-0087-registration (+self-test), check-empty-changeset (+self-test), check-scripts-symbol-anchors (+self-test), release-rehearsal-clone --self-test, release-pending-publish --self-test, check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:engine-double-contract, check:entry-guard, check:objectql-double-limit, check:objectui-changeset, check:parse-guard, check:pm-changeset-deadline-census, check:pnpm-filter-targets, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher",
"engine_double_contract": "the pin file's findOne double has its row in scripts/engine-double-contract.pinned.json; node scripts/check-engine-double-contract.mjs --write leaves the file byte-identical (blob bc77050a7b before, after and at HEAD): '6 seam row(s), 0 added or grown, 0 lost'",
"not_measured": "the 5 path-scheduled CI jobs (Test Core shards, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core) and the 4 CI type-check lanes dispatch-gates names as CI's own; left to CI",
"ci": "one read at 2026-10-05T11:58Z on b5492dc: 31 check runs, 11 success, 3 skipped, 17 in_progress, 0 failure; not waited on"
},
"line_budget": "n/a",
"files_changed": [
"packages/metadata-protocol/src/protocol.ts +111/-21",
"packages/metadata-protocol/src/protocol.scoped-list-fallback.test.ts +449/-0 (new)",
"packages/metadata-protocol/src/protocol.list-slot-prefer-local.test.ts +3/-4 (docblock pointer only)",
"scripts/engine-double-contract.pinned.json +5/-0",
".changeset/21817-scoped-list-fallback.md +13/-0 (@objectstack/metadata-protocol patch, Clause-②: no)"
],
"census": {
"basis": "engine double; every subset of five rows (env package-less, env A, env B, org package-less, org A), every row order (326 orderings), with/without A's artifact, with/without an organization, packages A and B, on view and dashboard; base = protocol.ts blob 182c66778c (18fe681 == 9f9510f), head = b5492dc. PR 21815's 16 arrangements are not listed in its record, so this is their superset",
"H1_slot_vs_getMetaItem": [
{
"request": "no organization",
"package": "A",
"base": "49/587",
"head": "0/587"
},
{
"request": "no organization",
"package": "B",
"base": "0/522",
"head": "0/522"
},
{
"request": "organization",
"package": "A",
"base": "90/636",
"head": "0/636"
},
{
"request": "organization",
"package": "B",
"base": "424/522",
"head": "0/522"
}
],
"H1_note": "identical on view and dashboard; a comparison is a case where the scoped list has a slot for the name",
"H3_membership": "scoped name sets (name and _packageId) differing base vs head: 0 of 5216; slot-count changes 0; 1126 scoped dumps differ, all in the name's slot, 0 outside it, 1126/1126 now equal getMetaItem (B env B -> org package-less 848; A env A -> org package-less 152; A artifact -> env package-less 104; A artifact -> org package-less 22); unscoped list dumps 0 of 2608 differ; getMetaItem dumps 0 of 5216 differ",
"H4_lock": "7830 cases (dashboard, A's artifact, one row at a time declaring no-overlay/no-delete/full, with/without organization): slot lock family (_lock, _lockReason) differs base vs head in 0 of 7830, including the 993 where the served row moved; getMetaItem envelopes 0 differ; head: slot _lock == envelope lock 7830/7830, reason 7830/7830, label 7830/7830 (base label 6837/7830)"
},
"mcp_calls": "0 — no MCP GitHub tool used (reads went through gh api REST reads)",
"api_writes": "3 — REST proxy writes, each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed by the fleet-write relay as: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, PR 21871; read-back 12713/12713 bytes identical); POST /repos//issues/21871/assignees (label-write --assign os-project-manager; read back MATCHES); POST /repos//issues/21817/comments (this os-dev-report). Plus 1 git push (1d226f1..b5492dc), not REST",
"deviations": [
"H1 census set: PR 21815's census names 16 arrangements but its record does not list them, so the 25/240 figure is not re-derivable verbatim; H1 was measured over the superset (all 32 subsets, every order) and reported as such.",
"Base window, first attempt: a mistyped full sha made its own landing check refuse before anything ran (FAILURE: base did not land; restore proven blob == HEAD). Rerun with the correct sha 9f9510f; only the rerun's readings are reported.",
"Gate runner batch 1 overran the foreground cap and the harness moved it to the background; it was waited on in the foreground by its PID (tail --pid) to its exit, and later batches ran with a tighter budget. All 129 result rows point to logs this run wrote after 11:33:35Z.",
"The killed run's scratchpad files (gates.out, glogs-a/, census-*.json, pr-body.md) were present in the same scratchpad directory; none of their contents was used, and pr-body.md was overwritten.",
"Both merge commits on the branch (b029b18 from the killed run, b5492dc from this run) carry git's default merge message without the model-free trailer pair; history not rewritten, as the order says."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted in PR 21871 Acceptance notes, not filed — by triage's ruling the scoped list's membership does not grow: where a package ships nothing of a name its scoped list has no slot, while getMetaItem naming that package still answers the package-less row (census: 63 such cases for A, 218 for B per type, identical at base and head)",
"carrier: 承接者:无 · noted in PR 21871 Acceptance notes, not filed — a package-less stored view container in a scoped list is a stand-in like any row (held back, dropped unless the package seats its name), and its expansions are not served there; unchanged from base, where the scoped list did not read package-less rows at all; not measured against getMetaItem naming the package for an expanded name"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded: PR #21871 →
2e780467b1onmain(merged 2026-10-05T13:14Z through the merge queue, entered 2026-10-05T12:33Z), verified at 2026-10-05T13:15Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash is on
origin/mainas a single-parent commit. Its diffstat is the reviewed one: 5 files, +581/-25. - The fix is on
main. Inpackages/metadata-protocol/src/protocol.ts, a list scoped to a package passes the package-less rows in its scope intomergePackageAwareOverlayas stand-ins, which serve a slot the package seats and never seat one. Fixes #21817closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body.- From this release on (
@objectstack/metadata-protocolpatch),getMetaItems({ type, packageId }),GET /api/v1/meta/:type?package=andgetMetaItemsForExecutionserve in each slot the rowgetMetaItemnaming the package serves. The list's membership and each item's lock are unchanged. - Recorded, not filed (triage's ruling holds membership fixed): a package that ships nothing of a name has no slot for it in its scoped list, and a package-less stored view container is not expanded in a scoped list.
Generated by Claude Code
- The squash is on
- added 2 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a product defect or a semantics gap, class (b), for triage to rule. Two reads of one address serve different content.
sys_metadatarow (an ordinary customization) of an item a package ships, with no row of that package's own.out_of_scope_findings[1]): 25 of 240 package-scoped comparisons disagree. That is 5 of 120 with no organization and 20 of 120 with one. Identical at18c2ddc1ecand at the PR head. 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
getMetaItemnaming package A serves that package-less row: the "scoped-then-global" fallback,findServedOverlayRow.getMetaItemsscoped to package A (packageId,GET /api/v1/meta/:type?package=) reads only A's rows, so its slot for the item never takes the package-less row.Why it is a question, not yet a contract break
mergePackageAwareOverlay's docblock scopes its "the list and single-item paths agree" promise to the unscoped list.getMetaItem(0 of 120 disagreements) and left the scoped list unchanged.Direction (triage's call)
Either the package-scoped list serves what
getMetaItemnaming that package serves (the package's row, else the package-less row, through the same candidate orderservedOverlayRowCandidatesthe unscoped list andfindServedOverlayRownow share), or the scoped list's narrower meaning is stated where a reader finds it.Related
#21804 · PR #21815 · #21761 · ADR-0048 · #1828.
Dedupe words: package-scoped list fallback, getMetaItems packageId package-less row, scoped list vs getMetaItem, readFlattenedMetaItems package row read. MCP
search_issuesscoped to this repo gave 7 hits. #21804 is the unscoped list (in flight); the rest are closed and name other seams (#21334 view containers, #16525 ETag scope, #6215 restore). None names this one.Generated by Claude Code