@abelxiaoxing/cadence 1.4.0 → 1.5.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 (66) hide show
  1. package/README.md +130 -8
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/config/plan-draft.quick.example.json +78 -0
  8. package/package.json +10 -4
  9. package/prompts/abel-design.md +101 -44
  10. package/prompts/abel-implement.md +9 -0
  11. package/src/activity-contracts.ts +82 -0
  12. package/src/candidate-artifact.ts +143 -0
  13. package/src/candidate-patch.ts +31 -11
  14. package/src/candidate-workspace.ts +74 -0
  15. package/src/change-contract-schema.ts +191 -0
  16. package/src/change-contract.ts +5 -0
  17. package/src/change-verification.ts +920 -0
  18. package/src/child-budget.ts +58 -0
  19. package/src/child-model.ts +142 -0
  20. package/src/child-session.ts +289 -326
  21. package/src/contracts.ts +200 -76
  22. package/src/delivery-compiler.ts +105 -120
  23. package/src/delivery-revision.ts +42 -0
  24. package/src/design-control.ts +113 -4
  25. package/src/design-diagnostics.ts +150 -0
  26. package/src/durable-contracts.ts +213 -0
  27. package/src/durable-workflow.ts +148 -3570
  28. package/src/evidence-draft.ts +122 -0
  29. package/src/execution-profile.ts +38 -0
  30. package/src/implement-graph.ts +3 -0
  31. package/src/implement-plan.ts +70 -0
  32. package/src/index.ts +59 -522
  33. package/src/isolation-backend.ts +62 -40
  34. package/src/model-source.ts +22 -0
  35. package/src/operator-cli-entry.mjs +24 -0
  36. package/src/operator-cli.mjs +1421 -0
  37. package/src/operator-tools.ts +249 -0
  38. package/src/package-candidate.ts +227 -0
  39. package/src/package-verification.ts +86 -8
  40. package/src/package-workflow.ts +291 -0
  41. package/src/packet-runtime.ts +27 -48
  42. package/src/parent-provider.ts +86 -198
  43. package/src/phase-execution.ts +2248 -0
  44. package/src/pi-adapter.ts +19 -0
  45. package/src/plan-draft-summary.ts +149 -0
  46. package/src/plan-draft.ts +493 -0
  47. package/src/single-task-draft.ts +142 -0
  48. package/src/subagent-activity.ts +32 -66
  49. package/src/submit-tool.ts +20 -59
  50. package/src/transport-budget.ts +52 -0
  51. package/src/verification-capability.ts +5 -3
  52. package/src/verification-diagnostics.ts +76 -0
  53. package/src/verification-environment-io.ts +280 -0
  54. package/src/verification-environment.ts +56 -154
  55. package/src/verification-scan.ts +64 -0
  56. package/src/worker-broker.ts +39 -57
  57. package/src/workflow-policy.ts +14 -53
  58. package/src/workflow-recovery-policy.ts +95 -0
  59. package/src/workflow-scheduling.ts +105 -0
  60. package/src/workflow-state-machine.ts +105 -497
  61. package/src/workflow-status.ts +384 -0
  62. package/src/workspace-io-entry.mjs +19 -1
  63. package/src/workspace-io-worker.mjs +275 -50
  64. package/src/workspace-io.ts +6 -1
  65. package/src/empty-resource-loader.ts +0 -38
  66. package/src/parent-payload-bridge.ts +0 -523
