@sublang/slc 0.4.0 → 0.6.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 (120) hide show
  1. package/README.md +100 -131
  2. package/dist/app.d.ts +20 -20
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +37 -30
  5. package/dist/app.js.map +1 -1
  6. package/dist/artifacts.d.ts +6 -6
  7. package/dist/artifacts.d.ts.map +1 -1
  8. package/dist/artifacts.js +9 -9
  9. package/dist/artifacts.js.map +1 -1
  10. package/dist/cli.js +3 -3
  11. package/dist/cligent-agent.d.ts +1 -1
  12. package/dist/cligent-agent.js +5 -5
  13. package/dist/compiled-executor.d.ts +14 -6
  14. package/dist/compiled-executor.d.ts.map +1 -1
  15. package/dist/compiled-executor.js +133 -31
  16. package/dist/compiled-executor.js.map +1 -1
  17. package/dist/config-file.d.ts +10 -6
  18. package/dist/config-file.d.ts.map +1 -1
  19. package/dist/config-file.js +23 -10
  20. package/dist/config-file.js.map +1 -1
  21. package/dist/config.d.ts +22 -18
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +90 -27
  24. package/dist/config.js.map +1 -1
  25. package/dist/emitted-imports.d.ts +14 -0
  26. package/dist/emitted-imports.d.ts.map +1 -1
  27. package/dist/emitted-imports.js +63 -7
  28. package/dist/emitted-imports.js.map +1 -1
  29. package/dist/entry-module.d.ts +5 -3
  30. package/dist/entry-module.d.ts.map +1 -1
  31. package/dist/entry-module.js +41 -13
  32. package/dist/entry-module.js.map +1 -1
  33. package/dist/execution.d.ts +10 -10
  34. package/dist/execution.d.ts.map +1 -1
  35. package/dist/execution.js +13 -13
  36. package/dist/execution.js.map +1 -1
  37. package/dist/hash.js +2 -2
  38. package/dist/host-capabilities.d.ts +115 -0
  39. package/dist/host-capabilities.d.ts.map +1 -0
  40. package/dist/host-capabilities.js +507 -0
  41. package/dist/host-capabilities.js.map +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/interpreter.d.ts +2 -2
  47. package/dist/interpreter.d.ts.map +1 -1
  48. package/dist/interpreter.js +7 -7
  49. package/dist/invocation.d.ts +6 -6
  50. package/dist/invocation.d.ts.map +1 -1
  51. package/dist/invocation.js +1 -1
  52. package/dist/link.d.ts +4 -4
  53. package/dist/link.d.ts.map +1 -1
  54. package/dist/link.js +11 -11
  55. package/dist/link.js.map +1 -1
  56. package/dist/phase-runner.d.ts +4 -4
  57. package/dist/phase-runner.js +9 -9
  58. package/dist/phase.d.ts +4 -4
  59. package/dist/phase.d.ts.map +1 -1
  60. package/dist/phase.js +7 -7
  61. package/dist/phase.js.map +1 -1
  62. package/dist/pin-closure.d.ts +35 -7
  63. package/dist/pin-closure.d.ts.map +1 -1
  64. package/dist/pin-closure.js +135 -21
  65. package/dist/pin-closure.js.map +1 -1
  66. package/dist/pin-currency.d.ts +9 -4
  67. package/dist/pin-currency.d.ts.map +1 -1
  68. package/dist/pin-currency.js +57 -20
  69. package/dist/pin-currency.js.map +1 -1
  70. package/dist/pin-generate.d.ts +7 -7
  71. package/dist/pin-generate.d.ts.map +1 -1
  72. package/dist/pin-generate.js +14 -8
  73. package/dist/pin-generate.js.map +1 -1
  74. package/dist/pin-inputs.d.ts +41 -0
  75. package/dist/pin-inputs.d.ts.map +1 -0
  76. package/dist/pin-inputs.js +179 -0
  77. package/dist/pin-inputs.js.map +1 -0
  78. package/dist/pin-paths.d.ts +1 -1
  79. package/dist/pin-paths.js +4 -4
  80. package/dist/pin-paths.js.map +1 -1
  81. package/dist/pins.d.ts +4 -4
  82. package/dist/pins.d.ts.map +1 -1
  83. package/dist/pins.js +5 -5
  84. package/dist/pins.js.map +1 -1
  85. package/dist/pipeline.d.ts +5 -5
  86. package/dist/pipeline.js +10 -10
  87. package/dist/playbook-contract.d.ts +53 -5
  88. package/dist/playbook-contract.d.ts.map +1 -1
  89. package/dist/playbook-contract.js +70 -21
  90. package/dist/playbook-contract.js.map +1 -1
  91. package/dist/playbook-ports.d.ts +9 -9
  92. package/dist/playbook-ports.d.ts.map +1 -1
  93. package/dist/playbook-ports.js +6 -4
  94. package/dist/playbook-ports.js.map +1 -1
  95. package/dist/progress.d.ts +8 -9
  96. package/dist/progress.d.ts.map +1 -1
  97. package/dist/progress.js +3 -3
  98. package/dist/progress.js.map +1 -1
  99. package/dist/resolver.d.ts +9 -9
  100. package/dist/resolver.d.ts.map +1 -1
  101. package/dist/resolver.js +12 -12
  102. package/dist/resolver.js.map +1 -1
  103. package/dist/reviewing-agent.d.ts +18 -0
  104. package/dist/reviewing-agent.d.ts.map +1 -0
  105. package/dist/reviewing-agent.js +295 -0
  106. package/dist/reviewing-agent.js.map +1 -0
  107. package/dist/runner.d.ts +1 -1
  108. package/dist/runner.d.ts.map +1 -1
  109. package/dist/runner.js +195 -80
  110. package/dist/runner.js.map +1 -1
  111. package/dist/slc.config.template.yaml +6 -2
  112. package/dist/verify-coverage.d.ts +4 -3
  113. package/dist/verify-coverage.d.ts.map +1 -1
  114. package/dist/verify-coverage.js +509 -71
  115. package/dist/verify-coverage.js.map +1 -1
  116. package/dist/verify.d.ts +111 -15
  117. package/dist/verify.d.ts.map +1 -1
  118. package/dist/verify.js +656 -59
  119. package/dist/verify.js.map +1 -1
  120. package/package.json +3 -3
