@sublang/slc 0.3.0 → 0.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 (98) hide show
  1. package/README.md +102 -84
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +13 -5
  4. package/dist/app.js.map +1 -1
  5. package/dist/build-history.d.ts +74 -0
  6. package/dist/build-history.d.ts.map +1 -0
  7. package/dist/build-history.js +309 -0
  8. package/dist/build-history.js.map +1 -0
  9. package/dist/cli.js +2 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/compiled-executor.d.ts +1 -1
  12. package/dist/compiled-executor.d.ts.map +1 -1
  13. package/dist/compiled-executor.js +10 -3
  14. package/dist/compiled-executor.js.map +1 -1
  15. package/dist/config-file.d.ts +4 -0
  16. package/dist/config-file.d.ts.map +1 -1
  17. package/dist/config-file.js +19 -8
  18. package/dist/config-file.js.map +1 -1
  19. package/dist/config.d.ts +6 -4
  20. package/dist/config.d.ts.map +1 -1
  21. package/dist/config.js +65 -12
  22. package/dist/config.js.map +1 -1
  23. package/dist/errors.d.ts +14 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +28 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/execution.d.ts +42 -0
  28. package/dist/execution.d.ts.map +1 -1
  29. package/dist/execution.js +224 -29
  30. package/dist/execution.js.map +1 -1
  31. package/dist/hash.d.ts +22 -0
  32. package/dist/hash.d.ts.map +1 -1
  33. package/dist/hash.js +47 -0
  34. package/dist/hash.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/interpreter.d.ts +1 -1
  40. package/dist/interpreter.d.ts.map +1 -1
  41. package/dist/interpreter.js +9 -0
  42. package/dist/interpreter.js.map +1 -1
  43. package/dist/invocation.d.ts +5 -1
  44. package/dist/invocation.d.ts.map +1 -1
  45. package/dist/invocation.js +14 -3
  46. package/dist/invocation.js.map +1 -1
  47. package/dist/line-diff.d.ts +7 -0
  48. package/dist/line-diff.d.ts.map +1 -0
  49. package/dist/line-diff.js +66 -0
  50. package/dist/line-diff.js.map +1 -0
  51. package/dist/phase.d.ts +2 -0
  52. package/dist/phase.d.ts.map +1 -1
  53. package/dist/phase.js +19 -3
  54. package/dist/phase.js.map +1 -1
  55. package/dist/pin-closure.d.ts +2 -2
  56. package/dist/pin-closure.d.ts.map +1 -1
  57. package/dist/pin-closure.js +4 -3
  58. package/dist/pin-closure.js.map +1 -1
  59. package/dist/pin-currency.d.ts +6 -2
  60. package/dist/pin-currency.d.ts.map +1 -1
  61. package/dist/pin-currency.js +24 -43
  62. package/dist/pin-currency.js.map +1 -1
  63. package/dist/pin-generate.d.ts.map +1 -1
  64. package/dist/pin-generate.js +1 -3
  65. package/dist/pin-generate.js.map +1 -1
  66. package/dist/pin-paths.d.ts.map +1 -1
  67. package/dist/pin-paths.js +2 -13
  68. package/dist/pin-paths.js.map +1 -1
  69. package/dist/pins.d.ts.map +1 -1
  70. package/dist/pins.js +5 -15
  71. package/dist/pins.js.map +1 -1
  72. package/dist/playbook-ports.d.ts +9 -0
  73. package/dist/playbook-ports.d.ts.map +1 -1
  74. package/dist/playbook-ports.js +10 -5
  75. package/dist/playbook-ports.js.map +1 -1
  76. package/dist/reviewing-agent.d.ts +18 -0
  77. package/dist/reviewing-agent.d.ts.map +1 -0
  78. package/dist/reviewing-agent.js +295 -0
  79. package/dist/reviewing-agent.js.map +1 -0
  80. package/dist/runner.d.ts +2 -0
  81. package/dist/runner.d.ts.map +1 -1
  82. package/dist/runner.js +628 -88
  83. package/dist/runner.js.map +1 -1
  84. package/dist/runtime-package.d.ts.map +1 -1
  85. package/dist/runtime-package.js +3 -11
  86. package/dist/runtime-package.js.map +1 -1
  87. package/dist/slc.config.template.yaml +6 -2
  88. package/dist/verify-coverage.d.ts.map +1 -1
  89. package/dist/verify-coverage.js +2 -0
  90. package/dist/verify-coverage.js.map +1 -1
  91. package/dist/verify-support.d.ts +7 -0
  92. package/dist/verify-support.d.ts.map +1 -1
  93. package/dist/verify-support.js +13 -6
  94. package/dist/verify-support.js.map +1 -1
  95. package/dist/verify.d.ts.map +1 -1
  96. package/dist/verify.js +2 -0
  97. package/dist/verify.js.map +1 -1
  98. package/package.json +1 -1
package/dist/runner.js CHANGED
@@ -9,21 +9,26 @@
9
9
  * injected so a host wires the real pipeline resolution and Cligent agent while
10
10
  * tests supply fakes. See specs/dev/pipeline.md and specs/dev/phase-execution.md.
11
11
  */
12
- import { mkdir } from 'node:fs/promises';
13
- import { dirname, join } from 'node:path';
12
+ import { mkdir, readFile, stat } from 'node:fs/promises';
13
+ import { dirname, join, relative, resolve, sep } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { artifactDir, planArtifacts, parseSource } from './artifacts.js';
16
+ import { encodeLocator, invalidateBuildHistory, loadBuildHistory, recordBuild, verifiedInput, } from './build-history.js';
16
17
  import { emitEntryModule } from './entry-module.js';
17
18
  import { unresolvableRelativeImports } from './emitted-imports.js';
18
- import { formatFailureReport, runPhase, } from './execution.js';
19
+ import { messageOf } from './errors.js';
20
+ import { assertSafeTarget, formatFailureReport, pathsAlias, runPhase, } from './execution.js';
21
+ import { hashBytes, isHash } from './hash.js';
19
22
  import { parseInvocation } from './invocation.js';
23
+ import { unifiedLineDiff } from './line-diff.js';
20
24
  import { linkedArtifactPath, loadLinkFile } from './link.js';
