Skip to content

Commit 0ebb080

Browse files
feat(objectql): one package-scoped resolver for behavioural metadata and a shared declared-closure helper (ADR-0150 D3/D4) (#31637)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XU6SjEqQ4aAk9NdivD3NcoK)_ epic:#31527#1 · os-project-manager · session_01FEkQ5ovAHLWY1qohbSBYdw Fixes #31575 Refs #31527 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:新增一个共享解析器 `resolvePackageScopedName`,按 ADR-0150 D3 回答「这个裸名字指哪个软件包的元数据」:先调用方自己的包,再它声明的依赖闭包(加 `sys`)里唯一的持有者,否则拒绝并列出全部候选;无包调用方先环境项,再全环境唯一持有者,否则拒绝。依赖闭包抽成一个导出函数 `packageDependencyClosure`,保存门(`resolveWritePackageScope`)改用它,两边永不分歧。注册表新增三个只读方法,实现 D4 的「按包绑定覆盖层」:覆盖层只绑定到它定制的那个包的同名项。 - 为什么改:专题 #31527 的第 2 步 R;后续 E、G、S 各读者都只引用这一个解析器,避免各门各写一套规则再漂移。 - 风险与回滚:纯新增。没有任何读者接入,`getItem` 一行未动,阶段一的单持有者拒绝照旧生效,保存门的判定不变(metadata-protocol 全量测试 22591 条全绿)。回滚即还原本 PR。 - 先看:`packages/objectql/src/package-scoped-resolver.ts` 的 `resolvePackageScopedName`(三步与拒绝);`registry.ts` 的 `overlaySlotBinding`(覆盖层绑定规则);`protocol-write-package-scope.ts` 的 `packageDependencyClosure`。 - 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run src/package-scoped-resolver.test.ts`,预期 33 条全绿,含两条红线负向 pin 与阶段一一致性。 - 需要你做的:无,席位落地(须在 ADR-0150 #31586 由你合并之后)。 ## What changes **`@objectstack/objectql` — `src/package-scoped-resolver.ts` (new), exported from the package barrel only.** - `resolvePackageScopedName(registry, { type, name, callerPackageId?, member? })`, ADR-0150 D3 exactly: - caller of package P: (1) P's own item (its overlay when one binds to it, else its composite slot); (2) else the unique holder in P's declared dependency closure plus every `sys`-namespace package; (3) else `refused`, naming every candidate. - caller with no package (`undefined`, `null`, `''` or the `sys_metadata` sentinel): the environment's own item, else the unique package holder, else `refused` naming every holder. - zero holders: `not-found`, never a refusal. - `PLATFORM_REQUESTED_BARE_NAMES_BY_TYPE` (`permission` → `guest_portal`, the same list as the CLI's `PLATFORM_REQUESTED_BARE_NAMES`, keyed by registry type): environment-wide whatever the caller, one holder. - `PACKAGE_SCOPED_TYPES`: D1 buckets 2 and 3 as the registry spells them (24 types). `position` (D7, Q2) and every bucket-1 type are not in it, and asking for one throws a `TypeError`. - An overridable baseline (`OVERRIDABLE_BASELINE_PACKAGES`, the email service's built-in templates) yields to any other candidate of the same step and answers only when it is the only one, which is how `bare-name-one-holder.ts` already reads it ("neither holds a name nor is refused one"). - The refusal is a value, not a throw: `{ status: 'refused', reason, type, name, callerPackageId, candidates, message }`. `reason` is `ambiguous-in-closure`, `ambiguous-in-environment` or `outside-closure`. `candidates` reuse the vocabulary's `BareNameHolder` (`kind: 'package'`) plus one kind this rule needs, `{ kind: 'environment' }`; `message` names each with `describeBareNameHolder`. The doors (child G) map it to `409 RESOURCE_CONFLICT`; nothing is minted here. - `packageResolutionClosure(registry, P)`: the save door's closure plus `sys`, exported so a reader can show what P reaches. - Not on the lean `./core` entry: the module reads `@objectstack/metadata-protocol`, which ADR-0076 D2 keeps out of it (built `core.d.ts` carries 0 hits of `resolvePackageScopedName`; `index.d.ts` carries the export). **`@objectstack/objectql` — `src/registry.ts`, three read-only `SchemaRegistry` methods (ADR-0150 D4 row "registry overlay slot").** - `packageScopedHoldersOf(type, name)`: every package holding the name, in store order: each composite `PACKAGE:NAME` slot (read off the KEY, as `getItem`'s prefer-local reads it), then the package a bare-slot entry is its own stored item of. - `getPackageScopedItem(type, name, packageId, member?)`: the bare-slot entry when it binds to `packageId`, else that package's composite slot. Never another package's overlay, never the environment's item. - `getEnvironmentScopedItem(type, name, member?)`: the bare-slot entry when it binds to no package. - The binding rule (`overlaySlotBinding`), on the entry's `_packageId` (the stored row's `package_id` column, or the artifact envelope the hydration grafted, ADR-0010 §3.3): - package-less (none, `''` or the `sys_metadata` sentinel) and no package holds the name: the environment's own item (bucket 4); - package-less and exactly one package holds the name: that holder's overlay. This is ADR-0150 D3's "a package-less row that bears a package-held name is a customization, never a second item", read at call time, so a package installed later makes the row its overlay; - package-less and two or more packages hold the name: bound to none, and no environment item either (never a guess about whose customization it is); - a composite holder: its overlay; a `BASE.custom` pair whose base is a composite holder: the base's overlay (ADR-0146 D9); any other package: its own stored item. - The registry still stores one bare slot per name and `getItem` still serves it first to every caller; the per-package slot is how these new reads bind it. Bundles (`email_template`) bind per member. **`@objectstack/metadata-protocol` — `src/protocol-write-package-scope.ts`.** - `packageDependencyClosure(packageId, registry)`: the walk (`resolvePackageClosure`, shared with Studio's pickers) plus the App-pair widening, exactly the body `resolveWritePackageScope` had. `resolveWritePackageScope` is now that call inside its existing silent `catch`. Exported from the package root. **Tests.** `packages/objectql/src/package-scoped-resolver.test.ts` (new, 33 cases), and one census row in `federated-injected-column-readers.test.ts` (see Acceptance notes). **Changeset.** `.changeset/31575-package-scoped-resolver.md`: `patch` for both packages. ## Tests On head 87d197e (after the one `origin/main` merge) unless a line says otherwise: - `pnpm --filter @objectstack/objectql exec vitest run src/package-scoped-resolver.test.ts`: 33/33. The blocks: D3 with a caller package (7), D3 with no caller (5), red line 1 (2), red line 2 (3), D3 exceptions (4), the closure is the save door's (1), D4 overlay slot (6), phase-1 parity (5). - `pnpm --filter @objectstack/objectql test`: 571/571 files, 11604/11604 tests, exit 0 (the lock's `VERDICT command-exit 0`). - `pnpm --filter @objectstack/objectql typecheck` (tsc plus `check:test-typecheck`): exit 0. - `pnpm --filter @objectstack/metadata-protocol test` (head 54ec46b; no `packages/metadata-protocol` file changed after it, and the merged `origin/main` commits touch none): 327 files passed, 4 skipped; 22591 tests passed, 26 skipped; exit 0. This is the save door's unchanged behaviour: `protocol.package-closure-gate`, `protocol-publish-drafts-closure`, `protocol.runtime-authoring-gate`, `protocol.packaged-base-refusal` and the pair-widening suites all run through the switched `resolveWritePackageScope`. - `pnpm --filter @objectstack/metadata-protocol typecheck` (same head): exit 0. - `pnpm exec eslint --no-inline-config` over the seven touched source and test files: exit 0. <details><summary>Ablations (each through <code>scripts/ablation-replace.mjs</code>: mutation proven on disk, run, restored to the HEAD blob with <code>git diff HEAD</code> empty)</summary> The subject is imported from `src/` (relative import in the test), so no `dist/` rebuild is in the path. | # | Red line | Mutation | Red output | |:--|:--|:--|:--| | A1 | 1 | closure with two candidates answers the first-registered one instead of refusing | `× two holders in the closure refuse: no first-registered pick, even with a package-less row beside them`, `× in the closure: all three holders, in store order, each in the sentence`; `AssertionError: expected 'resolved' to be 'refused'`; 2 failed / 31 passed | | A2 | 1 | environment with two holders answers the first-registered one | `× two package holders refuse a caller with no package: no first-registered pick`, `× in the environment: all three holders…`, `× a platform-requested name…`, `× a package-less row over a name two packages hold binds to neither…`, `× a row bound to a package that ships no item…`; 5 failed / 28 passed | | A3 | 1 | nothing in the closure falls back to an environment-wide resolution | `× a caller package the registry cannot produce declares nothing…`, `× refuses, naming the holder, when the name is held only outside the closure — never an environment-wide guess`, `× a platform-requested name…`; 3 failed / 30 passed | | A4 | 2 | a refusal keeps only its first two candidates | `× in the closure: all three holders…`, `× in the environment: all three holders…`; `expected [ …(1) ] to deeply equal [ …(2) ]`; 2 failed / 31 passed | | A5 | D4 | `getPackageScopedItem` takes the bare slot whatever it binds to (today's `getItem` reading) | `× an overlay of one package's item never shadows another package's same-named item`, `× a package-less row over a name two packages hold binds to neither…`, `× a row bound to a package that ships no item…`; 3 failed / 30 passed | | A6 | D3 (newer text) | every package-less row is the environment's own item, whoever holds the name | `× a package-less row under the one holder's name is that holder's overlay, never a second item`, `× a package-less row is the environment's own item only while no package holds the name`, `× a package-less row over a name two packages hold…`, `× every covered type, one package holder and a package-less row over its name: the same item…`; 4 failed / 29 passed | The readings above are the final code's (head after the ADR re-read). An earlier round on the first head read the same reds; its A3 first attempt kept the anchor inside its replacement and was refused by the tool before running (a no-op, not a reading), then re-run with a rewriting anchor. </details> ## Acceptance notes **ADR-0150 Execution plan, readings (a)–(d).** - **(a) phase-1 one-holder tests stay green.** `registry-bare-name-one-holder.test.ts` and its request, flow and AI family files run in the full objectql suite above. The new test's last case also pins that a second package holder is still refused once sealed. - **(b) bare-name behaviour unchanged.** No reader imports the resolver: `git grep package-scoped-resolver -- packages ui` answers only the module, its test, the barrel and one comment in `registry.ts`. `getItem` has no diff. The phase-1 parity block asserts, for all 11 covered registry types, under a sealed registry, with and without an overlay of the one holder, for a caller with no package, the holder itself and a dependent, that the resolver's item IS `getItem`'s item (`toBe`), and the same for an environment-only item. - **(c) migration.** None in this PR: no stored shape, no column, no backfill. - **(d) declarations.** `Clause-②: no`: the diff adds TypeScript exports to two published packages (`@objectstack/objectql` `index.d.ts`; `@objectstack/metadata-protocol` `index.d.ts`, `packageDependencyClosure`) and changes no spec schema, protocol, SDK, door request or response. `Metadata-protocol: none`: no authorable value is accepted or refused differently. ADR-0087: no migration entry owed (nothing stored or authored changes). **Red lines 1 and 2.** Pinned by the `red line` blocks and ablated (A1–A4 above). Every refusal carries every candidate of the step that refused; `outside-closure` carries every holder there is plus the environment when it holds one. **Readings this PR had to make where ADR-0150 is silent (named for the seat's review).** 1. **ADR re-read.** ADR-0150's head moved to 565c63c while this was built. Its new D3 paragraph ("a package-less row that bears a package-held name is a customization, never a second item") changed the binding rule; this PR follows it (commit "a package-less row over a package-held name binds to that holder"), and A6 pins it. The rest of D3 and D4 is unchanged; the R row now says the resolver adds `sys` itself, which it does. 2. A caller with a package whose name only the environment's own item holds is refused, naming the environment (`outside-closure`), not served it and not answered not-found. D3 lists the environment item only for a caller with no package. This is the only reading that neither loosens nor hides a holder; it is in the report's `open_questions` with the four axes. 3. A package-less row over a name two packages hold binds to neither (the newer D3 text speaks of "that one phase-1 holder"; for an uncovered type with two holders, picking one would be a guess). The environment-wide answer is then the refusal naming both. Today the hydration grafts the first holder's envelope onto such a row (`_packageId`), so it reads as that holder's overlay here, exactly as the bare slot already serves it; this PR does not change the hydration. 4. A bare-slot row bound to a package that ships no item of the name counts as that package's own stored item, so with another package's shipped item the environment-wide answer is a refusal. Phase 1's holder set never reads the bare slot; the ADR identity (`sys_metadata` unique on package) does. Pinned by the D4 block. 5. Disabled packages are not filtered, as `getItem` does not filter them. 6. `rag_pipeline` (D1 bucket 3) has no registry type since ADR-0049 retired the collection, so the type list has 24 entries. 7. An object-bound action is resolved by the registry's `(action, name)` key only; `(package, objectName, name)` is child E2's key. **Census row.** `federated-injected-column-readers.test.ts` scans every string literal in `src/` that equals an injected column name. The type list's `'queue'` (the metadata type, ADR-0150 D1) matched the ADR-0142 ownership-queue column; the row records it as `not-a-read`, as the same file already does for `registry.ts`'s and `assignment-rules.ts`'s `queue`. **Two copies of the platform-requested list.** `PLATFORM_REQUESTED_BARE_NAMES_BY_TYPE` here and the CLI's `PLATFORM_REQUESTED_BARE_NAMES` (keyed by collection) hold the same one name. objectql cannot import the CLI, and the CLI file is outside this card's surface. Child P (which retires the CLI's phase-2 finding) is the natural place to make the CLI read objectql's. class: observation · carrier: #31527 child P. ## Verification - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on head 87d197e: 71 families derived; each run with its exit captured before any pipe: **71 exit 0**. `--ran` reconciliation: "71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED". - On the first head (a0b690f) `check-plugin-teardown-shape --self-test` answered exit 3 (shallow clone, its fixture commit missing) and passed after `git fetch --depth=1` of that commit; on 87d197e it ran green in the loop. `check:dual-build-cjs-loads` answered exit 3 (prerequisite) on the first head and exit 0 on 87d197e ("122 published require entry point(s) across 72 package(s) load"). - Built entries: `packages/objectql/dist/index.d.ts` carries `resolvePackageScopedName` (2 hits), `dist/core.d.ts` carries none; `packages/metadata-protocol/dist/index.d.ts` carries `packageDependencyClosure`. - Not run locally, owned by CI: `test:repo` (no file of that project changed), the full lint farm, dogfood (no `!` in the title, no narrowing arm). - Record: `pm-orders/epic-31527/31575-gates-ran.txt` in the project files. --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5ovAHLWY1qohbSBYdw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 18cdcb2 commit 0ebb080

8 files changed

Lines changed: 1109 additions & 7 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/objectql': patch
3+
'@objectstack/metadata-protocol': patch
4+
---
5+
6+
feat(objectql, metadata-protocol): one package-scoped resolver for behavioural metadata, and one declared dependency closure for it and the save door (ADR-0150 D3/D4)
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **`@objectstack/objectql`** exports `resolvePackageScopedName`, the resolution rule of ADR-0150 D3 for the metadata types addressed by `(package, name)` (`PACKAGE_SCOPED_TYPES`). A caller of package P is answered P's own item, else the unique holder in P's declared dependency closure plus the `sys` packages, else a refusal that names every candidate; a caller with no package is answered the environment's own item, else the unique package holder, else the refusal. Zero holders is `not-found`. The platform-requested name `guest_portal` resolves environment-wide whatever the caller. `SchemaRegistry` gains three read-only methods the resolver reads, `packageScopedHoldersOf`, `getPackageScopedItem` and `getEnvironmentScopedItem`, which apply the per-package overlay slot (ADR-0150 D4): a stored overlay binds to the package whose item it customizes, so it never shadows another package's same-named item there.
12+
- **`@objectstack/metadata-protocol`** exports `packageDependencyClosure`, a package's declared dependency closure (its manifest's `dependencies`, transitively, with an App's customization pair widened by the App's members). The save door's write scope now calls it, so the closure a write is judged against and the closure a name resolves in are one implementation.
13+
14+
Nothing changes for a running deployment: no reader calls the resolver yet, `getItem` keeps serving the bare slot first to every caller, the one-holder refusal of ADR-0048 §3.4 stays in force, and the save door admits and refuses exactly what it did.