package/dist/runner.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * pipeline, computes artifact paths, then runs each phase through `runPhase`,
8
8
  * stopping at the first failure with its report. The resolver and executor are
9
9
  * injected so a host wires the real pipeline resolution and Cligent agent while
10
- * tests supply fakes. See specs/dev/pipeline.md and specs/dev/phase-execution.md.
10
+ * tests supply fakes. See specs/packages/pipeline.md and specs/packages/phase-execution.md.
11
11
  */
12
12
  import { mkdir, readFile, stat } from 'node:fs/promises';
13
13
  import { dirname, join, relative, resolve, sep } from 'node:path';
@@ -15,19 +15,21 @@ import { fileURLToPath } from 'node:url';
15
15
  import { artifactDir, planArtifacts, parseSource } from './artifacts.js';
16
16
  import { encodeLocator, invalidateBuildHistory, loadBuildHistory, recordBuild, verifiedInput, } from './build-history.js';
17
17
  import { emitEntryModule } from './entry-module.js';
18
- import { unresolvableRelativeImports } from './emitted-imports.js';
18
+ import { reconcileLinkObjectImportSpecifiers, unresolvableRelativeImports, } from './emitted-imports.js';
19
19
  import { messageOf } from './errors.js';
20
20
  import { assertSafeTarget, formatFailureReport, pathsAlias, runPhase, } from './execution.js';
21
- import { hashBytes, isHash } from './hash.js';
21
+ import { compareUtf8, hashBytes, isHash } from './hash.js';
22
22
  import { parseInvocation } from './invocation.js';
23
23
  import { unifiedLineDiff } from './line-diff.js';
24
24
  import { linkedArtifactPath, loadLinkFile } from './link.js';
25
- import { deriveClosure } from './pin-closure.js';
25
+ import { inspectClosureWithDeclaration, } from './pin-closure.js';
26
+ import { PIN_INPUTS_FILE } from './pin-inputs.js';
27
+ import { resolvePinPath } from './pin-paths.js';
26
28
  import { evaluatePin, evaluatePinFile, hashTree } from './pin-currency.js';
27
29
  import { PINS_FILE, PinError, loadPinFile, } from './pins.js';
28
30
  import { loadPipeline, resolvePipeline, } from './pipeline.js';
29
31
  import { defaultPlaybookLinkTarget, isReservedPipeline } from './resolver.js';
30
- import { emitFsmCoverageTest, emitFsmIntrospectionTest, emitGearsFsmConformanceTest, emitPromptContractTest, } from './verify.js';
32
+ import { emitFsmCoverageTest, emitFsmIntrospectionTest, emitGearsFsmConformanceTest, emitPromptContractTest, findMachineConfig, loadFsmModule, loadLinkedModuleForVerification, playbookProvenanceForLinkTarget, resolveArtifactSchemaForVerification, } from './verify.js';
31
33
  import { VERIFIER_SUPPORT_MODULE, emitVerifierSupport, verifierSupportFiles, } from './verify-support.js';
