@sublang/slc 0.5.0 → 0.7.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 +10 -5
  2. package/dist/app.d.ts +20 -20
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +44 -31
  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 +9 -2
  12. package/dist/cligent-agent.d.ts.map +1 -1
  13. package/dist/cligent-agent.js +6 -5
  14. package/dist/cligent-agent.js.map +1 -1
  15. package/dist/compiled-execution.d.ts +44 -0
  16. package/dist/compiled-execution.d.ts.map +1 -0
  17. package/dist/compiled-execution.js +184 -0
  18. package/dist/compiled-execution.js.map +1 -0
  19. package/dist/compiled-executor.d.ts +16 -6
  20. package/dist/compiled-executor.d.ts.map +1 -1
  21. package/dist/compiled-executor.js +182 -35
  22. package/dist/compiled-executor.js.map +1 -1
  23. package/dist/config-file.d.ts +9 -6
  24. package/dist/config-file.d.ts.map +1 -1
  25. package/dist/config-file.js +27 -9
  26. package/dist/config-file.js.map +1 -1
  27. package/dist/config.d.ts +77 -24
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +128 -52
  30. package/dist/config.js.map +1 -1
  31. package/dist/emitted-imports.d.ts +14 -0
  32. package/dist/emitted-imports.d.ts.map +1 -1
  33. package/dist/emitted-imports.js +63 -7
  34. package/dist/emitted-imports.js.map +1 -1
  35. package/dist/entry-module.d.ts +5 -3
  36. package/dist/entry-module.d.ts.map +1 -1
  37. package/dist/entry-module.js +41 -13
  38. package/dist/entry-module.js.map +1 -1
  39. package/dist/execution.d.ts +10 -10
  40. package/dist/execution.d.ts.map +1 -1
  41. package/dist/execution.js +13 -13
  42. package/dist/execution.js.map +1 -1
  43. package/dist/hash.js +2 -2
  44. package/dist/interpreter.d.ts +2 -2
  45. package/dist/interpreter.d.ts.map +1 -1
  46. package/dist/interpreter.js +7 -7
  47. package/dist/invocation.d.ts +6 -6
  48. package/dist/invocation.d.ts.map +1 -1
  49. package/dist/invocation.js +1 -1
  50. package/dist/link.d.ts +4 -4
  51. package/dist/link.d.ts.map +1 -1
  52. package/dist/link.js +11 -11
  53. package/dist/link.js.map +1 -1
  54. package/dist/phase-runner.d.ts +13 -6
  55. package/dist/phase-runner.d.ts.map +1 -1
  56. package/dist/phase-runner.js +12 -10
  57. package/dist/phase-runner.js.map +1 -1
  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 +24 -10
  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 +49 -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 +3 -3
  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/runner.d.ts +2 -2
  104. package/dist/runner.d.ts.map +1 -1
  105. package/dist/runner.js +218 -81
  106. package/dist/runner.js.map +1 -1
  107. package/dist/runtime-contract.d.ts +17 -0
  108. package/dist/runtime-contract.d.ts.map +1 -0
  109. package/dist/runtime-contract.js +114 -0
  110. package/dist/runtime-contract.js.map +1 -0
  111. package/dist/slc.config.template.yaml +7 -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 +153 -15
  117. package/dist/verify.d.ts.map +1 -1
  118. package/dist/verify.js +707 -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,22 @@ 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 { readPlaybookRuntimeDeclaration, } from './runtime-contract.js';
33
+ import { artifactSchemaForPlaybookProvenance, emitFsmCoverageTest, emitFsmIntrospectionTest, emitGearsFsmConformanceTest, emitPromptContractTest, findMachineConfig, loadFsmModule, loadLinkedModuleForVerification, playbookProvenanceForLinkTarget, resolveArtifactSchemaForVerification, } from './verify.js';
31
34
  import { VERIFIER_SUPPORT_MODULE, emitVerifierSupport, verifierSupportFiles, } from './verify-support.js';