‎packages/metadata-protocol/src/index.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,11 @@ export type { PackagelessOverlayCensus, PackagelessOverlayMigration } from './pr
258258
// read by the automation boot pull, which judges a flow override on the
259259
// column (`isServedOverrideRowOf`), ⛔ never on the body's `_packageId`.
260260
export { bindStoredRow, storedRowBindingOf } from './stored-row-binding.js';
261+
262+
// [#31575, ADR-0150 D3] A package's declared dependency closure: the ONE
263+
// function the save door's write scope (`resolveWritePackageScope`) and
264+
// objectql's package-scoped name resolver both ask, so the two never disagree.
265+
export { packageDependencyClosure } from './protocol-write-package-scope.js';
261266
export { shadowedStoredRowsOf } from './stored-flow-slot.js';
262267

263268
// [ADR-0131 D6] The `unmanaged` install mode's walled-posture refusal — the

‎packages/metadata-protocol/src/protocol-write-package-scope.ts‎

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,16 @@
2626
* gate's host (cut 8, `protocol-authoring-gate.ts`), whose closure calls it.
2727
*
2828
* The body reads no module-level declaration of `protocol.ts`; it imports the
29-
* class type and the shared walk (`resolvePackageClosure`, below). Nothing
30-
* re-exports this module, so the published surface does not move.
29+
* class type and the shared walk (`resolvePackageClosure`, below).
30+
*
31+
* ## One declared dependency closure for every reader (#31575, ADR-0150 D3)
32+
*
33+
* {@link packageDependencyClosure} is the one answer to "what may package P
34+
* reach by declaration": the save door's write scope (the method above, which
35+
* now only adds its silent `catch`) and objectql's package-scoped name
36+
* resolver (`resolvePackageScopedName`) both ask it, so the closure a write is
37+
* judged against and the closure a bare name resolves in can never disagree.
38+
* It is the one export of this module the package root re-exports.
3139
*/
3240

