@gmickel/gno 1.22.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +33 -12
  2. package/assets/skill/SKILL.md +41 -19
  3. package/package.json +1 -1
  4. package/spec/cli.md +127 -20
  5. package/spec/evals-agentic.md +35 -0
  6. package/spec/evals.md +6 -0
  7. package/spec/mcp.md +18 -0
  8. package/spec/output-schemas/query-diagnose-v1.schema.json +123 -0
  9. package/spec/output-schemas/query-diagnose.schema.json +89 -2
  10. package/spec/output-schemas/setup-activation-result.schema.json +456 -0
  11. package/spec/output-schemas/setup-command-result.schema.json +93 -0
  12. package/spec/output-schemas/setup-receipt.schema.json +258 -0
  13. package/spec/output-schemas/setup-semantic-receipt.schema.json +195 -0
  14. package/src/app/context-runtime-types.ts +3 -0
  15. package/src/app/context-runtime.ts +1 -0
  16. package/src/app/context-surface.ts +4 -2
  17. package/src/cli/commands/ask.ts +31 -20
  18. package/src/cli/commands/completion/scripts.ts +2 -0
  19. package/src/cli/commands/context-build.ts +17 -7
  20. package/src/cli/commands/embed.ts +7 -2
  21. package/src/cli/commands/query.ts +58 -37
  22. package/src/cli/commands/search.ts +29 -19
  23. package/src/cli/commands/setup-activation.ts +324 -0
  24. package/src/cli/commands/setup-semantic.ts +591 -0
  25. package/src/cli/commands/setup.ts +410 -0
  26. package/src/cli/commands/vsearch.ts +31 -22
  27. package/src/cli/options.ts +39 -0
  28. package/src/cli/program.ts +112 -0
  29. package/src/cli/setup-semantic-worker.ts +177 -0
  30. package/src/config/defaults.ts +10 -1
  31. package/src/config/types.ts +71 -0
  32. package/src/core/config-mutation.ts +94 -64
  33. package/src/core/file-lock.ts +70 -31
  34. package/src/core/folder-setup-planning.ts +453 -0
  35. package/src/core/folder-setup.ts +490 -0
  36. package/src/core/project-affinity-surface.ts +114 -0
  37. package/src/core/project-affinity.ts +330 -0
  38. package/src/core/setup-activation.ts +309 -0
  39. package/src/core/setup-receipt.ts +321 -0
  40. package/src/core/validation.ts +20 -1
  41. package/src/mcp/tools/ask.ts +10 -1
  42. package/src/mcp/tools/context.ts +18 -0
  43. package/src/mcp/tools/index.ts +13 -2
  44. package/src/mcp/tools/query.ts +12 -0
  45. package/src/mcp/tools/search.ts +7 -0
  46. package/src/mcp/tools/vsearch.ts +7 -0
  47. package/src/pipeline/diagnose.ts +48 -3
  48. package/src/pipeline/explain.ts +54 -13
  49. package/src/pipeline/hybrid.ts +100 -59
  50. package/src/pipeline/project-affinity.ts +162 -0
  51. package/src/pipeline/search.ts +76 -10
  52. package/src/pipeline/types.ts +9 -0
  53. package/src/pipeline/vsearch.ts +117 -91
  54. package/src/sdk/client.ts +80 -20
  55. package/src/sdk/index.ts +2 -0
  56. package/src/sdk/types.ts +20 -7
  57. package/src/serve/connectors.ts +29 -2
  58. package/src/serve/context-capsule.ts +18 -1
  59. package/src/serve/routes/api.ts +69 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Trusted project-root resolution for project-aware retrieval affinity.