32
34
  /**
33
35
  * Parses argv and runs the requested pipeline, phase, or link end-to-end.
@@ -46,7 +48,7 @@ export async function runSlc(argv, deps) {
46
48
  switch (invocation.kind) {
47
49
  case 'full':
48
50
  // The reserved playbook pipeline supplies a default link target
49
- // (SELFHOST-13): a bare full run becomes a full-link against the
51
+ // (self-hosting-13): a bare full run becomes a full-link against the
50
52
  // installed @sublang/playbook runtime contract module (DR-014).
51
53
  if (invocation.pipeline === 'playbook') {
52
54
  return await runFullLink({
@@ -136,7 +138,7 @@ async function runSinglePhase(invocation, deps) {
136
138
  entry: pipeline.phases[0] === phase,
137
139
  });
138
140
  if (raw) {
139
- // A named phase cannot normalize (PIPE-37), so a raw entry source has no
141
+ // A named phase cannot normalize (pipeline-37), so a raw entry source has no
140
142
  // path into the phase's declared source format here.
141
143
  return failure(`source "${invocation.source}" is a raw input; run the full pipeline to normalize it`);
142
144
  }
@@ -207,7 +209,7 @@ async function runFullLink(invocation, deps) {
207
209
  const normalize = invocation.normalize || raw;
208
210
  const artDir = artifactDir(cwd, basename, invocation.pipeline);
209
211
  await mkdir(artDir, { recursive: true });
210
- // Compile chain: the exit artifact becomes the object artifact (PIPE-15).
212
+ // Compile chain: the exit artifact becomes the object artifact (pipeline-15).
211
213
  const plan = planArtifacts({ phases: pipeline.phases, basename, artDir });
212
214
  const compileSteps = buildCompileSteps({
213
215
  pipeline,
@@ -252,6 +254,7 @@ async function runFullLink(invocation, deps) {
252
254
  plan,
253
255
  artDir,
254
256
  basename,
257
+ linkTarget: resolve(cwd, invocation.linkTarget),
255
258
  };
256
259
  return executeSteps(steps, pipeline, deps, {
257
260
  incremental: incrementalRun(invocation, artDir, source),
@@ -270,7 +273,7 @@ async function runFullLink(invocation, deps) {
270
273
  ],
271
274
  };
272
275
  }
273
- // Entry-module emission (DR-014, SELFHOST-15): only the playbook
276
+ // Entry-module emission (DR-014, self-hosting-15): only the playbook
274
277
  // pipeline, only with the linked artifact at its canonical path.
275
278
  if (verified.ok && entryPath !== null && gearsPlan !== undefined) {
276
279
  await guardTarget(entryPath);
@@ -292,7 +295,7 @@ async function runFullLink(invocation, deps) {
292
295
  diagnostics: [
293
296
  ...verified.diagnostics,
294
297
  `entry module ${entryPath} has unresolvable relative imports: ` +
295
- `${missing.join(', ')} (VERIFY-18)`,
298
+ `${missing.join(', ')} (verification-18)`,
296
299
  ],
297
300
  };
298
301
  }
@@ -310,11 +313,11 @@ function runCwd(deps) {
310
313
  * After a reserved-pipeline full run produces a `gears` intermediate and an `fsm`
311
314
  * object at their canonical `<basename>.playbook/` locations, emits the
312
315
  * artifact-local checker support plus compilation-correctness tests beside
313
- * them as `slc` output, appending their paths to the outputs (VERIFY-2,
314
- * VERIFY-4;
316
+ * them as `slc` output, appending their paths to the outputs (verification-2,
317
+ * verification-4;
315
318
  * [DR-009](../decisions/009-slc-playbook-pipeline-compilation.md)).
316
319
  * Non-reserved pipelines, runs without a gears+fsm pair, and runs whose `fsm` was
317
- * relocated out of that directory by `-o` (PIPE-8) are left unchanged, so an
320
+ * relocated out of that directory by `-o` (pipeline-8) are left unchanged, so an
318
321
  * emitted test never imports a file that was not written beside it. A test whose
319
322
  * emission needs the produced `fsm` imported (the pinned introspection) degrades
320
323
  * to a diagnostic when the artifact cannot be loaded, leaving the run outcome
@@ -329,6 +332,11 @@ async function emitVerification(result, ctx, guardTarget) {
329
332
  const fsm = ctx.plan.find((artifact) => artifact.phase.target.format === 'fsm');
330
333
  if (fsm === undefined)
331
334
  throw new Error('verification plan lost its FSM');
335
+ const provenance = ctx.linkTarget === undefined
336
+ ? undefined
337
+ : await playbookProvenanceForLinkTarget(ctx.linkTarget);
338
+ const schemaResolution = await resolveVerificationSchema(ctx, provenance);
339
+ const artifactSchema = schemaResolution.artifactSchema;
332
340
  const outputs = [...result.outputs];
333
341
  const diagnostics = [...result.diagnostics];
334
342
  if (guardTarget !== undefined) {
@@ -344,6 +352,8 @@ async function emitVerification(result, ctx, guardTarget) {
344
352
  artifactDir: ctx.artDir,
345
353
  basename: ctx.basename,
346
354
  verifyModule: VERIFIER_SUPPORT_MODULE,
355
+ ...(artifactSchema === undefined ? {} : { artifactSchema }),
356
+ schemaFindings: schemaResolution.findings,
347
357
  }));
348
358
  try {
349
359
  if (guardTarget !== undefined) {
@@ -366,6 +376,8 @@ async function emitVerification(result, ctx, guardTarget) {
366
376
  artifactDir: ctx.artDir,
367
377
  basename: ctx.basename,
368
378
  verifyModule: VERIFIER_SUPPORT_MODULE,
379
+ ...(artifactSchema === undefined ? {} : { artifactSchema }),
380
+ ...(provenance === undefined ? {} : { provenance }),
369
381
  });
370
382
  outputs.push(promptContract.path);
371
383
  diagnostics.push(...promptContract.diagnostics.map((diagnostic) => `verification: ${diagnostic}`));
@@ -390,6 +402,34 @@ async function emitVerification(result, ctx, guardTarget) {
390
402
  }
391
403
  return { ...result, outputs, diagnostics };
392
404
  }
405
+ /** Reconciles every schema witness available from one just-built artifact. */
406
+ async function resolveVerificationSchema(ctx, provenance) {
407
+ const fsmPath = join(ctx.artDir, `${ctx.basename}.fsm.ts`);
408
+ let config;
409
+ try {
410
+ config = findMachineConfig(await loadFsmModule(fsmPath));
411
+ }
412
+ catch {
413
+ // Broken FSM loading already degrades the generated derivation checks to
414
+ // diagnostics; reviewed provenance remains usable conformance evidence.
415
+ }
416
+ let linked;
417
+ try {
418
+ linked = (await loadLinkedModuleForVerification({
419
+ linkedPath: join(ctx.artDir, `${ctx.basename}.playbook.ts`),
420
+ fsmPath,
421
+ }));
422
+ }
423
+ catch {
424
+ // A missing module supplies no signal; prompt emission reports an
425
+ // unloadable module through its existing diagnostic.
426
+ }
427
+ return resolveArtifactSchemaForVerification({
428
+ ...(provenance === undefined ? {} : { provenance }),
429
+ ...(config === undefined ? {} : { config }),
430
+ ...(linked === undefined ? {} : { linked }),
431
+ });
432
+ }
393
433
  /** Exact deterministic writes attempted by {@link emitVerification}. */