3341
import { resolvePackageClosure } from '@objectstack/lint/runtime';
@@ -105,15 +113,45 @@ export function resolveWritePackageScope(
105113
packageId: string | null | undefined,
106114
): { packageId: string; dependencies: string[] } | undefined {
107115
try {
108-
const registry = this.engine.registry as MemberRegistry | undefined;
109-
const getPackage = registry?.getPackage;
110-
if (typeof getPackage !== 'function') return undefined;
111-
const closure = resolvePackageClosure(packageId, (id) => getPackage.call(registry, id));
112-
return closure === undefined ? undefined : widenPairClosureToAppMembers(closure, registry);
116+
return packageDependencyClosure(packageId, this.engine.registry as MemberRegistry | undefined);
113117
} catch {
114118
// Context gathering must never fail a write (the same contract
115119
// `listCollection` above holds). An unreadable registry means an
116120
// unnarrowed gate, not a refused publish.
117121
return undefined;
118122
}
119123
}
124+
125+
/**
126+
* [#31575, ADR-0150 D3, ADR-0141] Package `packageId`'s DECLARED dependency
127+
* closure, read from `registry`: the package itself and every package its
128+
* manifest's `dependencies` name, transitively (`resolvePackageClosure`, the
129+
* walk Studio's object pickers share), with an App's pair widened by the App's
130+
* members ({@link widenPairClosureToAppMembers}). The one implementation: the
131+
* save door's write scope ({@link resolveWritePackageScope}) is this call
132+
* inside a silent `catch`, and objectql's package-scoped name resolver asks it
133+
* for the closure a bare name resolves in.
134+
*
135+
* Answers `undefined` exactly where the walk does — no package id, the
136+
* `sys_metadata` sentinel, a package `registry` cannot produce — and for a
137+
* registry with no `getPackage` (a metadata-only test double). Each reader
138+
* decides what `undefined` means for it: the save door narrows nothing, the
139+
* resolver reaches nothing beyond the caller's own items and `sys`.
140+
*
141+
* Not a widening of anything: it adds no package the walk and the pair
142+
* widening did not already add. ⛔ It does NOT add the `sys` kernel namespace
143+
* ADR-0150 D3 puts in a resolution closure: the save door's scope stays as it
144+
* was, so the resolver adds `sys` itself.
145+
*
146+
* A `getPackage` that throws propagates; the caller decides what an unreadable
147+
* registry means.
148+
*/
149+
export function packageDependencyClosure(
150+
packageId: string | null | undefined,
151+
registry: MemberRegistry | undefined,
152+
): { packageId: string; dependencies: string[] } | undefined {
153+
const getPackage = registry?.getPackage;
154+
if (typeof getPackage !== 'function') return undefined;
155+
const closure = resolvePackageClosure(packageId, (id) => getPackage.call(registry, id));
156+
return closure === undefined ? undefined : widenPairClosureToAppMembers(closure, registry);
157+
}