3
+ *
4
+ * This module resolves local roots only. Remote hints remain opaque and never
5
+ * trigger filesystem probes. Returned metadata is path-redacted; ranking is a
6
+ * separate concern.
7
+ *
8
+ * @module src/core/project-affinity
9
+ */
10
+
11
+ // node:fs/promises for directory stat/realpath (no Bun directory equivalent)
12
+ import { realpath, stat } from "node:fs/promises";
13
+ // node:path for path utilities (no Bun path utils)
14
+ import { dirname, join, relative, sep } from "node:path";
15
+
16
+ import type {
17
+ Collection,
18
+ ProjectAffinityInput,
19
+ ProjectAffinityRoot,
20
+ TrustedProjectRootSource,
21
+ } from "../config/types";
22
+
23
+ import { isCanonicalPathContained } from "./validation";
24
+
25
+ export type ProjectAffinityZeroReason =
26
+ | "no_collection_match"
27
+ | "root_unavailable"
28
+ | "untrusted_remote_hint";
29
+
30
+ export type ProjectAffinityRelation =
31
+ | "collection_contains_root"
32
+ | "exact"
33
+ | "root_contains_collection";
34
+
35
+ export interface ProjectAffinityMatch {
36
+ collection: string;
37
+ collectionAlias: string;
38
+ distance: number;
39
+ relation: ProjectAffinityRelation;
40
+ rootAlias: string;
41
+ source: TrustedProjectRootSource;
42
+ }
43
+
44
+ export interface ProjectAffinityRootMetadata {
45
+ collectionAliases: string[];
46
+ reason: ProjectAffinityZeroReason | null;
47
+ repositoryRootDiscovered: boolean;
48
+ rootAlias: string;
49
+ source: ProjectAffinityRoot["source"];
50
+ status: "matched" | "zero";
51
+ }
52
+
53
+ export interface ProjectAffinityResolution {
54
+ matches: ProjectAffinityMatch[];
55
+ roots: ProjectAffinityRootMetadata[];
56
+ }
57
+
58
+ export interface ProjectAffinityResolverDependencies {
59
+ canonicalizePath: (path: string) => Promise<string>;
60
+ discoverRepositoryRoot: (path: string) => Promise<string | null>;
61
+ }
62
+
63
+ /**
64
+ * Trust is supplied by the invoking surface, never inferred from payload data.
65
+ */
66
+ export interface ProjectAffinityResolverContext {
67
+ channel: "local" | "remote";
68
+ }
69
+
70
+ interface CanonicalCollection {
71
+ alias: string;
72
+ name: string;
73
+ path: string;
74
+ }
75
+
76
+ interface CanonicalTrustedRoot {
77
+ path: string;
78
+ repositoryRootDiscovered: boolean;
79
+ rootAlias: string;
80
+ source: TrustedProjectRootSource;
81
+ }
82
+
83
+ const alias = (namespace: "collection" | "root", value: string): string => {
84
+ const hash = new Bun.CryptoHasher("sha256")
85
+ .update(`${namespace}\0${value}`)
86
+ .digest("hex")
87
+ .slice(0, 12);
88
+ return `${namespace}_${hash}`;
89
+ };
90
+
91
+ const defaultCanonicalizePath = (path: string): Promise<string> =>
92
+ realpath(path);
93
+
94
+ const isDirectoryOrFile = async (path: string): Promise<boolean> => {
95
+ try {
96
+ await stat(path);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ };
102
+
103
+ const defaultDiscoverRepositoryRoot = async (
104
+ startingPath: string
105
+ ): Promise<string | null> => {
106
+ let current = startingPath;
107
+ while (true) {
108
+ if (await isDirectoryOrFile(join(current, ".git"))) {
109
+ return current;
110
+ }
111
+ const parent = dirname(current);
112
+ if (parent === current) {
113
+ return null;
114
+ }
115
+ current = parent;
116
+ }
117
+ };
118
+
119
+ const pathDistance = (parent: string, child: string): number => {
120
+ const nestedPath = relative(parent, child);
121
+ if (nestedPath === "") {
122
+ return 0;
123
+ }
124
+ return nestedPath.split(sep).filter(Boolean).length;
125
+ };
126
+
127
+ const describeRelationship = (
128
+ collectionPath: string,
129
+ rootPath: string
130
+ ): Pick<ProjectAffinityMatch, "distance" | "relation"> | null => {
131
+ if (collectionPath === rootPath) {
132
+ return { distance: 0, relation: "exact" };
133
+ }
134
+ if (isCanonicalPathContained(collectionPath, rootPath)) {
135
+ return {
136
+ distance: pathDistance(collectionPath, rootPath),
137
+ relation: "collection_contains_root",
138
+ };
139
+ }
140
+ if (isCanonicalPathContained(rootPath, collectionPath)) {
141
+ return {
142
+ distance: pathDistance(rootPath, collectionPath),
143
+ relation: "root_contains_collection",
144
+ };
145
+ }
146
+ return null;
147
+ };
148
+
149
+ const canonicalizeCollections = async (
150
+ collections: readonly Collection[],
151
+ canonicalizePath: ProjectAffinityResolverDependencies["canonicalizePath"]
152
+ ): Promise<CanonicalCollection[]> => {
153
+ const resolved = await Promise.all(
154
+ collections.map(async (collection): Promise<CanonicalCollection | null> => {
155
+ try {
156
+ const path = await canonicalizePath(collection.path);
157
+ return {
158
+ alias: alias("collection", collection.name),
159
+ name: collection.name,
160
+ path,
161
+ };
162
+ } catch {
163
+ return null;
164
+ }
165
+ })
166
+ );
167
+ return resolved
168
+ .filter((collection): collection is CanonicalCollection =>
169
+ Boolean(collection)
170
+ )
171
+ .sort((left, right) => left.name.localeCompare(right.name));
172
+ };
173
+
174
+ const canonicalizeTrustedRoot = async (
175
+ root: Extract<ProjectAffinityRoot, { path: string }>,
176
+ dependencies: ProjectAffinityResolverDependencies
177
+ ): Promise<CanonicalTrustedRoot | null> => {
178
+ let canonicalPath: string;
179
+ try {
180
+ canonicalPath = await dependencies.canonicalizePath(root.path);
181
+ } catch {
182
+ return null;
183
+ }
184
+
185
+ let repositoryRootDiscovered = false;
186
+ if (root.source === "cli_cwd" || root.source === "cli_worktree") {
187
+ let discovered: string | null = null;
188
+ try {
189
+ discovered = await dependencies.discoverRepositoryRoot(canonicalPath);
190
+ } catch {
191
+ // Repository discovery is opportunistic; the trusted canonical cwd
192
+ // remains a valid affinity root when discovery is unavailable.
193
+ }
194
+ if (discovered) {
195
+ try {
196
+ canonicalPath = await dependencies.canonicalizePath(discovered);
197
+ repositoryRootDiscovered = true;
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+ }
203
+
204
+ return {
205
+ path: canonicalPath,
206
+ repositoryRootDiscovered,
207
+ rootAlias: alias("root", canonicalPath),
208
+ source: root.source,
209
+ };
210
+ };
211
+
212
+ const zeroMetadata = (
213
+ root: ProjectAffinityRoot,
214
+ reason: ProjectAffinityZeroReason,
215
+ source: ProjectAffinityRoot["source"] = root.source
216
+ ): ProjectAffinityRootMetadata => ({
217
+ collectionAliases: [],
218
+ reason,
219
+ repositoryRootDiscovered: false,
220
+ rootAlias: alias(
221
+ "root",
222
+ root.source === "remote_hint" ? root.hint : root.path
223
+ ),
224
+ source,
225
+ status: "zero",
226
+ });
227
+
228
+ const relationOrder: Record<ProjectAffinityRelation, number> = {
229
+ exact: 0,
230
+ collection_contains_root: 1,
231
+ root_contains_collection: 2,
232
+ };
233
+
234
+ const compareMatches = (
235
+ left: ProjectAffinityMatch,
236
+ right: ProjectAffinityMatch
237
+ ): number =>
238
+ relationOrder[left.relation] - relationOrder[right.relation] ||
239
+ left.distance - right.distance ||
240
+ left.collection.localeCompare(right.collection) ||
241
+ left.rootAlias.localeCompare(right.rootAlias);
242
+
243
+ export async function resolveProjectAffinity(
244
+ input: ProjectAffinityInput,
245
+ collections: readonly Collection[],
246
+ context: ProjectAffinityResolverContext,
247
+ overrides: Partial<ProjectAffinityResolverDependencies> = {}
248
+ ): Promise<ProjectAffinityResolution> {
249
+ if (context.channel === "remote") {
250
+ return {
251
+ matches: [],
252
+ roots: input.roots.map((root) =>
253
+ zeroMetadata(root, "untrusted_remote_hint", "remote_hint")
254
+ ),
255
+ };
256
+ }
257
+
258
+ const dependencies: ProjectAffinityResolverDependencies = {
259
+ canonicalizePath: overrides.canonicalizePath ?? defaultCanonicalizePath,
260
+ discoverRepositoryRoot:
261
+ overrides.discoverRepositoryRoot ?? defaultDiscoverRepositoryRoot,
262
+ };
263
+ const trustedRoots = input.roots.filter(
264
+ (root): root is Extract<ProjectAffinityRoot, { path: string }> =>
265
+ root.source !== "remote_hint"
266
+ );
267
+ const canonicalCollections =
268
+ trustedRoots.length === 0
269
+ ? []
270
+ : await canonicalizeCollections(
271
+ collections,
272
+ dependencies.canonicalizePath
273
+ );
274
+ const matches: ProjectAffinityMatch[] = [];
275
+ const roots: ProjectAffinityRootMetadata[] = [];
276
+
277
+ for (const root of input.roots) {
278
+ if (root.source === "remote_hint") {
279
+ roots.push(zeroMetadata(root, "untrusted_remote_hint"));
280
+ continue;
281
+ }
282
+
283
+ const canonicalRoot = await canonicalizeTrustedRoot(root, dependencies);
284
+ if (!canonicalRoot) {
285
+ roots.push(zeroMetadata(root, "root_unavailable"));
286
+ continue;
287
+ }
288
+
289
+ const rootMatches = canonicalCollections
290
+ .map((collection): ProjectAffinityMatch | null => {
291
+ const relationship = describeRelationship(
292
+ collection.path,
293
+ canonicalRoot.path
294
+ );
295
+ return relationship
296
+ ? {
297
+ collection: collection.name,
298
+ collectionAlias: collection.alias,
299
+ rootAlias: canonicalRoot.rootAlias,
300
+ source: canonicalRoot.source,
301
+ ...relationship,
302
+ }
303
+ : null;
304
+ })
305
+ .filter((match): match is ProjectAffinityMatch => Boolean(match))
306
+ .sort(compareMatches);
307
+
308
+ if (rootMatches.length === 0) {
309
+ roots.push({
310
+ ...zeroMetadata(root, "no_collection_match"),
311
+ repositoryRootDiscovered: canonicalRoot.repositoryRootDiscovered,
312
+ rootAlias: canonicalRoot.rootAlias,
313
+ });
314
+ continue;
315
+ }
316
+
317
+ matches.push(...rootMatches);
318
+ roots.push({
319
+ collectionAliases: rootMatches.map((match) => match.collectionAlias),
320
+ reason: null,
321
+ repositoryRootDiscovered: canonicalRoot.repositoryRootDiscovered,
322
+ rootAlias: canonicalRoot.rootAlias,
323
+ source: root.source,
324
+ status: "matched",
325
+ });
326
+ }
327
+
328
+ matches.sort(compareMatches);
329
+ return { matches, roots };
330
+ }
@@ -0,0 +1,309 @@
1
+ import type {
2
+ ActivationVerificationCode,
3
+ ActivationVerificationReceipt,
4
+ StorePort,
5
+ StoreResult,
6
+ } from "../store/types";
7
+ import type { ConnectorVerificationCode } from "./connector-policy";
8
+
9
+ import { getConnectorVerificationRemediation } from "./connector-policy";
10
+
11
+ export const SETUP_ACTIVATION_SCHEMA_VERSION = "1.0" as const;
12
+
13
+ export type SetupConnectorInstallation = "installed" | "reused" | "failed";
14
+ export type SetupConnectorVerification =
15
+ | "passed"
16
+ | "failed"
17
+ | "skipped"
18
+ | "not_run";
19
+ export type SetupConnectorCode =
20
+ | "connector_verified"
21
+ | "connector_configuration_unavailable"
22
+ | "connector_install_failed"
23
+ | "connector_verification_failed"
24
+ | ConnectorVerificationCode;
25
+
26
+ export interface SetupConnectorDefinition {
27
+ id: string;
28
+ kind: "skill" | "mcp";
29
+ target: string;
30
+ scope: "user" | "project";
31
+ }
32
+
33
+ export interface SetupConnectorState extends SetupConnectorDefinition {
34
+ installed: boolean;
35
+ configurationError: boolean;
36
+ }
37
+
38
+ export interface SetupConnectorResult {
39
+ connectorId: string;
40
+ kind: "skill" | "mcp";
41
+ target: string;
42
+ scope: "user" | "project";
43
+ installation: SetupConnectorInstallation;
44
+ verification: SetupConnectorVerification;
45
+ code: SetupConnectorCode;
46
+ remediation: string;
47
+ receipt: ActivationVerificationReceipt | null;
48
+ }
49
+
50
+ export interface SetupConnectorComposition {
51
+ status: "completed" | "completed_with_actions";
52
+ connectors: SetupConnectorResult[];
53
+ }
54
+
55
+ export interface SetupConnectorInstallContext {
56
+ indexName: string;
57
+ configPath: string;
58
+ }
59
+
60
+ export interface SetupConnectorCompositionDeps {
61
+ getStates: () => Promise<SetupConnectorState[]>;
62
+ install: (
63
+ connectorId: string,
64
+ context: SetupConnectorInstallContext
65
+ ) => Promise<SetupConnectorState>;
66
+ verify: (
67
+ connectorId: string,
68
+ store: StorePort,
69
+ collection: string
70
+ ) => Promise<StoreResult<ActivationVerificationReceipt>>;
71
+ }
72
+
73
+ export interface ComposeSetupConnectorsOptions {
74
+ connectorIds: string[];
75
+ definitions: SetupConnectorDefinition[];
76
+ collection: string;
77
+ store: StorePort;
78
+ installContext: SetupConnectorInstallContext;
79
+ deps: SetupConnectorCompositionDeps;
80
+ }
81
+
82
+ const completedRemediation = "No action required.";
83
+ const installFailureRemediation =
84
+ "Repair the selected connector configuration or permissions, then rerun the same setup command.";
85
+ const configurationFailureRemediation =
86
+ "Repair the selected connector configuration without overwriting it, then rerun the same setup command.";
87
+ const verificationFailureRemediation =
88
+ "Retry the same setup command; if verification still fails, run gno doctor.";
89
+
90
+ function isConnectorVerificationCode(
91
+ code: ActivationVerificationCode | undefined
92
+ ): code is ConnectorVerificationCode {
93
+ return (
94
+ code === "connector_not_configured" ||
95
+ code === "connector_probe_unavailable" ||
96
+ code === "connector_unsupported_config" ||
97
+ code === "connector_start_failed" ||
98
+ code === "connector_timeout" ||
99
+ code === "connector_missing_tools" ||
100
+ code === "connector_status_failed" ||
101
+ code === "connector_search_failed" ||
102
+ code === "connector_result_mismatch" ||
103
+ code === "target_runtime_unverifiable"
104
+ );
105
+ }
106
+
107
+ function dedupeConnectorIds(connectorIds: string[]): string[] {
108
+ const seen = new Set<string>();
109
+ return connectorIds.filter((connectorId) => {
110
+ if (seen.has(connectorId)) {
111
+ return false;
112
+ }
113
+ seen.add(connectorId);
114
+ return true;
115
+ });
116
+ }
117
+
118
+ function failedResult(
119
+ definition: SetupConnectorDefinition,
120
+ input: {
121
+ installation: SetupConnectorInstallation;
122
+ code:
123
+ | "connector_configuration_unavailable"
124
+ | "connector_install_failed"
125
+ | "connector_verification_failed";
126
+ remediation: string;
127
+ }
128
+ ): SetupConnectorResult {
129
+ return {
130
+ connectorId: definition.id,
131
+ kind: definition.kind,
132
+ target: definition.target,
133
+ scope: definition.scope,
134
+ installation: input.installation,
135
+ verification:
136
+ input.code === "connector_verification_failed" ? "failed" : "not_run",
137
+ code: input.code,
138
+ remediation: input.remediation,
139
+ receipt: null,
140
+ };
141
+ }
142
+
143
+ function receiptResult(
144
+ definition: SetupConnectorDefinition,
145
+ installation: SetupConnectorInstallation,
146
+ receipt: ActivationVerificationReceipt
147
+ ): SetupConnectorResult {
148
+ const connectorStage = receipt.stages.connector;
149
+ const verification =
150
+ connectorStage.status === "passed" ||
151
+ connectorStage.status === "failed" ||
152
+ connectorStage.status === "skipped"
153
+ ? connectorStage.status
154
+ : "failed";
155
+ const code =
156
+ connectorStage.status === "passed"
157
+ ? "connector_verified"
158
+ : isConnectorVerificationCode(connectorStage.code)
159
+ ? connectorStage.code
160
+ : "connector_verification_failed";
161
+ const remediation =
162
+ code === "connector_verified"
163
+ ? completedRemediation
164
+ : code === "connector_verification_failed"
165
+ ? verificationFailureRemediation
166
+ : getConnectorVerificationRemediation(code, definition.target);
167
+ return {
168
+ connectorId: definition.id,
169
+ kind: definition.kind,
170
+ target: definition.target,
171
+ scope: definition.scope,
172
+ installation,
173
+ verification,
174
+ code,
175
+ remediation,
176
+ receipt,
177
+ };
178
+ }
179
+
180
+ export function unavailableSetupConnectorComposition(
181
+ definitions: SetupConnectorDefinition[]
182
+ ): SetupConnectorComposition {
183
+ return {
184
+ status: "completed_with_actions",
185
+ connectors: definitions.map((definition) => ({
186
+ connectorId: definition.id,
187
+ kind: definition.kind,
188
+ target: definition.target,
189
+ scope: definition.scope,
190
+ installation: "failed",
191
+ verification: "not_run",
192
+ code: "connector_verification_failed",
193
+ remediation: verificationFailureRemediation,
194
+ receipt: null,
195
+ })),
196
+ };
197
+ }
198
+
199
+ async function currentStateAfterInstallFailure(
200
+ connectorId: string,
201
+ deps: SetupConnectorCompositionDeps
202
+ ): Promise<SetupConnectorState | null> {
203
+ try {
204
+ const states = await deps.getStates();
205
+ return states.find(({ id }) => id === connectorId) ?? null;
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ async function composeOneConnector(
212
+ definition: SetupConnectorDefinition,
213
+ initialState: SetupConnectorState | null,
214
+ options: ComposeSetupConnectorsOptions
215
+ ): Promise<SetupConnectorResult> {
216
+ if (!initialState || initialState.configurationError) {
217
+ return failedResult(definition, {
218
+ installation: "failed",
219
+ code: "connector_configuration_unavailable",
220
+ remediation: configurationFailureRemediation,
221
+ });
222
+ }
223
+
224
+ let installation: SetupConnectorInstallation = "reused";
225
+ if (!initialState.installed) {
226
+ try {
227
+ const installed = await options.deps.install(
228
+ definition.id,
229
+ options.installContext
230
+ );
231
+ if (!installed.installed || installed.configurationError) {
232
+ return failedResult(definition, {
233
+ installation: "failed",
234
+ code: "connector_install_failed",
235
+ remediation: installFailureRemediation,
236
+ });
237
+ }
238
+ installation = "installed";
239
+ } catch {
240
+ const racedState = await currentStateAfterInstallFailure(
241
+ definition.id,
242
+ options.deps
243
+ );
244
+ if (!racedState?.installed || racedState.configurationError) {
245
+ return failedResult(definition, {
246
+ installation: "failed",
247
+ code: "connector_install_failed",
248
+ remediation: installFailureRemediation,
249
+ });
250
+ }
251
+ installation = "reused";
252
+ }
253
+ }
254
+
255
+ try {
256
+ const verification = await options.deps.verify(
257
+ definition.id,
258
+ options.store,
259
+ options.collection
260
+ );
261
+ if (!verification.ok) {
262
+ return failedResult(definition, {
263
+ installation,
264
+ code: "connector_verification_failed",
265
+ remediation: verificationFailureRemediation,
266
+ });
267
+ }
268
+ return receiptResult(definition, installation, verification.value);
269
+ } catch {
270
+ return failedResult(definition, {
271
+ installation,
272
+ code: "connector_verification_failed",
273
+ remediation: verificationFailureRemediation,
274
+ });
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Compose requested connector install/verification after lexical setup.
280
+ * Callers own store lifecycle and validation of connector IDs.
281
+ */
282
+ export async function composeSetupConnectors(
283
+ options: ComposeSetupConnectorsOptions
284
+ ): Promise<SetupConnectorComposition> {
285
+ const requestedIds = dedupeConnectorIds(options.connectorIds);
286
+ let states: SetupConnectorState[] = [];
287
+ try {
288
+ states = await options.deps.getStates();
289
+ } catch {
290
+ // Per-target bounded results below deliberately hide the unbounded error.
291
+ }
292
+
293
+ const results: SetupConnectorResult[] = [];
294
+ for (const connectorId of requestedIds) {
295
+ const definition = options.definitions.find(({ id }) => id === connectorId);
296
+ if (!definition) {
297
+ continue;
298
+ }
299
+ const state = states.find(({ id }) => id === connectorId) ?? null;
300
+ results.push(await composeOneConnector(definition, state, options));
301
+ }
302
+
303
+ return {
304
+ status: results.every(({ verification }) => verification === "passed")
305
+ ? "completed"
306
+ : "completed_with_actions",
307
+ connectors: results,
308
+ };
309
+ }