21
- import { evaluatePin, evaluatePinFile } from './pin-currency.js';
22
- import { PinError, loadPinFile } from './pins.js';
25
+ import { deriveClosure } from './pin-closure.js';
26
+ import { evaluatePin, evaluatePinFile, hashTree } from './pin-currency.js';
27
+ import { PINS_FILE, PinError, loadPinFile, } from './pins.js';
23
28
  import { loadPipeline, resolvePipeline, } from './pipeline.js';
24
29
  import { defaultPlaybookLinkTarget, isReservedPipeline } from './resolver.js';
25
30
  import { emitFsmCoverageTest, emitFsmIntrospectionTest, emitGearsFsmConformanceTest, emitPromptContractTest, } from './verify.js';
26
- import { VERIFIER_SUPPORT_MODULE, emitVerifierSupport, } from './verify-support.js';
31
+ import { VERIFIER_SUPPORT_MODULE, emitVerifierSupport, verifierSupportFiles, } from './verify-support.js';
27
32
  /**
28
33
  * Parses argv and runs the requested pipeline, phase, or link end-to-end.
29
34
  * Never rejects: malformed invocations, refusals, and phase failures all return
@@ -65,47 +70,67 @@ export async function runSlc(argv, deps) {
65
70
  }
66
71
  }
67
72
  async function runFull(invocation, deps) {
73
+ const cwd = runCwd(deps);
68
74
  const pipeline = await loadPipeline(await resolvePipeline(invocation.pipeline, deps.resolver));
69
75
  const entry = pipeline.phases[0];
76
+ const source = resolve(cwd, invocation.source);
70
77
  const { basename, raw } = parseSource({
71
- path: invocation.source,
78
+ path: source,
72
79
  sourceFormat: entry.source.format,
73
80
  ext: entry.source.ext,
74
81
  entry: true,
75
82
  });
76
- const artDir = artifactDir(runCwd(deps), basename, invocation.pipeline);
83
+ const artDir = artifactDir(cwd, basename, invocation.pipeline);
77
84
  await mkdir(artDir, { recursive: true });
78
85
  const plan = planArtifacts({
79
86
  phases: pipeline.phases,
80
87
  basename,
81
88
  artDir,
82
- output: invocation.output ?? undefined,
89
+ output: invocation.output === null ? undefined : resolve(cwd, invocation.output),
83
90
  });
84
91
  const steps = buildCompileSteps({
85
92
  pipeline,
86
93
  plan,
87
- source: invocation.source,
94
+ source,
88
95
  artDir,
89
96
  basename,
90
97
  optimize: !invocation.noOptimize,
91
98
  normalize: invocation.normalize || raw,
92
99
  });
93
- const result = await executeSteps(steps, pipeline, deps);
94
- return emitVerification(result, {
100
+ const verification = {
95
101
  pipeline: invocation.pipeline,
96
102
  plan,
97
103
  artDir,
98
104
  basename,
105
+ };
106
+ return executeSteps(steps, pipeline, deps, {
107
+ incremental: incrementalRun(invocation, artDir, source),
108
+ hostTargets: verificationHostTargets(verification),
109
+ complete: (result, guardTarget) => emitVerification(result, verification, guardTarget),
99
110
  });
100
111
  }
112
+ /** History covers only canonical full/full-link runs outside the `slc` meta-pipeline. */
113
+ function incrementalRun(invocation, artDir, source) {
114
+ if (invocation.output !== null || invocation.pipeline === 'slc') {
115
+ return undefined;
116
+ }
117
+ return {
118
+ artDir,
119
+ source,
120
+ pipelineName: invocation.pipeline,
121
+ rebuild: invocation.rebuild === true,
122
+ };
123
+ }
101
124
  async function runSinglePhase(invocation, deps) {
125
+ const cwd = runCwd(deps);
102
126
  const pipeline = await loadPipeline(await resolvePipeline(invocation.pipeline, deps.resolver));
103
127
  const phase = [...pipeline.phases, ...pipeline.passes].find((candidate) => candidate.name === invocation.phase);
104
128
  if (phase === undefined) {
105
129
  return failure(`phase "${invocation.phase}" is not part of pipeline "${invocation.pipeline}"`);
106
130
  }
131
+ const source = resolve(cwd, invocation.source);
107
132
  const { basename, raw } = parseSource({
108
- path: invocation.source,
133
+ path: source,
109
134
  sourceFormat: phase.source.format,
110
135
  ext: phase.source.ext,
111
136
  entry: pipeline.phases[0] === phase,
@@ -115,14 +140,14 @@ async function runSinglePhase(invocation, deps) {
115
140
  // path into the phase's declared source format here.
116
141
  return failure(`source "${invocation.source}" is a raw input; run the full pipeline to normalize it`);
117
142
  }
118
- const artDir = artifactDir(runCwd(deps), basename, invocation.pipeline);
143
+ const artDir = artifactDir(cwd, basename, invocation.pipeline);
119
144
  await mkdir(artDir, { recursive: true });
120
145
  if (phase.pass) {
121
146
  // A standalone pass run cannot overwrite its own source: it writes the
122
147
  // `.opt` sibling unless `-o` relocates it (DR-013).
123
- const target = invocation.output ??
148
+ const target = (invocation.output === null ? null : resolve(cwd, invocation.output)) ??
124
149
  join(artDir, `${basename}.${phase.target.format}.opt${phase.target.ext}`);
125
- const step = compileStep(pipeline, phase, invocation.source, target);
150
+ const step = compileStep(pipeline, phase, source, target);
126
151
  return executeSteps([step], pipeline, deps);
127
152
  }
128
153
  // Plan over the whole chain so the named phase keeps its pipeline role: a
@@ -132,58 +157,62 @@ async function runSinglePhase(invocation, deps) {
132
157
  phases: pipeline.phases,
133
158
  basename,
134
159
  artDir,
135
- output: invocation.output ?? undefined,
160
+ output: invocation.output === null ? undefined : resolve(cwd, invocation.output),
136
161
  });
137
162
  const artifact = plan[pipeline.phases.indexOf(phase)];
138
- const step = compileStep(pipeline, phase, invocation.source, artifact.path);
163
+ const step = compileStep(pipeline, phase, source, artifact.path);
139
164
  return executeSteps([step], pipeline, deps);
140
165
  }
141
166
  async function runDirectLink(invocation, deps) {
167
+ const cwd = runCwd(deps);
142
168
  const pipeline = await loadPipeline(await resolvePipeline(invocation.pipeline, deps.resolver));
143
169
  const link = await requireLink(pipeline, invocation.pipeline);
144
170
  const linked = linkedArtifactPath({
145
171
  kind: 'link',
146
172
  pipeline: invocation.pipeline,
147
- objects: invocation.objects,
173
+ objects: invocation.objects.map((path) => resolve(cwd, path)),
148
174
  source: link.source,
149
175
  linked: link.target,
150
- output: invocation.output,
151
- cwd: runCwd(deps),
176
+ output: invocation.output === null ? null : resolve(cwd, invocation.output),
177
+ cwd,
152
178
  });
153
179
  await mkdir(dirname(linked), { recursive: true });
154
180
  const step = {
155
181
  request: {
156
182
  kind: 'link',
157
183
  definitionPath: pipeline.linkFile,
158
- objects: invocation.objects,
159
- linkTarget: invocation.linkTarget,
184
+ objects: invocation.objects.map((path) => resolve(cwd, path)),
185
+ linkTarget: resolve(cwd, invocation.linkTarget),
160
186
  options: invocation.options,
161
187
  linked,
162
188
  },
163
189
  phase: 'link',
190
+ pinKey: 'link',
164
191
  targetExt: link.target.ext,
165
192
  };
166
193
  return executeSteps([step], pipeline, deps);
167
194
  }
168
195
  async function runFullLink(invocation, deps) {
196
+ const cwd = runCwd(deps);
169
197
  const pipeline = await loadPipeline(await resolvePipeline(invocation.pipeline, deps.resolver));
170
198
  const link = await requireLink(pipeline, invocation.pipeline);
171
199
  const entry = pipeline.phases[0];
200
+ const source = resolve(cwd, invocation.source);
172
201
  const { basename, raw } = parseSource({
173
- path: invocation.source,
202
+ path: source,
174
203
  sourceFormat: entry.source.format,
175
204
  ext: entry.source.ext,
176
205
  entry: true,
177
206
  });
178
207
  const normalize = invocation.normalize || raw;
179
- const artDir = artifactDir(runCwd(deps), basename, invocation.pipeline);
208
+ const artDir = artifactDir(cwd, basename, invocation.pipeline);
180
209
  await mkdir(artDir, { recursive: true });
181
210
  // Compile chain: the exit artifact becomes the object artifact (PIPE-15).
182
211
  const plan = planArtifacts({ phases: pipeline.phases, basename, artDir });
183
212
  const compileSteps = buildCompileSteps({
184
213
  pipeline,
185
214
  plan,
186
- source: invocation.source,
215
+ source,
187
216
  artDir,
188
217
  basename,
189
218
  optimize: !invocation.noOptimize,
@@ -194,64 +223,88 @@ async function runFullLink(invocation, deps) {
194
223
  artDir,
195
224
  basename,
196
225
  linked: link.target,
197
- output: invocation.output,
226
+ output: invocation.output === null ? null : resolve(cwd, invocation.output),
198
227
  });
199
228
  const linkStep = {
200
229
  request: {
201
230
  kind: 'link',
202
231
  definitionPath: pipeline.linkFile,
203
232
  objects: [plan[plan.length - 1].path],
204
- linkTarget: invocation.linkTarget,
233
+ linkTarget: resolve(cwd, invocation.linkTarget),
205
234
  options: invocation.options,
206
235
  linked,
207
236
  },
208
237
  phase: 'link',
238
+ pinKey: 'link',
209
239
  targetExt: link.target.ext,
210
240
  };
211
- const result = await executeSteps([...compileSteps, linkStep], pipeline, deps);
212
- const verified = await emitVerification(result, {
241
+ const steps = [...compileSteps, linkStep];
242
+ const gearsPlan = plan.find((artifact) => artifact.phase.target.format === 'gears');
243
+ const entryCandidate = invocation.pipeline === 'playbook' &&
244
+ invocation.output === null &&
245
+ gearsPlan !== undefined
246
+ ? resolve(cwd, `${basename}.ts`)
247
+ : null;
248
+ const entryAliasesSource = entryCandidate !== null && (await pathsAlias(entryCandidate, source));
249
+ const entryPath = entryAliasesSource ? null : entryCandidate;
250
+ const verification = {
213
251
  pipeline: invocation.pipeline,
214
252
  plan,
215
253
  artDir,
216
254
  basename,
217
- });
218
- // Entry-module emission (DR-014, SELFHOST-15): only the playbook pipeline,
219
- // only with the linked artifact at its canonical path.
220
- if (verified.ok &&
221
- invocation.pipeline === 'playbook' &&
222
- invocation.output === null) {
223
- const gearsPlan = plan.find((artifact) => artifact.phase.target.format === 'gears');
224
- if (gearsPlan !== undefined) {
225
- const textPath = normalize
226
- ? join(artDir, `${basename}.${entry.source.format}${entry.source.ext}`)
227
- : invocation.source;
228
- const entryPath = await emitEntryModule({
229
- cwd: runCwd(deps),
230
- basename,
231
- pipeline: invocation.pipeline,
232
- gearsPath: gearsPlan.path,
233
- textPath,
234
- });
235
- const missing = await unresolvableRelativeImports(entryPath);
236
- if (missing.length > 0) {
255
+ };
256
+ return executeSteps(steps, pipeline, deps, {
257
+ incremental: incrementalRun(invocation, artDir, source),
258
+ hostTargets: [
259
+ ...verificationHostTargets(verification),
260
+ ...(entryPath === null ? [] : [{ path: entryPath }]),
261
+ ],
262
+ complete: async (result, guardTarget) => {
263
+ const verified = await emitVerification(result, verification, guardTarget);
264
+ if (verified.ok && entryCandidate !== null && entryAliasesSource) {
237
265
  return {
238
- ok: false,
239
- outputs: verified.outputs,
266
+ ...verified,
240
267
  diagnostics: [
241
268
  ...verified.diagnostics,
242
- `entry module ${entryPath} has unresolvable relative imports: ` +
243
- `${missing.join(', ')} (VERIFY-18)`,
269
+ `entry module ${entryCandidate} not emitted because it aliases the invocation source`,
244
270
  ],
245
271
  };
246
272
  }
247
- return { ...verified, outputs: [...verified.outputs, entryPath] };
248
- }
249
- }
250
- return verified;
273
+ // Entry-module emission (DR-014, SELFHOST-15): only the playbook
274
+ // pipeline, only with the linked artifact at its canonical path.
275
+ if (verified.ok && entryPath !== null && gearsPlan !== undefined) {
276
+ await guardTarget(entryPath);
277
+ const textPath = normalize
278
+ ? join(artDir, `${basename}.${entry.source.format}${entry.source.ext}`)
279
+ : source;
280
+ await emitEntryModule({
281
+ cwd,
282
+ basename,
283
+ pipeline: invocation.pipeline,
284
+ gearsPath: gearsPlan.path,
285
+ textPath,
286
+ });
287
+ const missing = await unresolvableRelativeImports(entryPath);
288
+ if (missing.length > 0) {
289
+ return {
290
+ ok: false,
291
+ outputs: verified.outputs,
292
+ diagnostics: [
293
+ ...verified.diagnostics,
294
+ `entry module ${entryPath} has unresolvable relative imports: ` +
295
+ `${missing.join(', ')} (VERIFY-18)`,
296
+ ],
297
+ };
298
+ }
299
+ return { ...verified, outputs: [...verified.outputs, entryPath] };
300
+ }
301
+ return verified;
302
+ },
303
+ });
251
304
  }
252
305
  /** The invocation working directory anchoring artifact placement (DR-014). */
253
306
  function runCwd(deps) {
254
- return deps.cwd ?? process.cwd();
307
+ return resolve(deps.cwd ?? process.cwd());
255
308
  }
256
309
  /**
257
310
  * After a reserved-pipeline full run produces a `gears` intermediate and an `fsm`
@@ -267,28 +320,35 @@ function runCwd(deps) {
267
320
  * to a diagnostic when the artifact cannot be loaded, leaving the run outcome
268
321
  * unchanged — the conformance test still fails at test time on a broken module.
269
322
  */
270
- async function emitVerification(result, ctx) {
271
- if (!result.ok || !isReservedPipeline(ctx.pipeline))
272
- return result;
273
- const fsm = ctx.plan.find((artifact) => artifact.phase.target.format === 'fsm');
274
- const hasGears = ctx.plan.some((artifact) => artifact.phase.target.format === 'gears');
275
- if (fsm === undefined || !hasGears)
323
+ async function emitVerification(result, ctx, guardTarget) {
324
+ if (!result.ok)
276
325
  return result;
277
- // The emitted tests import the canonical sibling FSM through the NodeNext
278
- // `./<basename>.fsm.js` specifier; skip when `-o` relocated the physical FSM
279
- // elsewhere (PIPE-8), so that edge never targets a missing sibling artifact.
280
- const canonicalFsm = join(ctx.artDir, `${ctx.basename}.fsm${fsm.phase.target.ext}`);
281
- if (fsm.path !== canonicalFsm)
326
+ const hostTargets = verificationHostTargets(ctx);
327
+ if (hostTargets.length === 0)
282
328
  return result;
329
+ const fsm = ctx.plan.find((artifact) => artifact.phase.target.format === 'fsm');
330
+ if (fsm === undefined)
331
+ throw new Error('verification plan lost its FSM');
283
332
  const outputs = [...result.outputs];
284
333
  const diagnostics = [...result.diagnostics];
334
+ if (guardTarget !== undefined) {
335
+ for (const target of verifierSupportFiles(ctx.artDir)) {
336
+ await guardTarget(target.target);
337
+ }
338
+ }
285
339
  outputs.push(...(await emitVerifierSupport(ctx.artDir)));
340
+ if (guardTarget !== undefined) {
341
+ await guardTarget(join(ctx.artDir, `${ctx.basename}.gears-fsm.test.ts`));
342
+ }
286
343
  outputs.push(await emitGearsFsmConformanceTest({
287
344
  artifactDir: ctx.artDir,
288
345
  basename: ctx.basename,
289
346
  verifyModule: VERIFIER_SUPPORT_MODULE,
290
347
  }));
291
348
  try {
349
+ if (guardTarget !== undefined) {
350
+ await guardTarget(join(ctx.artDir, `${ctx.basename}.fsm.introspect.test.ts`));
351
+ }
292
352
  outputs.push(await emitFsmIntrospectionTest({
293
353
  artifactDir: ctx.artDir,
294
354
  basename: ctx.basename,
@@ -299,6 +359,9 @@ async function emitVerification(result, ctx) {
299
359
  diagnostics.push(`verification: introspection test not emitted: ${messageOf(error)}`);
300
360
  }
301
361
  try {
362
+ if (guardTarget !== undefined) {
363
+ await guardTarget(join(ctx.artDir, `${ctx.basename}.prompt-contract.test.ts`));
364
+ }
302
365
  const promptContract = await emitPromptContractTest({
303
366
  artifactDir: ctx.artDir,
304
367
  basename: ctx.basename,
@@ -311,6 +374,9 @@ async function emitVerification(result, ctx) {
311
374
  diagnostics.push(`verification: prompt-contract test not emitted: ${messageOf(error)}`);
312
375
  }
313
376
  try {
377
+ if (guardTarget !== undefined) {
378
+ await guardTarget(join(ctx.artDir, `${ctx.basename}.fsm.coverage.test.ts`));
379
+ }
314
380
  const coverage = await emitFsmCoverageTest({
315
381
  artifactDir: ctx.artDir,
316
382
  basename: ctx.basename,
@@ -324,6 +390,38 @@ async function emitVerification(result, ctx) {
324
390
  }
325
391
  return { ...result, outputs, diagnostics };
326
392
  }
393
+ /** Exact deterministic writes attempted by {@link emitVerification}. */
394
+ function verificationHostTargets(ctx) {
395
+ if (!isReservedPipeline(ctx.pipeline))
396
+ return [];
397
+ const fsm = ctx.plan.find((artifact) => artifact.phase.target.format === 'fsm');
398
+ const hasGears = ctx.plan.some((artifact) => artifact.phase.target.format === 'gears');
399
+ if (fsm === undefined || !hasGears)
400
+ return [];
401
+ const canonicalFsm = join(ctx.artDir, `${ctx.basename}.fsm${fsm.phase.target.ext}`);
402
+ if (fsm.path !== canonicalFsm)
403
+ return [];
404
+ return [
405
+ ...verifierSupportFiles(ctx.artDir).map(({ source, target }) => ({
406
+ path: target,
407
+ protectedInputs: [source],
408
+ allowVerifierOutput: true,
409
+ })),
410
+ { path: join(ctx.artDir, `${ctx.basename}.gears-fsm.test.ts`) },
411
+ {
412
+ path: join(ctx.artDir, `${ctx.basename}.fsm.introspect.test.ts`),
413
+ required: false,
414
+ },
415
+ {
416
+ path: join(ctx.artDir, `${ctx.basename}.prompt-contract.test.ts`),
417
+ required: false,
418
+ },
419
+ {
420
+ path: join(ctx.artDir, `${ctx.basename}.fsm.coverage.test.ts`),
421
+ required: false,
422
+ },
423
+ ];
424
+ }
327
425
  /** Resolves the built-in pipeline-agnostic normalize definition (DR-013). */
328
426
  export function normalizeDefinitionPath() {
329
427
  return fileURLToPath(new URL('./normalize.md', import.meta.url));
@@ -389,6 +487,7 @@ function compileStep(pipeline, phase, source, target) {
389
487
  target,
390
488
  },
391
489
  phase: phase.name,
490
+ pinKey: phase.name,
392
491
  targetExt: phase.target.ext,
393
492
  };
394
493
  }
@@ -397,7 +496,9 @@ function compileStep(pipeline, phase, source, target) {
397
496
  * the pin index and stopping at the first failure with its report (PHEXEC-9,
398
497
  * PHEXEC-27). An unparseable pin file fails the run closed before any phase.
399
498
  */
400
- async function executeSteps(steps, pipeline, deps) {
499
+ async function executeSteps(steps, pipeline, deps, opts = {}) {
500
+ const complete = opts.complete ?? (async (result) => result);
501
+ const pinInputs = new Set([join(pipeline.dir, PINS_FILE)]);
401
502
  let pinFile;
402
503
  try {
403
504
  pinFile = (await loadPinFile(pipeline.dir)).file;
@@ -409,7 +510,9 @@ async function executeSteps(steps, pipeline, deps) {
409
510
  throw error;
410
511
  }
411
512
  if (pinFile !== undefined) {
412
- const verdicts = await evaluatePinFile(pipeline.dir, pinFile);
513
+ const verdicts = await evaluatePinFile(pipeline.dir, pinFile, {
514
+ observePath: (path) => pinInputs.add(resolve(path)),
515
+ });
413
516
  const malformed = Object.entries(verdicts).find(([, verdict]) => verdict.status === 'malformed');
414
517
  if (malformed !== undefined) {
415
518
  return {
@@ -423,13 +526,107 @@ async function executeSteps(steps, pipeline, deps) {
423
526
  };
424
527
  }
425
528
  }
426
- const definitions = chainDefinitions(pipeline);
529
+ const definitions = [
530
+ ...new Set([
531
+ ...chainDefinitions(pipeline),
532
+ ...steps.map((step) => step.request.definitionPath),
533
+ ]),
534
+ ];
427
535
  const outputs = [];
428
536
  const diagnostics = [];
429
- for (const step of steps) {
430
- const target = step.request.kind === 'compile'
431
- ? step.request.target
432
- : step.request.linked;
537
+ const state = opts.incremental === undefined
538
+ ? undefined
539
+ : await loadIncremental(opts.incremental);
540
+ const invalidatedTargetDirs = new Set();
541
+ const targets = steps.map(stepTarget);
542
+ const declaredByStep = await Promise.all(steps.map((step) => stepDeclaredInputs(step, pipeline, pinFile)));
543
+ const declaredInputs = [
544
+ ...new Set(declaredByStep.flatMap((declared) => declared.paths)),
545
+ ];
546
+ const immutableInputs = externalInputPaths(steps);
547
+ const hostInputs = (opts.hostTargets ?? []).flatMap((target) => target.protectedInputs ?? []);
548
+ const protectedInputs = [
549
+ ...new Set([
550
+ ...immutableInputs,
551
+ ...definitions,
552
+ ...declaredInputs,
553
+ ...hostInputs,
554
+ ...pinInputs,
555
+ ]),
556
+ ];
557
+ const plannedTargets = opts.hostTargets ?? [];
558
+ for (let index = 0; index < plannedTargets.length; index++) {
559
+ const planned = plannedTargets[index];
560
+ try {
561
+ await assertSafeTarget(planned.path, [
562
+ ...protectedInputs,
563
+ ...targets,
564
+ ...(planned.protectedInputs ?? []),
565
+ ...plannedTargets
566
+ .filter((_, candidate) => candidate !== index)
567
+ .map((candidate) => candidate.path),
568
+ ], { allowVerifierOutput: planned.allowVerifierOutput });
569
+ }
570
+ catch (error) {
571
+ if (planned.required !== false)
572
+ return failure(messageOf(error));
573
+ }
574
+ }
575
+ const hostTargets = new Map((opts.hostTargets ?? []).map((target) => [resolve(target.path), target]));
576
+ const guardCompletionTarget = async (path) => {
577
+ const planned = hostTargets.get(resolve(path));
578
+ if (planned === undefined) {
579
+ throw new Error(`unplanned deterministic output: ${path}`);
580
+ }
581
+ await assertSafeTarget(planned.path, [...protectedInputs, ...targets, ...(planned.protectedInputs ?? [])], { allowVerifierOutput: planned.allowVerifierOutput });
582
+ };
583
+ for (let index = 0; index < steps.length; index++) {
584
+ const step = steps[index];
585
+ const target = targets[index];
586
+ const stepDeclared = declaredByStep[index];
587
+ const identity = state === undefined
588
+ ? null
589
+ : await stepIdentity(step, state, index, stepDeclared.complete ? stepDeclared.paths : null);
590
+ const mode = state === undefined
591
+ ? { mode: 'ordinary' }
592
+ : await selectStepMode(step, index, target, identity, state);
593
+ // Reuse still honors pin currency, but it constructs no executor because
594
+ // no phase runs. A stale/malformed pin can never be bypassed by history.
595
+ if (mode.mode === 'reuse') {
596
+ if (state === undefined) {
597
+ throw new Error('internal error: reuse selected without history state');
598
+ }
599
+ let reasons;
600
+ try {
601
+ reasons = await reusePinFailure(step.pinKey, pipeline.dir, pinFile);
602
+ }
603
+ catch (error) {
604
+ reasons = [messageOf(error)];
605
+ }
606
+ if (reasons !== null) {
607
+ const startedAt = Date.now();
608
+ deps.progress?.({ kind: 'phase-start', phase: step.phase, target });
609
+ deps.progress?.({
610
+ kind: 'phase-fail',
611
+ phase: step.phase,
612
+ target,
613
+ elapsedMs: Date.now() - startedAt,
614
+ });
615
+ diagnostics.push(formatFailureReport({ phase: step.phase, target, reasons }));
616
+ return { ok: false, outputs, diagnostics };
617
+ }
618
+ deps.progress?.({
619
+ kind: 'status',
620
+ text: `reusing ${step.phase} → ${target}`,
621
+ });
622
+ state.accepted.push({
623
+ step,
624
+ target,
625
+ inputs: identity?.inputs ?? null,
626
+ output: hashBytes(mode.bytes),
627
+ });
628
+ continue;
629
+ }
433
630
  // In-run progress (DR-019, CLI-32): announce the phase, then report its
434
631
  // outcome with the elapsed time.
435
632
  const startedAt = Date.now();
@@ -448,7 +645,7 @@ async function executeSteps(steps, pipeline, deps) {
448
645
  // phase and target from the report (CLI-4, CLI-32, PHEXEC-27).
449
646
  let selection;
450
647
  try {
451
- selection = await selectExecutor(step.phase, pipeline.dir, pinFile, deps);
648
+ selection = await selectExecutor(step.phase, step.pinKey, pipeline.dir, pinFile, deps);
452
649
  }
453
650
  catch (error) {
454
651
  selection = { kind: 'fail', reasons: [messageOf(error)] };
@@ -462,12 +659,33 @@ async function executeSteps(steps, pipeline, deps) {
462
659
  }));
463
660
  return { ok: false, outputs, diagnostics };
464
661
  }
662
+ if (mode.mode === 'update') {
663
+ deps.progress?.({
664
+ kind: 'status',
665
+ text: `updating ${step.phase} → ${target}`,
666
+ });
667
+ }
668
+ const request = mode.mode === 'update' && step.request.kind === 'compile'
669
+ ? {
670
+ ...step.request,
671
+ update: { priorInput: mode.priorInput, diff: mode.diff },
672
+ }
673
+ : step.request;
465
674
  const result = await runPhase({
466
- request: step.request,
675
+ request,
467
676
  phase: step.phase,
468
677
  targetExt: step.targetExt,
469
678
  executor: selection.executor,
470
679
  definitions,
680
+ protectedInputs: stepDeclared.paths,
681
+ aliasInputs: [
682
+ ...immutableInputs,
683
+ ...declaredInputs,
684
+ ...hostInputs,
685
+ ...pinInputs,
686
+ ...targets.filter((_, candidate) => candidate !== index),
687
+ ],
688
+ beforeExecute: () => invalidateForExecution(state, target, invalidatedTargetDirs),
471
689
  revalidate: () => revalidateChain(pipeline.dir),
472
690
  signal: deps.signal,
473
691
  });
@@ -511,8 +729,326 @@ async function executeSteps(steps, pipeline, deps) {
511
729
  elapsedMs: Date.now() - startedAt,
512
730
  });
513
731
  outputs.push(target);
732
+ if (state !== undefined) {
733
+ let output = null;
734
+ try {
735
+ output = hashBytes(await readFile(target));
736
+ }
737
+ catch {
738
+ // A successful compile remains successful when only history capture is
739
+ // unavailable; final publication reports the advisory diagnostic.
740
+ }
741
+ state.executed++;
742
+ state.accepted.push({
743
+ step,
744
+ target,
745
+ inputs: identity?.inputs ?? null,
746
+ output,
747
+ });
748
+ }
749
+ }
750
+ const selected = {
751
+ ok: true,
752
+ outputs,
753
+ diagnostics,
754
+ ...(state !== undefined &&
755
+ state.executed === 0 &&
756
+ steps.length > 0 &&
757
+ state.accepted.length === steps.length
758
+ ? { outcome: 'up-to-date' }
759
+ : {}),
760
+ };
761
+ const completed = await complete(selected, guardCompletionTarget);
762
+ return state === undefined
763
+ ? completed
764
+ : publishIncremental(completed, state, steps);
765
+ }
766
+ function stepTarget(step) {
767
+ return step.request.kind === 'compile'
768
+ ? step.request.target
769
+ : step.request.linked;
770
+ }
771
+ /**
772
+ * Inputs supplied from outside this schedule. A normal predecessor target may
773
+ * feed its successor, but source/reference/link operands supplied by the user
774
+ * remain protected even when they collide with some scheduled output.
775
+ */
776
+ function externalInputPaths(steps) {
777
+ const produced = new Set();
778
+ const external = new Set();
779
+ for (const step of steps) {
780
+ if (step.request.kind === 'compile') {
781
+ const source = resolve(step.request.source);
782
+ if (!produced.has(source))
783
+ external.add(source);
784
+ for (const reference of step.request.references ?? []) {
785
+ external.add(resolve(reference));
786
+ }
787
+ }
788
+ else {
789
+ for (const object of step.request.objects) {
790
+ const path = resolve(object);
791
+ if (!produced.has(path))
792
+ external.add(path);
793
+ }
794
+ external.add(resolve(step.request.linkTarget));
795
+ }
796
+ produced.add(resolve(stepTarget(step)));
797
+ }
798
+ return [...external];
799
+ }
800
+ async function loadIncremental(run) {
801
+ const sourceBytes = await readFile(run.source);
802
+ const active = await loadBuildHistory(run.artDir);
803
+ const sourceLocator = encodeLocator(run.artDir, run.source);
804
+ const history = !run.rebuild &&
805
+ active?.manifest.pipeline === run.pipelineName &&
806
+ active.manifest.source.path === sourceLocator
807
+ ? active
808
+ : null;
809
+ return {
810
+ ...run,
811
+ active: active !== null,
812
+ history,
813
+ sourceBytes,
814
+ invalidated: false,
815
+ executed: 0,
816
+ accepted: [],
817
+ };
818
+ }
819
+ async function stepIdentity(step, state, index, declared) {
820
+ if (declared === null)
821
+ return null;
822
+ try {
823
+ if (step.request.kind === 'compile') {
824
+ const chainedInput = index === 0 ? state.sourceBytes : await readFile(step.request.source);
825
+ const roots = [
826
+ step.request.definitionPath,
827
+ ...(step.request.references ?? []),
828
+ ];
829
+ const rootHashes = await Promise.all(roots.map(async (path) => hashBytes(await readFile(path))));
830
+ return {
831
+ chainedInput,
832
+ inputs: [
833
+ hashBytes(chainedInput),
834
+ ...rootHashes,
835
+ ...(await Promise.all(declared.map(async (path) => hashBytes(await readFile(path))))),
836
+ ],
837
+ };
838
+ }
839
+ const inputs = [];
840
+ for (const object of step.request.objects) {
841
+ const path = resolve(object);
842
+ inputs.push(framedHash('object', encodeLocator(state.artDir, path), hashBytes(await readFile(path))));
843
+ }
844
+ inputs.push(hashBytes(await readFile(step.request.definitionPath)));
845
+ inputs.push(...(await Promise.all(declared.map(async (path) => hashBytes(await readFile(path))))));
846
+ const linkTarget = resolve(step.request.linkTarget);
847
+ const targetInfo = await stat(linkTarget);
848
+ let targetKind;
849
+ let targetIdentity;
850
+ if (targetInfo.isFile()) {
851
+ targetKind = 'file';
852
+ targetIdentity = hashBytes(await readFile(linkTarget));
853
+ }
854
+ else if (targetInfo.isDirectory()) {
855
+ targetKind = 'directory';
856
+ const identity = await hashTree(linkTarget);
857
+ if (!isHash(identity))
858
+ throw new Error('invalid link-target tree hash');
859
+ targetIdentity = identity;
860
+ }
861
+ else {
862
+ throw new Error('link target is neither a file nor directory');
863
+ }
864
+ inputs.push(framedHash('link-target', encodeLocator(state.artDir, linkTarget), targetKind, targetIdentity));
865
+ for (const option of step.request.options) {
866
+ inputs.push(framedHash('option', option.name, option.value));
867
+ }
868
+ return { inputs };
514
869
  }
515
- return { ok: true, outputs, diagnostics };
870
+ catch {
871
+ return null;
872
+ }
873
+ }
874
+ async function stepDeclaredInputs(step, pipeline, pinFile) {
875
+ const roots = step.request.kind === 'compile'
876
+ ? [step.request.definitionPath, ...(step.request.references ?? [])]
877
+ : [step.request.definitionPath];
878
+ return declaredInputPaths(pipeline, pinFile, roots);
879
+ }
880
+ /** Current local semantic-input closure for definitions and references. */
881
+ async function declaredInputPaths(pipeline, pinFile, roots) {
882
+ const boundary = pinFile?.pathBoundary.path ?? '.';
883
+ const base = new Set(roots.map((path) => resolve(path)));
884
+ const seen = new Set(base);
885
+ const inputs = [];
886
+ let complete = true;
887
+ const normalize = resolve(normalizeDefinitionPath());
888
+ for (const root of roots) {
889
+ const absolute = resolve(root);
890
+ if (!absolute.toLowerCase().endsWith('.md') || absolute === normalize) {
891
+ continue;
892
+ }
893
+ const locator = relative(pipeline.dir, absolute).split(sep).join('/');
894
+ const closure = new Set();
895
+ try {
896
+ await deriveClosure(pipeline.dir, boundary, locator, (path) => closure.add(path));
897
+ }
898
+ catch {
899
+ // This derivation exists only for incremental identity and best-effort
900
+ // target protection. Pin generation/currency remain fail-closed, while an
901
+ // otherwise interpretable phase degrades to Ordinary execution.
902
+ complete = false;
903
+ }
904
+ for (const path of closure) {
905
+ const absolutePath = resolve(path);
906
+ if (seen.has(absolutePath))
907
+ continue;
908
+ seen.add(absolutePath);
909
+ inputs.push(absolutePath);
910
+ }
911
+ }
912
+ return { paths: inputs, complete };
913
+ }
914
+ async function selectStepMode(step, index, target, identity, state) {
915
+ if (state.rebuild || state.history === null || identity === null) {
916
+ return { mode: 'ordinary' };
917
+ }
918
+ const record = state.history.manifest.steps[index];
919
+ const kind = step.request.kind;
920
+ if (record === undefined ||
921
+ record.kind !== kind ||
922
+ record.name !== step.phase ||
923
+ record.target !== encodeLocator(state.artDir, resolve(target))) {
924
+ return { mode: 'ordinary' };
925
+ }
926
+ let targetBytes;
927
+ try {
928
+ targetBytes = await readFile(target);
929
+ }
930
+ catch {
931
+ return { mode: 'ordinary' };
932
+ }
933
+ if (sameHashes(record.inputs, identity.inputs)) {
934
+ return { mode: 'reuse', bytes: targetBytes };
935
+ }
936
+ if (step.request.kind !== 'compile' || identity.chainedInput === undefined) {
937
+ return { mode: 'ordinary' };
938
+ }
939
+ const prior = await verifiedInput(state.history, index, record.inputs[0]);
940
+ if (prior === null)
941
+ return { mode: 'ordinary' };
942
+ const diff = record.inputs[0] === identity.inputs[0]
943
+ ? ''
944
+ : renderInputDiff(prior.bytes, identity.chainedInput);
945
+ return { mode: 'update', priorInput: prior.path, diff };
946
+ }
947
+ function renderInputDiff(prior, current) {
948
+ try {
949
+ const decoder = new TextDecoder('utf-8', { fatal: true });
950
+ return unifiedLineDiff(decoder.decode(prior), decoder.decode(current));
951
+ }
952
+ catch {
953
+ return null;
954
+ }
955
+ }
956
+ function framedHash(tag, ...fields) {
957
+ return hashBytes(Buffer.from(JSON.stringify([tag, ...fields]), 'utf8'));
958
+ }
959
+ function sameHashes(left, right) {
960
+ return (left.length === right.length &&
961
+ left.every((hash, index) => hash === right[index]));
962
+ }
963
+ async function reusePinFailure(pinKey, pipelineDir, pinFile) {
964
+ const record = ownPin(pinFile, pinKey);
965
+ if (pinFile === undefined || pinKey === undefined || record === undefined) {
966
+ return null;
967
+ }
968
+ const verdict = await evaluatePin(pipelineDir, pinFile, pinKey, record);
969
+ return verdict.status === 'current'
970
+ ? null
971
+ : [`pin is ${verdict.status}: ${verdict.reason}`];
972
+ }
973
+ /**
974
+ * Removes any complete snapshot that could otherwise bless this target after
975
+ * executor work. Eligible runs already know their artifact directory; excluded
976
+ * forms perform only this safety invalidation and never select from or publish
977
+ * history.
978
+ */
979
+ async function invalidateForExecution(state, target, invalidatedTargetDirs) {
980
+ if (state !== undefined) {
981
+ if (state.invalidated)
982
+ return;
983
+ try {
984
+ await invalidateBuildHistory(state.artDir, state.active);
985
+ state.invalidated = true;
986
+ return;
987
+ }
988
+ catch (error) {
989
+ throw new Error(`active build history could not be invalidated: ${messageOf(error)}`, { cause: error });
990
+ }
991
+ }
992
+ const targetDir = dirname(resolve(target));
993
+ if (invalidatedTargetDirs.has(targetDir))
994
+ return;
995
+ const active = await loadBuildHistory(targetDir);
996
+ const targetLocator = encodeLocator(targetDir, resolve(target));
997
+ if (active !== null &&
998
+ active.manifest.steps.some((step) => step.target === targetLocator)) {
999
+ try {
1000
+ await invalidateBuildHistory(targetDir, true);
1001
+ }
1002
+ catch (error) {
1003
+ throw new Error(`active build history could not be invalidated: ${messageOf(error)}`, { cause: error });
1004
+ }
1005
+ invalidatedTargetDirs.add(targetDir);
1006
+ }
1007
+ }
1008
+ async function publishIncremental(result, state, steps) {
1009
+ if (!result.ok || state.executed === 0)
1010
+ return result;
1011
+ if (state.accepted.length !== steps.length ||
1012
+ state.accepted.some((entry) => entry.inputs === null || entry.output === null)) {
1013
+ return historyDiagnostic(result, 'one or more phase identities or output bytes were unavailable');
1014
+ }
1015
+ const records = [];
1016
+ try {
1017
+ for (const accepted of state.accepted) {
1018
+ const bytes = await readFile(accepted.target);
1019
+ if (hashBytes(bytes) !== accepted.output) {
1020
+ return historyDiagnostic(result, `accepted output changed before publication: ${accepted.target}`);
1021
+ }
1022
+ records.push({
1023
+ kind: accepted.step.request.kind,
1024
+ name: accepted.step.phase,
1025
+ target: accepted.target,
1026
+ inputs: accepted.inputs,
1027
+ output: accepted.output,
1028
+ bytes,
1029
+ });
1030
+ }
1031
+ await recordBuild({
1032
+ artDir: state.artDir,
1033
+ pipeline: state.pipelineName,
1034
+ sourcePath: state.source,
1035
+ sourceBytes: state.sourceBytes,
1036
+ steps: records,
1037
+ });
1038
+ return result;
1039
+ }
1040
+ catch (error) {
1041
+ return historyDiagnostic(result, messageOf(error));
1042
+ }
1043
+ }
1044
+ function historyDiagnostic(result, reason) {
1045
+ return {
1046
+ ...result,
1047
+ diagnostics: [
1048
+ ...result.diagnostics,
1049
+ `slc: build history not recorded: ${reason}`,
1050
+ ],
1051
+ };
516
1052
  }
517
1053
  /**
518
1054
  * Selects a phase's execution strategy from the pin index (PHEXEC-27; DR-005,
@@ -520,12 +1056,12 @@ async function executeSteps(steps, pipeline, deps) {
520
1056
  * artifact, and a stale or malformed pin fails closed and is never silently
521
1057
  * interpreted.
522
1058
  */
523
- async function selectExecutor(phase, pipelineDir, pinFile, deps) {
524
- const record = pinFile?.pins[phase];
525
- if (pinFile === undefined || record === undefined) {
1059
+ async function selectExecutor(phase, pinKey, pipelineDir, pinFile, deps) {
1060
+ const record = ownPin(pinFile, pinKey);
1061
+ if (pinFile === undefined || pinKey === undefined || record === undefined) {
526
1062
  return { kind: 'run', executor: deps.executor };
527
1063
  }
528
- const verdict = await evaluatePin(pipelineDir, pinFile, phase, record);
1064
+ const verdict = await evaluatePin(pipelineDir, pinFile, pinKey, record);
529
1065
  if (verdict.status === 'current') {
530
1066
  if (deps.compiled === undefined) {
531
1067
  return {
@@ -537,7 +1073,7 @@ async function selectExecutor(phase, pipelineDir, pinFile, deps) {
537
1073
  }
538
1074
  return {
539
1075
  kind: 'run',
540
- executor: deps.compiled({ phase, pipelineDir, record }),
1076
+ executor: deps.compiled({ phase: pinKey, pipelineDir, record }),
541
1077
  };
542
1078
  }
543
1079
  return {
@@ -545,6 +1081,13 @@ async function selectExecutor(phase, pipelineDir, pinFile, deps) {
545
1081
  reasons: [`pin is ${verdict.status}: ${verdict.reason}`],
546
1082
  };
547
1083
  }
1084
+ function ownPin(pinFile, pinKey) {
1085
+ return pinFile !== undefined &&
1086
+ pinKey !== undefined &&
1087
+ Object.hasOwn(pinFile.pins, pinKey)
1088
+ ? pinFile.pins[pinKey]
1089
+ : undefined;
1090
+ }
548
1091
  function phaseDefinition(pipeline, name) {
549
1092
  return join(pipeline.dir, `${name}.md`);
550
1093
  }
@@ -570,7 +1113,4 @@ async function revalidateChain(dir) {
570
1113
  function failure(message) {
571
1114
  return { ok: false, outputs: [], diagnostics: [message] };
572
1115
  }
573
- function messageOf(error) {
574
- return error instanceof Error ? error.message : String(error);
575
- }
576
1116
  //# sourceMappingURL=runner.js.map