fix(metadata-protocol): a package-scoped list slot serves the package-less row getMetaItem naming the package serves - #21871
Conversation
…-less row getMetaItem naming the package serves The package-less rows in scope enter each merge of a package-scoped list as stand-ins: they serve a slot the package seats, by the shared candidate order, and never seat one, so the list's membership is unchanged. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…tem naming the package, and its membership Seven pins: the generated table over every subset and order of five rows, with and without an organization and the package's artifact; named row orders; membership; the MetadataService layer; the draft preview; the view container expansion; the lock family. Plus the patch changeset. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…oped-list-fallback
…e engine-double ledger Written by `node scripts/check-engine-double-contract.mjs --write` (1 row added, 0 lost). Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…oped-list-fallback
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ce1363e58706940e93a0faca2435422362d63b0f && git checkout ce1363e58706940e93a0faca2435422362d63b0f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9f9510f25e6aa65aa61ce3effb42706fabcab92e b5492dce3ecd1614a131f1f9afdea680f646b792 && git checkout -B drift-repro 9f9510f25e6aa65aa61ce3effb42706fabcab92e && git merge --no-ff b5492dce3ecd1614a131f1f9afdea680f646b792
node scripts/docs-audit/affected-docs.mjs --json 9f9510f25e6aa65aa61ce3effb42706fabcab92e |
ACCEPT (seat review) — PR #21871 at head
|
Fixes #21817
Clause-②: no
What changes
A slot in a list scoped to one package (
getMetaItems({ type, packageId }),GET /api/v1/meta/:type?package=, andgetMetaItemsForExecution, which reads through the same method) now serves the rowgetMetaItemnaming that package serves: the package's own row, else the package-less row (ADR-0048), the organization's rows before the env-wide rows (ADR-0005). The list's membership is unchanged. It still lists only the items the package ships.readFlattenedMetaItemsinpackages/metadata-protocol/src/protocol.ts, and the merge it calls,mergePackageAwareOverlay. Nopackages/specchange, no export change, no new error code.standInon a record). The merge picks a slot's stored row throughservedStoredRow, which walksservedOverlayRowCandidates, the one orderfindServedOverlayRowand the unscoped list already share. There is no second resolver.readActiveOverlayRows({ type }, orgId)for the lock (the lock-row read). That set is filtered back to the rows whosepackage_idis null (standInRows).previewDraftsand a package: one package-agnostic draft read per scope, filtered back to the package-less rows (standInDraftRecords).unseatedset. A later layer (the draft preview, the MetadataService listing, the view-container expansion) may still seat it. Whatever is still recorded after the last merge is dropped.resolveOverlayLockLayerfrom every row in scope (PR fix(metadata-protocol)!: an item's lock is the strictest among the installed packages that ship its name (#21803) #21844).H1: the scoped read at the base
At
18fe6815a2. Theprotocol.tsblob there is182c66778c, the same blob as at9f9510f25e, the merge base of this head. WithpackageIdset,readFlattenedMetaItems:listItems(type, packageId));readActiveOverlayRows(request, orgId), whosequeryByOrgputspackage_id = packageIdin thewhere. That is the package's own rows only, env-wide and the organization's;lockRows);package_id = packageIdonly, and keeps only the MetadataService items stamped with the package.Census: the scoped slot against
getMetaItemnaming the package, base vs this headEngine double. PR #21815's census names "16 arrangements over five rows" but does not list them, so this census runs their superset:
view(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) and ondashboard.A comparison is a case where the scoped list has a slot for the name.
The figures are identical on
viewand ondashboard. The base column is the card's defect class (25 of 240 on PR #21815's subset), measured over every arrangement.H3: membership unchanged, nothing else moved
_packageId) differs base vs head in 0 of 5216 lists. Slot-count changes: 0.getMetaItemnaming the package (1126 of 1126):getMetaItemdumps: 0 of 5216 differ.H4: the lock stays the item's
Lock census on
dashboard, with A's artifact, every subset and order, one row at a time declaringno-overlay,no-deleteorfull, with and without an organization: 7830 cases._lock,_lockReason) differs base vs head in 0 of 7830. That includes the 993 cases where the served row moved.getMetaItemenvelopes: 0 differ._lockequals the envelope lock in 7830 of 7830, the reason equals the envelope item's in 7830 of 7830, and the label equals the envelope item's in 7830 of 7830. At base the label matched in 6837 of 7830.Pins
protocol.scoped-list-fallback.test.ts, 155 tests:getMetaItemnaming the package serves the same. Where it ships nothing, the scoped list has no slot.dashboardandview); the organization's package-less row over an env-wide row of A; the organization's row of A over the env-wide package-less row.getMetaItem's envelope, for three lock levels on either row.protocol.list-slot-prefer-local.test.ts: its docblock's "out of this card" paragraph now points at the new pin file. No test changed.Reverse verification
On the committed head
b5492dce3e. Every restore isgit checkout HEAD -- PATH, proven by the blob equal to HEAD's and an emptygit diff HEAD.protocol.tsrestored whole (blob182c66778c)standInRowsemptied)standInDraftRecordsemptied)scripts/ablation-replace.mjs: anchor 1 → 0, blobc96ce0d942→1935e0b66f(A) and798b96b4a4(B). Each was restored toc96ce0d942, HEAD's blob, withgit diff HEADempty. After the restore both pin files ran 240 of 240 green../protocol.js, a relative import that vitest resolves tosrc/. Nodist/is on the path, so no rebuild was owed between the legs.Tests (at
b5492dce3e)specthroughmetadata) was built first.pnpm --filter @objectstack/metadata-protocol build:check-dts-emitted2/2 declared declaration files present.pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.tsc --listFilescompiles 218 of the package's test files, both pin files among them.pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: Test Files 215 passed, 3 skipped (218). Tests 27900 passed, 19 skipped (27919).Gates (at
b5492dce3e)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths: 5 paths against merge base9f9510f25, 72 commands. All 72 ran and exited 0.--ranprinted: "✓ dispatch-gates --ran: 72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED".check-closing-target-claim,check-partof-closing-keywordandcheck-single-claim-paths. They are re-run against this PR in its report on the card.check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchorsandcheck:adr-anchors.protocol.tsalone. All exited 0:check-adr-0087-registration,check-empty-changesetandcheck-scripts-symbol-anchors, each with its self-test;release-rehearsal-clone --self-testandrelease-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-changesetandcheck:parse-guard;check:pm-changeset-deadline-census,check:pnpm-filter-targets,check:query-options-erasure,check:type-check-coverage,check:type-check-debtandcheck:where-matcher.check:engine-double-contract: the pin file'sfindOnedouble has its row inscripts/engine-double-contract.pinned.json. Re-runningnode scripts/check-engine-double-contract.mjs --writeleaves the file byte-identical (blobbc77050a7b).pnpm lintis CI's run:pnpm exec eslint --no-inline-config --format jsonover the 3 changed TypeScript files linted 3 files with 0 errors and 0 warnings.eslint.config.mjs's block for every TypeScript and JavaScript file, minus the build directories. None of the three is ignored.parserOptions.project), so this diff cannot move any untouched file's verdict.dispatch-gatesnames as CI's own. They are left to CI.Changeset
.changeset/21817-scoped-list-fallback.md:@objectstack/metadata-protocolpatch,Clause-②: no. It says that a package-scoped list now serves a package-less customization of an item the package ships, asgetMetaItemnaming the package does, and that its membership is unchanged.Acceptance notes
getMetaItemnaming that package still answers a package-less row of the name (the by-name fallback). The census counts 63 such cases for A and 218 for B per type, identical at base and head.sys_metadataread per scope (the package-agnostic drafts). The active arm makes none, because it reuses the lock-row read.Files
packages/metadata-protocol/src/protocol.ts: the fix.packages/metadata-protocol/src/protocol.scoped-list-fallback.test.ts: the pins (new).packages/metadata-protocol/src/protocol.list-slot-prefer-local.test.ts: docblock pointer only.scripts/engine-double-contract.pinned.json: one ledger row for the new pin file's double..changeset/21817-scoped-list-fallback.md.Generated by Claude Code