Repository navigation
Commit 2e78046
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
- .changeset
- packages/metadata-protocol/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
42 | 41 | | |
43 | 42 | | |
44 | 43 | | |
| |||
0 commit comments