@sublang/slc 0.9.0 → 0.11.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 (91) hide show
  1. package/README.md +44 -15
  2. package/dist/app.d.ts +2 -2
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +6 -4
  5. package/dist/app.js.map +1 -1
  6. package/dist/artifacts.d.ts +5 -1
  7. package/dist/artifacts.d.ts.map +1 -1
  8. package/dist/artifacts.js +19 -3
  9. package/dist/artifacts.js.map +1 -1
  10. package/dist/clarification.d.ts +32 -0
  11. package/dist/clarification.d.ts.map +1 -0
  12. package/dist/clarification.js +93 -0
  13. package/dist/clarification.js.map +1 -0
  14. package/dist/compiled-execution.js +1 -1
  15. package/dist/compiled-execution.js.map +1 -1
  16. package/dist/compiled-executor.d.ts.map +1 -1
  17. package/dist/compiled-executor.js +58 -22
  18. package/dist/compiled-executor.js.map +1 -1
  19. package/dist/config-file.d.ts.map +1 -1
  20. package/dist/config-file.js.map +1 -1
  21. package/dist/config.d.ts +1 -1
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +1 -1
  24. package/dist/config.js.map +1 -1
  25. package/dist/entry-module.d.ts.map +1 -1
  26. package/dist/entry-module.js +16 -7
  27. package/dist/entry-module.js.map +1 -1
  28. package/dist/entry-options.d.ts +13 -0
  29. package/dist/entry-options.d.ts.map +1 -0
  30. package/dist/entry-options.js +116 -0
  31. package/dist/entry-options.js.map +1 -0
  32. package/dist/execution.d.ts +21 -4
  33. package/dist/execution.d.ts.map +1 -1
  34. package/dist/execution.js +58 -19
  35. package/dist/execution.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/interpreter.d.ts.map +1 -1
  41. package/dist/interpreter.js +20 -1
  42. package/dist/interpreter.js.map +1 -1
  43. package/dist/invocation.d.ts.map +1 -1
  44. package/dist/link.d.ts.map +1 -1
  45. package/dist/phase-runner.d.ts +1 -4
  46. package/dist/phase-runner.d.ts.map +1 -1
  47. package/dist/phase-runner.js +4 -1
  48. package/dist/phase-runner.js.map +1 -1
  49. package/dist/phase.d.ts.map +1 -1
  50. package/dist/phase.js.map +1 -1
  51. package/dist/pin-currency.js.map +1 -1
  52. package/dist/pin-generate.js.map +1 -1
  53. package/dist/pin-inputs.js.map +1 -1
  54. package/dist/pins.d.ts.map +1 -1
  55. package/dist/pins.js.map +1 -1
  56. package/dist/pipeline.d.ts.map +1 -1
  57. package/dist/playbook-contract.d.ts.map +1 -1
  58. package/dist/playbook-contract.js.map +1 -1
  59. package/dist/playbook-ports.d.ts +9 -1
  60. package/dist/playbook-ports.d.ts.map +1 -1
  61. package/dist/playbook-ports.js +24 -6
  62. package/dist/playbook-ports.js.map +1 -1
  63. package/dist/progress.js.map +1 -1
  64. package/dist/resolver.js.map +1 -1
  65. package/dist/reviewing-agent.d.ts +2 -2
  66. package/dist/reviewing-agent.d.ts.map +1 -1
  67. package/dist/reviewing-agent.js +34 -12
  68. package/dist/reviewing-agent.js.map +1 -1
  69. package/dist/runner.d.ts +4 -2
  70. package/dist/runner.d.ts.map +1 -1
  71. package/dist/runner.js +361 -45
  72. package/dist/runner.js.map +1 -1
  73. package/dist/slc.config.template.yaml +1 -1
  74. package/dist/verify-coverage.d.ts +7 -0
  75. package/dist/verify-coverage.d.ts.map +1 -1
  76. package/dist/verify-coverage.js +805 -254
  77. package/dist/verify-coverage.js.map +1 -1
  78. package/dist/verify-source.d.ts +2 -0
  79. package/dist/verify-source.d.ts.map +1 -1
  80. package/dist/verify-source.js +370 -24
  81. package/dist/verify-source.js.map +1 -1
  82. package/dist/verify-support.js.map +1 -1
  83. package/dist/verify-typescript.d.ts +3 -0
  84. package/dist/verify-typescript.d.ts.map +1 -0
  85. package/dist/verify-typescript.js +87 -0
  86. package/dist/verify-typescript.js.map +1 -0
  87. package/dist/verify.d.ts +28 -10
  88. package/dist/verify.d.ts.map +1 -1
  89. package/dist/verify.js +604 -133
  90. package/dist/verify.js.map +1 -1
  91. package/package.json +17 -16