32
35
  /**
33
36
  * Parses argv and runs the requested pipeline, phase, or link end-to-end.
@@ -46,7 +49,7 @@ export async function runSlc(argv, deps) {
46
49
  switch (invocation.kind) {
47
50
  case 'full':
48
51
  // The reserved playbook pipeline supplies a default link target
49
- // (SELFHOST-13): a bare full run becomes a full-link against the
52
+ // (self-hosting-13): a bare full run becomes a full-link against the
50
53
  // installed @sublang/playbook runtime contract module (DR-014).
51
54
  if (invocation.pipeline === 'playbook') {
52
55
  return await runFullLink({
@@ -136,7 +139,7 @@ async function runSinglePhase(invocation, deps) {
136
139
  entry: pipeline.phases[0] === phase,
137
140
  });
138
141
  if (raw) {
139
- // A named phase cannot normalize (PIPE-37), so a raw entry source has no
142
+ // A named phase cannot normalize (pipeline-37), so a raw entry source has no
140
143
  // path into the phase's declared source format here.
141
144
  return failure(`source "${invocation.source}" is a raw input; run the full pipeline to normalize it`);
142
145
  }
@@ -207,7 +210,7 @@ async function runFullLink(invocation, deps) {
207
210
  const normalize = invocation.normalize || raw;
208
211
  const artDir = artifactDir(cwd, basename, invocation.pipeline);
209
212
  await mkdir(artDir, { recursive: true });
210
- // Compile chain: the exit artifact becomes the object artifact (PIPE-15).
213
+ // Compile chain: the exit artifact becomes the object artifact (pipeline-15).
211
214
  const plan = planArtifacts({ phases: pipeline.phases, basename, artDir });
212
215
  const compileSteps = buildCompileSteps({
213
216
  pipeline,
@@ -252,6 +255,7 @@ async function runFullLink(invocation, deps) {
252
255
  plan,
253
256
  artDir,
254
257
  basename,
258
+ linkTarget: resolve(cwd, invocation.linkTarget),
255
259
  };
256
260
  return executeSteps(steps, pipeline, deps, {
257
261
  incremental: incrementalRun(invocation, artDir, source),
@@ -270,7 +274,7 @@ async function runFullLink(invocation, deps) {
270
274
  ],
271
275
  };
272
276
  }
273
- // Entry-module emission (DR-014, SELFHOST-15): only the playbook
277
+ // Entry-module emission (DR-014, self-hosting-15): only the playbook
274
278
  // pipeline, only with the linked artifact at its canonical path.
275
279
  if (verified.ok && entryPath !== null && gearsPlan !== undefined) {
276
280
  await guardTarget(entryPath);
@@ -292,7 +296,7 @@ async function runFullLink(invocation, deps) {
292
296
  diagnostics: [
293
297
  ...verified.diagnostics,
294
298
  `entry module ${entryPath} has unresolvable relative imports: ` +
295
- `${missing.join(', ')} (VERIFY-18)`,
299
+ `${missing.join(', ')} (verification-18)`,
296
300
  ],
297
301
  };
298
302
  }
@@ -310,11 +314,11 @@ function runCwd(deps) {
310
314
  * After a reserved-pipeline full run produces a `gears` intermediate and an `fsm`
311
315
  * object at their canonical `<basename>.playbook/` locations, emits the
312
316
  * 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;
317
+ * them as `slc` output, appending their paths to the outputs (verification-2,
318
+ * verification-4;
315
319
  * [DR-009](../decisions/009-slc-playbook-pipeline-compilation.md)).
316
320
  * 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
321
+ * relocated out of that directory by `-o` (pipeline-8) are left unchanged, so an
318
322
  * emitted test never imports a file that was not written beside it. A test whose
319
323
  * emission needs the produced `fsm` imported (the pinned introspection) degrades
320
324
  * to a diagnostic when the artifact cannot be loaded, leaving the run outcome
@@ -329,6 +333,22 @@ async function emitVerification(result, ctx, guardTarget) {
329
333
  const fsm = ctx.plan.find((artifact) => artifact.phase.target.format === 'fsm');
330
334
  if (fsm === undefined)
331
335
  throw new Error('verification plan lost its FSM');
336
+ const provenance = ctx.linkTarget === undefined
337
+ ? undefined
338
+ : await playbookProvenanceForLinkTarget(ctx.linkTarget);
339
+ // A provenance outside the exact historical map is schema evidence only
340
+ // through the installed engine's own declaration (DR-028); an unreadable
341
+ // engine simply supplies no signal, and the emission reports the gap.
342
+ const runtimeDeclaration = ctx.linkTarget === undefined ||
343
+ artifactSchemaForPlaybookProvenance(provenance) !== undefined
344
+ ? undefined
345
+ : await linkTargetRuntimeDeclaration(ctx.linkTarget);
346
+ const evidence = {
347
+ ...(provenance === undefined ? {} : { provenance }),
348
+ ...(runtimeDeclaration === undefined ? {} : { runtimeDeclaration }),
349
+ };
350
+ const schemaResolution = await resolveVerificationSchema(ctx, evidence);
351
+ const artifactSchema = schemaResolution.artifactSchema;
332
352
  const outputs = [...result.outputs];
333
353
  const diagnostics = [...result.diagnostics];
334
354
  if (guardTarget !== undefined) {
@@ -344,6 +364,8 @@ async function emitVerification(result, ctx, guardTarget) {
344
364
  artifactDir: ctx.artDir,
345
365
  basename: ctx.basename,
346
366
  verifyModule: VERIFIER_SUPPORT_MODULE,
367
+ ...(artifactSchema === undefined ? {} : { artifactSchema }),
368
+ schemaFindings: schemaResolution.findings,
347
369
  }));
348
370
  try {
349
371
  if (guardTarget !== undefined) {
@@ -366,6 +388,8 @@ async function emitVerification(result, ctx, guardTarget) {
366
388
  artifactDir: ctx.artDir,
367
389
  basename: ctx.basename,
368
390
  verifyModule: VERIFIER_SUPPORT_MODULE,
391
+ ...(artifactSchema === undefined ? {} : { artifactSchema }),
392
+ ...evidence,
369
393
  });
370
394
  outputs.push(promptContract.path);
371
395
  diagnostics.push(...promptContract.diagnostics.map((diagnostic) => `verification: ${diagnostic}`));
@@ -390,6 +414,43 @@ async function emitVerification(result, ctx, guardTarget) {
390
414
  }
391
415
  return { ...result, outputs, diagnostics };
392
416
  }
417
+ /** Reads the link target's engine declaration; unreadable is no evidence. */
418
+ async function linkTargetRuntimeDeclaration(linkTarget) {
419
+ try {
420
+ return await readPlaybookRuntimeDeclaration(linkTarget);
421
+ }
422
+ catch {
423
+ return undefined;
424
+ }
425
+ }
426
+ /** Reconciles every schema witness available from one just-built artifact. */
427
+ async function resolveVerificationSchema(ctx, evidence) {
428
+ const fsmPath = join(ctx.artDir, `${ctx.basename}.fsm.ts`);
429
+ let config;
430
+ try {
431
+ config = findMachineConfig(await loadFsmModule(fsmPath));
432
+ }
433
+ catch {
434
+ // Broken FSM loading already degrades the generated derivation checks to
435
+ // diagnostics; reviewed provenance remains usable conformance evidence.
436
+ }
437
+ let linked;
438
+ try {
439
+ linked = (await loadLinkedModuleForVerification({
440
+ linkedPath: join(ctx.artDir, `${ctx.basename}.playbook.ts`),
441
+ fsmPath,
442
+ }));
443
+ }
444
+ catch {
445
+ // A missing module supplies no signal; prompt emission reports an
446
+ // unloadable module through its existing diagnostic.
447
+ }
448
+ return resolveArtifactSchemaForVerification({
449
+ ...evidence,
450
+ ...(config === undefined ? {} : { config }),
451
+ ...(linked === undefined ? {} : { linked }),
452
+ });
453
+ }
393
454
  /** Exact deterministic writes attempted by {@link emitVerification}. */
