You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(spec): declare IMetadataService.loadManyKeyed beside its plural-read siblings (#19609)
Fixes#15385
Declares `loadManyKeyed?` on `IMetadataService`, deletes the local
structural type the ObjectQL governance audit used to reach it, and
documents the member.
Execution of the recorded ruling — director seat, decision batch #123
item 5, 2026-09-12, comment `5644711080`, maintainer verbatim 「同意」. That
ruling picks **option 1** of the two the card put to triage and
enumerates four items; all four are below. Option 2 (leave it undeclared
on purpose) is ruled out and is not re-opened here.
Clause-②: yes
## The declaration
`packages/spec/src/contracts/metadata-service.ts` — the new member sits
immediately after its unkeyed twin `loadMany?` (line 675 on the base
commit), inside the same `IMetadataService` declaration that already
carries `loadMany?` and `loadDiagnosed?`.
⚠️ The signature is spelled in words here **on purpose**: this surface
deletes tag-shaped tokens, generics included, from prose and from code
fences alike, so a literal copy of it would arrive mutilated.
`loadManyKeyed?` is optional, is generic in one parameter `T` that
defaults to `unknown`, takes `type: string` plus an optional `options`
bag typed as a Record from string to unknown, and resolves to an Array
of `{ name: string; data: T }` pairs. That is the signature the ruling
names, member for member. **The diff is the authority on it, not this
paragraph.**
It is **optional**, like both siblings, so every existing
`IMetadataService` implementation still satisfies the contract unchanged
and the `typeof ... === 'function'` probe stays the way a caller asks
for it.
### Why an inline pair shape rather than the published
`MetadataKeyedItem`
The ruling writes the return type inline, and that is also the only
spelling available. `MetadataKeyedItem` is declared in
`packages/metadata/src/loaders/loader-interface.ts` and exported from
`@objectstack/metadata`, which **depends on** `@objectstack/spec`;
`packages/spec` declares no workspace dependency at all
(`pg-connection-string` and `zod`). Importing the named type here would
invert that edge and close a cycle. The inline pair is also the file's
own precedent: `loadDiagnosed?` declares its result inline in exactly
the same way.
The two shapes are structurally identical — `MetadataKeyedItem` is
`readonly name: string` beside `readonly data: T`, and a readonly
property is assignable to a mutable one — so `MetadataManager implements
IMetadataService` keeps compiling with no edit to `packages/metadata`,
which is what the green build below shows.
## The four ruled items
1. **Declared** — as above, with a docblock citing this ruling the way
`loadDiagnosed`'s cites `#4127 batch 4`.
2. **Local structural type deleted** — `packages/objectql/src/plugin.ts`
loses `KeyedPluralMetadataRead` (the type and its docblock), and the
three service lookups in `resolveGovernanceMetadataService` now ask for
`IMetadataService` alone instead of intersecting it. Occurrences of that
type name under `packages/` go **4 to 0** — 1 declaration plus 3 use
sites; an earlier draft of this line said 3, corrected against the blob
by the at-tier review (lit control: `IMetadataService` in the same file
= 13, so the zero is a reading). `check:slot-lookup` stays green — see
below.
3. **Docs** — `content/docs/kernel/contracts/metadata-service.mdx` gains
the member, in the interface excerpt's `Loader reads (optional)` group
and as a new `loadManyKeyed` subsection. Section choice and a contrary
fact about it are in the acceptance notes.
4. **Carriers** — `Clause-②: yes` above; `minor` changeset
(`@objectstack/spec`, whose changesets `fixed` group already carries
`@objectstack/objectql`). The `needs:contract-review` carrier was
**not** hung by this branch — the dev never wrote a label. ⚠️
**Corrected provenance:** the owning seat hung it on
2026-09-21T16:58:44Z, after the dev's push, which the PR's own event log
records; a reader checking the labels today will find it present. ⛔ The
earlier "see the acceptance notes" pointer is dropped: those notes never
mentioned the carrier.
## Round 2 — head `b96baa08f2` (2 files, +38 / −13)
Three corrections, all prose; the PR's file list is unchanged at 5 and
no new path was pulled in.
1. **The dangling citation.** `check:issue-citations` was RED at
`97a639c5b3`: the new docblock cited an issue that returns **404** (LIT
CONTROL: its neighbour `#14424` → 200, so the 404 is a reading). ⭐ The
replacement was **not guessed** — `#15378` was verified four ways before
being named: HTTP 200, `merged: true`, `merged_at`, base `main`, and the
depth-immune one — `origin/main` **holds the implementation it added**
(`git grep 'async loadManyKeyed' origin/main --
packages/metadata/src/metadata-manager.ts` = 1; nonsense control = 0).
⚠️ **The gate's own remedy arm two does not work**, and this is filed as
**#19614**: keeping the `#` still matches `CITATION_RE`, and
`NON_CITATION_HEADS` excuses only ordinal heads — there is no
prose-acknowledgement mechanism in the script. So the dead card is kept
as **bare digits without a leading hash**, with a sentence saying why.
Greppable, and nothing dangles.
2. **The docs example taught what its own Callout rejects.** It probed
nothing and null-coalesced to `[]`, turning absence into an empty set —
on the one member whose reason for existing is that silent drops are
dangerous. It now reads the member into a local, guards on `typeof ===
'function'`, and its `else` branch says why absence is not emptiness,
matching `plugin.ts`. `?? []` is gone from the page (0 hits).
3. **A name that named nothing.** "customization container" had **0**
hits on `origin/main` across `packages/`, `content/` and `docs/`. The
right vocabulary came from `MetadataKeyedItem`'s own docblock: an
**aggregated `defineView` container** "has no own `name` BY DESIGN (its
identity is the target object)". Both carriers now say that, each with
an explicit disclaimer that it is **not** the ADR-0005 `sys_metadata`
org customization overlay — which this same page documents separately.
**Gate readings at `b96baa08f2`** — ⚠️ `check:issue-citations` is
recorded here and ⛔ not in the derived-families row (which is **below**,
in the Local runs table — an earlier draft of this line said "above"),
because its root script is **`--self-test` only** while CI runs the
self-test *and* the scan; reporting the alias as a pass is what produced
the red in the first place. Run as the **SCAN**: `node
scripts/check-issue-citations.mjs --base origin/main` → **EXIT=0**,
captured before any pipe, re-run at the final head → EXIT=0 (4 citations
judged across 13 files; 2 resolves, 2 resolves-as-pull-request).
`pnpm lint` whole repo EXIT=0 · spec build success · `check:generated`
all 15 up to date against a fresh build · spec typecheck pass · spec
test 509 files / 14901 passed · objectql typecheck EXIT=0 ·
`check:slot-lookup` holds · `check:nul-bytes` OK plus a hand
control-character scan of both edited files.
**Mechanical proof the `.ts` edit is docblock-only:** every added and
removed line in `git diff 97a639c..b96baa0 --
packages/spec/src/contracts/metadata-service.ts` is a comment line —
zero non-comment lines. No type or runtime surface moved, so the
ablation recorded at `97a639c5b3` still stands and was not re-run.
⚠️ **Two prerequisite failures, resolved rather than reported as
passes**, both artefacts of a fresh worktree and neither about the diff:
`check:docs-transcript-drift` exit 3 (`@objectstack/lint` unbuilt) →
built, re-ran, EXIT=0; objectql typecheck first exit 2 with 42 errors,
**all** `TS2307 Cannot find module` from an unbuilt dependency closure →
built, re-ran, EXIT=0.
⚠️ **A negative reading deliberately NOT relied on:** `git merge-base
--is-ancestor` on #15378's squash commit exited 1, but this checkout is
shallow and the control leg was a shallow-window near-relative — so that
negative is **void, not evidence**. The tree read and the API's `merged`
/ `merged_at` answer the question without a history walk.
## Verification
Reverse verification, because this is a cross-package type change and a
green typecheck against a stale `.d.ts` is indistinguishable from a real
one. Run from the committed state through
`scripts/ablation-replace.mjs`, with the on-disk and in-`dist` evidence
the tool produces:
- **Mutate** — the declared member renamed at its anchor. Anchor hits 1
to 0, blob `bd37483b1715` to `3a9e85d219ad`.
- **Reached the artifact** — `scripts/ablation-dist-preflight.mjs` found
the mutated marker in 2 built files
(`packages/spec/dist/contracts/index.d.ts` and `.d.mts`), so the run
below read the rebuilt declarations and not a cache.
- **The ablation run** — `tsc --noEmit` in `packages/objectql` went red
with **exactly one** error, and it is the call site:
`src/plugin.ts(2593,35): error TS2339: Property 'loadManyKeyed' does not
exist on type 'IMetadataService'.`
- **Restore** — blob back to `bd37483b1715`, equal to HEAD, `git diff
HEAD` empty, whole-tree `git status --porcelain` empty. After a rebuild
the mutated marker is gone from `dist` (0 occurrences) and the real
member is back (2), and `tsc --noEmit` in `packages/objectql` is green
with zero output.
That is the proof for ruled item 2: the call site now reads the
contract, and it reads *only* the contract.
Local runs. ⚠️ **Provenance corrected — this table is not all from one
head.** The nine readings restated in the Round 2 section were taken at
the final head `b96baa08f2`; every other row here — the objectql
typecheck, the changeset gates, the 14 docs gates and the 20 further
derived families — was measured at `97a639c5b3`, **before** round 2
rewrote the `.mdx`. ⛔ Nothing is actually unmeasured at the final head:
CI ran the whole docs family green there, including
"`packages/spec/src/**` doc-block symbol anchors resolve". It is the
sentence that over-claimed its own provenance, not the work.
| check | result |
|:--|:--|
| `pnpm lint` (whole repo, `eslint . --no-inline-config`) | **0** —
clean |
| `pnpm --filter @objectstack/spec build` | success; 34/34 declaration
files emitted |
| `pnpm --filter @objectstack/spec check:generated` | **All 15 generated
artifacts up to date** |
| `pnpm --filter @objectstack/spec typecheck` | pass (includes
`check:test-typecheck`) |
| `pnpm --filter @objectstack/spec test` | 509 files, **14901 passed**,
1 todo |
| `pnpm --filter @objectstack/objectql typecheck` | pass |
| `pnpm --filter @objectstack/objectql test` | 303 files, **5050
passed** |
| `pnpm check:slot-lookup` | ✓ holds — 106 unswept sites in 25 files,
**none new**, baseline key set verified against `0e658fb`: no files
added |
| `pnpm check:nul-bytes` | ✓ 9156 text files scanned, no raw control
bytes |
| changeset gates (`check:empty-changeset`,
`check:adr-0087-registration`, `check:changeset-no-major`,
`check:changeset-gate-self-tests`) | pass |
| docs gates (`check:doc-anchors`, `check:doc-authoring`,
`check:doc-frontmatter`, `check:docs-section-name`,
`check:docs-single-h1`, `check:docs-redirects`,
`check:docs-spec-enumerations`, `check:docs-transcript-drift`,
`check:docs-audit-scope`, `check:doc-route-spelling`,
`docs-audit/check-affected-docs`, `docs-audit/check-drift-comment`,
`check:section-landing-index`, `check:keyed-text-bounds`) | pass |
| further derived families run (`check:type-check-coverage`,
`check:test-source-alias`, `check:published-files`, `check:dts-closure`,
`check:lean-entry-closure`, `check:cross-package-test-inputs`,
`check:spec-docblock-symbol-anchors`, `check:comment-mask-adoption`,
`check:comment-mask-corpus`, `check:undeclared-dep-imports`,
`check:query-options-erasure`, `check:spec-parsed-alias`,
`check:objectql-double-limit`, `check:engine-double-contract`,
`check:durability-log-level`, `check:published-readme-links`,
`check:pm-prior-rulings`, `check:sourcemap-no-sources-content`,
`check:strictness-ledger`, `check:skill-refs`) | pass |
| `check:type-check-debt`, `check:dual-build-cjs-loads` | **NOT
MEASURED** — both exited 3 (`PREREQUISITE NOT MET`); each needs a
whole-workspace build this branch did not run. Neither a pass nor a
finding. Declared to CI. |
### What the generators actually moved: nothing
Measured rather than inferred, and this was the one prediction worth
testing. Six generators were run against the built tree —
`gen:api-surface`, `gen:export-origins`, `gen:spec-changes`,
`gen:schema`, `gen:docs`, `gen:declaration-map` — each exiting 0, after
which `git status --porcelain` listed **no** generated artefact.
`check:generated` independently reports all 15 up to date. So an
optional member on a published interface moves none of the four
artefacts that name `IMetadataService`, exactly as the claim predicted.
⚠️ One reading on the way there was **not** a finding and should not be
read as one: `check:api-surface` first reported stale with `PREREQUISITE
NOT MET — this gate reads built output, and what is on disk predates the
sources`. The `dist` had been built before a later edit to the test
file, which is a build input. Rebuilding cleared it. It was never an
artefact move.
## Acceptance notes
⛔ Noted, not filed, and deliberately **not** fixed here — each is
outside this card's ruled four items.
- **The docs page documents `loadManyKeyed` ahead of its own declared
sibling `loadMany?`.** On
`content/docs/kernel/contracts/metadata-service.mdx`, `loadMany`
appeared **0** times before this change (lit control on the same page:
`loadDiagnosed` = 5, so the zero is a reading). The page's interface
excerpt is explicitly partial and says so — its line 29 points at
`IMetadataService` in the source for the full member list — so this is a
documentation gap rather than a contradiction, but the ordering is odd
for a reader and it is being handed to the seat to file as its own card.
Widening this PR to also document `loadMany` was declined on purpose.
- **Section choice, and why.** The new member is documented as a
`loadManyKeyed` subsection under `Core CRUD`, immediately after `load /
loadDiagnosed` and before `list / listNames`. `Bulk Operations` was
considered and rejected: despite the name, that section on this page
documents bulk **writes** (`bulkRegister` / `bulkUnregister`), so a
plural *loader read* filed there would sit in the write section. The
chosen spot is the page's loader-read run, one step from the plural
registry reads a reader would be comparing it against.
- **The page's own `loadDiagnosed` example still teaches the shape this
PR's new example refuses.** At
`content/docs/kernel/contracts/metadata-service.mdx:138`, two sections
above the new probe-first example, the pre-existing `loadDiagnosed`
snippet spells an optional call plus `?? {}` — absence collapsing into a
value, which is exactly what the new example's `else` branch says not to
do ("Do NOT fall through to an empty set") and what the info Callout
restates ("never as an empty set"). It is **present at this PR's merge
base and untouched here** (`?? []` on this page at head: **0**, git grep
exit 1 captured before any pipe; lit control `?? {}` on the same page:
**1**, at `:138`, so the zero is a reading). The page is now internally
inconsistent in style rather than wrong. ⛔ Recorded here rather than
filed as a card, per the standing rule: the question "which PR will
touch this file?" has an answer, and it is this one — so the note
belongs where the next editor of the page will read it. Widening this PR
to rewrite a snippet outside its four ruled items was declined on
purpose.
- **The `#16090` serialisation caveat recorded in the ruling's item 3 is
spent.** That issue is closed, and no open pull request holds the page.
Nothing was serialised against and nothing waited.
## Landing
⛔ Draft on purpose, and it stays that way from this branch. No flip to
ready, no enqueue, no auto-merge. Landing is the owning seat's act after
an at-tier contract review.
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`IMetadataService` declares `loadManyKeyed?` — the keyed plural loader read now sits on the contract beside its two declared siblings `loadMany?` and `loadDiagnosed?` (#15385).
6
+
7
+
Clause-②: yes
8
+
9
+
A verb family lives whole on the contract. `MetadataManager.loadManyKeyed(type)` shipped as a public member with no declaration on the interface its siblings are declared on, so the one cross-package caller — the ObjectQL governance audit — narrowed the service slot with a **local structural type** written beside the call site. That local type is deleted in the same change and the call site reads the contract.
10
+
11
+
The vocabulary is not new: `loadManyKeyed`, and the `{ name, data }` item shape it answers with, are already published on `MetadataLoader`, which declares the same member as optional over its own loader-local options type. What this adds is the member's place on `IMetadataService`.
12
+
13
+
```ts
14
+
loadManyKeyed?<T = unknown>(
15
+
type: string,
16
+
options?:Record<string, unknown>,
17
+
):Promise<Array<{ name: string; data: T }>>;
18
+
```
19
+
20
+
**What it is for.** The key is a fact about the **store** — `register()`'s own `name` argument — and it travels *beside*`data`, never folded into it, so `data` stays byte-identical to what the unkeyed plural read would return and no consumer ever sees a synthesised `name`. An item whose stored body has no top-level `name` is legal and deliberate (an org customization container's identity is the object it targets), and such an item has no identity at all in a plural read keyed by `data.name` — it is dropped, silently. That is why this is a second member rather than a widened return type on the existing one.
21
+
22
+
**What moves for consumers.** Nothing breaks. The member is **optional**, like `loadMany?` and `loadDiagnosed?` beside it, so every existing `IMetadataService` implementation still satisfies the contract unchanged and the `typeof … === 'function'` probe stays the way a caller asks for it. What changes is that a caller no longer has to declare the shape itself to stay typed: intersecting the slot with a hand-written structural type was the only way to reach the member without erasing the lookup to `any`, and that workaround is now unnecessary. `MetadataManager`, which already implements the member, needs no edit.
23
+
24
+
This is the position `loadDiagnosed` was in before #4127 batch 4 declared it, and it is resolved the same way. Ruled in decision batch #123 item 5 (2026-09-12), maintainer verbatim: 「同意」.
25
+
26
+
`content/docs/kernel/contracts/metadata-service.mdx` gains the member in the same change.
0 commit comments