package/dist/runner.js CHANGED
@@ -13,11 +13,12 @@ import { mkdir, readFile, stat } from 'node:fs/promises';
13
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 { formatClarificationReport, } from './clarification.js';
16
17
  import { encodeLocator, invalidateBuildHistory, loadBuildHistory, recordBuild, verifiedInput, } from './build-history.js';
17
18
  import { emitEntryModule } from './entry-module.js';
18
19
  import { reconcileLinkObjectImportSpecifiers, unresolvableRelativeImports, } from './emitted-imports.js';
19
20
  import { messageOf } from './errors.js';
20
- import { assertSafeTarget, formatFailureReport, pathsAlias, runPhase, } from './execution.js';
21
+ import { assertSafeTarget, formatFailureReport, phaseProtectedPaths, pathsAlias, runPhase, watchProtectedPaths, } from './execution.js';
21
22
  import { compareUtf8, hashBytes, isHash } from './hash.js';
22
23
  import { parseInvocation } from './invocation.js';
23
24
  import { unifiedLineDiff } from './line-diff.js';
@@ -30,9 +31,11 @@ import { PINS_FILE, PinError, loadPinFile, } from './pins.js';
30
31
  import { loadPipeline, resolvePipeline, } from './pipeline.js';
31
32
  import { RESERVED_SLC_PIPELINE, defaultPlaybookLinkTarget, isReservedPipeline, } from './resolver.js';
32
33
  import { readPlaybookRuntimeDeclaration, } from './runtime-contract.js';
33
- import { artifactSchemaForPlaybookProvenance, checkLinkedModuleContract, emitFsmCoverageTest, emitFsmIntrospectionTest, emitGearsFsmConformanceTest, emitPromptContractTest, findMachineConfig, loadFsmModule, loadLinkedModuleForVerification, playbookProvenanceForLinkTarget, resolveArtifactSchemaForVerification, } from './verify.js';
34
+ import { artifactSchemaForPlaybookProvenance, checkFsmContinuationInputs, checkFsmChildSuspension, checkFsmCoverage, checkGearsActorContract, checkGearsFsmConformance, checkGearsResultContract, checkLinkedModuleContract, emitFsmCoverageTest, FsmCoverageDeadlineError, emitFsmIntrospectionTest, emitGearsFsmConformanceTest, emitPromptContractTest, findConcurrentRoleSets, inspectGearsRoleContract, findMachineConfig, loadFsmModule, loadLinkedModuleForVerification, playbookProvenanceForLinkTarget, resolveArtifactSchemaForVerification, } from './verify.js';
34
35
  import { VERIFIER_SUPPORT_MODULE, emitVerifierSupport, verifierSupportFiles, } from './verify-support.js';
35
36
  import { checkSourceGearsContract } from './verify-source.js';