394
434
  function verificationHostTargets(ctx) {
395
435
  if (!isReservedPipeline(ctx.pipeline))
@@ -493,15 +533,35 @@ function compileStep(pipeline, phase, source, target) {
493
533
  }
494
534
  /**
495
535
  * Runs steps in order, selecting interpreted or compiled execution per phase from
496
- * the pin index and stopping at the first failure with its report (PHEXEC-9,
497
- * PHEXEC-27). An unparseable pin file fails the run closed before any phase.
536
+ * the pin index and stopping at the first failure with its report (phase-execution-9,
537
+ * phase-execution-27). An unparseable pin file fails the run closed before any phase.
498
538
  */
499
539
  async function executeSteps(steps, pipeline, deps, opts = {}) {
500
540
  const complete = opts.complete ?? (async (result) => result);
501
- const pinInputs = new Set([join(pipeline.dir, PINS_FILE)]);
541
+ const pinInputs = new Set([
542
+ join(pipeline.dir, PINS_FILE),
543
+ join(pipeline.dir, PIN_INPUTS_FILE),
544
+ ]);
502
545
  let pinFile;
503
546
  try {
504
547
  pinFile = (await loadPinFile(pipeline.dir)).file;
548
+ if (pinFile !== undefined) {
549
+ const verdicts = await evaluatePinFile(pipeline.dir, pinFile, {
550
+ observePath: (path) => pinInputs.add(resolve(path)),
551
+ });
552
+ const malformed = Object.entries(verdicts).find(([, verdict]) => verdict.status === 'malformed');
553
+ if (malformed !== undefined) {
554
+ return {
555
+ ok: false,
556
+ outputs: [],
557
+ diagnostics: [
558
+ `pin is malformed: ${malformed[1].status === 'malformed'
559
+ ? malformed[1].reason
560
+ : 'invalid pin index'}`,
561
+ ],
562
+ };
563
+ }
564
+ }
505
565
  }
506
566
  catch (error) {
507
567
  if (error instanceof PinError) {
@@ -509,23 +569,6 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
509
569
  }
510
570
  throw error;
511
571
  }
512
- if (pinFile !== undefined) {
513
- const verdicts = await evaluatePinFile(pipeline.dir, pinFile, {
514
- observePath: (path) => pinInputs.add(resolve(path)),
515
- });
516
- const malformed = Object.entries(verdicts).find(([, verdict]) => verdict.status === 'malformed');
517
- if (malformed !== undefined) {
518
- return {
519
- ok: false,
520
- outputs: [],
521
- diagnostics: [
522
- `pin is malformed: ${malformed[1].status === 'malformed'
523
- ? malformed[1].reason
524
- : 'invalid pin index'}`,
525
- ],
526
- };
527
- }
528
- }
529
572
  const definitions = [
530
573
  ...new Set([
531
574
  ...chainDefinitions(pipeline),
@@ -586,7 +629,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
586
629
  const stepDeclared = declaredByStep[index];
587
630
  const identity = state === undefined
588
631
  ? null
589
- : await stepIdentity(step, state, index, stepDeclared.complete ? stepDeclared.paths : null);
632
+ : await stepIdentity(step, state, index, stepDeclared.complete ? stepDeclared : null);
590
633
  const mode = state === undefined
591
634
  ? { mode: 'ordinary' }
592
635
  : await selectStepMode(step, index, target, identity, state);
@@ -598,7 +641,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
598
641
  }
599
642
  let reasons;
600
643
  try {
601
- reasons = await reusePinFailure(step.pinKey, pipeline.dir, pinFile);
644
+ reasons = await reusePinFailure(step, pipeline.dir, pinFile);
602
645
  }
603
646
  catch (error) {
604
647
  reasons = [messageOf(error)];
@@ -627,7 +670,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
627
670
  });
628
671
  continue;
629
672
  }
630
- // In-run progress (DR-019, CLI-32): announce the phase, then report its
673
+ // In-run progress (DR-019, cli-32): announce the phase, then report its
631
674
  // outcome with the elapsed time.
632
675
  const startedAt = Date.now();
633
676
  deps.progress?.({ kind: 'phase-start', phase: step.phase, target });
@@ -639,13 +682,13 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
639
682
  });