394
455
  function verificationHostTargets(ctx) {
395
456
  if (!isReservedPipeline(ctx.pipeline))
@@ -493,15 +554,35 @@ function compileStep(pipeline, phase, source, target) {
493
554
  }
494
555
  /**
495
556
  * 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.
557
+ * the pin index and stopping at the first failure with its report (phase-execution-9,
558
+ * phase-execution-27). An unparseable pin file fails the run closed before any phase.
498
559
  */
499
560
  async function executeSteps(steps, pipeline, deps, opts = {}) {
500
561
  const complete = opts.complete ?? (async (result) => result);
501
- const pinInputs = new Set([join(pipeline.dir, PINS_FILE)]);
562
+ const pinInputs = new Set([
563
+ join(pipeline.dir, PINS_FILE),
564
+ join(pipeline.dir, PIN_INPUTS_FILE),
565
+ ]);
502
566
  let pinFile;
503
567
  try {
504
568
  pinFile = (await loadPinFile(pipeline.dir)).file;
569
+ if (pinFile !== undefined) {
570
+ const verdicts = await evaluatePinFile(pipeline.dir, pinFile, {
571
+ observePath: (path) => pinInputs.add(resolve(path)),
572
+ });
573
+ const malformed = Object.entries(verdicts).find(([, verdict]) => verdict.status === 'malformed');
574
+ if (malformed !== undefined) {
575
+ return {
576
+ ok: false,
577
+ outputs: [],
578
+ diagnostics: [
579
+ `pin is malformed: ${malformed[1].status === 'malformed'
580
+ ? malformed[1].reason
581
+ : 'invalid pin index'}`,
582
+ ],
583
+ };
584
+ }
585
+ }
505
586
  }
506
587
  catch (error) {
507
588
  if (error instanceof PinError) {
@@ -509,23 +590,6 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
509
590
  }
510
591
  throw error;
511
592
  }
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
593
  const definitions = [
530
594
  ...new Set([
531
595
  ...chainDefinitions(pipeline),
@@ -586,7 +650,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
586
650
  const stepDeclared = declaredByStep[index];
587
651
  const identity = state === undefined
588
652
  ? null
589
- : await stepIdentity(step, state, index, stepDeclared.complete ? stepDeclared.paths : null);
653
+ : await stepIdentity(step, state, index, stepDeclared.complete ? stepDeclared : null);
590
654
  const mode = state === undefined
591
655
  ? { mode: 'ordinary' }
592
656
  : await selectStepMode(step, index, target, identity, state);
@@ -598,7 +662,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
598
662
  }
599
663
  let reasons;
600
664
  try {
601
- reasons = await reusePinFailure(step.pinKey, pipeline.dir, pinFile);
665
+ reasons = await reusePinFailure(step, pipeline.dir, pinFile);
602
666
  }
603
667
  catch (error) {
604
668
  reasons = [messageOf(error)];
@@ -627,7 +691,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
627
691
  });
628
692
  continue;
629
693
  }
630
- // In-run progress (DR-019, CLI-32): announce the phase, then report its
694
+ // In-run progress (DR-019, cli-32): announce the phase, then report its
631
695
  // outcome with the elapsed time.
632
696
  const startedAt = Date.now();
633
697
  deps.progress?.({ kind: 'phase-start', phase: step.phase, target });
@@ -638,14 +702,15 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
638
702
  elapsedMs: Date.now() - startedAt,
639
703
  });