37
+ import { checkFsmTypeScript } from './verify-typescript.js';
38
+ import { checkEntryOptions, ENTRY_OPTIONS_CONTRACT } from './entry-options.js';
36
39
  /**
37
40
  * Parses argv and runs the requested pipeline, phase, or link end-to-end.
38
41
  * Never rejects: malformed invocations, refusals, and phase failures all return
@@ -46,6 +49,24 @@ export async function runSlc(argv, deps) {
46
49
  catch (error) {
47
50
  return failure(messageOf(error));
48
51
  }
52
+ const finish = (result) => {
53
+ if (result.clarification === undefined)
54
+ return result;
55
+ const clarification = {
56
+ ...result.clarification,
57
+ sources: (invocation.kind === 'link'
58
+ ? invocation.objects
59
+ : [invocation.source]).map((path) => resolve(runCwd(deps), path)),
60
+ };
61
+ return {
62
+ ...result,
63
+ clarification,
64
+ diagnostics: [
65
+ ...result.diagnostics,
66
+ formatClarificationReport(clarification),
67
+ ],
68
+ };
69
+ };
49
70
  try {
50
71
  switch (invocation.kind) {
51
72
  case 'full':
@@ -53,20 +74,20 @@ export async function runSlc(argv, deps) {
53
74
  // (self-hosting-13): a bare full run becomes a full-link against the
54
75
  // installed @sublang/playbook runtime contract module (DR-014).
55
76
  if (invocation.pipeline === 'playbook') {
56
- return await runFullLink({
77
+ return finish(await runFullLink({
57
78
  ...invocation,
58
79
  kind: 'full-link',
59
80
  linkTarget: defaultPlaybookLinkTarget(),
60
81
  options: [],
61
- }, deps);
82
+ }, deps));
62
83
  }
63
- return await runFull(invocation, deps);
84
+ return finish(await runFull(invocation, deps));
64
85
  case 'phase':
65
- return await runSinglePhase(invocation, deps);
86
+ return finish(await runSinglePhase(invocation, deps));
66
87
  case 'link':
67
- return await runDirectLink(invocation, deps);
88
+ return finish(await runDirectLink(invocation, deps));
68
89
  case 'full-link':
69
- return await runFullLink(invocation, deps);
90
+ return finish(await runFullLink(invocation, deps));
70
91
  }
71
92
  }
72
93
  catch (error) {
@@ -101,6 +122,7 @@ async function runFull(invocation, deps) {
101
122
  optimize: !invocation.noOptimize,
102
123
  normalize: invocation.normalize || raw,
103
124
  sourceFidelity: invocation.pipeline !== RESERVED_SLC_PIPELINE,
125
+ signal: deps.signal,
104
126
  });
105
127
  const verification = {
106
128
  pipeline: invocation.pipeline,
@@ -139,6 +161,7 @@ async function runSinglePhase(invocation, deps) {
139
161
  sourceFormat: phase.source.format,
140
162
  ext: phase.source.ext,
141
163
  entry: pipeline.phases[0] === phase,
164
+ staged: phase.pass,
142
165
  });
143
166
  if (raw) {
144
167
  // A named phase cannot normalize (pipeline-37), so a raw entry source has no
@@ -153,7 +176,7 @@ async function runSinglePhase(invocation, deps) {
153
176
  // `.opt` sibling unless `-o` relocates it (DR-013).
154
177
  const target = (invocation.output === null ? null : resolve(cwd, invocation.output)) ??
155
178
  join(artDir, `${basename}.${phase.target.format}.opt${phase.target.ext}`);
156
- const step = compileStep(pipeline, phase, source, target, sourceFidelity);
179
+ const step = compileStep(pipeline, phase, source, target, sourceFidelity, undefined, deps.signal);
157
180
  return executeSteps([step], pipeline, deps);
158
181
  }
159
182
  // Plan over the whole chain so the named phase keeps its pipeline role: a
@@ -166,7 +189,7 @@ async function runSinglePhase(invocation, deps) {
166
189
  output: invocation.output === null ? undefined : resolve(cwd, invocation.output),
167
190
  });
168
191
  const artifact = plan[pipeline.phases.indexOf(phase)];
169
- const step = compileStep(pipeline, phase, source, artifact.path, sourceFidelity);
192
+ const step = compileStep(pipeline, phase, source, artifact.path, sourceFidelity, undefined, deps.signal);
170
193
  return executeSteps([step], pipeline, deps);
171
194
  }
172
195
  async function runDirectLink(invocation, deps) {
@@ -191,6 +214,7 @@ async function runDirectLink(invocation, deps) {
191
214
  linkTarget: resolve(cwd, invocation.linkTarget),
192
215
  options: invocation.options,
193
216
  linked,
217
+ signal: deps.signal,
194
218
  }),
195
219
  ], pipeline, deps);
196
220
  }
@@ -220,6 +244,8 @@ async function runFullLink(invocation, deps) {
220
244
  optimize: !invocation.noOptimize,
221
245
  normalize,
222
246
  sourceFidelity: invocation.pipeline !== RESERVED_SLC_PIPELINE,
247
+ signal: deps.signal,
248
+ linkTarget: resolve(cwd, invocation.linkTarget),
223
249
  });
224
250
  const linked = linkedArtifactPath({
225
251
  kind: 'full',
@@ -228,6 +254,14 @@ async function runFullLink(invocation, deps) {
228
254
  linked: link.target,
229
255
  output: invocation.output === null ? null : resolve(cwd, invocation.output),
230
256
  });
257
+ const gearsPlan = plan.find((artifact) => artifact.phase.target.format === 'gears');
258
+ const entryCandidate = invocation.pipeline === 'playbook' &&
259
+ invocation.output === null &&
260
+ gearsPlan !== undefined
261
+ ? resolve(cwd, `${basename}.ts`)
262
+ : null;
263
+ const entryAliasesSource = entryCandidate !== null && (await pathsAlias(entryCandidate, source));
264
+ const entryPath = entryAliasesSource ? null : entryCandidate;
231
265
  const steps = [
232
266
  ...compileSteps,
233
267
  linkStep({
@@ -237,16 +271,12 @@ async function runFullLink(invocation, deps) {
237
271
  linkTarget: resolve(cwd, invocation.linkTarget),
238
272
  options: invocation.options,
239
273
  linked,
274
+ signal: deps.signal,
275
+ ...(entryPath !== null && gearsPlan !== undefined
276
+ ? { entryGearsPath: gearsPlan.path }
277
+ : {}),
240
278
  }),
241
279
  ];
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
280
  const verification = {
251
281
  pipeline: invocation.pipeline,
252
282
  plan,
@@ -500,6 +530,9 @@ function buildCompileSteps(opts) {
500
530
  if (opts.normalize) {
501
531
  const entry = pipeline.phases[0];
502
532
  const normalized = join(artDir, `${basename}.${entry.source.format}${entry.source.ext}`);
533
+ const gearsContract = entry.source.format === 'gears'
534
+ ? () => gearsContractFindings(normalized)
535
+ : undefined;
503
536
  steps.push({
504
537
  request: {
505
538
  kind: 'compile',
@@ -507,9 +540,15 @@ function buildCompileSteps(opts) {
507
540
  source: previous,
508
541
  target: normalized,
509
542
  references: [phaseDefinition(pipeline, entry.name)],
543
+ ...(gearsContract === undefined
544
+ ? {}
545
+ : { mechanicalReview: gearsContract }),
510
546
  },
511
547
  phase: 'normalize',
512
548
  targetExt: entry.source.ext,
549
+ ...(gearsContract === undefined
550
+ ? {}
551
+ : { compileFidelity: gearsContract }),
513
552
  });
514
553
  previous = normalized;
515
554
  }
@@ -519,46 +558,92 @@ function buildCompileSteps(opts) {
519
558
  ? pipeline.passes.filter((pass) => pass.source.format === phase.target.format)
520
559
  : [];
521
560
  if (passes.length === 0) {
522
- steps.push(compileStep(pipeline, phase, previous, artifact.path, opts.sourceFidelity));
561
+ steps.push(compileStep(pipeline, phase, previous, artifact.path, opts.sourceFidelity, opts.linkTarget, opts.signal));
523
562
  previous = artifact.path;
524
563
  continue;
525
564
  }
526
565
  const raw = join(artDir, `${basename}.${phase.target.format}.raw${phase.target.ext}`);
527
- steps.push(compileStep(pipeline, phase, previous, raw, opts.sourceFidelity));
566
+ steps.push(compileStep(pipeline, phase, previous, raw, opts.sourceFidelity, opts.linkTarget, opts.signal));
528
567
  previous = raw;
529
568
  passes.forEach((pass, index) => {
530
569
  const target = index === passes.length - 1
531
570
  ? artifact.path
532
571
  : join(artDir, `${basename}.${phase.target.format}.opt${index + 1}${phase.target.ext}`);
533
- steps.push(compileStep(pipeline, pass, previous, target, opts.sourceFidelity));
572
+ steps.push(compileStep(pipeline, pass, previous, target, opts.sourceFidelity, opts.linkTarget, opts.signal));
534
573
  previous = target;
535
574
  });
536
575
  }
537
576
  return steps;
538
577
  }
539
- function compileStep(pipeline, phase, source, target, sourceFidelity) {
578
+ function compileStep(pipeline, phase, source, target, sourceFidelity, linkTarget, signal) {
540
579
  // Only a text-to-GEARS phase conserves authored Source fragments, and the
541
580
  // reserved meta-pipeline compiles definitions under its own fidelity gate
542
581
  // (DR-029, phase-execution-51, verification-23).
543
582
  const gated = sourceFidelity &&
544
583
  phase.source.format === 'text' &&
545
584
  phase.target.format === 'gears';
585
+ const fsmConformance = phase.source.format === 'gears' && phase.target.format === 'fsm'
586
+ ? async () => {
587
+ if (phase.target.ext === '.ts') {
588
+ const findings = await checkFsmTypeScript(target, signal);
589
+ if (findings.length > 0)
590
+ return findings;
591
+ }
592
+ return fsmConformanceFindings(source, target, linkTarget, signal);
593
+ }
594
+ : undefined;
595
+ const checks = [
596
+ ...(gated ? [() => sourceFidelityFindings(source, target)] : []),
597
+ ...(phase.target.format === 'gears'
598
+ ? [() => gearsContractFindings(target)]
599
+ : []),
600
+ ...(fsmConformance === undefined ? [] : [fsmConformance]),
601
+ ];
602
+ const mechanicalReview = checks.length === 0
603
+ ? undefined
604
+ : async () => (await Promise.all(checks.map((check) => check()))).flat();
546
605
  return {
547
606
  request: {
548
607
  kind: 'compile',
549
608
  definitionPath: phaseDefinition(pipeline, phase.name),
550
609
  source,
551
610
  target,
552
- ...(gated
553
- ? { mechanicalReview: () => sourceFidelityFindings(source, target) }
554
- : {}),
611
+ ...(mechanicalReview === undefined ? {} : { mechanicalReview }),
555
612
  },
556
613
  phase: phase.name,
557
614
  pinKey: phase.name,
558
615
  targetExt: phase.target.ext,
559
- ...(gated ? { sourceFidelity: true } : {}),
616
+ ...(mechanicalReview === undefined
617
+ ? {}
618
+ : { compileFidelity: mechanicalReview }),
619
+ ...(phase.source.format === 'gears'
620
+ ? { gearsSourceContract: () => gearsContractFindings(source) }
621
+ : {}),
622
+ ...(phase.source.format === 'fsm' && phase.source.ext === '.ts'
623
+ ? { fsmSourceTypecheck: () => checkFsmTypeScript(source, signal) }
624
+ : {}),
625
+ ...(phase.source.format === 'fsm'
626
+ ? {
627
+ fsmSourceContinuation: () => fsmContinuationFindings(source, linkTarget, signal),
628
+ }
629
+ : {}),
560
630
  };
561
631
  }
632
+ /** Parser-only findings belong to the phase that can still edit the GEARS. */
633
+ async function gearsContractFindings(path) {
634
+ try {
635
+ const gears = await readFile(path, 'utf8');
636
+ return [
637
+ ...checkGearsResultContract(gears),
638
+ ...checkGearsActorContract(gears),
639
+ ];
640
+ }
641
+ catch (error) {
642
+ if (error.code === 'ENOENT')
643
+ return [];
644
+ return [`GEARS contract could not be checked: ${messageOf(error)}`];
645
+ }
646
+ }
562
647
  /**
563
648
  * The DR-029 gate over one text-to-GEARS phase's live target: the deterministic
564
649
  * conservation findings against the Source that phase reads (phase-execution-51,
@@ -587,6 +672,88 @@ async function sourceFidelityFindings(source, target) {
587
672
  return [`Source fidelity could not be checked: ${messageOf(error)}`];
588
673
  }
589
674
  }
675
+ /** Reuses the emitted GEARS-to-FSM suite before accepting its input (DR-033). */
676
+ async function fsmConformanceFindings(source, target, linkTarget, signal) {
677
+ signal?.throwIfAborted();
678
+ try {
679
+ await stat(target);
680
+ }
681
+ catch (error) {
682
+ // Absence belongs to the generic target check; all other failures are
683
+ // actionable findings rather than permission to skip the gate.
684
+ if (error.code === 'ENOENT')
685
+ return [];
686
+ return [`FSM conformance could not be checked: ${messageOf(error)}`];
687
+ }
688
+ try {
689
+ const [gears, fsm] = await Promise.all([
690
+ readFile(source, 'utf8'),
691
+ loadFsmModule(target),
692
+ ]);
693
+ const config = findMachineConfig(fsm);
694
+ const schema = await fsmSchema(config, linkTarget);
695
+ const findings = [
696
+ ...schema.findings,
697
+ ...checkGearsFsmConformance(gears, config, {
698
+ concurrentRoleSets: findConcurrentRoleSets(fsm),
699
+ ...(schema.artifactSchema === undefined
700
+ ? {}
701
+ : { artifactSchema: schema.artifactSchema }),
702
+ }),
703
+ ...(schema.artifactSchema === undefined || schema.findings.length > 0
704
+ ? []
705
+ : checkFsmContinuationInputs(config, schema.artifactSchema)),
706
+ ];
707
+ signal?.throwIfAborted();
708
+ return findings.length > 0
709
+ ? findings
710
+ : await checkFsmCoverage(fsm, {
711
+ sourceText: await readFile(target, 'utf8'),
712
+ signal,
713
+ });
714
+ }
715
+ catch (error) {
716
+ signal?.throwIfAborted();
717
+ if (error instanceof FsmCoverageDeadlineError)
718
+ throw error;
719
+ return [`FSM conformance could not be checked: ${messageOf(error)}`];
720
+ }
721
+ }
722
+ async function fsmSchema(config, linkTarget) {
723
+ const provenance = linkTarget === undefined
724
+ ? undefined
725
+ : await playbookProvenanceForLinkTarget(linkTarget);
726
+ const runtimeDeclaration = linkTarget === undefined ||
727
+ artifactSchemaForPlaybookProvenance(provenance) !== undefined
728
+ ? undefined
729
+ : await linkTargetRuntimeDeclaration(linkTarget);
730
+ return resolveArtifactSchemaForVerification({
731
+ requireContinuationSchema: false,
732
+ config,
733
+ ...(provenance === undefined ? {} : { provenance }),
734
+ ...(runtimeDeclaration === undefined ? {} : { runtimeDeclaration }),
735
+ });
736
+ }
737
+ async function fsmContinuationFindings(source, linkTarget, signal) {
738
+ signal?.throwIfAborted();
739
+ const fsm = await loadFsmModule(source);
740
+ const config = findMachineConfig(fsm);
741
+ const schema = await fsmSchema(config, linkTarget);
742
+ signal?.throwIfAborted();
743
+ const findings = [
744
+ ...schema.findings,
745
+ ...checkFsmChildSuspension(config),
746
+ ...(schema.artifactSchema === undefined || schema.findings.length > 0
747
+ ? []
748
+ : checkFsmContinuationInputs(config, schema.artifactSchema)),
749
+ ];
750
+ return findings.length > 0
751
+ ? findings
752
+ : await checkFsmCoverage(fsm, {
753
+ sourceText: await readFile(source, 'utf8'),
754
+ signal,
755
+ });
756
+ }
590
757
  /**
591
758
  * Builds one link step, gated by DR-030 when the link emits Playbook's
592
759
  * `playbook` module over exactly the one FSM object the checks read
@@ -596,8 +763,20 @@ async function sourceFidelityFindings(source, target) {
596
763
  function linkStep(opts) {
597
764
  const { link } = opts;
598
765
  const fsmObjects = opts.objects.filter((object) => object.endsWith(`.${link.source.format}${link.source.ext}`));
766
+ const entryRequired = async () => opts.entryGearsPath !== undefined &&
767
+ inspectGearsRoleContract(await readFile(opts.entryGearsPath, 'utf8'))
768
+ .generation === 'schema-3';
599
769
  const gated = link.target.format === PLAYBOOK_LINKED_FORMAT && fsmObjects.length === 1
600
- ? () => linkFidelityFindings(opts.linked, fsmObjects[0])
770
+ ? async () => [
771
+ ...(await linkFidelityFindings(opts.linked, fsmObjects[0])),
772
+ ...((await entryRequired())
773
+ ? await checkEntryOptions({
774
+ linkedPath: opts.linked,
775
+ fsmPath: fsmObjects[0],
776
+ signal: opts.signal,
777
+ })
778
+ : []),
779
+ ]
601
780
  : undefined;
602
781
  return {
603
782
  request: {
@@ -609,10 +788,27 @@ function linkStep(opts) {
609
788
  linked: opts.linked,
610
789
  ...(gated === undefined ? {} : { mechanicalReview: gated }),
611
790
  },
791
+ ...(gated !== undefined && opts.entryGearsPath !== undefined
792
+ ? {
793
+ outputContract: async () => (await entryRequired()) ? ENTRY_OPTIONS_CONTRACT : undefined,
794
+ }
795
+ : {}),
612
796
  phase: 'link',
613
797
  pinKey: 'link',
614
798
  targetExt: link.target.ext,
615
799
  ...(gated === undefined ? {} : { linkFidelity: gated }),
800
+ ...(gated !== undefined &&
801
+ link.source.format === 'fsm' &&
802
+ link.source.ext === '.ts'
803
+ ? {
804
+ fsmSourceTypecheck: () => checkFsmTypeScript(fsmObjects[0], opts.signal),
805
+ }
806
+ : {}),
807
+ ...(gated !== undefined && link.source.format === 'fsm'
808
+ ? {
809
+ fsmSourceContinuation: () => fsmContinuationFindings(fsmObjects[0], opts.linkTarget, opts.signal),
810
+ }
811
+ : {}),
616
812
  };
617
813
  }
618
814
  /**
@@ -736,7 +932,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
736
932
  : [
737
933
  ...diagnostics,
738
934
  ...state.accepted.map((accepted) => `accepted phase "${accepted.step.phase}" left its live target at ${accepted.target}`),
739
- `resume from the last accepted target with: ${resumeCommand(state.pipelineName, steps[failedIndex], state.accepted[state.accepted.length - 1].target)}`,
935
+ `resume from the last accepted target with: ${resumeCommand(state.pipelineName, steps[failedIndex], state.accepted[state.accepted.length - 1].target, pipeline.passes)}`,
740
936
  ],
741
937
  });
742
938
  for (let index = 0; index < steps.length; index++) {
@@ -796,6 +992,71 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
796
992
  target,
797
993
  elapsedMs: Date.now() - startedAt,
798
994
  });
995
+ let request = mode.mode === 'update' && step.request.kind === 'compile'
996
+ ? {
997
+ ...step.request,
998
+ update: { priorInput: mode.priorInput, diff: mode.diff },
999
+ }
1000
+ : step.request;
1001
+ if (request.kind === 'link' && step.outputContract !== undefined) {
1002
+ const outputContract = await step.outputContract();
1003
+ if (outputContract !== undefined)
1004
+ request = { ...request, outputContract };
1005
+ }
1006
+ // The consumer cannot repair its protected source. Reject these existing
1007
+ // parser findings before selecting or constructing any execution strategy.
1008
+ if (step.gearsSourceContract !== undefined) {
1009
+ const findings = await step.gearsSourceContract();
1010
+ if (findings.length > 0 && step.request.kind === 'compile') {
1011
+ fail();
1012
+ diagnostics.push(formatFailureReport({
1013
+ phase: step.phase,
1014
+ target,
1015
+ reasons: [
1016
+ `invalid GEARS source: ${step.request.source}`,
1017
+ ...findings,
1018
+ ],
1019
+ }));
1020
+ return stopped(index);
1021
+ }
1022
+ }
1023
+ for (const [check, name] of [
1024
+ [step.fsmSourceTypecheck, 'TypeScript'],
1025
+ [step.fsmSourceContinuation, 'continuation'],
1026
+ ]) {
1027
+ if (check === undefined)
1028
+ continue;
1029
+ let reasons = [];
1030
+ let checkProtectedPaths;
1031
+ try {
1032
+ if (name === 'continuation') {
1033
+ checkProtectedPaths = await watchProtectedPaths(phaseProtectedPaths(request, {
1034
+ definitions,
1035
+ protectedInputs: [...immutableInputs, ...stepDeclared.paths],
1036
+ }));
1037
+ }
1038
+ const findings = await check();
1039
+ if (findings.length > 0)
1040
+ reasons = ['invalid FSM source', ...findings];
1041
+ }
1042
+ catch (error) {
1043
+ reasons = [`FSM ${name} check could not run: ${messageOf(error)}`];
1044
+ }
1045
+ // Module imports can execute code even when the later probe fails or
1046
+ // throws. Their mutations outrank every checker outcome, before selection.
1047
+ const changes = await checkProtectedPaths?.();
1048
+ if (changes !== undefined && changes.length > 0)
1049
+ reasons = changes;
1050
+ if (reasons.length > 0) {
1051
+ fail();
1052
+ diagnostics.push(formatFailureReport({
1053
+ phase: step.phase,
1054
+ target,
1055
+ reasons,
1056
+ }));
1057
+ return stopped(index);
1058
+ }
1059
+ }
799
1060
  // Selecting a compiled executor can throw rather than return a verdict —
800
1061
  // notably a pinned link target whose installed engine declares no
801
1062
  // supported contract, which the host factory rejects (phase-execution-30).
@@ -825,19 +1086,15 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
825
1086
  text: `updating ${step.phase} → ${target}`,
826
1087
  });
827
1088
  }
828
- const request = mode.mode === 'update' && step.request.kind === 'compile'
829
- ? {
830
- ...step.request,
831
- update: { priorInput: mode.priorInput, diff: mode.diff },
832
- }
833
- : step.request;
834
1089
  const result = await runPhase({
835
1090
  request,
836
1091
  phase: step.phase,
837
1092
  targetExt: step.targetExt,
838
1093
  executor: selection.executor,
839
1094
  definitions,
840
- protectedInputs: stepDeclared.paths,
1095
+ // Original invocation inputs remain immutable even when a later phase
1096
+ // reads an intermediate and asks the author to clarify that original.
1097
+ protectedInputs: [...immutableInputs, ...stepDeclared.paths],
841
1098
  aliasInputs: [
842
1099
  ...immutableInputs,
843
1100
  ...declaredInputs,
@@ -851,16 +1108,45 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
851
1108
  });
852
1109
  if (!result.ok) {
853
1110
  fail();
1111
+ if (result.clarification !== undefined) {
1112
+ return {
1113
+ ok: false,
1114
+ outputs,
1115
+ diagnostics,
1116
+ outcome: 'clarification-required',
1117
+ clarification: {
1118
+ schema: 'sublang.slc.clarification.v1',
1119
+ phase: step.phase,
1120
+ target,
1121
+ sources: [],
1122
+ questions: result.clarification,
1123
+ },
1124
+ };
1125
+ }
854
1126
  diagnostics.push(formatFailureReport(result.report));
855
1127
  return stopped(index);
856
1128
  }
857
1129
  diagnostics.push(...result.diagnostics);
858
- // The DR-029 gate on the accepted result: a reviewed loop already relayed
859
- // its findings to the Coder for repair, so a finding surviving to here is
860
- // an unreviewed — or unrepaired — Source-fidelity break and fails the phase
861
- // closed (phase-execution-51).
862
- if (step.sourceFidelity === true && step.request.kind === 'compile') {
863
- const findings = await sourceFidelityFindings(step.request.source, target);
1130
+ // Recheck even custom executors that do not consume mechanicalReview.
1131
+ // runPhase has already enforced the unchanged generic protection checks.
1132
+ if (step.compileFidelity !== undefined) {
1133
+ let findings;
1134
+ let checkProtectedPaths;
1135
+ try {
1136
+ checkProtectedPaths = await watchProtectedPaths(phaseProtectedPaths(request, {
1137
+ definitions,
1138
+ protectedInputs: [...immutableInputs, ...stepDeclared.paths],
1139
+ }));
1140
+ findings = await step.compileFidelity();
1141
+ }
1142
+ catch (error) {
1143
+ findings = [`mechanical review could not run: ${messageOf(error)}`];
1144
+ }
1145
+ // Coverage executes artifact guards/actions after generic acceptance.
1146
+ // Their protected-input mutations outrank success, findings and errors.
1147
+ const changes = await checkProtectedPaths?.();
1148
+ if (changes !== undefined && changes.length > 0)
1149
+ findings = changes;
864
1150
  if (findings.length > 0) {
865
1151
  fail();
866
1152
  diagnostics.push(formatFailureReport({
@@ -967,10 +1253,40 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
967
1253
  * the run stopped at, so a stopped link step resumes through the direct-link
968
1254
  * form with that target as its object.
969
1255
  */