640
683
  // Selecting a compiled executor can throw rather than return a verdict —
641
684
  // notably an unmapped pinned Playbook provenance, which the host factory
642
- // rejects (PHEXEC-30). That is the same fail-closed family as a stale pin,
685
+ // rejects (phase-execution-30). That is the same fail-closed family as a stale pin,
643
686
  // so it is reported through the phase-failure path; letting it unwind
644
687
  // would strand the phase-start line with no terminal event and drop the
645
- // phase and target from the report (CLI-4, CLI-32, PHEXEC-27).
688
+ // phase and target from the report (cli-4, cli-32, phase-execution-27).
646
689
  let selection;
647
690
  try {
648
- selection = await selectExecutor(step.phase, step.pinKey, pipeline.dir, pinFile, deps);
691
+ selection = await selectExecutor(step, pipeline.dir, pinFile, deps);
649
692
  }
650
693
  catch (error) {
651
694
  selection = { kind: 'fail', reasons: [messageOf(error)] };
@@ -695,30 +738,35 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
695
738
  return { ok: false, outputs, diagnostics };
696
739
  }
697
740
  diagnostics.push(...result.diagnostics);
698
- // A linked module that cannot resolve its own relative imports cannot
699
- // load under `playbook run`; fail the link rather than report success
700
- // for a dead artifact (VERIFY-18).
741
+ // Settle an agent-chosen `.js`/`.ts` link-object edge from the sibling that
742
+ // currently exists (pipeline-40), then keep rejecting every genuinely
743
+ // unresolved import (verification-18).
701
744
  if (step.phase === 'link' &&
702
745
  (step.targetExt === '.ts' || step.targetExt === '.js')) {
703
746
  let missing;
704
747
  try {
705
- missing = await unresolvableRelativeImports(step.request.kind === 'link' ? step.request.linked : target);
748
+ const linked = step.request.kind === 'link' ? step.request.linked : target;
749
+ const rewrites = await reconcileLinkObjectImportSpecifiers(linked, step.request.kind === 'link' ? step.request.objects : []);
750
+ diagnostics.push(...rewrites.map(({ from, to }) => `linked module ${linked} reconciled link-object import ` +
751
+ `${JSON.stringify(from)} to ${JSON.stringify(to)} (pipeline-40)`));
752
+ missing = await unresolvableRelativeImports(linked);
706
753
  }
707
754
  catch (error) {
708
- // A target that cannot even be read is a dead artifact too — a
709
- // directory at the linked path passes the DR-003 existence and
710
- // extension checks and then fails the read. Fail the link here so
711
- // every started phase still reaches a terminal event (CLI-32).
755
+ // A target that cannot be reconciled and checked is a dead artifact
756
+ // too. Fail the link here so every started phase still reaches a
757
+ // terminal event (cli-32).
712
758
  fail();
713
- diagnostics.push(`linked module ${target} could not be read: ${messageOf(error)} ` +
714
- 'an emitted module that cannot load fails the link (VERIFY-18)');
759
+ diagnostics.push(`linked module ${target} could not be settled and checked: ` +
760
+ `${messageOf(error)} ` +
761
+ 'post-link completion and load integrity are mandatory ' +
762
+ '(pipeline-40, verification-18)');
715
763
  return { ok: false, outputs, diagnostics };
716
764
  }
717
765
  if (missing.length > 0) {
718
766
  fail();
719
767
  diagnostics.push(`linked module ${target} has unresolvable relative imports: ` +
720
768
  `${missing.join(', ')} — an emitted module that cannot load ` +
721
- 'fails the link (VERIFY-18)');
769
+ 'fails the link (verification-18)');
722
770
  return { ok: false, outputs, diagnostics };
723
771
  }