640
704
  // Selecting a compiled executor can throw rather than return a verdict —
641
- // 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,
705
+ // notably a pinned link target whose installed engine declares no
706
+ // supported contract, which the host factory rejects (phase-execution-30).
707
+ // That is the same fail-closed family as a stale pin,
643
708
  // so it is reported through the phase-failure path; letting it unwind
644
709
  // 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).
710
+ // phase and target from the report (cli-4, cli-32, phase-execution-27).
646
711
  let selection;
647
712
  try {
648
- selection = await selectExecutor(step.phase, step.pinKey, pipeline.dir, pinFile, deps);
713
+ selection = await selectExecutor(step, pipeline.dir, pinFile, deps);
649
714
  }
650
715
  catch (error) {
651
716
  selection = { kind: 'fail', reasons: [messageOf(error)] };
@@ -695,30 +760,35 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
695
760
  return { ok: false, outputs, diagnostics };
696
761
  }
697
762
  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).
763
+ // Settle an agent-chosen `.js`/`.ts` link-object edge from the sibling that
764
+ // currently exists (pipeline-40), then keep rejecting every genuinely
765
+ // unresolved import (verification-18).
701
766
  if (step.phase === 'link' &&
702
767
  (step.targetExt === '.ts' || step.targetExt === '.js')) {
703
768
  let missing;
704
769
  try {
705
- missing = await unresolvableRelativeImports(step.request.kind === 'link' ? step.request.linked : target);
770
+ const linked = step.request.kind === 'link' ? step.request.linked : target;
771
+ const rewrites = await reconcileLinkObjectImportSpecifiers(linked, step.request.kind === 'link' ? step.request.objects : []);
772
+ diagnostics.push(...rewrites.map(({ from, to }) => `linked module ${linked} reconciled link-object import ` +
773
+ `${JSON.stringify(from)} to ${JSON.stringify(to)} (pipeline-40)`));
774
+ missing = await unresolvableRelativeImports(linked);
706
775
  }
707
776
  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).