@@ -0,0 +1,280 @@
1
+ import {
2
+ constants,
3
+ cpSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ realpathSync,
8
+ rmSync,
9
+ symlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import path from "node:path";
13
+ import type { ExecutionProfile } from "./execution-profile.ts";
14
+ import type { IsolationMount } from "./isolation-backend.ts";
15
+ import type { VerificationRunnerBinding } from "./verification-capability.ts";
16
+
17
+ const CACHES = [".vite", ".vite-temp"];
18
+ function within(root: string, candidate: string): boolean {
19
+ const relative = path.relative(root, candidate);
20
+ return (
21
+ relative !== ".." &&
22
+ !relative.startsWith(`..${path.sep}`) &&
23
+ !path.isAbsolute(relative)
24
+ );
25
+ }
26
+
27
+ /** A disposable dependency view: immutable packages and independently writable caches. */
28
+ export function prepareVerificationEnvironmentIo(
29
+ root: string,
30
+ dependencyOwner: string,
31
+ runnerBindings: readonly VerificationRunnerBinding[],
32
+ profile: ExecutionProfile,
33
+ environment: NodeJS.ProcessEnv,
34
+ checkCancelled: () => void,
35
+ privateRoot: string,
36
+ ) {
37
+ const cleanup = () => rmSync(privateRoot, { recursive: true, force: true });
38
+ try {
39
+ const local = profile.mode === "local-trusted";
40
+ if (local && realpathSync(root) === realpathSync(dependencyOwner))
41
+ throw new Error("trusted-candidate-required");
42
+ const mounts: IsolationMount[] = [
43
+ { source: privateRoot, target: "/cadence", writable: true },
44
+ ];
45
+ for (const directory of [
46
+ "home",
47
+ "cache",
48
+ "tmp",
49
+ "reports",
50
+ "dependencies",
51
+ ]) {
52
+ mkdirSync(path.join(privateRoot, directory), { mode: 0o700 });
53
+ }
54
+ writeFileSync(path.join(privateRoot, "home/user.npmrc"), "");
55
+ writeFileSync(path.join(privateRoot, "home/global.npmrc"), "");
56
+ const dependencies = path.join(dependencyOwner, "node_modules");
57
+ const stat = lstatSync(dependencies, { throwIfNoEntry: false });
58
+ if (stat) {
59
+ const target = path.join(root, "node_modules");
60
+ const targetStat = lstatSync(target, { throwIfNoEntry: false });
61
+ if (
62
+ !stat.isDirectory() ||
63
+ stat.isSymbolicLink() ||
64
+ (targetStat &&
65
+ (!targetStat.isDirectory() || targetStat.isSymbolicLink()))
66
+ ) {
67
+ throw new Error("dependency-path-unsafe");
68
+ }
69
+ if (!targetStat) mkdirSync(target, { mode: 0o700 });
70
+ const view = path.join(privateRoot, "dependencies");
71
+ mounts.push({ source: view, target: "/workspace/node_modules" });
72
+ const entries = readdirSync(dependencies);
73
+ if (entries.length > 20_000) throw new Error("dependency-view-too-large");
74
+ const canonical = realpathSync(dependencies);
75
+ let viewEntries = 0;
76
+ const projectEntry = (name: string) => {
77
+ checkCancelled();
78
+ if (++viewEntries > 20_000)
79
+ throw new Error("dependency-view-too-large");
80
+ if (CACHES.includes(name)) return;
81
+ const source = path.join(dependencies, name);
82
+ const placeholder = path.join(view, name);
83
+ const entry = lstatSync(source);
84
+ if (entry.isSymbolicLink()) {
85
+ const resolved = realpathSync(source);
86
+ if (within(canonical, resolved)) {
87
+ symlinkSync(
88
+ path.relative(path.dirname(source), resolved),
89
+ placeholder,
90
+ );
91
+ } else if (within(realpathSync(dependencyOwner), resolved)) {
92
+ const relative = path.relative(
93
+ realpathSync(dependencyOwner),
94
+ resolved,
95
+ );
96
+ const candidate = path.join(root, relative);
97
+ if (!lstatSync(candidate, { throwIfNoEntry: false }))
98
+ throw new Error("workspace-dependency-missing");
99
+ symlinkSync(
100
+ local
101
+ ? candidate
102
+ : `/workspace/${relative.split(path.sep).join("/")}`,
103
+ placeholder,
104
+ );
105
+ } else throw new Error("dependency-path-unsafe");
106
+ } else if (
107
+ entry.isDirectory() &&
108
+ name.startsWith("@") &&
109
+ !name.includes(path.sep)
110
+ ) {
111
+ mkdirSync(placeholder);
112
+ for (const child of readdirSync(source))
113
+ projectEntry(path.join(name, child));
114
+ } else {
115
+ if (entry.isDirectory()) mkdirSync(placeholder);
116
+ else if (entry.isFile()) writeFileSync(placeholder, "");
117
+ else throw new Error("dependency-path-unsafe");
118
+ mounts.push({
119
+ source,
120
+ target: `/workspace/node_modules/${name.split(path.sep).join("/")}`,
121
+ });
122
+ }
123
+ };
124
+ for (const name of entries) projectEntry(name);
125
+ for (const cache of CACHES) {
126
+ mkdirSync(path.join(view, cache));
127
+ const source = path.join(privateRoot, "cache", cache);
128
+ mkdirSync(source);
129
+ mounts.push({
130
+ source,
131
+ target: `/workspace/node_modules/${cache}`,
132
+ writable: true,
133
+ });
134
+ }
135
+ }
136
+ if (local && stat) {
137
+ // Private copies keep package caches/writes away from consumer dependencies.
138
+ const copyEntry = (name: string) => {
139
+ checkCancelled();
140
+ const source = path.join(dependencies, name);
141
+ const destination = path.join(root, "node_modules", name);
142
+ if (CACHES.includes(name)) {
143
+ mkdirSync(destination, { recursive: true });
144
+ return;
145
+ }
146
+ removeEntry(destination, checkCancelled);
147
+ const stat = lstatSync(source);
148
+ if (stat.isSymbolicLink()) {
149
+ const resolved = realpathSync(source);
150
+ const translated = within(canonicalRoot(dependencyOwner), resolved)
151
+ ? path.join(
152
+ root,
153
+ path.relative(canonicalRoot(dependencyOwner), resolved),
154
+ )
155
+ : resolved;
156
+ symlinkSync(translated, destination);
157
+ } else if (
158
+ stat.isDirectory() &&
159
+ name.startsWith("@") &&
160
+ !name.includes(path.sep)
161
+ ) {
162
+ mkdirSync(destination);
163
+ for (const child of readdirSync(source))
164
+ copyEntry(path.join(name, child));
165
+ } else
166
+ cpSync(source, destination, {
167
+ recursive: true,
168
+ filter: () => {
169
+ checkCancelled();
170
+ return true;
171
+ },
172
+ mode: constants.COPYFILE_FICLONE,
173
+ verbatimSymlinks: true,
174
+ });
175
+ };
176
+ for (const name of readdirSync(path.join(privateRoot, "dependencies")))
177
+ copyEntry(name);
178
+ }
179
+
180
+ const sources = new Map<string, string>();
181
+ const bindings = runnerBindings.map((binding) => {
182
+ if (local || !binding.mountSource) return { ...binding };
183
+ const source = path.resolve(binding.mountSource);
184
+ const stat = lstatSync(source);
185
+ const executable = realpathSync(binding.executablePath);
186
+ if (
187
+ !stat.isDirectory() ||
188
+ stat.isSymbolicLink() ||
189
+ !within(source, executable)
190
+ ) {
191
+ throw new Error("runner-mount-unavailable");
192
+ }
193
+ let target = sources.get(source);
194
+ if (!target) {
195
+ target = `/cadence-runners/${sources.size}`;
196
+ sources.set(source, target);
197
+ mounts.push({ source, target });
198
+ }
199
+ const translate = (file: string) =>
200
+ path.posix.join(target, ...path.relative(source, file).split(path.sep));
201
+ return {
202
+ ...binding,
203
+ executablePath: translate(executable),
204
+ ...(binding.fixedArgs
205
+ ? {
206
+ fixedArgs: binding.fixedArgs.map((arg) =>
207
+ path.isAbsolute(arg) && within(source, arg)
208
+ ? translate(arg)
209
+ : arg,
210
+ ),
211
+ }
212
+ : {}),
213
+ };
214
+ });
215
+ checkCancelled();
216
+ return {
217
+ privateRoot,
218
+ mounts,
219
+ bindings,
220
+ reportRoot: local ? privateRoot : "/cadence",
221
+ environment: {
222
+ ...(local
223
+ ? Object.fromEntries(
224
+ profile.inheritEnvironment.flatMap((name) =>
225
+ environment[name] === undefined
226
+ ? []
227
+ : [[name, environment[name] ?? ""]],
228
+ ),
229
+ )
230
+ : {}),
231
+ CI: "1",
232
+ HOME: local ? path.join(privateRoot, "home") : "/cadence/home",
233
+ XDG_CACHE_HOME: local
234
+ ? path.join(privateRoot, "cache")
235
+ : "/cadence/cache",
236
+ TMPDIR: local ? path.join(privateRoot, "tmp") : "/cadence/tmp",
237
+ npm_config_cache: local
238
+ ? path.join(privateRoot, "cache/npm")
239
+ : "/cadence/cache/npm",
240
+ npm_config_userconfig: local
241
+ ? path.join(privateRoot, "home/user.npmrc")
242
+ : "/cadence/home/user.npmrc",
243
+ npm_config_globalconfig: local
244
+ ? path.join(privateRoot, "home/global.npmrc")
245
+ : "/cadence/home/global.npmrc",
246
+ PATH: [
247
+ ...new Set(
248
+ // Honor explicitly resolved private runtimes before system fallbacks.
249
+ [
250
+ ...bindings.filter((binding) => binding.mountSource),
251
+ ...bindings.filter((binding) => !binding.mountSource),
252
+ ].map((binding) => path.posix.dirname(binding.executablePath)),
253
+ ),
254
+ ...(local ? (environment.PATH ?? "").split(path.delimiter) : []),
255
+ "/usr/local/bin",
256
+ "/usr/bin",
257
+ "/bin",
258
+ ].join(":"),
259
+ },
260
+ };
261
+ } catch (error) {
262
+ cleanup();
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ function canonicalRoot(root: string): string {
268
+ return realpathSync(root);
269
+ }
270
+
271
+ /** Per-entry cancellation, including replacement of an earlier dependency view. */
272
+ function removeEntry(file: string, checkCancelled: () => void): void {
273
+ checkCancelled();
274
+ const stat = lstatSync(file, { throwIfNoEntry: false });
275
+ if (!stat) return;
276
+ if (stat.isDirectory())
277
+ for (const name of readdirSync(file))
278
+ removeEntry(path.join(file, name), checkCancelled);
279
+ rmSync(file, { recursive: stat.isDirectory(), force: true });
280
+ }
@@ -1,26 +1,20 @@
1
- import {
2
- lstatSync,
3
- mkdirSync,
4
- mkdtempSync,
5
- readdirSync,
6
- readlinkSync,
7
- realpathSync,
8
- rmSync,
9
- symlinkSync,
10
- writeFileSync,
11
- } from "node:fs";
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { mkdtemp } from "node:fs/promises";
12
4
  import path from "node:path";
13
5
  import {
14
6
  type StructuredVerificationContract,
15
7
  VERIFICATION_CONFIGURATION_PATHS,
16
8
  } from "./contracts.ts";
17
- import type { IsolationMount } from "./isolation-backend.ts";
9
+ import { executionProfile } from "./execution-profile.ts";
18
10
  import {
19
11
  resolveVerificationRunner,
20
12
  type VerificationRunnerBinding,
21
13
  validateVerificationAdapterCapability,
22
14
  verificationRunnerFiles,
23
15
  } from "./verification-capability.ts";
16
+ import type { prepareVerificationEnvironmentIo } from "./verification-environment-io.ts";
17
+ import type { WorkspaceIoMetrics } from "./workspace-io.ts";
24
18
  import { runWorkspaceIo } from "./workspace-io.ts";
25
19
 
26
20
  export async function captureVerificationEnvironmentIdentity(
@@ -49,165 +43,73 @@ export async function captureVerificationEnvironmentIdentity(
49
43
  for (const binding of capability.runnerBindings)
50
44
  if (binding.mountSource) roots.push(binding.mountSource);
51
45
  }
52
- return runWorkspaceIo<string>({
46
+ const installed = await runWorkspaceIo<string>({
53
47
  root: dependencyOwner,
54
48
  artifactRoot: dependencyOwner,
55
49
  operation: "verificationIdentity",
56
50
  args: [roots],
57
51
  signal,
58
52
  });
53
+ const profile = executionProfile();
54
+ return createHash("sha256")
55
+ .update(installed)
56
+ .update(JSON.stringify(profile))
57
+ .update(
58
+ JSON.stringify(
59
+ profile.inheritEnvironment.map((name) => [
60
+ name,
61
+ process.env[name] ?? null,
62
+ ]),
63
+ ),
64
+ )
65
+ .digest("hex");
59
66
  }
60
67
 
61
- const CACHES = [".vite", ".vite-temp"];
62
- function within(root: string, candidate: string): boolean {
63
- const relative = path.relative(root, candidate);
64
- return (
65
- relative !== ".." &&
66
- !relative.startsWith(`..${path.sep}`) &&
67
- !path.isAbsolute(relative)
68
- );
69
- }
70
-
71
- /** A disposable dependency view: immutable packages and independently writable caches. */
72
- export function prepareVerificationEnvironment(
68
+ /** All dependency traversal/copy/removal runs outside the host event loop. */
69
+ export async function prepareVerificationEnvironment(
73
70
  root: string,
74
71
  dependencyOwner: string,
75
72
  runnerBindings: readonly VerificationRunnerBinding[],
73
+ signal?: AbortSignal,
74
+ onMetrics?: (metrics: WorkspaceIoMetrics) => void,
76
75
  ) {
77
- const privateRoot = mkdtempSync(
76
+ signal?.throwIfAborted();
77
+ const profile = executionProfile();
78
+ const privateRoot = await mkdtemp(
78
79
  path.join(path.dirname(root), ".cadence-verification-"),
79
80
  );
80
- const cleanup = () => rmSync(privateRoot, { recursive: true, force: true });
81
- try {
82
- const mounts: IsolationMount[] = [
83
- { source: privateRoot, target: "/cadence", writable: true },
84
- ];
85
- for (const directory of [
86
- "home",
87
- "cache",
88
- "tmp",
89
- "reports",
90
- "dependencies",
91
- ]) {
92
- mkdirSync(path.join(privateRoot, directory), { mode: 0o700 });
93
- }
94
- writeFileSync(path.join(privateRoot, "home/user.npmrc"), "");
95
- writeFileSync(path.join(privateRoot, "home/global.npmrc"), "");
96
- const dependencies = path.join(dependencyOwner, "node_modules");
97
- const stat = lstatSync(dependencies, { throwIfNoEntry: false });
98
- if (stat) {
99
- const target = path.join(root, "node_modules");
100
- const targetStat = lstatSync(target, { throwIfNoEntry: false });
101
- if (
102
- !stat.isDirectory() ||
103
- stat.isSymbolicLink() ||
104
- (targetStat &&
105
- (!targetStat.isDirectory() || targetStat.isSymbolicLink()))
106
- ) {
107
- throw new Error("dependency-path-unsafe");
108
- }
109
- if (!targetStat) mkdirSync(target, { mode: 0o700 });
110
- const view = path.join(privateRoot, "dependencies");
111
- mounts.push({ source: view, target: "/workspace/node_modules" });
112
- const entries = readdirSync(dependencies);
113
- if (entries.length > 20_000) throw new Error("dependency-view-too-large");
114
- const canonical = realpathSync(dependencies);
115
- for (const name of entries) {
116
- if (CACHES.includes(name)) continue;
117
- const source = path.join(dependencies, name);
118
- const placeholder = path.join(view, name);
119
- const entry = lstatSync(source);
120
- if (entry.isSymbolicLink()) {
121
- const link = readlinkSync(source);
122
- if (
123
- path.isAbsolute(link) ||
124
- !within(canonical, realpathSync(source))
125
- ) {
126
- throw new Error("dependency-path-unsafe");
127
- }
128
- symlinkSync(link, placeholder);
129
- } else {
130
- if (entry.isDirectory()) mkdirSync(placeholder);
131
- else if (entry.isFile()) writeFileSync(placeholder, "");
132
- else throw new Error("dependency-path-unsafe");
133
- mounts.push({ source, target: `/workspace/node_modules/${name}` });
134
- }
135
- }
136
- for (const cache of CACHES) {
137
- mkdirSync(path.join(view, cache));
138
- const source = path.join(privateRoot, "cache", cache);
139
- mkdirSync(source);
140
- mounts.push({
141
- source,
142
- target: `/workspace/node_modules/${cache}`,
143
- writable: true,
144
- });
145
- }
146
- }
147
- const sources = new Map<string, string>();
148
- const bindings = runnerBindings.map((binding) => {
149
- if (!binding.mountSource) return { ...binding };
150
- const source = path.resolve(binding.mountSource);
151
- const stat = lstatSync(source);
152
- const executable = realpathSync(binding.executablePath);
153
- if (
154
- !stat.isDirectory() ||
155
- stat.isSymbolicLink() ||
156
- !within(source, executable)
157
- ) {
158
- throw new Error("runner-mount-unavailable");
159
- }
160
- let target = sources.get(source);
161
- if (!target) {
162
- target = `/cadence-runners/${sources.size}`;
163
- sources.set(source, target);
164
- mounts.push({ source, target });
165
- }
166
- const translate = (file: string) =>
167
- path.posix.join(target, ...path.relative(source, file).split(path.sep));
168
- return {
169
- ...binding,
170
- executablePath: translate(executable),
171
- ...(binding.fixedArgs
172
- ? {
173
- fixedArgs: binding.fixedArgs.map((arg) =>
174
- path.isAbsolute(arg) && within(source, arg)
175
- ? translate(arg)
176
- : arg,
177
- ),
178
- }
179
- : {}),
180
- };
81
+ const cleanup = () =>
82
+ runWorkspaceIo<void>({
83
+ root,
84
+ artifactRoot: root,
85
+ operation: "cleanupVerification",
86
+ args: [privateRoot],
181
87
  });
182
- return {
183
- privateRoot,
184
- mounts,
185
- bindings,
186
- cleanup,
187
- environment: {
188
- CI: "1",
189
- HOME: "/cadence/home",
190
- XDG_CACHE_HOME: "/cadence/cache",
191
- TMPDIR: "/cadence/tmp",
192
- npm_config_cache: "/cadence/cache/npm",
193
- npm_config_userconfig: "/cadence/home/user.npmrc",
194
- npm_config_globalconfig: "/cadence/home/global.npmrc",
195
- PATH: [
196
- ...new Set(
197
- // Honor explicitly resolved private runtimes before system fallbacks.
198
- [
199
- ...bindings.filter((binding) => binding.mountSource),
200
- ...bindings.filter((binding) => !binding.mountSource),
201
- ].map((binding) => path.posix.dirname(binding.executablePath)),
88
+ try {
89
+ const prepared = await runWorkspaceIo<
90
+ ReturnType<typeof prepareVerificationEnvironmentIo>
91
+ >({
92
+ root,
93
+ artifactRoot: root,
94
+ operation: "prepareVerification",
95
+ args: [
96
+ root,
97
+ dependencyOwner,
98
+ runnerBindings,
99
+ profile,
100
+ Object.fromEntries(
101
+ ["PATH", ...profile.inheritEnvironment].flatMap((name) =>
102
+ process.env[name] === undefined ? [] : [[name, process.env[name]]],
202
103
  ),
203
- "/usr/local/bin",
204
- "/usr/bin",
205
- "/bin",
206
- ].join(":"),
207
- },
208
- };
104
+ ),
105
+ privateRoot,
106
+ ],
107
+ signal,
108
+ onMetrics,
109
+ });
110
+ return { ...prepared, cleanup };
209
111
  } catch (error) {
210
- cleanup();
112
+ await cleanup();
211
113
  throw error;
212
114
  }
213
115
  }
@@ -0,0 +1,64 @@
1
+ /** Only callers admitted before I/O begins share a scan. A later currentness check
2
+ * must observe bytes after its own admission, not join an older in-flight scan.
3
+ * Last cancellation drains
4
+ * the worker before returning; a cancelled scan never admits a new subscriber. */
5
+ export function coalesceVerificationScans<P>(
6
+ scan: (plan: P, signal: AbortSignal) => Promise<string>,
7
+ ) {
8
+ type Pending = {
9
+ controller: AbortController;
10
+ promise: Promise<string>;
11
+ subscribers: number;
12
+ started: boolean;
13
+ };
14
+ const pending = new Map<string, Pending>();
15
+ return (plan: P, signal?: AbortSignal): Promise<string> => {
16
+ signal?.throwIfAborted();
17
+ const key = JSON.stringify(plan);
18
+ let entry = pending.get(key);
19
+ if (!entry || entry.started || entry.controller.signal.aborted) {
20
+ const controller = new AbortController();
21
+ const created: Pending = {
22
+ controller,
23
+ subscribers: 0,
24
+ started: false,
25
+ promise: Promise.resolve(""),
26
+ };
27
+ created.promise = Promise.resolve()
28
+ .then(() => {
29
+ created.started = true;
30
+ return scan(plan, controller.signal);
31
+ })
32
+ .finally(() => {
33
+ if (pending.get(key) === created) pending.delete(key);
34
+ });
35
+ entry = created;
36
+ pending.set(key, entry);
37
+ }
38
+ const shared = entry;
39
+ shared.subscribers++;
40
+ return new Promise((resolve, reject) => {
41
+ let cancelled = false;
42
+ const abort = () => {
43
+ if (cancelled) return;
44
+ cancelled = true;
45
+ if (--shared.subscribers === 0) shared.controller.abort(signal?.reason);
46
+ };
47
+ signal?.addEventListener("abort", abort, { once: true });
48
+ if (signal?.aborted) abort();
49
+ const settle = (value?: string, error?: unknown) => {
50
+ signal?.removeEventListener("abort", abort);
51
+ if (!cancelled) shared.subscribers--;
52
+ if (cancelled)
53
+ reject(signal?.reason ?? new DOMException("cancelled", "AbortError"));
54
+ else if (error !== undefined) reject(error);
55
+ else if (value !== undefined) resolve(value);
56
+ else reject(new Error("verification-scan-result-unavailable"));
57
+ };
58
+ shared.promise.then(
59
+ (value) => settle(value),
60
+ (error) => settle(undefined, error),
61
+ );
62
+ });
63
+ };
64
+ }