724
772
  }
@@ -758,7 +806,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
758
806
  ? { outcome: 'up-to-date' }
759
807
  : {}),
760
808
  };
761
- const completed = await complete(selected, guardCompletionTarget);
809
+ const completed = await complete(selected, guardCompletionTarget, pinFile);
762
810
  return state === undefined
763
811
  ? completed
764
812
  : publishIncremental(completed, state, steps);
@@ -832,7 +880,8 @@ async function stepIdentity(step, state, index, declared) {
832
880
  inputs: [
833
881
  hashBytes(chainedInput),
834
882
  ...rootHashes,
835
- ...(await Promise.all(declared.map(async (path) => hashBytes(await readFile(path))))),
883
+ ...declared.declarationHashes,
884
+ ...(await Promise.all(declared.paths.map(async (path) => hashBytes(await readFile(path))))),
836
885
  ],
837
886
  };
838
887
  }
@@ -842,7 +891,8 @@ async function stepIdentity(step, state, index, declared) {
842
891
  inputs.push(framedHash('object', encodeLocator(state.artDir, path), hashBytes(await readFile(path))));
843
892
  }
844
893
  inputs.push(hashBytes(await readFile(step.request.definitionPath)));
845
- inputs.push(...(await Promise.all(declared.map(async (path) => hashBytes(await readFile(path))))));
894
+ inputs.push(...declared.declarationHashes);
895
+ inputs.push(...(await Promise.all(declared.paths.map(async (path) => hashBytes(await readFile(path))))));
846
896
  const linkTarget = resolve(step.request.linkTarget);
847
897
  const targetInfo = await stat(linkTarget);
848
898
  let targetKind;
@@ -872,36 +922,73 @@ async function stepIdentity(step, state, index, declared) {
872
922
  }
873
923
  }
874
924
  async function stepDeclaredInputs(step, pipeline, pinFile) {
875
- const roots = step.request.kind === 'compile'
876
- ? [step.request.definitionPath, ...(step.request.references ?? [])]
877
- : [step.request.definitionPath];
925
+ const roots = [
926
+ { path: step.request.definitionPath, phase: step.pinKey },
927
+ ];
928
+ if (step.request.kind === 'compile') {
929
+ for (const reference of step.request.references ?? []) {
930
+ roots.push({ path: reference });
931
+ }
932
+ }
878
933
  return declaredInputPaths(pipeline, pinFile, roots);
879
934
  }