777
+ // A target that cannot be reconciled and checked is a dead artifact
778
+ // too. Fail the link here so every started phase still reaches a
779
+ // terminal event (cli-32).
712
780
  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)');
781
+ diagnostics.push(`linked module ${target} could not be settled and checked: ` +
782
+ `${messageOf(error)} ` +
783
+ 'post-link completion and load integrity are mandatory ' +
784
+ '(pipeline-40, verification-18)');
715
785
  return { ok: false, outputs, diagnostics };
716
786
  }
717
787
  if (missing.length > 0) {
718
788
  fail();
719
789
  diagnostics.push(`linked module ${target} has unresolvable relative imports: ` +
720
790
  `${missing.join(', ')} — an emitted module that cannot load ` +
721
- 'fails the link (VERIFY-18)');
791
+ 'fails the link (verification-18)');
722
792
  return { ok: false, outputs, diagnostics };
723
793
  }
724
794
  }
@@ -758,7 +828,7 @@ async function executeSteps(steps, pipeline, deps, opts = {}) {
758
828
  ? { outcome: 'up-to-date' }
759
829
  : {}),
760
830
  };
761
- const completed = await complete(selected, guardCompletionTarget);
831
+ const completed = await complete(selected, guardCompletionTarget, pinFile);
762
832
  return state === undefined
763
833
  ? completed
764
834
  : publishIncremental(completed, state, steps);
@@ -832,7 +902,8 @@ async function stepIdentity(step, state, index, declared) {
832
902
  inputs: [
833
903
  hashBytes(chainedInput),
834
904
  ...rootHashes,
835
- ...(await Promise.all(declared.map(async (path) => hashBytes(await readFile(path))))),
905
+ ...declared.declarationHashes,
906
+ ...(await Promise.all(declared.paths.map(async (path) => hashBytes(await readFile(path))))),
836
907
  ],
837
908
  };
838
909
  }
@@ -842,7 +913,8 @@ async function stepIdentity(step, state, index, declared) {
842
913
  inputs.push(framedHash('object', encodeLocator(state.artDir, path), hashBytes(await readFile(path))));
843
914
  }
844
915
  inputs.push(hashBytes(await readFile(step.request.definitionPath)));
845
- inputs.push(...(await Promise.all(declared.map(async (path) => hashBytes(await readFile(path))))));
916
+ inputs.push(...declared.declarationHashes);
917
+ inputs.push(...(await Promise.all(declared.paths.map(async (path) => hashBytes(await readFile(path))))));
846
918
  const linkTarget = resolve(step.request.linkTarget);
847
919
  const targetInfo = await stat(linkTarget);
848
920
  let targetKind;
@@ -872,36 +944,73 @@ async function stepIdentity(step, state, index, declared) {
872
944
  }
873
945
  }
874
946
  async function stepDeclaredInputs(step, pipeline, pinFile) {
875
- const roots = step.request.kind === 'compile'
876
- ? [step.request.definitionPath, ...(step.request.references ?? [])]
877
- : [step.request.definitionPath];
947
+ const roots = [
948
+ { path: step.request.definitionPath, phase: step.pinKey },
949
+ ];
950
+ if (step.request.kind === 'compile') {
951
+ for (const reference of step.request.references ?? []) {
952
+ roots.push({ path: reference });
953
+ }
954
+ }
878
955
  return declaredInputPaths(pipeline, pinFile, roots);
879
956
  }
880
957
  /** Current local semantic-input closure for definitions and references. */
