audit-tools 0.42.1 → 0.43.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 (224) hide show
  1. package/dispatch/merge-results.mjs +229 -140
  2. package/dispatch/validate-result.mjs +98 -40
  3. package/dist/audit/cli/conceptualDispatch.d.ts.map +1 -1
  4. package/dist/audit/cli/conceptualDispatch.js +35 -3
  5. package/dist/audit/cli/conceptualDispatch.js.map +1 -1
  6. package/dist/audit/cli/dispatch/hostHandoff.d.ts +9 -1
  7. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  8. package/dist/audit/cli/dispatch/hostHandoff.js +94 -38
  9. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  10. package/dist/audit/cli/nextStepCommand.d.ts +64 -0
  11. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  12. package/dist/audit/cli/nextStepCommand.js +877 -783
  13. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  14. package/dist/audit/cli/nextStepHelpers.d.ts +12 -0
  15. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  16. package/dist/audit/cli/nextStepHelpers.js +34 -1
  17. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  18. package/dist/audit/cli/steps.d.ts +2 -2
  19. package/dist/audit/extractors/graph.d.ts +28 -2
  20. package/dist/audit/extractors/graph.d.ts.map +1 -1
  21. package/dist/audit/extractors/graph.js +122 -11
  22. package/dist/audit/extractors/graph.js.map +1 -1
  23. package/dist/audit/extractors/graphRoutes.d.ts +8 -0
  24. package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
  25. package/dist/audit/extractors/graphRoutes.js +71 -4
  26. package/dist/audit/extractors/graphRoutes.js.map +1 -1
  27. package/dist/audit/io/artifacts.d.ts +55 -0
  28. package/dist/audit/io/artifacts.d.ts.map +1 -1
  29. package/dist/audit/io/artifacts.js +189 -12
  30. package/dist/audit/io/artifacts.js.map +1 -1
  31. package/dist/audit/io/toolingManifest.d.ts +11 -0
  32. package/dist/audit/io/toolingManifest.d.ts.map +1 -1
  33. package/dist/audit/io/toolingManifest.js +13 -3
  34. package/dist/audit/io/toolingManifest.js.map +1 -1
  35. package/dist/audit/orchestrator/advance.d.ts +64 -0
  36. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  37. package/dist/audit/orchestrator/advance.js +191 -57
  38. package/dist/audit/orchestrator/advance.js.map +1 -1
  39. package/dist/audit/orchestrator/charterClarificationExecutor.d.ts.map +1 -1
  40. package/dist/audit/orchestrator/charterClarificationExecutor.js +55 -21
  41. package/dist/audit/orchestrator/charterClarificationExecutor.js.map +1 -1
  42. package/dist/audit/orchestrator/flowCoverage.d.ts.map +1 -1
  43. package/dist/audit/orchestrator/flowCoverage.js +7 -10
  44. package/dist/audit/orchestrator/flowCoverage.js.map +1 -1
  45. package/dist/audit/orchestrator/flowPlanning.d.ts +54 -1
  46. package/dist/audit/orchestrator/flowPlanning.d.ts.map +1 -1
  47. package/dist/audit/orchestrator/flowPlanning.js +77 -18
  48. package/dist/audit/orchestrator/flowPlanning.js.map +1 -1
  49. package/dist/audit/orchestrator/flowRequeue.d.ts.map +1 -1
  50. package/dist/audit/orchestrator/flowRequeue.js +6 -10
  51. package/dist/audit/orchestrator/flowRequeue.js.map +1 -1
  52. package/dist/audit/orchestrator/requeueFold.d.ts.map +1 -1
  53. package/dist/audit/orchestrator/requeueFold.js +18 -5
  54. package/dist/audit/orchestrator/requeueFold.js.map +1 -1
  55. package/dist/audit/orchestrator/staleness.d.ts +24 -1
  56. package/dist/audit/orchestrator/staleness.d.ts.map +1 -1
  57. package/dist/audit/orchestrator/staleness.js +147 -16
  58. package/dist/audit/orchestrator/staleness.js.map +1 -1
  59. package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts +26 -0
  60. package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts.map +1 -1
  61. package/dist/audit/orchestrator/syntaxResolutionExecutor.js +97 -12
  62. package/dist/audit/orchestrator/syntaxResolutionExecutor.js.map +1 -1
  63. package/dist/audit/orchestrator/taskBuilder.d.ts.map +1 -1
  64. package/dist/audit/orchestrator/taskBuilder.js +111 -21
  65. package/dist/audit/orchestrator/taskBuilder.js.map +1 -1
  66. package/dist/audit/orchestrator/trivialAudit.d.ts +17 -1
  67. package/dist/audit/orchestrator/trivialAudit.d.ts.map +1 -1
  68. package/dist/audit/orchestrator/trivialAudit.js +31 -4
  69. package/dist/audit/orchestrator/trivialAudit.js.map +1 -1
  70. package/dist/audit/types/analyzerCapability.d.ts +6 -6
  71. package/dist/audit/types/charterRegister.d.ts +30 -12
  72. package/dist/audit/types/charterRegister.d.ts.map +1 -1
  73. package/dist/audit/types/charterRegister.js +22 -9
  74. package/dist/audit/types/charterRegister.js.map +1 -1
  75. package/dist/audit/validation/anchorGrounding.d.ts.map +1 -1
  76. package/dist/audit/validation/anchorGrounding.js +17 -0
  77. package/dist/audit/validation/anchorGrounding.js.map +1 -1
  78. package/dist/audit/validation/auditResults.d.ts +17 -0
  79. package/dist/audit/validation/auditResults.d.ts.map +1 -1
  80. package/dist/audit/validation/auditResults.js +50 -17
  81. package/dist/audit/validation/auditResults.js.map +1 -1
  82. package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts +48 -0
  83. package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts.map +1 -1
  84. package/dist/remediate/contractPipeline/cyclicSeamResolution.js +128 -0
  85. package/dist/remediate/contractPipeline/cyclicSeamResolution.js.map +1 -1
  86. package/dist/remediate/contractPipeline/derive.d.ts +8 -5
  87. package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
  88. package/dist/remediate/contractPipeline/derive.js +8 -16
  89. package/dist/remediate/contractPipeline/derive.js.map +1 -1
  90. package/dist/remediate/index.d.ts.map +1 -1
  91. package/dist/remediate/index.js +43 -2
  92. package/dist/remediate/index.js.map +1 -1
  93. package/dist/remediate/intent/intentOrdering.d.ts +13 -5
  94. package/dist/remediate/intent/intentOrdering.d.ts.map +1 -1
  95. package/dist/remediate/intent/intentOrdering.js +56 -25
  96. package/dist/remediate/intent/intentOrdering.js.map +1 -1
  97. package/dist/remediate/phases/close.d.ts +57 -2
  98. package/dist/remediate/phases/close.d.ts.map +1 -1
  99. package/dist/remediate/phases/close.js +260 -36
  100. package/dist/remediate/phases/close.js.map +1 -1
  101. package/dist/remediate/state/disposition.d.ts +26 -4
  102. package/dist/remediate/state/disposition.d.ts.map +1 -1
  103. package/dist/remediate/state/itemStatus.d.ts +33 -6
  104. package/dist/remediate/state/itemStatus.d.ts.map +1 -1
  105. package/dist/remediate/state/itemStatus.js +154 -39
  106. package/dist/remediate/state/itemStatus.js.map +1 -1
  107. package/dist/remediate/state/types.d.ts +41 -2
  108. package/dist/remediate/state/types.d.ts.map +1 -1
  109. package/dist/remediate/state/types.js.map +1 -1
  110. package/dist/remediate/steps/contractPipeline.d.ts +287 -15
  111. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  112. package/dist/remediate/steps/contractPipeline.js +1655 -981
  113. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  114. package/dist/remediate/steps/dispatch/hostHandoff.d.ts +137 -2
  115. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  116. package/dist/remediate/steps/dispatch/hostHandoff.js +713 -59
  117. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  118. package/dist/remediate/steps/finalGate.d.ts +136 -37
  119. package/dist/remediate/steps/finalGate.d.ts.map +1 -1
  120. package/dist/remediate/steps/finalGate.js +120 -86
  121. package/dist/remediate/steps/finalGate.js.map +1 -1
  122. package/dist/remediate/steps/gateCommands.d.ts +8 -0
  123. package/dist/remediate/steps/gateCommands.d.ts.map +1 -1
  124. package/dist/remediate/steps/gateCommands.js +40 -11
  125. package/dist/remediate/steps/gateCommands.js.map +1 -1
  126. package/dist/remediate/steps/nextStep.d.ts +96 -5
  127. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  128. package/dist/remediate/steps/nextStep.js +668 -147
  129. package/dist/remediate/steps/nextStep.js.map +1 -1
  130. package/dist/remediate/steps/types.d.ts +4 -1
  131. package/dist/remediate/steps/types.d.ts.map +1 -1
  132. package/dist/remediate/steps/types.js +10 -0
  133. package/dist/remediate/steps/types.js.map +1 -1
  134. package/dist/remediate/validation/artifacts.d.ts +20 -0
  135. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  136. package/dist/remediate/validation/artifacts.js +239 -13
  137. package/dist/remediate/validation/artifacts.js.map +1 -1
  138. package/dist/remediate/validation/contractPipelineGates.d.ts +58 -0
  139. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  140. package/dist/remediate/validation/contractPipelineGates.js +143 -27
  141. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  142. package/dist/shared/analyzers/acquisitionEngine.d.ts +72 -25
  143. package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
  144. package/dist/shared/analyzers/acquisitionEngine.js +149 -15
  145. package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
  146. package/dist/shared/analyzers/binaryAcquisition.d.ts +27 -2
  147. package/dist/shared/analyzers/binaryAcquisition.d.ts.map +1 -1
  148. package/dist/shared/analyzers/binaryAcquisition.js +149 -43
  149. package/dist/shared/analyzers/binaryAcquisition.js.map +1 -1
  150. package/dist/shared/analyzers/candidates.d.ts +9 -8
  151. package/dist/shared/analyzers/candidates.d.ts.map +1 -1
  152. package/dist/shared/analyzers/candidates.js +96 -64
  153. package/dist/shared/analyzers/candidates.js.map +1 -1
  154. package/dist/shared/analyzers/normalizeExternal.d.ts +40 -12
  155. package/dist/shared/analyzers/normalizeExternal.d.ts.map +1 -1
  156. package/dist/shared/analyzers/normalizeExternal.js +74 -18
  157. package/dist/shared/analyzers/normalizeExternal.js.map +1 -1
  158. package/dist/shared/analyzers/rubocop.d.ts +14 -13
  159. package/dist/shared/analyzers/rubocop.d.ts.map +1 -1
  160. package/dist/shared/analyzers/rubocop.js +29 -6
  161. package/dist/shared/analyzers/rubocop.js.map +1 -1
  162. package/dist/shared/analyzers/types.d.ts +196 -13
  163. package/dist/shared/analyzers/types.d.ts.map +1 -1
  164. package/dist/shared/analyzers/types.js +122 -9
  165. package/dist/shared/analyzers/types.js.map +1 -1
  166. package/dist/shared/decompose/charterClarification.d.ts +12 -1
  167. package/dist/shared/decompose/charterClarification.d.ts.map +1 -1
  168. package/dist/shared/decompose/charterClarification.js +11 -4
  169. package/dist/shared/decompose/charterClarification.js.map +1 -1
  170. package/dist/shared/decompose/charterExtraction.d.ts +20 -10
  171. package/dist/shared/decompose/charterExtraction.d.ts.map +1 -1
  172. package/dist/shared/decompose/charterExtraction.js +52 -23
  173. package/dist/shared/decompose/charterExtraction.js.map +1 -1
  174. package/dist/shared/intent/freeFormIntentInterpreter.d.ts +12 -2
  175. package/dist/shared/intent/freeFormIntentInterpreter.d.ts.map +1 -1
  176. package/dist/shared/intent/freeFormIntentInterpreter.js +27 -22
  177. package/dist/shared/intent/freeFormIntentInterpreter.js.map +1 -1
  178. package/dist/shared/intent/pathScope.d.ts +12 -1
  179. package/dist/shared/intent/pathScope.d.ts.map +1 -1
  180. package/dist/shared/intent/pathScope.js +56 -19
  181. package/dist/shared/intent/pathScope.js.map +1 -1
  182. package/dist/shared/intent/sharedIntentData.d.ts +19 -1
  183. package/dist/shared/intent/sharedIntentData.d.ts.map +1 -1
  184. package/dist/shared/intent/sharedIntentData.js +33 -8
  185. package/dist/shared/intent/sharedIntentData.js.map +1 -1
  186. package/dist/shared/io/fileLock.d.ts +12 -0
  187. package/dist/shared/io/fileLock.d.ts.map +1 -1
  188. package/dist/shared/io/fileLock.js +48 -5
  189. package/dist/shared/io/fileLock.js.map +1 -1
  190. package/dist/shared/io/json.d.ts +13 -0
  191. package/dist/shared/io/json.d.ts.map +1 -1
  192. package/dist/shared/io/json.js +19 -1
  193. package/dist/shared/io/json.js.map +1 -1
  194. package/dist/shared/steps/stepEmissionScaffold.d.ts +69 -0
  195. package/dist/shared/steps/stepEmissionScaffold.d.ts.map +1 -0
  196. package/dist/shared/steps/stepEmissionScaffold.js +83 -0
  197. package/dist/shared/steps/stepEmissionScaffold.js.map +1 -0
  198. package/dist/shared/submission/handRecovery.d.ts.map +1 -1
  199. package/dist/shared/submission/handRecovery.js +69 -4
  200. package/dist/shared/submission/handRecovery.js.map +1 -1
  201. package/dist/shared/submission/submissionIdentity.d.ts +10 -1
  202. package/dist/shared/submission/submissionIdentity.d.ts.map +1 -1
  203. package/dist/shared/submission/submissionIdentity.js +12 -3
  204. package/dist/shared/submission/submissionIdentity.js.map +1 -1
  205. package/dist/shared/submission/submissionLedger.d.ts +43 -2
  206. package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
  207. package/dist/shared/submission/submissionLedger.js +50 -7
  208. package/dist/shared/submission/submissionLedger.js.map +1 -1
  209. package/dist/shared/tooling/allowlistedExec.d.ts +9 -0
  210. package/dist/shared/tooling/allowlistedExec.d.ts.map +1 -1
  211. package/dist/shared/tooling/allowlistedExec.js +19 -2
  212. package/dist/shared/tooling/allowlistedExec.js.map +1 -1
  213. package/dist/shared/types/charter.d.ts +24 -0
  214. package/dist/shared/types/charter.d.ts.map +1 -1
  215. package/dist/shared/types/charter.js +9 -0
  216. package/dist/shared/types/charter.js.map +1 -1
  217. package/dist/shared/types/remediationOutcome.d.ts +282 -9
  218. package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
  219. package/dist/shared/types/remediationOutcome.js +174 -1
  220. package/dist/shared/types/remediationOutcome.js.map +1 -1
  221. package/package.json +2 -1
  222. package/remediate-code.mjs +15 -0
  223. package/wrapper/remediate-code-wrapper-install-hosts.mjs +45 -4
  224. package/wrapper/remediate-code-wrapper-install-renderers.mjs +12 -2