‎packages/objectql/src/federated-injected-column-readers.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,10 @@ const READERS: Record<string, Row> = {
440440
},
441441
'assignment-rules.ts#<module> :: owner_id': { disposition: 'not-a-read', why: VOCABULARY },
442442
'assignment-rules.ts#<module> :: queue': { disposition: 'not-a-read', why: VOCABULARY },
443+
'package-scoped-resolver.ts#<module> :: queue': {
444+
disposition: 'not-a-read',
445+
why: 'the metadata TYPE `queue` (ADR-0150 D1 bucket 2) in the list of types addressed by package, not the column; it reads no row',
446+
},
443447
'assignment-rules.ts#carriesQueueColumn :: resolveInjectedColumnProvenance()': {
444448
disposition: 'excludes',
445449
why: 'an object whose queue column is unprovisioned (federated) evaluates no assignment rule',

‎packages/objectql/src/index.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,35 @@ export {
9696
// stay private to the engine.
9797
export { findPackageHeldSecurityCatalogNames } from './registry.js';
9898

99+
// [#31575, ADR-0150 D3/D4] The ONE package-scoped resolver for behavioural
100+
// metadata: the caller package's own item, else the unique holder in its
101+
// declared dependency closure (plus `sys`), else a refusal naming every
102+
// candidate. Exported for the readers epic #31527's later children switch to
103+
// it (runtime indexes, doors, stored references); no reader calls it yet. On
104+
// this barrel only: it reads `@objectstack/metadata-protocol`'s closure, which
105+
// the lean `./core` entry never imports (ADR-0076 D2).
106+
export {
107+
resolvePackageScopedName,
108+
packageResolutionClosure,
109+
packageScopedRefusalMessage,
110+
describePackageScopedHolder,
111+
isPackageScopedType,
112+
isPlatformRequestedBareName,
113+
PACKAGE_SCOPED_TYPES,
114+
PLATFORM_REQUESTED_BARE_NAMES_BY_TYPE,
115+
KERNEL_NAMESPACE,
116+
} from './package-scoped-resolver.js';
117+
export type {
118+
PackageScopedType,
119+
PackageScopedHolder,
120+
PackageScopedQuery,
121+
PackageScopedRegistry,
122+
PackageScopedResolution,
123+
PackageScopedResolutionStep,
124+
PackageScopedRefusal,
125+
PackageScopedRefusalReason,
126+
} from './package-scoped-resolver.js';
127+
99128
// [#15204 W2] Every `(type, name)` a manifest declares, read from the sources
100129
// the engine's registration seams read. Exported for the install-local door
101130
// (`@objectstack/cloud-connection`), which refuses a catalog-carrying package

0 commit comments

Comments
 (0)