881
958
  async function declaredInputPaths(pipeline, pinFile, roots) {
882
959
  const boundary = pinFile?.pathBoundary.path ?? '.';
883
- const base = new Set(roots.map((path) => resolve(path)));
960
+ const base = new Set(roots.map((root) => resolve(root.path)));
884
961
  const seen = new Set(base);
885
962
  const inputs = [];
963
+ const declarationHashes = [];
886
964
  let complete = true;
887
965
  const normalize = resolve(normalizeDefinitionPath());
888
966
  for (const root of roots) {
889
- const absolute = resolve(root);
967
+ const absolute = resolve(root.path);
890
968
  if (!absolute.toLowerCase().endsWith('.md') || absolute === normalize) {
891
969
  continue;
892
970
  }
893
971
  const locator = relative(pipeline.dir, absolute).split(sep).join('/');
894
- const closure = new Set();
972
+ let closure;
895
973
  try {
896
- await deriveClosure(pipeline.dir, boundary, locator, (path) => closure.add(path));
974
+ closure = await inspectClosureWithDeclaration(pipeline.dir, boundary, locator, root.phase);
897
975
  }
898
976
  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.
977
+ // An unexpected inspection failure still degrades runner-only identity
978
+ // to Ordinary execution; structured PinError outcomes retain safe paths.
902
979
  complete = false;
980
+ continue;
903
981
  }
904
- for (const path of closure) {
982
+ if (!closure.complete) {
983
+ // An incomplete closure is unusable for identity, but every independently
984
+ // resolved in-boundary member remains protected from output aliasing.
985
+ complete = false;
986
+ for (const path of closure.paths) {
987
+ const absolutePath = resolve(path);
988
+ if (seen.has(absolutePath))
989
+ continue;
990
+ seen.add(absolutePath);
991
+ inputs.push(absolutePath);
992
+ }
993
+ continue;
994
+ }
995
+ if (closure.declaration === 'sidecar' && root.phase !== undefined) {
996
+ const members = [...closure.paths]
997
+ .map((path) => resolve(path))
998
+ .filter((path) => path !== absolute)
999
+ .map((path) => ({
1000
+ path,
1001
+ locator: relative(pipeline.dir, path).split(sep).join('/'),
1002
+ }))
1003
+ .sort((left, right) => compareUtf8(left.locator, right.locator));
1004
+ declarationHashes.push(framedHash('sidecar-closure-v1', root.phase, ...members.map((member) => member.locator)));
1005
+ for (const member of members) {
1006
+ if (seen.has(member.path))
1007
+ continue;
1008
+ seen.add(member.path);
1009
+ inputs.push(member.path);
1010
+ }
1011
+ continue;
1012
+ }
1013
+ for (const path of closure.paths) {
905
1014
  const absolutePath = resolve(path);
906
1015
  if (seen.has(absolutePath))
907
1016
  continue;
@@ -909,7 +1018,7 @@ async function declaredInputPaths(pipeline, pinFile, roots) {
909
1018
  inputs.push(absolutePath);
910
1019
  }
911
1020
  }
912
- return { paths: inputs, complete };
1021
+ return { paths: inputs, declarationHashes, complete };
913
1022
  }
914
1023
  async function selectStepMode(step, index, target, identity, state) {
915
1024
  if (state.rebuild || state.history === null || identity === null) {
@@ -960,15 +1069,33 @@ function sameHashes(left, right) {
960
1069
  return (left.length === right.length &&
961
1070
  left.every((hash, index) => hash === right[index]));
962
1071
  }
963
- async function reusePinFailure(pinKey, pipelineDir, pinFile) {
964
- const record = ownPin(pinFile, pinKey);
965
- if (pinFile === undefined || pinKey === undefined || record === undefined) {
1072
+ async function reusePinFailure(step, pipelineDir, pinFile) {
1073
+ const record = ownPin(pinFile, step.pinKey);
1074
+ if (pinFile === undefined ||
1075
+ step.pinKey === undefined ||
1076
+ record === undefined) {
966
1077
  return null;
967
1078
  }
968
- const verdict = await evaluatePin(pipelineDir, pinFile, pinKey, record);
969
- return verdict.status === 'current'
970
- ? null
971
- : [`pin is ${verdict.status}: ${verdict.reason}`];
1079
+ const verdict = await evaluatePin(pipelineDir, pinFile, step.pinKey, record);
1080
+ if (verdict.status !== 'current') {
1081
+ return [`pin is ${verdict.status}: ${verdict.reason}`];
1082
+ }
1083
+ const definitionIssue = await currentPinDefinitionIssue(step, pipelineDir, pinFile, record);
1084
+ return definitionIssue === null ? null : [definitionIssue];
1085
+ }
1086
+ /**
1087
+ * Keeps external-definition pinning dormant until pipeline discovery selects
1088
+ * the same physical definition (phase-execution-27; DR-026).
1089
+ */
1090
+ async function currentPinDefinitionIssue(step, pipelineDir, pinFile, record) {
1091
+ const recorded = resolvePinPath(pipelineDir, pinFile.pathBoundary.path, record.definition.path, 'definition');
1092
+ const selected = resolve(step.request.definitionPath);
1093
+ if (await pathsAlias(recorded, selected))
1094
+ return null;
1095
+ const selectedLocator = relative(pipelineDir, selected).split(sep).join('/');
1096
+ return (`pin-recorded definition "${record.definition.path}" does not match ` +
1097
+ `selected pipeline definition "${selectedLocator}"; ` +
1098
+ 'external-definition execution remains disabled until pipeline resolution selects the same physical definition');
972
1099
  }
973
1100
  /**
974
1101
  * Removes any complete snapshot that could otherwise bless this target after
@@ -1051,29 +1178,39 @@ function historyDiagnostic(result, reason) {
1051
1178
  };
1052
1179
  }
1053
1180
  /**
1054
- * Selects a phase's execution strategy from the pin index (PHEXEC-27; DR-005,
1181
+ * Selects a phase's execution strategy from the pin index (phase-execution-27; DR-005,
1055
1182
  * DR-007): a phase with no pin interprets, a current pin runs its compiled
1056
1183
  * artifact, and a stale or malformed pin fails closed and is never silently
1057
1184
  * interpreted.
1058
1185
  */
1059
- async function selectExecutor(phase, pinKey, pipelineDir, pinFile, deps) {
1060
- const record = ownPin(pinFile, pinKey);
1061
- if (pinFile === undefined || pinKey === undefined || record === undefined) {
1186
+ async function selectExecutor(step, pipelineDir, pinFile, deps) {
1187
+ const record = ownPin(pinFile, step.pinKey);
1188
+ if (pinFile === undefined ||
1189
+ step.pinKey === undefined ||
1190
+ record === undefined) {
1062
1191
  return { kind: 'run', executor: deps.executor };
1063
1192
  }
1064
- const verdict = await evaluatePin(pipelineDir, pinFile, pinKey, record);
1193
+ const verdict = await evaluatePin(pipelineDir, pinFile, step.pinKey, record);
1065
1194
  if (verdict.status === 'current') {
1195
+ const definitionIssue = await currentPinDefinitionIssue(step, pipelineDir, pinFile, record);
1196
+ if (definitionIssue !== null) {
1197
+ return { kind: 'fail', reasons: [definitionIssue] };
1198
+ }
1066
1199
  if (deps.compiled === undefined) {
1067
1200
  return {
1068
1201
  kind: 'fail',
1069
1202
  reasons: [
1070
- `phase "${phase}" is pinned to a compiled artifact, but this host has no compiled executor configured`,
1203
+ `phase "${step.phase}" is pinned to a compiled artifact, but this host has no compiled executor configured`,
1071
1204
  ],
1072
1205
  };
1073
1206
  }
1074
1207
  return {
1075
1208
  kind: 'run',
1076
- executor: deps.compiled({ phase: pinKey, pipelineDir, record }),
1209
+ executor: await deps.compiled({
1210
+ phase: step.pinKey,
1211
+ pipelineDir,
1212
+ record,
1213
+ }),
1077
1214
  };
1078
1215
  }
1079
1216
  return {
@@ -1103,7 +1240,7 @@ async function requireLink(pipeline, reference) {
1103
1240
  throw new Error(`pipeline "${reference}" has no link phase`);
1104
1241
  }
1105
1242
  // `loadLinkFile` relaxes the ## Link Targets requirement intrinsically for
1106
- // Playbook's `playbook` linked format (DR-002, DR-009, PIPE-11), so the
1243
+ // Playbook's `playbook` linked format (DR-002, DR-009, pipeline-11), so the
1107
1244
  // requirement does not depend on how the pipeline reference resolved.
1108
1245
  return loadLinkFile(pipeline.linkFile);
1109
1246
  }