@@ -1,168 +1,257 @@
1
1
  import { join } from "node:path";
2
2
  import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
3
+ import { pathToFileURL } from "node:url";
3
4
  import { validateResult } from "./validate.mjs";
4
- import { PACKET_SCHEMA_FILENAMES } from "../dist/audit/io/runArtifacts.js";
5
5
  import { resolveArtifactsDir } from "./artifacts-dir.mjs";
6
6
 
7
- const runIdIdx = process.argv.indexOf("--run-id");
8
- if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
9
- console.error("Usage: node dispatch/merge-results.mjs --run-id <run_id> [--artifacts-dir <dir>]");
10
- process.exit(1);
11
- }
12
- const runId = process.argv[runIdIdx + 1];
13
-
14
- // Default must match where the orchestrator/wrapper actually writes runs:
15
- // <root>/.audit-tools/audit (COR-bf5c7331). The prior `.audit-artifacts`
16
- // default resolved to a directory the pipeline never populates.
17
- const artifactsDir = resolveArtifactsDir(process.argv);
18
-
19
- const taskResultsDir = join(artifactsDir, "runs", runId, "task-results");
20
- const auditResultsPath = join(artifactsDir, "runs", runId, "run-results.json");
21
- const failedTasksPath = join(artifactsDir, "runs", runId, "failed-tasks.json");
22
- const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
23
-
24
- if (!existsSync(taskResultsDir)) {
25
- console.error(`task-results directory not found: ${taskResultsDir}`);
26
- process.exit(1);
27
- }
7
+ /**
8
+ * Importable merge core (CP-NODE-21 / dispatch-scripts-test-reach). Every
9
+ * acceptance branch this script owns — fail-closed manifest read, INV-01
10
+ * array-payload expansion, duplicate-task_id rejection, unknown-task_id
11
+ * rejection, missing-result-is-a-failure, no-destructive-truncation, and the
12
+ * INV-03 partial-merge signal (surfaced here as `failing.length > 0`, which
13
+ * the CLI shim below turns into a non-zero exit) — lives in this function so
14
+ * a test can drive it in-process against a fabricated run directory, without
15
+ * spawning a child process and without process.exit ever firing mid-test.
16
+ * The CLI shim is a byte-compatible wrapper: same argv parsing, same
17
+ * stdout/stderr text, same exit codes as before this split.
18
+ *
19
+ * @param {{ artifactsDir: string, runId: string }} options
20
+ * @returns {{
21
+ * ok: boolean,
22
+ * fatal?: { message: string },
23
+ * passing: object[],
24
+ * failing: { task_id: string, errors: string[] }[],
25
+ * total: number,
26
+ * auditResultsPath: string,
27
+ * failedTasksPath: string,
28
+ * wroteAuditResults: boolean,
29
+ * wroteFailedTasks: boolean,
30
+ * }}
31
+ */
32
+ export function mergeResults({ artifactsDir, runId }) {
33
+ const taskResultsDir = join(artifactsDir, "runs", runId, "task-results");
34
+ const auditResultsPath = join(artifactsDir, "runs", runId, "run-results.json");
35
+ const failedTasksPath = join(artifactsDir, "runs", runId, "failed-tasks.json");
36
+ const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
28
37
 
29
- // Manifest-reconciled completeness (COR-7602834d cluster): acceptance is judged against the
30
- // pending manifest, never against whichever task-results files happen to exist.
31
- // A missing or unreadable manifest means no task context can be established, so
32
- // the merge hard-fails BEFORE any write (fail-closed task-identity gate) instead
33
- // of validating results fail-open with no assigned task.
34
- let tasks;
35
- try {
36
- tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
37
- if (!Array.isArray(tasks)) {
38
- throw new Error("pending-audit-tasks.json must be a JSON array of AuditTasks");
38
+ if (!existsSync(taskResultsDir)) {
39
+ return {
40
+ ok: false,
41
+ fatal: { message: `task-results directory not found: ${taskResultsDir}` },
42
+ passing: [],
43
+ failing: [],
44
+ total: 0,
45
+ auditResultsPath,
46
+ failedTasksPath,
47
+ wroteAuditResults: false,
48
+ wroteFailedTasks: false,
49
+ };
39
50
  }
40
- } catch (e) {
41
- console.error(
42
- `Cannot read pending manifest ${tasksPath}: ${e.message}\n` +
43
- "merge-results judges acceptance against the pending manifest (fail-closed); aborting before any write.",
44
- );
45
- process.exit(1);
46
- }
47
- const taskMap = new Map(tasks.filter(t => t && typeof t.task_id === "string").map(t => [t.task_id, t]));
48
-
49
- // Result-file selection excludes the schema pointer files prepare-dispatch
50
- // copies into task-results/ for optional worker self-validation — they are
51
- // support artifacts, never results, and must not be counted as failures.
52
- const PACKET_SCHEMA_FILENAME_SET = new Set(PACKET_SCHEMA_FILENAMES);
53
- const files = readdirSync(taskResultsDir)
54
- .filter(f => f.endsWith(".json") && !PACKET_SCHEMA_FILENAME_SET.has(f))
55
- .sort();
56
-
57
- const failing = [];
58
- const resultsByTaskId = new Map();
59
-
60
- for (const filename of files) {
61
- const filePath = join(taskResultsDir, filename);
62
- let parsed;
51
+
52
+ // Manifest-reconciled completeness (COR-7602834d cluster): acceptance is judged against the
53
+ // pending manifest, never against whichever task-results files happen to exist.
54
+ // A missing or unreadable manifest means no task context can be established, so
55
+ // the merge hard-fails BEFORE any write (fail-closed task-identity gate) instead
56
+ // of validating results fail-open with no assigned task.
57
+ let tasks;
63
58
  try {
64
- parsed = JSON.parse(readFileSync(filePath, "utf8"));
59
+ tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
60
+ if (!Array.isArray(tasks)) {
61
+ throw new Error("pending-audit-tasks.json must be a JSON array of AuditTasks");
62
+ }
65
63
  } catch (e) {
66
- failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
67
- continue;
64
+ return {
65
+ ok: false,
66
+ fatal: {
67
+ message:
68
+ `Cannot read pending manifest ${tasksPath}: ${e.message}\n` +
69
+ "merge-results judges acceptance against the pending manifest (fail-closed); aborting before any write.",
70
+ },
71
+ passing: [],
72
+ failing: [],
73
+ total: 0,
74
+ auditResultsPath,
75
+ failedTasksPath,
76
+ wroteAuditResults: false,
77
+ wroteFailedTasks: false,
78
+ };
68
79
  }
80
+ const taskMap = new Map(tasks.filter(t => t && typeof t.task_id === "string").map(t => [t.task_id, t]));
81
+
82
+ // Schema-pointer-file exclusion was retired along with the rest of the
83
+ // schema-copy feature in 467b1e8f ("retire the execution substrate: zero
84
+ // adapters, host-owned execution"), which deleted PACKET_SCHEMA_FILENAMES
85
+ // and its sole producer (nothing in src/ copies schema files into
86
+ // task-results/ anymore) but left this import pointing at the deleted
87
+ // export — an orphaned reference that made this whole script unimportable
88
+ // (ESM throws on a missing named export). If a schema-pointer-file
89
+ // convention is reintroduced, re-add the matching exclusion here sourced
90
+ // from wherever that filename list lives then; there is nothing to exclude
91
+ // today.
92
+ const files = readdirSync(taskResultsDir)
93
+ .filter(f => f.endsWith(".json"))
94
+ .sort();
69
95
 
70
- // Expand top-level AuditResult[] arrays — a worker that emits an array
71
- // payload must not be treated as one invalid object (INV-01 / COR-bf5c7331).
72
- const candidates = Array.isArray(parsed) ? parsed : [parsed];
73
- for (const resultObj of candidates) {
74
- const taskId =
75
- resultObj && typeof resultObj === "object" && !Array.isArray(resultObj) &&
76
- typeof resultObj.task_id === "string"
77
- ? resultObj.task_id
78
- : undefined;
79
- if (!taskId) {
80
- failing.push({
81
- task_id: filename,
82
- errors: ["Result has no task_id; cannot bind it to an assigned task."],
83
- });
96
+ const failing = [];
97
+ const resultsByTaskId = new Map();
98
+
99
+ for (const filename of files) {
100
+ const filePath = join(taskResultsDir, filename);
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(readFileSync(filePath, "utf8"));
104
+ } catch (e) {
105
+ failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
84
106
  continue;
85
107
  }
86
- if (resultsByTaskId.has(taskId)) {
87
- // Dedup-by-task_id: the first on-disk result for a task is authoritative;
88
- // later copies are rejected, never double-merged.
89
- failing.push({
90
- task_id: taskId,
91
- errors: [`Duplicate audit result for assigned task '${taskId}'.`],
92
- });
93
- continue;
108
+
109
+ // Expand top-level AuditResult[] arrays a worker that emits an array
110
+ // payload must not be treated as one invalid object (INV-01 / COR-bf5c7331).
111
+ const candidates = Array.isArray(parsed) ? parsed : [parsed];
112
+ for (const resultObj of candidates) {
113
+ const taskId =
114
+ resultObj && typeof resultObj === "object" && !Array.isArray(resultObj) &&
115
+ typeof resultObj.task_id === "string"
116
+ ? resultObj.task_id
117
+ : undefined;
118
+ if (!taskId) {
119
+ failing.push({
120
+ task_id: filename,
121
+ errors: ["Result has no task_id; cannot bind it to an assigned task."],
122
+ });
123
+ continue;
124
+ }
125
+ if (resultsByTaskId.has(taskId)) {
126
+ // Dedup-by-task_id: the first on-disk result for a task is authoritative;
127
+ // later copies are rejected, never double-merged.
128
+ failing.push({
129
+ task_id: taskId,
130
+ errors: [`Duplicate audit result for assigned task '${taskId}'.`],
131
+ });
132
+ continue;
133
+ }
134
+ if (!taskMap.has(taskId)) {
135
+ // Identity is the tool's authority: a result that matches no assigned
136
+ // task in the pending manifest never validates (fail-closed).
137
+ failing.push({
138
+ task_id: taskId,
139
+ errors: [`Unknown task_id '${taskId}': not in the pending manifest for run '${runId}'.`],
140
+ });
141
+ continue;
142
+ }
143
+ resultsByTaskId.set(taskId, resultObj);
94
144
  }
95
- if (!taskMap.has(taskId)) {
96
- // Identity is the tool's authority: a result that matches no assigned
97
- // task in the pending manifest never validates (fail-closed).
98
- failing.push({
99
- task_id: taskId,
100
- errors: [`Unknown task_id '${taskId}': not in the pending manifest for run '${runId}'.`],
101
- });
145
+ }
146
+
147
+ // Validate each MANIFEST task's result: present validate in the assigned
148
+ // task's context; absent → a genuine failure (the manifest, not the file
149
+ // listing, decides completeness).
150
+ const passing = [];
151
+ for (const task of taskMap.values()) {
152
+ const resultObj = resultsByTaskId.get(task.task_id);
153
+ if (resultObj === undefined) {
154
+ failing.push({ task_id: task.task_id, errors: ["Missing audit result for assigned task."] });
102
155
  continue;
103
156
  }
104
- resultsByTaskId.set(taskId, resultObj);
157
+ const { valid, errors } = validateResult(resultObj, task);
158
+ if (valid) {
159
+ passing.push(resultObj);
160
+ } else {
161
+ failing.push({ task_id: task.task_id, errors });
162
+ }
105
163
  }
106
- }
107
164
 
108
- // Validate each MANIFEST task's result: present → validate in the assigned
109
- // task's context; absent → a genuine failure (the manifest, not the file
110
- // listing, decides completeness).
111
- const passing = [];
112
- for (const task of taskMap.values()) {
113
- const resultObj = resultsByTaskId.get(task.task_id);
114
- if (resultObj === undefined) {
115
- failing.push({ task_id: task.task_id, errors: ["Missing audit result for assigned task."] });
116
- continue;
165
+ let wroteFailedTasks = false;
166
+ if (failing.length > 0) {
167
+ writeFileSync(failedTasksPath, JSON.stringify(failing, null, 2));
168
+ wroteFailedTasks = true;
117
169
  }
118
- const { valid, errors } = validateResult(resultObj, task);
119
- if (valid) {
120
- passing.push(resultObj);
121
- } else {
122
- failing.push({ task_id: task.task_id, errors });
170
+
171
+ // No destructive truncation on re-run (COR-44bea9c0 cluster): run-results.json is written
172
+ // only when there is something to merge. A blocked no-op (nothing passed while
173
+ // something failed) — e.g. a stray re-invocation after a successful merge —
174
+ // must never truncate a previously written run-results.json to [].
175
+ let wroteAuditResults = false;
176
+ if (passing.length > 0) {
177
+ writeFileSync(auditResultsPath, JSON.stringify(passing, null, 2));
178
+ wroteAuditResults = true;
123
179
  }
180
+
181
+ return {
182
+ ok: true,
183
+ passing,
184
+ failing,
185
+ total: passing.length + failing.length,
186
+ auditResultsPath,
187
+ failedTasksPath,
188
+ wroteAuditResults,
189
+ wroteFailedTasks,
190
+ };
124
191
  }
125
192
 
126
- if (failing.length > 0) {
127
- writeFileSync(failedTasksPath, JSON.stringify(failing, null, 2));
128
- process.stderr.write(`${failing.length} task(s) failed validation and were excluded:\n`);
129
- for (const f of failing) {
130
- for (const err of f.errors) {
131
- process.stderr.write(` ✗ ${f.task_id}: ${err}\n`);
193
+ function runCli() {
194
+ const runIdIdx = process.argv.indexOf("--run-id");
195
+ if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
196
+ console.error("Usage: node dispatch/merge-results.mjs --run-id <run_id> [--artifacts-dir <dir>]");
197
+ process.exit(1);
198
+ }
199
+ const runId = process.argv[runIdIdx + 1];
200
+
201
+ // Default must match where the orchestrator/wrapper actually writes runs:
202
+ // <root>/.audit-tools/audit (COR-bf5c7331). The prior `.audit-artifacts`
203
+ // default resolved to a directory the pipeline never populates.
204
+ const artifactsDir = resolveArtifactsDir(process.argv);
205
+
206
+ const result = mergeResults({ artifactsDir, runId });
207
+
208
+ if (!result.ok) {
209
+ console.error(result.fatal.message);
210
+ process.exit(1);
211
+ }
212
+
213
+ const { passing, failing, total, auditResultsPath, failedTasksPath } = result;
214
+
215
+ if (failing.length > 0) {
216
+ process.stderr.write(`${failing.length} task(s) failed validation and were excluded:\n`);
217
+ for (const f of failing) {
218
+ for (const err of f.errors) {
219
+ process.stderr.write(` ✗ ${f.task_id}: ${err}\n`);
220
+ }
132
221
  }
133
222
  }
134
- }
135
223
 
136
- // No destructive truncation on re-run (COR-44bea9c0 cluster): run-results.json is written
137
- // only when there is something to merge. A blocked no-op (nothing passed while
138
- // something failed) e.g. a stray re-invocation after a successful merge
139
- // must never truncate a previously written run-results.json to [].
140
- if (passing.length > 0) {
141
- writeFileSync(auditResultsPath, JSON.stringify(passing, null, 2));
142
- }
224
+ // FND-OBS-bf5c7331: emit a structured JSON summary line to stdout so callers
225
+ // get a machine-readable payload alongside the human-readable text line.
226
+ // Both are written to stdout; the JSON summary line always comes first so
227
+ // programmatic consumers can parse it without stripping the text line.
228
+ process.stdout.write(
229
+ JSON.stringify({
230
+ ts: new Date().toISOString(),
231
+ source: "audit-code:merge-results",
232
+ event: "merge_summary",
233
+ total,
234
+ accepted: passing.length,
235
+ rejected: failing.length,
236
+ audit_results_path: auditResultsPath,
237
+ ...(failing.length > 0 ? { failed_tasks_path: failedTasksPath } : {}),
238
+ }) + "\n",
239
+ );
240
+ console.log(`✓ ${passing.length}/${total} tasks valid → ${auditResultsPath}`);
241
+ if (failing.length > 0) {
242
+ console.log(" Re-run those tasks in the next cycle.");
243
+ }
143
244
 
144
- const total = passing.length + failing.length;
145
- // FND-OBS-bf5c7331: emit a structured JSON summary line to stdout so callers
146
- // get a machine-readable payload alongside the human-readable text line.
147
- // Both are written to stdout; the JSON summary line always comes first so
148
- // programmatic consumers can parse it without stripping the text line.
149
- process.stdout.write(
150
- JSON.stringify({
151
- ts: new Date().toISOString(),
152
- source: "audit-code:merge-results",
153
- event: "merge_summary",
154
- total,
155
- accepted: passing.length,
156
- rejected: failing.length,
157
- audit_results_path: auditResultsPath,
158
- ...(failing.length > 0 ? { failed_tasks_path: failedTasksPath } : {}),
159
- }) + "\n",
160
- );
161
- console.log(`✓ ${passing.length}/${total} tasks valid → ${auditResultsPath}`);
162
- if (failing.length > 0) {
163
- console.log(" Re-run those tasks in the next cycle.");
245
+ // Exit non-zero when any result failed validation so callers can detect
246
+ // partial merges and avoid treating them as clean success (INV-03 / COR-bf5c7331-2).
247
+ process.exit(failing.length > 0 ? 1 : 0);
164
248
  }
165
249
 
166
- // Exit non-zero when any result failed validation so callers can detect
167
- // partial merges and avoid treating them as clean success (INV-03 / COR-bf5c7331-2).
168
- process.exit(failing.length > 0 ? 1 : 0);
250
+ // Only run the CLI when this file is the process entrypoint not when a test
251
+ // imports mergeResults() directly. Compares resolved file:// URLs (not raw
252
+ // argv[1] strings) so this is correct on Windows, where argv[1] is a
253
+ // backslash path that never equals import.meta.url's forward-slash form.
254
+ const isMainModule = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
255
+ if (isMainModule) {
256
+ runCli();
257
+ }
@@ -1,57 +1,115 @@
1
1
  import { join } from "node:path";
2
2
  import { readFileSync, existsSync } from "node:fs";
3
+ import { pathToFileURL } from "node:url";
3
4
  import { validateResult } from "./validate.mjs";
4
5
  import { resolveArtifactsDir } from "./artifacts-dir.mjs";
5
6
 
6
- const runIdIdx = process.argv.indexOf("--run-id");
7
- const taskIdIdx = process.argv.indexOf("--task-id");
7
+ /**
8
+ * Resolve the AuditTask context for `taskId` from the run's pending
9
+ * manifest. Degrades to `task: null` (never throws) when the manifest is
10
+ * absent or unreadable — validateResult's fail-closed null-task branch is the
11
+ * single enforcement point downstream; this helper only ever supplies
12
+ * context, it never itself decides pass/fail.
13
+ *
14
+ * @param {{ artifactsDir: string, runId: string, taskId: string }} options
15
+ * @returns {{ task: object | null, warning?: string }}
16
+ */
17
+ export function resolveTaskContext({ artifactsDir, runId, taskId }) {
18
+ const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
19
+ if (!existsSync(tasksPath)) {
20
+ return { task: null };
21
+ }
22
+ try {
23
+ const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
24
+ return { task: tasks.find(t => t.task_id === taskId) ?? null };
25
+ } catch (e) {
26
+ return {
27
+ task: null,
28
+ warning: `[warn] Could not read pending-audit-tasks.json; line-count validation will be skipped: ${e.message}`,
29
+ };
30
+ }
31
+ }
8
32
 
9
- const runId = runIdIdx !== -1 ? process.argv[runIdIdx + 1] : undefined;
10
- const taskId = taskIdIdx !== -1 ? process.argv[taskIdIdx + 1] : undefined;
33
+ /**
34
+ * Importable core (CP-NODE-21 / dispatch-scripts-test-reach): reads one
35
+ * task-results file, resolves its task context via {@link resolveTaskContext},
36
+ * and validates it. No process.exit — the CLI shim below translates the
37
+ * outcome into console output and an exit code, byte-compatible with this
38
+ * script's behavior before the split.
39
+ *
40
+ * @param {{ artifactsDir: string, runId: string, taskId: string }} options
41
+ * @returns {{
42
+ * ok: boolean,
43
+ * fatal?: { message: string },
44
+ * warning?: string,
45
+ * resultPath: string,
46
+ * valid?: boolean,
47
+ * errors?: string[],
48
+ * }}
49
+ */
50
+ export function validateOneResult({ artifactsDir, runId, taskId }) {
51
+ const sanitized = taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
52
+ const resultPath = join(artifactsDir, "runs", runId, "task-results", sanitized + ".json");
53
+
54
+ if (!existsSync(resultPath)) {
55
+ return { ok: false, fatal: { message: `File not found: ${resultPath}` }, resultPath };
56
+ }
57
+
58
+ let resultObj;
59
+ try {
60
+ resultObj = JSON.parse(readFileSync(resultPath, "utf8"));
61
+ } catch (e) {
62
+ return { ok: false, fatal: { message: `Invalid JSON in ${resultPath}: ${e.message}` }, resultPath };
63
+ }
11
64
 
12
- if (!runId || !taskId) {
13
- console.error("Usage: node dispatch/validate-result.mjs --run-id <run_id> --task-id <task_id> [--artifacts-dir <dir>]");
14
- process.exit(1);
65
+ const { task, warning } = resolveTaskContext({ artifactsDir, runId, taskId });
66
+ const { valid, errors } = validateResult(resultObj, task);
67
+ return { ok: true, warning, resultPath, valid, errors };
15
68
  }
16
69
 
17
- // Default must match where the orchestrator/wrapper actually writes runs:
18
- // <root>/.audit-tools/audit (COR-bf5c7331), not the legacy `.audit-artifacts`.
19
- const artifactsDir = resolveArtifactsDir(process.argv);
70
+ function runCli() {
71
+ const runIdIdx = process.argv.indexOf("--run-id");
72
+ const taskIdIdx = process.argv.indexOf("--task-id");
20
73
 
21
- const sanitized = taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
22
- const resultPath = join(artifactsDir, "runs", runId, "task-results", sanitized + ".json");
74
+ const runId = runIdIdx !== -1 ? process.argv[runIdIdx + 1] : undefined;
75
+ const taskId = taskIdIdx !== -1 ? process.argv[taskIdIdx + 1] : undefined;
23
76
 
24
- if (!existsSync(resultPath)) {
25
- console.error(`File not found: ${resultPath}`);
26
- process.exit(1);
27
- }
77
+ if (!runId || !taskId) {
78
+ console.error("Usage: node dispatch/validate-result.mjs --run-id <run_id> --task-id <task_id> [--artifacts-dir <dir>]");
79
+ process.exit(1);
80
+ }
28
81
 
29
- let resultObj;
30
- try {
31
- resultObj = JSON.parse(readFileSync(resultPath, "utf8"));
32
- } catch (e) {
33
- console.error(`Invalid JSON in ${resultPath}: ${e.message}`);
34
- process.exit(1);
35
- }
82
+ // Default must match where the orchestrator/wrapper actually writes runs:
83
+ // <root>/.audit-tools/audit (COR-bf5c7331), not the legacy `.audit-artifacts`.
84
+ const artifactsDir = resolveArtifactsDir(process.argv);
36
85
 
37
- const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
38
- let task = null;
39
- if (existsSync(tasksPath)) {
40
- try {
41
- const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
42
- task = tasks.find(t => t.task_id === taskId) ?? null;
43
- } catch (e) {
44
- process.stderr.write(`[warn] Could not read pending-audit-tasks.json; line-count validation will be skipped: ${e.message}\n`);
86
+ const result = validateOneResult({ artifactsDir, runId, taskId });
87
+
88
+ if (!result.ok) {
89
+ console.error(result.fatal.message);
90
+ process.exit(1);
91
+ }
92
+
93
+ if (result.warning) {
94
+ process.stderr.write(result.warning + "\n");
45
95
  }
46
- }
47
96
 
48
- const { valid, errors } = validateResult(resultObj, task);
97
+ if (result.valid) {
98
+ console.log("✓ valid:", taskId);
99
+ process.exit(0);
100
+ } else {
101
+ console.error("✗ invalid:", taskId);
102
+ console.error(JSON.stringify(result.errors, null, 2));
103
+ process.exit(1);
104
+ }
105
+ }
49
106
 
50
- if (valid) {
51
- console.log("✓ valid:", taskId);
52
- process.exit(0);
53
- } else {
54
- console.error("✗ invalid:", taskId);
55
- console.error(JSON.stringify(errors, null, 2));
56
- process.exit(1);
107
+ // Only run the CLI when this file is the process entrypoint — not when a test
108
+ // imports validateOneResult()/resolveTaskContext() directly. Compares
109
+ // resolved file:// URLs (not raw argv[1] strings) so this is correct on
110
+ // Windows, where argv[1] is a backslash path that never equals
111
+ // import.meta.url's forward-slash form.
112
+ const isMainModule = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
113
+ if (isMainModule) {
114
+ runCli();
57
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"conceptualDispatch.d.ts","sourceRoot":"","sources":["../../../src/audit/cli/conceptualDispatch.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAUzD,OAAO,EAML,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,SAAS,GAAG,MAAM,CAAC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,WAAW,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC,EAC1D,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,aAAa,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,EACjD,aAAa,EAAE,SAAS,GAAG,MAAM,GAChC,MAAM,CAUR;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,cAAc,GACrB,wBAAwB,CA+B1B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd;;;OAGG;IACH,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,8CAA8C;IAC9C,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,mEAAmE;IACnE,SAAS,EAAE,uBAAuB,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,wBAAwB,CAAC;IACnC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoK9B"}
1
+ {"version":3,"file":"conceptualDispatch.d.ts","sourceRoot":"","sources":["../../../src/audit/cli/conceptualDispatch.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAUzD,OAAO,EAML,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,wBAAwB;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,SAAS,GAAG,MAAM,CAAC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,WAAW,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC,EAC1D,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,aAAa,EAAE,gBAAgB,CAAC,gBAAgB,CAAC,EACjD,aAAa,EAAE,SAAS,GAAG,MAAM,GAChC,MAAM,CAUR;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,cAAc,GACrB,wBAAwB,CA+B1B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAC;IACd;;;OAGG;IACH,qBAAqB,EAAE,MAAM,CAAC;IAC9B,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,8CAA8C;IAC9C,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,mEAAmE;IACnE,SAAS,EAAE,uBAAuB,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,wBAAwB,CAAC;IACnC;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAqM9B"}
@@ -158,7 +158,22 @@ export async function prepareConceptualDispatch(opts) {
158
158
  ...f,
159
159
  promptPath: promptPathFor(f.lane),
160
160
  }));
161
- const perspectiveLines = perspectivePrompts.map((f, i) => ` - Perspective ${i + 1} (${f.name}): prompt \`${f.promptPath}\` → findings \`${f.resultsPath}\``);
161
+ // Resume (COR-4c8bd93a) narrows the INSTRUCTION surface only. writePaths/
162
+ // readPaths/artifactPaths below stay the full, stable perspective set —
163
+ // round identity (conceptual-perspective-round-identity.test.ts) pins a
164
+ // re-emission of the SAME round to re-declare identical bound paths
165
+ // regardless of partial delivery, so they are a stable per-round access
166
+ // declaration, not a must-write list. What resume changes is which lanes
167
+ // the narrative tells the host to actually run: a delivered lane is never
168
+ // re-instructed and its landed submission is never clobbered, because the
169
+ // host is simply never told to write there again.
170
+ const pendingLaneIds = new Set(fanout.pendingLanes.map((lane) => lane.id));
171
+ const pendingPerspectives = perspectivePrompts
172
+ .map((f, i) => ({ ordinal: i + 1, f }))
173
+ .filter(({ f }) => pendingLaneIds.has(f.lane));
174
+ const pendingCount = pendingPerspectives.length;
175
+ const deliveredCount = perspectivePrompts.length - pendingCount;
176
+ const perspectiveLines = pendingPerspectives.map(({ ordinal, f }) => ` - Perspective ${ordinal} (${f.name}): prompt \`${f.promptPath}\` → findings \`${f.resultsPath}\``);
162
177
  const artifactPaths = {
163
178
  conceptual_results: conceptualResultsPath,
164
179
  conceptual_judge_prompt: judgePromptPath,
@@ -173,8 +188,25 @@ export async function prepareConceptualDispatch(opts) {
173
188
  instructionLines: [
174
189
  ...(settings.reuse_notice ? [settings.reuse_notice] : []),
175
190
  `**Conceptual review** (generative, deep — ${total}-perspective fan-out):`,
176
- `1. Execute these ${total} independent perspective lanes one subagent per lane **in parallel** if a subagent facility exists, else sequentially yourself. Each lane reviews only through its own value system and must NOT see the others' output:`,
177
- ...perspectiveLines,
191
+ // A resumed round with some (but not all) perspectives already delivered
192
+ // renders this notice so the host sees, in prose, exactly which lanes are
193
+ // being skipped and why — the mechanical guarantee is the narrowed
194
+ // perspectiveLines/step-1 list below; this line is purely informational,
195
+ // the same "state what changed, never rely on the host noticing" pattern
196
+ // `reuse_notice` above uses for checkpoint reuse.
197
+ ...(deliveredCount > 0
198
+ ? [
199
+ `_${deliveredCount} of ${total} perspective lane(s) already delivered a submission this round — reusing that output, not re-executing them._`,
200
+ ]
201
+ : []),
202
+ ...(pendingCount > 0
203
+ ? [
204
+ `1. Execute ${pendingCount === total ? `these ${total}` : `these ${pendingCount} still-pending`} independent perspective lane(s) — one subagent per lane **in parallel** if a subagent facility exists, else sequentially yourself. Each lane reviews only through its own value system and must NOT see the others' output:`,
205
+ ...perspectiveLines,
206
+ ]
207
+ : [
208
+ `1. All ${total} perspective lanes have already delivered a submission this round — nothing to execute here.`,
209
+ ]),
178
210
  `2. When all ${total} perspectives have written their findings, execute ONE **independent judge** lane — a fresh subagent that is not any of the perspectives when a facility exists; with no facility, execute it yourself as the explicitly-degraded fallback, setting the perspectives' reasoning aside and merging only their written findings: read the prompt at \`${judgePromptPath}\`, write the merged findings to \`${conceptualResultsPath}\`.`,
179
211
  "Each prompt file above is self-contained — it already defines the reviewer's persona, scope, file grants, and output schema. Pass the `prompt_path` to the executor as its instruction verbatim; do NOT restate the persona or re-describe the task in your dispatch message (the parenthesised name is only a label for you).",
180
212
  ],