970
- function resumeCommand(pipelineName, next, lastTarget) {
971
- return next.request.kind === 'link'
972
- ? `slc ${pipelineName}.link ${lastTarget} ${next.request.linkTarget}`
973
- : `slc ${pipelineName}.${next.phase} ${lastTarget}`;
1256
+ function resumeCommand(pipelineName, next, lastTarget, passes) {
1257
+ const operands = next.request.kind === 'link'
1258
+ ? [
1259
+ `${pipelineName}.link`,
1260
+ lastTarget,
1261
+ next.request.linkTarget,
1262
+ // The link phase's opaque options are part of the invocation
1263
+ // (pipeline-14); dropping them would resume a different link.
1264
+ ...next.request.options.flatMap((option) => [
1265
+ '--link-option',
1266
+ `${option.name}=${option.value}`,
1267
+ ]),
1268
+ ]
1269
+ : [
1270
+ `${pipelineName}.${next.phase}`,
1271
+ lastTarget,
1272
+ // A standalone pass otherwise writes the `.opt` sibling (pipeline-33);
1273
+ // name the target the stopped run was writing so the resumed pass
1274
+ // lands where its downstream phases read.
1275
+ ...(passes.some((pass) => pass.name === next.phase)
1276
+ ? ['-o', stepTarget(next)]
1277
+ : []),
1278
+ ];
1279
+ return ['slc', ...operands.map(shellOperand)].join(' ');
1280
+ }
1281
+ // An operand the shell would not read back verbatim — a path with a space, say
1282
+ // — must be quoted, or the reported line is a different invocation than the one
1283
+ // it resumes (pipeline-9, pipeline-12).
1284
+ const VERBATIM_OPERAND = /^[A-Za-z0-9_@%+=:,./-]+$/;
1285
+ /** One resume operand, POSIX-quoted only where the shell would reinterpret it. */
1286
+ function shellOperand(value) {
1287
+ return VERBATIM_OPERAND.test(value)
1288
+ ? value
1289
+ : `'${value.replaceAll("'", `'\\''`)}'`;
974
1290
  }
975
1291
  function stepTarget(step) {
976
1292
  return step.request.kind === 'compile'