Skip to content

Commit 488b3ed

Browse files
fix(studio): object pickers offer only the objects the edited package may reference (#28329)
domain:ui#3 · os-tesla · session_01L9qibvSmNNn2cV2emnvGXn <!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoWiWdGBWp9rocznTPtPPZM9)_ Fixes #28240 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Studio 的对象选择器(流程触发对象、流程节点对象、查找字段的关联对象、新建数据集的对象)现在只能选当前软件包可用的对象:自己的、已声明依赖包的、系统对象。范围外的对象放在最后一个默认折叠的分组里,灰显不可选,并说明原因,附「声明依赖」入口,直接打开软件包编辑框;保存后不用刷新即可选。 - 为什么改:发布闸门本来就拒绝范围外的对象,但选择器什么都列,作者在对象上把流程做完,保存后才看到拒绝。 - 风险与回滚:依赖遍历从服务端搬进 `@objectstack/lint` 的 `resolvePackageClosure`,服务端改为调用它,接纳范围不变(服务端原有闭包测试未改、全绿)。软件包列表读不到时不做任何收窄,由服务端判定,与闸门的失败方向一致。回滚即 revert 本 PR。 - 先看:`ui/packages/app-shell/src/views/metadata-admin/inspectors/objectPickerClosure.ts`、`ObjectPicker.tsx` 的分组与禁用逻辑、`packages/metadata-protocol/src/protocol-write-package-scope.ts`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/ObjectPicker.closure-28240.test.tsx`,预期 6/6 通过。 - 需要你做的:无,席位落地。 ## What changes - **`@objectstack/lint`**: new export `resolvePackageClosure(packageId, getPackage)`, on both the root and the `./runtime` entry. It is the dependency walk `resolveWritePackageScope` ran inline, moved verbatim: same guards (no id, the `sys_metadata` sentinel, an unknown package answer `undefined`), same DFS, a declared-but-unresolvable dependency still in the closure, own id skipped. A throwing `getPackage` propagates; the host decides. - **`@objectstack/metadata-protocol`**: `resolveWritePackageScope` keeps its two host guards (registry without `getPackage`, the silent `catch`) and calls the shared walk. What the gate admits is unchanged. - **Studio object picker** (`ObjectPicker.tsx`, shared by the flow New dialog's record presets, every flow `object` reference, a lookup's related object and the new-dataset dialog): when a package is being edited, it reads `GET /api/v1/packages` (the request `packages-io.ts` already shares) and lazily imports `@objectstack/lint`, then calls `resolvePackageClosure` and `narrowObjectsToPackageClosure` over the served object list. Neither rule is restated in ui. - Objects the closure does not admit are listed last, in a group that starts collapsed ("Show objects outside this package's dependencies (N)"), each `aria-disabled` with "From PACKAGE". A search reaches them without opening the group. - The group's first row carries the reason and, when the package is writable, **Declare a dependency**, which opens the package's edit dialog (`PackageFormDialog` mode `edit`). The picker re-reads the closure on `objectui:packages-changed`, so a declared dependency's objects become pickable without a reload. - Choosing a disabled row commits nothing. Free-text entry is unchanged (Enter or blur keeps what was typed; the server judges it). - No package in scope, the package list unreadable, or the package not listed: nothing is narrowed (the gate's own failure direction). - **i18n**: six `engine.inspector.objectPicker.*` keys, one contiguous block in each studio pack (en, zh). - **Changeset**: `.changeset/28240-studio-object-picker-closure.md` (patch: app-shell, i18n, lint, metadata-protocol). Presentation choice (card item 1, "absent or disabled"): **disabled with the reason and the declare door**, as the seat ruled. A missing object reads as "the platform has no such object"; a disabled one tells the author what to do. ## Tests - `packages/lint/src/runtime-gate.resolve-package-closure.test.ts` (new): the guards, own-only closure, transitive walk through a cycle, an unresolvable declared dependency kept, a throwing registry propagating, and that the narrow `./runtime` entry names the function. - `ui/.../inspectors/ObjectPicker.closure-28240.test.tsx` (new), with the real lint functions and only the two reads doubled: - **the card repro as a pin**: a package with no dependencies is not offered `showcase_task`; searching finds it disabled, with the reason, and clicking it commits nothing; - **CONTROL**: with `com.example.showcase` declared, `showcase_task` is offered and commits; - a dependency declared after mount makes the object pickable once the package list changes; - platform objects stay pickable with no dependency; - the declare row opens the package edit dialog; - an unreadable package list narrows nothing. - `ObjectPicker.test.tsx`: a module-scope `fetch` double for `GET /api/v1/packages` (empty list, so nothing narrows and its expectations are unchanged). The picker now reads that list on mount, and the network-escape guard reds a real socket. <details><summary>Readings</summary> ``` # lint + server closure tests (server behaviour unchanged; existing server tests untouched) packages/lint: runtime-gate.resolve-package-closure, runtime-gate.package-closure, rule-id-barrel-exports, authoring-rule-wiring Test Files 4 passed (4) · Tests 75 passed (75) packages/metadata-protocol: protocol.package-closure-gate, protocol-publish-drafts-closure, protocol.draft-publish-verdict, protocol.runtime-authoring-gate, sys-metadata-repository.package-writability, protocol.packageless-save-address Test Files 6 passed (6) · Tests 382 passed (382) # picker ObjectPicker.closure-28240.test.tsx + ObjectPicker.test.tsx :: Test Files 2 passed · Tests 16 passed # broad sweep (every test under views/metadata-admin/, views/studio-design/, console/ai/) pnpm --dir ui exec vitest run --maxWorkers=3 packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/views/studio-design/ packages/app-shell/src/console/ai/ Test Files 1010 passed (1010) · Tests 9500 passed | 1 skipped (9501) # ablation (source; the test imports ObjectPicker relatively, no dist in the path) node scripts/ablation-replace.mjs --anchor "admitted && !admitted.has(e.name)\n ? 'outside'" --replacement "false\n ? 'outside'" mutation landed: anchor 1 -> 0, blob df4f2d4 -> 92c0e63c ObjectPicker.closure-28240.test.tsx :: Tests 4 failed | 2 passed (repro, re-read, platform, declare red; CONTROL and unreadable stay green, as they must) restore: blob after restore df4f2d4 = HEAD, git diff HEAD empty # type-check pnpm --filter @objectstack/lint run typecheck · @objectstack/metadata-protocol typecheck · @object-ui/app-shell type-check · @object-ui/i18n type-check :: exit 0 ``` </details> ## Verification Head `3c5e468c` (branch merged with `origin/main`, full rebuild `pnpm turbo run build --filter='!@objectstack/docs'` 115/115). - Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives **64** families; each was run with `</dev/null` and every one exits **0**. Reconciliation `--ran`: `64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN`. - `node scripts/check-sdui-manifest.mjs` is among the 64 (exit 0). `ui check:new-line-citations`: 0 new. `pnpm --dir ui exec vitest run scripts/` (pre-merge head): 169 files, 4901 tests passed. - Rerun on this head: picker 2 files / 16 tests, lint 4 files / 75 tests, metadata-protocol closure tests 6 files / 382 tests, type-checks (lint, metadata-protocol, i18n, app-shell) exit 0. <details><summary>Live measure (card item 2, and the picker)</summary> Own stack: showcase backend `objectstack dev --seed-admin --fresh -p 41873`; the Console production build (`vite build`) served by `vite preview --port 51874` proxied to it. (The Vite **dev** server cannot load `@objectstack/lint` in the browser, see Acceptance notes, so the picker was measured on the production bundle.) Item 2, the dependencies editor is reachable from Studio, no wiring needed: - **New package** (Studio landing, *New package (writable base)*): `package-form-dependencies` visible before *Advanced* = `false`, after expanding *Advanced* = `true`. It sits in the collapsed Advanced section, which is likely why the walkthrough missed it. - **Package settings** (package chip, *Package info & settings*, *Edit*): `package-form-dependencies` visible = `true`, one row, `com.example.showcase` (declared in the step below). Item 1, the card's repro on `com.default.acme-custom` (no dependencies), Automations, New, *When a record is created*, Object: - pickable before search: `sys_inbox_message`, `sys_file`, `sys_upload_session` (platform objects); toggle "Show objects outside this package's dependencies (24)"; - typing `task`: `showcase_task` `aria-disabled="true"`, "From com.example.showcase", reason row "Acme Custom can use only its own objects, …"; clicking it left the input at the typed `task` (nothing committed); - *Declare a dependency* opened the package edit dialog; adding `com.example.showcase` / `*` and saving closed it; the stored manifest reads `{"com.example.showcase":"*"}`, and the same picker then offered `showcase_task` enabled. </details> ## Acceptance notes - **Vite dev Console cannot import `@objectstack/lint` in the browser** (measured on this branch: `Module "module" has been externalized for browser compatibility. Cannot access "module.createRequire" in client code`). The lint graph imports `node:module` in six rule files. In dev the picker therefore narrows nothing and the server judges, the documented failure direction. The production bundle loads it (measured above). The existing in-browser lint callers (`preview/capabilityLint.ts`, `preview/securityPostureLint.ts`, `metadata-admin/celAuthoring.ts`) take the same dynamic import, so they degrade the same way in dev. Not changed here; Refs #28333. - **Report data source**: a new report binds a dataset, not an object, so its picker is the dataset list and is not narrowed here. A new dataset's object picker is narrowed. - **Free text stays free**: a name typed and committed with Enter is still saved as typed (flow references may hold expressions), and the server refuses an out-of-closure name at publish as before. - Card item 3 "the server refusal no longer reachable through the picker" holds for every pick from the list; the free-text path above is the remaining way to reach it, by design. - File surface: `ObjectPicker.tsx` and the new `objectPickerClosure.ts` live in `views/metadata-admin/inspectors/` (the shared picker every Studio object picker uses), and `packages-io.ts` gains a manifest reader. `StudioDesignSurface.tsx` is not touched. `packages/lint` and `protocol-write-package-scope.ts` per the seat's claim amendment 6538025413. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L9qibvSmNNn2cV2emnvGXn --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibvSmNNn2cV2emnvGXn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 64c5668 commit 488b3ed

13 files changed

Lines changed: 644 additions & 44 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
'@objectstack/lint': patch
5+
'@objectstack/metadata-protocol': patch
6+
---
7+
8+
fix(studio): object pickers offer only the objects the edited package may reference
9+
10+
Clause-②: no
11+
Metadata-protocol: none
12+
13+
A package may reference only the objects inside its closure: its own, those of the packages it declares as dependencies (transitively), and the platform's. The publish gate refused anything else, but Studio's object picker (a record trigger's object, a flow node's object, a lookup's related object, a new dataset's object) offered every object, so the author found out only after building on one.
14+
15+
The picker now offers the closure. Objects outside it are listed last, disabled, in a group that starts collapsed, under the reason and a **Declare a dependency** entry that opens the package's edit dialog when the package is writable. A dependency declared there re-reads the closure, and the object becomes pickable without a reload. Where the package list cannot be read, nothing is disabled and the server judges the pick, as before. The value stays free text: a name typed and committed with Enter is still saved as typed and judged by the server.
16+
17+
`@objectstack/lint` exports `resolvePackageClosure(packageId, getPackage)`, the dependency walk the publish gate ran inline. `@objectstack/metadata-protocol`'s gate now calls it; what the gate admits is unchanged. Studio calls the same function over `GET /api/v1/packages`, and `narrowObjectsToPackageClosure` over the object list, so the picker and the gate cannot disagree.

‎packages/lint/src/index.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1112,13 +1112,14 @@ export type {
11121112

11131113
// The runtime publish gate over that registry. Also published as the
11141114
// `@objectstack/lint/runtime` subpath — the entry the kernel boot path imports.
1115-
// That subpath narrows the EXPORT surface to these six names, not the module
1115+
// That subpath narrows the EXPORT surface to these names, not the module
11161116
// graph: measured, it reaches 70 of this entry's 72 modules and 93.8% of its
11171117
// bundled bytes, and it does name the modules that reach the source parsers.
11181118
// `runtime.ts`'s header carries the measurement and what the narrowing buys.
11191119
export {
11201120
buildRuntimeWriteSnapshots,
11211121
narrowObjectsToPackageClosure,
1122+
resolvePackageClosure,
11221123
runRuntimeAuthoringRules,
11231124
runtimeAuthoringRulesFor,
11241125
runtimeGatedTypes,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #28240 — `resolvePackageClosure`, the one dependency walk behind both the
5+
* server's publish gate (`resolveWritePackageScope`) and Studio's object
6+
* pickers. Pinned here on its own, over a plain package map, so the walk's
7+
* answer is fixed whichever host reads the registry.
8+
*/
9+
import { describe, expect, it } from 'vitest';
10+
import { resolvePackageClosure } from './runtime-gate.js';
11+
import * as runtimeEntry from './runtime.js';
12+
13+
type Pkg = { manifest?: unknown };
14+
15+
function registryOf(packages: Record<string, Record<string, string> | undefined>) {
16+
return (id: string): Pkg | undefined =>
17+
id in packages ? { manifest: { id, ...(packages[id] ? { dependencies: packages[id] } : {}) } } : undefined;
18+
}
19+
20+
describe('resolvePackageClosure (#28240)', () => {
21+
it('answers undefined, narrowing nothing, for no package id, the overlay sentinel and an unknown package', () => {
22+
const get = registryOf({ 'com.acme.app': {} });
23+
expect(resolvePackageClosure(undefined, get)).toBeUndefined();
24+
expect(resolvePackageClosure(null, get)).toBeUndefined();
25+
expect(resolvePackageClosure('', get)).toBeUndefined();
26+
expect(resolvePackageClosure('sys_metadata', get)).toBeUndefined();
27+
expect(resolvePackageClosure('com.acme.missing', get)).toBeUndefined();
28+
});
29+
30+
it('a package with no declared dependencies is its own closure', () => {
31+
expect(resolvePackageClosure('com.acme.app', registryOf({ 'com.acme.app': undefined }))).toEqual({
32+
packageId: 'com.acme.app',
33+
dependencies: [],
34+
});
35+
});
36+
37+
it('walks declared dependencies transitively, once each, through a cycle', () => {
38+
const get = registryOf({
39+
'com.acme.custom': { 'com.acme.crm': '^1.0.0' },
40+
'com.acme.crm': { 'com.acme.core': '^1.0.0', 'com.acme.custom': '*' },
41+
'com.acme.core': { 'com.acme.crm': '*' },
42+
'com.acme.unrelated': {},
43+
});
44+
const scope = resolvePackageClosure('com.acme.custom', get);
45+
expect(scope?.packageId).toBe('com.acme.custom');
46+
expect([...(scope?.dependencies ?? [])].sort()).toEqual(['com.acme.core', 'com.acme.crm']);
47+
});
48+
49+
it('keeps a declared dependency the registry cannot produce, and stops the walk there', () => {
50+
const get = registryOf({ 'com.acme.app': { 'com.vendor.gone': '^2.0.0' } });
51+
expect(resolvePackageClosure('com.acme.app', get)?.dependencies).toEqual(['com.vendor.gone']);
52+
});
53+
54+
it('lets a throwing registry propagate: the host decides what an unreadable registry means', () => {
55+
expect(() =>
56+
resolvePackageClosure('com.acme.app', () => {
57+
throw new Error('registry down');
58+
}),
59+
).toThrow('registry down');
60+
});
61+
62+
it('is named on the narrow runtime entry the server reaches lint through', () => {
63+
expect(runtimeEntry.resolvePackageClosure).toBe(resolvePackageClosure);
64+
});
65+
});

‎packages/lint/src/runtime-gate.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,50 @@ const PACKAGE_PROVENANCE_KEY = '_packageId';
464464
*/
465465
const OVERLAY_PROVENANCE_SENTINEL = 'sys_metadata';
466466

467+
/**
468+
* The package closure a write by `packageId` is judged against (#9612): the
469+
* package and the transitive closure of the `dependencies` its manifest
470+
* declares, read through `getPackage` — the host's package registry on the
471+
* server (`resolveWritePackageScope` in `@objectstack/metadata-protocol`), the
472+
* served package list in Studio, whose object pickers offer what this closure
473+
* admits. ONE walk for both, so the picker and the publish gate cannot
474+
* disagree about what a package may reference.
475+
*
476+
* Answers `undefined` — narrow nothing — for no package id, the
477+
* `sys_metadata` sentinel (an overlay row, not a package), and a package
478+
* `getPackage` cannot produce: its manifest is the only declaration of what it
479+
* may reference, and narrowing without it would be narrowing on nothing.
480+
*
481+
* A declared dependency `getPackage` cannot produce is still IN the closure:
482+
* the package declared it, so an object stamped with it is reachable by
483+
* declaration. Only the transitive walk stops there.
484+
*
485+
* Pure. A `getPackage` that throws propagates; the caller decides what an
486+
* unreadable registry means (the server answers `undefined`).
487+
*/
488+
export function resolvePackageClosure(
489+
packageId: string | null | undefined,
490+
getPackage: (id: string) => { manifest?: unknown } | undefined,
491+
): { packageId: string; dependencies: string[] } | undefined {
492+
if (typeof packageId !== 'string' || packageId === '' || packageId === OVERLAY_PROVENANCE_SENTINEL) return undefined;
493+
if (!getPackage(packageId)) return undefined;
494+
495+
const dependencies = new Set<string>();
496+
const frontier = [packageId];
497+
while (frontier.length > 0) {
498+
const current = frontier.pop() as string;
499+
const manifest = getPackage(current)?.manifest as { dependencies?: Record<string, unknown> } | undefined;
500+
const declared = manifest?.dependencies;
501+
if (!declared || typeof declared !== 'object') continue;
502+
for (const dep of Object.keys(declared)) {
503+
if (dep === packageId || dependencies.has(dep)) continue;
504+
dependencies.add(dep);
505+
frontier.push(dep);
506+
}
507+
}
508+
return { packageId, dependencies: [...dependencies] };
509+
}
510+
467511
/**
468512
* The context collections the snapshot carries, in stack-key order — DERIVED
469513
* from {@link RuntimeStackContext}, not listed (#13977).

‎packages/lint/src/runtime.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
*
3636
* ## Why it exists anyway
3737
*
38-
* So the kernel boot path can name only the five gate functions below, and so a
38+
* So the kernel boot path can name only the gate functions below, and so a
3939
* test can prove it named nothing else. `@objectstack/metadata-protocol`'s
4040
* runtime gate must reach this package through this entry rather than the root
4141
* barrel, and `authoring-rule-wiring.test.ts` fails if it ever does otherwise.
@@ -64,6 +64,7 @@
6464
export {
6565
buildRuntimeWriteSnapshots,
6666
narrowObjectsToPackageClosure,
67+
resolvePackageClosure,
6768
runRuntimeAuthoringRules,
6869
runtimeAuthoringRulesFor,
6970
runtimeGatedTypes,

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

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
* wherever that cannot be read. Moved out of `protocol.ts` whole.
99
*
1010
* A MOVE, not a redesign. The function below is the body of the
11-
* `ObjectStackProtocolImplementation` method of the same name as it stood,
12-
* re-indented one level and otherwise unedited: the same guards, the same
13-
* dependency walk, and the same silent `catch` answering `undefined`.
11+
* `ObjectStackProtocolImplementation` method of the same name: the same
12+
* guards, the same dependency walk (since #28240 the shared
13+
* `resolvePackageClosure`, unchanged), and the same silent `catch` answering
14+
* `undefined`.
1415
*
1516
* ## How the moved body reaches the class
1617
*
@@ -24,11 +25,12 @@
2425
* calling the class method: `tenantAuthoredWriteRefusal`, and the authoring
2526
* gate's host (cut 8, `protocol-authoring-gate.ts`), whose closure calls it.
2627
*
27-
* The body reads no module-level declaration of `protocol.ts` and no import,
28-
* so this module imports only the class type. Nothing re-exports this module,
29-
* so the published surface does not move.
28+
* 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.
3031
*/
3132

33+
import { resolvePackageClosure } from '@objectstack/lint/runtime';
3234
import type { ObjectStackProtocolImplementation } from './protocol.js';
3335

3436
/**
@@ -42,43 +44,26 @@ export interface WritePackageScopeHost {
4244
readonly engine: ObjectStackProtocolImplementation['engine'];
4345
}
4446

45-
/** The body of `ObjectStackProtocolImplementation#resolveWritePackageScope`; the method's docblock there is its contract. */
47+
/**
48+
* The body of `ObjectStackProtocolImplementation#resolveWritePackageScope`; the method's docblock there is its contract.
49+
*
50+
* [#28240] The dependency walk itself is `resolvePackageClosure` in
51+
* `@objectstack/lint`, the same function Studio's object pickers call over
52+
* the served package list, so the picker offers exactly what this gate
53+
* admits. This body keeps the two host guards: a registry with no
54+
* `getPackage`, and the silent `catch` answering `undefined`.
55+
*/
4656
export function resolveWritePackageScope(
4757
this: Pick<WritePackageScopeHost, 'engine'>,
4858
packageId: string | null | undefined,
4959
): { packageId: string; dependencies: string[] } | undefined {
50-
if (typeof packageId !== 'string' || packageId === '' || packageId === 'sys_metadata') return undefined;
5160
try {
5261
const registry = this.engine.registry as
5362
| { getPackage?: (id: string) => { manifest?: unknown } | undefined }
5463
| undefined;
55-
if (typeof registry?.getPackage !== 'function') return undefined;
56-
// The written package itself must be readable: its manifest is the
57-
// ONLY declaration of what it may reference, and narrowing without
58-
// it would be narrowing on nothing.
59-
if (!registry.getPackage(packageId)) return undefined;
60-
61-
const dependencies = new Set<string>();
62-
const frontier = [packageId];
63-
while (frontier.length > 0) {
64-
const current = frontier.pop() as string;
65-
const manifest = registry.getPackage(current)?.manifest as
66-
| { dependencies?: Record<string, unknown> }
67-
| undefined;
68-
const declared = manifest?.dependencies;
69-
if (!declared || typeof declared !== 'object') continue;
70-
for (const dep of Object.keys(declared)) {
71-
// A dependency the registry cannot resolve is still IN the
72-
// closure: the package declared it, so an object stamped
73-
// with it is reachable by declaration. Only the transitive
74-
// walk stops there — `getPackage` returns undefined and the
75-
// loop above simply contributes nothing further.
76-
if (dep === packageId || dependencies.has(dep)) continue;
77-
dependencies.add(dep);
78-
frontier.push(dep);
79-
}
80-
}
81-
return { packageId, dependencies: [...dependencies] };
64+
const getPackage = registry?.getPackage;
65+
if (typeof getPackage !== 'function') return undefined;
66+
return resolvePackageClosure(packageId, (id) => getPackage.call(registry, id));
8267
} catch {
8368
// Context gathering must never fail a write (the same contract
8469
// `listCollection` above holds). An unreadable registry means an

0 commit comments

Comments
 (0)