Skip to content

Commit 2e78046

Browse files
fix(metadata-protocol): a package-scoped list slot serves the package-less row getMetaItem naming the package serves (#21871)
Fixes #21817 Clause-②: no ## What changes A slot in a list scoped to one package (`getMetaItems({ type, packageId })`, `GET /api/v1/meta/:type?package=`, and `getMetaItemsForExecution`, which reads through the same method) now serves the row `getMetaItem` naming 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. - **Landing point**, as the claim said: `readFlattenedMetaItems` in `packages/metadata-protocol/src/protocol.ts`, and the merge it calls, `mergePackageAwareOverlay`. No `packages/spec` change, no export change, no new error code. - **One candidate order (H2).** The package-less rows enter the list's merges as stand-ins (`standIn` on a record). The merge picks a slot's stored row through `servedStoredRow`, which walks `servedOverlayRowCandidates`, the one order `findServedOverlayRow` and the unscoped list already share. There is no second resolver. - **Where the package-less rows come from (H2).** - Active rows: no extra read. The scoped path already reads the package-agnostic set `readActiveOverlayRows({ type }, orgId)` for the lock (the lock-row read). That set is filtered back to the rows whose `package_id` is null (`standInRows`). - Draft preview, only with `previewDrafts` and a package: one package-agnostic draft read per scope, filtered back to the package-less rows (`standInDraftRecords`). - **Membership (H3).** A stand-in serves a slot the package seats and never seats one itself. A slot that only stand-ins reach is held back and recorded in a per-call `unseated` set. 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. - **The lock (H4).** Untouched. It is still selected by `resolveOverlayLockLayer` from every row in scope (PR #21844). ## H1: the scoped read at the base At `18fe6815a2`. The `protocol.ts` blob there is `182c66778c`, the same blob as at `9f9510f25e`, the merge base of this head. With `packageId` set, `readFlattenedMetaItems`: - lists the registry's items of the package (`listItems(type, packageId)`); - reads its stored rows with `readActiveOverlayRows(request, orgId)`, whose `queryByOrg` puts `package_id = packageId` in the `where`. That is the package's own rows only, env-wide and the organization's; - also reads the package-agnostic set, but only for the lock (`lockRows`); - merges the package's rows over its items. A package-less row never reaches the merge, so a slot serves the package's row or its artifact; - previews drafts with `package_id = packageId` only, and keeps only the MetadataService items stamped with the package. ## Census: the scoped slot against `getMetaItem` naming the package, base vs this head Engine double. PR #21815's census names "16 arrangements over five rows" but does not list them, so this census runs their superset: - every subset of the same five rows (env package-less, env A, env B, org package-less, org A); - every row order (326 orderings); - with and without A's artifact, and with and without an organization; - for packages A and B, on `view` (as PR #21815) and on `dashboard`. A comparison is a case where the scoped list has a slot for the name. | request | package | disagreements at base | at this head | |---|---|---|---| | no organization | A | 49 / 587 | 0 / 587 | | no organization | B | 0 / 522 | 0 / 522 | | organization | A | 90 / 636 | 0 / 636 | | organization | B | 424 / 522 | 0 / 522 | The figures are identical on `view` and on `dashboard`. 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 - **Membership.** Over the same cases, the scoped list's name set (name with `_packageId`) differs base vs head in 0 of 5216 lists. Slot-count changes: 0. - **Changed lists.** 1126 scoped-list dumps differ, all in the one slot for the name. 0 differences outside it. Every changed slot now equals `getMetaItem` naming the package (1126 of 1126): - B: env-wide row of B → org-scoped package-less row, 848; - A: env-wide row of A → org-scoped package-less row, 152; - A: A's artifact → env-wide package-less row, 104; - A: A's artifact → org-scoped package-less row, 22. - **Other reads.** Unscoped list dumps: 0 of 2608 differ. `getMetaItem` dumps: 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 declaring `no-overlay`, `no-delete` or `full`, with and without an organization: 7830 cases. - The scoped slot's lock family (`_lock`, `_lockReason`) differs base vs head in 0 of 7830. That includes the 993 cases where the served row moved. - `getMetaItem` envelopes: 0 differ. - At this head the slot's `_lock` equals 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: 1. Generated: every subset of the five rows, every row order, with and without an organization and A's artifact. For packages A and B: where the package ships the name, the scoped slot serves the row an oracle written from the rule names, and `getMetaItem` naming the package serves the same. Where it ships nothing, the scoped list has no slot. 2. Named, both row orders: A's artifact beside the env-wide package-less row (on `dashboard` and `view`); 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. 3. Membership: a package-less row of a name A does not ship adds no slot. The scoped list lists the same names with and without it, while the unscoped list still serves that row. 4. The MetadataService layer: a package-less row stands in for A's runtime item. A control shows no slot without the runtime item, and a lit control serves the runtime item alone. 5. The draft preview: a package-less draft stands in for A's slot. A's own draft wins over it in both orders. A package-less draft of a name A does not ship previews no slot. 6. The view-container expansion: a package-less row of a name A's stored container expands is served ahead of the expansion, stamped A. A lit control serves the expansion without it. 7. The lock: where the served row moves to the organization's package-less row, the slot's lock family equals `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 is `git checkout HEAD -- PATH`, proven by the blob equal to HEAD's and an empty `git diff HEAD`. | arm | red | membership pin 3 | other | |---|---|---|---| | base `protocol.ts` restored whole (blob `182c66778c`) | 48 / 155: pin 1 32, pin 2 6, pin 4 2, pin 5 1, pin 6 1, pin 7 6 | 3 / 3 green | controls green | | leg A: the active stand-in read off (`standInRows` emptied) | 47 / 155: pin 1 32, pin 2 6, pin 4 2, pin 6 1, pin 7 6 | 3 / 3 green | pin 5 4 / 4 green | | leg B: the draft stand-in read off (`standInDraftRecords` emptied) | 1 / 155: pin 5, A's artifact and a package-less draft | 3 / 3 green | pin 5's membership case green | - Both legs went through `scripts/ablation-replace.mjs`: anchor 1 → 0, blob `c96ce0d942` → `1935e0b66f` (A) and `798b96b4a4` (B). Each was restored to `c96ce0d942`, HEAD's blob, with `git diff HEAD` empty. After the restore both pin files ran 240 of 240 green. - The pin file imports `./protocol.js`, a relative import that vitest resolves to `src/`. No `dist/` is on the path, so no rebuild was owed between the legs. ## Tests (at `b5492dce3e`) - The dependency closure (12 packages, `spec` through `metadata`) was built first. - `pnpm --filter @objectstack/metadata-protocol build`: `check-dts-emitted` 2/2 declared declaration files present. - `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0. `tsc --listFiles` compiles 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 base `9f9510f25`, 72 commands. All 72 ran and exited 0. `--ran` printed: "✓ dispatch-gates --ran: 72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED". - The artifact-roster block printed outside that total: 54 families, 37 plus 17 that are self-test only. All exited 0 except three PR-context gates, which judged nothing without a PR (exit 2, "NOT WIRED" or "NOT MEASURED"): `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths`. They are re-run against this PR in its report on the card. - The four symbol-anchor sweeps exited 0: `check:adr-symbol-anchors`, `check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors` and `check:adr-anchors`. - Families derived now but not at dispatch, because they come from the whole change set rather than `protocol.ts` alone. All exited 0: - `check-adr-0087-registration`, `check-empty-changeset` and `check-scripts-symbol-anchors`, each with its self-test; - `release-rehearsal-clone --self-test` and `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` and `check:parse-guard`; - `check:pm-changeset-deadline-census`, `check:pnpm-filter-targets`, `check:query-options-erasure`, `check:type-check-coverage`, `check:type-check-debt` and `check:where-matcher`. - `check:engine-double-contract`: the pin file's `findOne` double has its row in `scripts/engine-double-contract.pinned.json`. Re-running `node scripts/check-engine-double-contract.mjs --write` leaves the file byte-identical (blob `bc77050a7b`). - Lint, narrowed, because `pnpm lint` is CI's run: `pnpm exec eslint --no-inline-config --format json` over the 3 changed TypeScript files linted 3 files with 0 errors and 0 warnings. - Population: `eslint.config.mjs`'s block for every TypeScript and JavaScript file, minus the build directories. None of the three is ignored. - Invariance: the config enables no type-aware linting (no `parserOptions.project`), so this diff cannot move any untouched file's verdict. - Not run locally: the path-scheduled CI jobs and the type-check lanes `dispatch-gates` names as CI's own. They are left to CI. ## Changeset `.changeset/21817-scoped-list-fallback.md`: `@objectstack/metadata-protocol` `patch`, `Clause-②: no`. It says that a package-scoped list now serves a package-less customization of an item the package ships, as `getMetaItem` naming the package does, and that its membership is unchanged. ## Acceptance notes - **Membership is unchanged, by triage's ruling.** Where a package ships nothing of a name, its scoped list has no slot for it, while `getMetaItem` naming 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. - **Package-less view containers in a scoped list.** The view-container expansion still expands only the package's own stored containers. A package-less stored container is a stand-in like any other row: it is held back, and dropped unless the package seats its name. At base the scoped list did not read package-less rows at all. - **Cost.** A draft preview scoped to a package makes one more `sys_metadata` read per scope (the package-agnostic drafts). The active arm makes none, because it reuses the lock-row read. - **Not measured over HTTP.** Engine double only, which is the card's own measurement basis. ## 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](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 124533b commit 2e78046

5 files changed

Lines changed: 581 additions & 25 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): a package-scoped metadata list serves a package-less customization of an item the package ships, as `getMetaItem` naming the package does (#21817)
6+
7+
Clause-②: no
8+
9+
ADR-0048 lets one item hold a package-less stored `sys_metadata` row, an ordinary customization, beside the package's own artifact or row. `getMetaItem` naming the package serves the package's own row, else the package-less row, the organization's rows before the env-wide rows (ADR-0005). A list scoped to the package (`getMetaItems({ type, packageId })`, `GET /api/v1/meta/:type?package=`, and the `getMetaItemsForExecution` view of it) read only the package's own rows. So it served the package's artifact over a package-less customization of it, and an env-wide row of the package over the organization's package-less row, while `getMetaItem` naming the package served the customization.
10+
11+
Now each slot of a package-scoped list resolves the way `getMetaItem` naming the package does: the package's own row, else the package-less row, by the same order the unscoped list uses. This holds over a registry item, a MetadataService item and a view the package's stored container expands, and in the `previewDrafts` list, where a package-less draft stands in when the package has none. The list's membership is unchanged: it still lists only the items the package ships, and a package-less row of an item the package does not ship adds no item to it. The lock each item reports is unchanged.
12+
13+
No key, export, status or error code changes.

‎packages/metadata-protocol/src/protocol.list-slot-prefer-local.test.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,9 @@
3535
* 5. The draft preview: A's draft and a package-less draft, both orders. The
3636
* previewed slot is A's draft, as `getMetaItem` with `previewDrafts`.
3737
*
38-
* Out of this card: a list scoped to a package (`packageId` on the list
39-
* request) reads only that package's rows, so a package-less row never
40-
* reaches its merge. That is the row read, not the merge, and these pins do
41-
* not cover it.
38+
* A list scoped to a package (`packageId` on the list request) is pinned in
39+
* `protocol.scoped-list-fallback.test.ts` (#21817): its package-less rows
40+
* reach the merge as stand-ins, so its slot takes the same order.
4241
*/
4342
import { describe, expect, it } from 'vitest';
4443
import { assertEngineFindOnePredicate, isCodeArtifactBody } from '@objectstack/metadata-core';

0 commit comments

Comments
 (0)