880
935
  /** Current local semantic-input closure for definitions and references. */
881
936
  async function declaredInputPaths(pipeline, pinFile, roots) {
882
937
  const boundary = pinFile?.pathBoundary.path ?? '.';
883
- const base = new Set(roots.map((path) => resolve(path)));
938
+ const base = new Set(roots.map((root) => resolve(root.path)));
884
939
  const seen = new Set(base);
885
940
  const inputs = [];
941
+ const declarationHashes = [];
886
942
  let complete = true;
887
943
  const normalize = resolve(normalizeDefinitionPath());
888
944
  for (const root of roots) {
889
- const absolute = resolve(root);
945
+ const absolute = resolve(root.path);
890
946
  if (!absolute.toLowerCase().endsWith('.md') || absolute === normalize) {
891
947
  continue;
892
948
  }
893
949
  const locator = relative(pipeline.dir, absolute).split(sep).join('/');
894
- const closure = new Set();
950
+ let closure;
895
951
  try {
896
- await deriveClosure(pipeline.dir, boundary, locator, (path) => closure.add(path));
952
+ closure = await inspectClosureWithDeclaration(pipeline.dir, boundary, locator, root.phase);
897
953
  }
898
954
  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.
955
+ // An unexpected inspection failure still degrades runner-only identity
956
+ // to Ordinary execution; structured PinError outcomes retain safe paths.
902
957
  complete = false;
958
+ continue;
903
959
  }
904
- for (const path of closure) {
960
+ if (!closure.complete) {
961
+ // An incomplete closure is unusable for identity, but every independently
962
+ // resolved in-boundary member remains protected from output aliasing.
963
+ complete = false;
964
+ for (const path of closure.paths) {
965
+ const absolutePath = resolve(path);
966
+ if (seen.has(absolutePath))
967
+ continue;
968
+ seen.add(absolutePath);
969
+ inputs.push(absolutePath);
970
+ }
971
+ continue;
972
+ }
973
+ if (closure.declaration === 'sidecar' && root.phase !== undefined) {
974
+ const members = [...closure.paths]
975
+ .map((path) => resolve(path))
976
+ .filter((path) => path !== absolute)
977
+ .map((path) => ({
978
+ path,
979
+ locator: relative(pipeline.dir, path).split(sep).join('/'),
980
+ }))
981
+ .sort((left, right) => compareUtf8(left.locator, right.locator));
982
+ declarationHashes.push(framedHash('sidecar-closure-v1', root.phase, ...members.map((member) => member.locator)));
983
+ for (const member of members) {
984
+ if (seen.has(member.path))
985
+ continue;
986
+ seen.add(member.path);
987
+ inputs.push(member.path);
988
+ }
989
+ continue;
990
+ }
991
+ for (const path of closure.paths) {
905
992
  const absolutePath = resolve(path);
906
993
  if (seen.has(absolutePath))
907
994
  continue;
@@ -909,7 +996,7 @@ async function declaredInputPaths(pipeline, pinFile, roots) {
909
996
  inputs.push(absolutePath);
910
997
  }
911
998
  }
912
- return { paths: inputs, complete };
999
+ return { paths: inputs, declarationHashes, complete };
913
1000
  }
914
1001
  async function selectStepMode(step, index, target, identity, state) {
915
1002
  if (state.rebuild || state.history === null || identity === null) {
@@ -960,15 +1047,33 @@ function sameHashes(left, right) {
960
1047
  return (left.length === right.length &&
961
1048
  left.every((hash, index) => hash === right[index]));
962
1049
  }
963
- async function reusePinFailure(pinKey, pipelineDir, pinFile) {
964
- const record = ownPin(pinFile, pinKey);
965
- if (pinFile === undefined || pinKey === undefined || record === undefined) {
1050
+ async function reusePinFailure(step, pipelineDir, pinFile) {
1051
+ const record = ownPin(pinFile, step.pinKey);
1052
+ if (pinFile === undefined ||
1053
+ step.pinKey === undefined ||
1054
+ record === undefined) {
966
1055
  return null;
967
1056
  }
968
- const verdict = await evaluatePin(pipelineDir, pinFile, pinKey, record);
969
- return verdict.status === 'current'
970
- ? null
971
- : [`pin is ${verdict.status}: ${verdict.reason}`];
1057
+ const verdict = await evaluatePin(pipelineDir, pinFile, step.pinKey, record);
1058
+ if (verdict.status !== 'current') {
1059
+ return [`pin is ${verdict.status}: ${verdict.reason}`];
1060
+ }
1061
+ const definitionIssue = await currentPinDefinitionIssue(step, pipelineDir, pinFile, record);
1062
+ return definitionIssue === null ? null : [definitionIssue];
1063
+ }
1064
+ /**
1065
+ * Keeps external-definition pinning dormant until pipeline discovery selects
1066
+ * the same physical definition (phase-execution-27; DR-026).
1067
+ */
1068
+ async function currentPinDefinitionIssue(step, pipelineDir, pinFile, record) {
1069
+ const recorded = resolvePinPath(pipelineDir, pinFile.pathBoundary.path, record.definition.path, 'definition');
1070
+ const selected = resolve(step.request.definitionPath);
1071
+ if (await pathsAlias(recorded, selected))
1072
+ return null;
1073
+ const selectedLocator = relative(pipelineDir, selected).split(sep).join('/');
1074
+ return (`pin-recorded definition "${record.definition.path}" does not match ` +
1075
+ `selected pipeline definition "${selectedLocator}"; ` +
1076
+ 'external-definition execution remains disabled until pipeline resolution selects the same physical definition');
972
1077
  }
973
1078
  /**
974
1079
  * Removes any complete snapshot that could otherwise bless this target after
@@ -1051,29 +1156,39 @@ function historyDiagnostic(result, reason) {
1051
1156
  };
1052
1157
  }
1053
1158
  /**
1054
- * Selects a phase's execution strategy from the pin index (PHEXEC-27; DR-005,
1159
+ * Selects a phase's execution strategy from the pin index (phase-execution-27; DR-005,
1055
1160
  * DR-007): a phase with no pin interprets, a current pin runs its compiled
1056
1161
  * artifact, and a stale or malformed pin fails closed and is never silently
1057
1162
  * interpreted.
1058
1163
  */
1059
- async function selectExecutor(phase, pinKey, pipelineDir, pinFile, deps) {
1060
- const record = ownPin(pinFile, pinKey);
1061
- if (pinFile === undefined || pinKey === undefined || record === undefined) {
1164
+ async function selectExecutor(step, pipelineDir, pinFile, deps) {
1165
+ const record = ownPin(pinFile, step.pinKey);
1166
+ if (pinFile === undefined ||
1167
+ step.pinKey === undefined ||
1168
+ record === undefined) {
1062
1169
  return { kind: 'run', executor: deps.executor };
1063
1170
  }
1064
- const verdict = await evaluatePin(pipelineDir, pinFile, pinKey, record);
1171
+ const verdict = await evaluatePin(pipelineDir, pinFile, step.pinKey, record);
1065
1172
  if (verdict.status === 'current') {
1173
+ const definitionIssue = await currentPinDefinitionIssue(step, pipelineDir, pinFile, record);
1174
+ if (definitionIssue !== null) {
1175
+ return { kind: 'fail', reasons: [definitionIssue] };
1176
+ }
1066
1177
  if (deps.compiled === undefined) {
1067
1178
  return {
1068
1179
  kind: 'fail',
1069
1180
  reasons: [
1070
- `phase "${phase}" is pinned to a compiled artifact, but this host has no compiled executor configured`,
1181
+ `phase "${step.phase}" is pinned to a compiled artifact, but this host has no compiled executor configured`,
1071
1182
  ],
1072
1183
  };
1073
1184
  }
1074
1185
  return {
1075
1186
  kind: 'run',
1076
- executor: deps.compiled({ phase: pinKey, pipelineDir, record }),
1187
+ executor: deps.compiled({
1188
+ phase: step.pinKey,
1189
+ pipelineDir,
1190
+ record,
1191
+ }),
1077
1192
  };
1078
1193
  }
1079
1194
  return {
@@ -1103,7 +1218,7 @@ async function requireLink(pipeline, reference) {
1103
1218
  throw new Error(`pipeline "${reference}" has no link phase`);
1104
1219
  }
1105
1220
  // `loadLinkFile` relaxes the ## Link Targets requirement intrinsically for
1106
- // Playbook's `playbook` linked format (DR-002, DR-009, PIPE-11), so the
1221
+ // Playbook's `playbook` linked format (DR-002, DR-009, pipeline-11), so the
1107
1222
  // requirement does not depend on how the pipeline reference resolved.
1108
1223
  return loadLinkFile(pipeline.linkFile);
1109
1224
  }