audit-tools 0.45.0 → 0.46.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 (227) hide show
  1. package/README.md +15 -2
  2. package/dist/audit/cli/charterExtractionPrompt.d.ts.map +1 -1
  3. package/dist/audit/cli/charterExtractionPrompt.js +11 -1
  4. package/dist/audit/cli/charterExtractionPrompt.js.map +1 -1
  5. package/dist/audit/cli/cleanupCommand.d.ts +25 -0
  6. package/dist/audit/cli/cleanupCommand.d.ts.map +1 -1
  7. package/dist/audit/cli/cleanupCommand.js +69 -0
  8. package/dist/audit/cli/cleanupCommand.js.map +1 -1
  9. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  10. package/dist/audit/cli/dispatch/hostHandoff.js +52 -76
  11. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  12. package/dist/audit/cli/dispatch/packetFilter.d.ts +1 -1
  13. package/dist/audit/cli/fanoutLanes.d.ts.map +1 -1
  14. package/dist/audit/cli/fanoutLanes.js +6 -1
  15. package/dist/audit/cli/fanoutLanes.js.map +1 -1
  16. package/dist/audit/cli/laneSubmissions.d.ts +13 -0
  17. package/dist/audit/cli/laneSubmissions.d.ts.map +1 -1
  18. package/dist/audit/cli/laneSubmissions.js +38 -27
  19. package/dist/audit/cli/laneSubmissions.js.map +1 -1
  20. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  21. package/dist/audit/cli/nextStepCommand.js +8 -6
  22. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  23. package/dist/audit/cli/nextStepHelpers.d.ts +3 -2
  24. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  25. package/dist/audit/cli/nextStepHelpers.js +37 -35
  26. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  27. package/dist/audit/cli/reviewRun.d.ts +1 -1
  28. package/dist/audit/cli/reviewRun.d.ts.map +1 -1
  29. package/dist/audit/cli/reviewRun.js +2 -9
  30. package/dist/audit/cli/reviewRun.js.map +1 -1
  31. package/dist/audit/cli/sampleRunCommand.d.ts.map +1 -1
  32. package/dist/audit/cli/sampleRunCommand.js +12 -1
  33. package/dist/audit/cli/sampleRunCommand.js.map +1 -1
  34. package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
  35. package/dist/audit/cli/semanticReviewStep.js +9 -1
  36. package/dist/audit/cli/semanticReviewStep.js.map +1 -1
  37. package/dist/audit/cli/steps.d.ts +6 -6
  38. package/dist/audit/contracts/workerSchemas.d.ts +36 -26
  39. package/dist/audit/contracts/workerSchemas.d.ts.map +1 -1
  40. package/dist/audit/contracts/workerSchemas.js +11 -1
  41. package/dist/audit/contracts/workerSchemas.js.map +1 -1
  42. package/dist/audit/contracts/wrapperResponse.d.ts +52 -28
  43. package/dist/audit/contracts/wrapperResponse.d.ts.map +1 -1
  44. package/dist/audit/contracts/wrapperResponse.js +8 -5
  45. package/dist/audit/contracts/wrapperResponse.js.map +1 -1
  46. package/dist/audit/decompose/sources.d.ts.map +1 -1
  47. package/dist/audit/decompose/sources.js +21 -5
  48. package/dist/audit/decompose/sources.js.map +1 -1
  49. package/dist/audit/extractors/analyzers/registry.d.ts +8 -5
  50. package/dist/audit/extractors/analyzers/registry.d.ts.map +1 -1
  51. package/dist/audit/extractors/analyzers/registry.js +28 -7
  52. package/dist/audit/extractors/analyzers/registry.js.map +1 -1
  53. package/dist/audit/extractors/designAssessment.js +1 -1
  54. package/dist/audit/extractors/gitHistory.d.ts +1 -1
  55. package/dist/audit/extractors/gitHistory.js +1 -1
  56. package/dist/audit/extractors/graph.d.ts +1 -1
  57. package/dist/audit/extractors/graph.js +1 -1
  58. package/dist/audit/extractors/risk.d.ts +1 -1
  59. package/dist/audit/extractors/risk.js +1 -1
  60. package/dist/audit/io/artifacts.d.ts.map +1 -1
  61. package/dist/audit/io/artifacts.js +5 -5
  62. package/dist/audit/io/artifacts.js.map +1 -1
  63. package/dist/audit/orchestrator/acquisitionExecutor.d.ts +11 -6
  64. package/dist/audit/orchestrator/acquisitionExecutor.d.ts.map +1 -1
  65. package/dist/audit/orchestrator/acquisitionExecutor.js +1 -1
  66. package/dist/audit/orchestrator/acquisitionExecutor.js.map +1 -1
  67. package/dist/audit/orchestrator/advance.d.ts +14 -10
  68. package/dist/audit/orchestrator/advance.d.ts.map +1 -1
  69. package/dist/audit/orchestrator/advance.js +49 -24
  70. package/dist/audit/orchestrator/advance.js.map +1 -1
  71. package/dist/audit/orchestrator/advanceTypes.d.ts +14 -0
  72. package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
  73. package/dist/audit/orchestrator/autoFixExecutor.d.ts +20 -1
  74. package/dist/audit/orchestrator/autoFixExecutor.d.ts.map +1 -1
  75. package/dist/audit/orchestrator/autoFixExecutor.js +77 -15
  76. package/dist/audit/orchestrator/autoFixExecutor.js.map +1 -1
  77. package/dist/audit/orchestrator/dependencyMap.js +1 -1
  78. package/dist/audit/orchestrator/dependencyMap.js.map +1 -1
  79. package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
  80. package/dist/audit/orchestrator/executorRunners.js +18 -4
  81. package/dist/audit/orchestrator/executorRunners.js.map +1 -1
  82. package/dist/audit/orchestrator/executors.d.ts +54 -0
  83. package/dist/audit/orchestrator/executors.d.ts.map +1 -1
  84. package/dist/audit/orchestrator/executors.js +352 -0
  85. package/dist/audit/orchestrator/executors.js.map +1 -1
  86. package/dist/audit/orchestrator/hostInputPause.d.ts +20 -11
  87. package/dist/audit/orchestrator/hostInputPause.d.ts.map +1 -1
  88. package/dist/audit/orchestrator/hostInputPause.js +17 -11
  89. package/dist/audit/orchestrator/hostInputPause.js.map +1 -1
  90. package/dist/audit/orchestrator/localCommands.d.ts +24 -1
  91. package/dist/audit/orchestrator/localCommands.d.ts.map +1 -1
  92. package/dist/audit/orchestrator/localCommands.js +44 -9
  93. package/dist/audit/orchestrator/localCommands.js.map +1 -1
  94. package/dist/audit/orchestrator/nextStep.d.ts +13 -0
  95. package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
  96. package/dist/audit/orchestrator/nextStep.js +13 -0
  97. package/dist/audit/orchestrator/nextStep.js.map +1 -1
  98. package/dist/audit/orchestrator/requeueCommand.d.ts +1 -1
  99. package/dist/audit/orchestrator/runtimeCommand.d.ts +32 -1
  100. package/dist/audit/orchestrator/runtimeCommand.d.ts.map +1 -1
  101. package/dist/audit/orchestrator/runtimeCommand.js +90 -3
  102. package/dist/audit/orchestrator/runtimeCommand.js.map +1 -1
  103. package/dist/audit/orchestrator/structureExecutors.js +1 -1
  104. package/dist/audit/orchestrator/structureExecutors.js.map +1 -1
  105. package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts +11 -1
  106. package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts.map +1 -1
  107. package/dist/audit/orchestrator/syntaxResolutionExecutor.js +25 -9
  108. package/dist/audit/orchestrator/syntaxResolutionExecutor.js.map +1 -1
  109. package/dist/audit/supervisor/operatorHandoff.d.ts +0 -1
  110. package/dist/audit/supervisor/operatorHandoff.d.ts.map +1 -1
  111. package/dist/audit/supervisor/operatorHandoff.js +2 -1
  112. package/dist/audit/supervisor/operatorHandoff.js.map +1 -1
  113. package/dist/audit/types.d.ts +18 -18
  114. package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
  115. package/dist/remediate/contractPipeline/derive.js +1 -0
  116. package/dist/remediate/contractPipeline/derive.js.map +1 -1
  117. package/dist/remediate/contractPipeline/semanticProjection.d.ts +5 -4
  118. package/dist/remediate/contractPipeline/semanticProjection.d.ts.map +1 -1
  119. package/dist/remediate/contractPipeline/semanticProjection.js +34 -19
  120. package/dist/remediate/contractPipeline/semanticProjection.js.map +1 -1
  121. package/dist/remediate/index.d.ts.map +1 -1
  122. package/dist/remediate/index.js +3 -2
  123. package/dist/remediate/index.js.map +1 -1
  124. package/dist/remediate/phases/close.d.ts +37 -2
  125. package/dist/remediate/phases/close.d.ts.map +1 -1
  126. package/dist/remediate/phases/close.js +100 -31
  127. package/dist/remediate/phases/close.js.map +1 -1
  128. package/dist/remediate/phases/closeVerifyAnalyzerLeads.js +2 -2
  129. package/dist/remediate/phases/closeVerifyAnalyzerLeads.js.map +1 -1
  130. package/dist/remediate/phases/triage.d.ts.map +1 -1
  131. package/dist/remediate/phases/triage.js +61 -19
  132. package/dist/remediate/phases/triage.js.map +1 -1
  133. package/dist/remediate/steps/contractPipeline.d.ts +13 -0
  134. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  135. package/dist/remediate/steps/contractPipeline.js +82 -3
  136. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  137. package/dist/remediate/steps/dispatch/hostHandoff.d.ts +1 -1
  138. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  139. package/dist/remediate/steps/dispatch/hostHandoff.js +297 -139
  140. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  141. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  142. package/dist/remediate/steps/nextStep.js +37 -7
  143. package/dist/remediate/steps/nextStep.js.map +1 -1
  144. package/dist/remediate/types/options.d.ts +1 -1
  145. package/dist/shared/analyzers/acquisitionEngine.d.ts +61 -28
  146. package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
  147. package/dist/shared/analyzers/acquisitionEngine.js +83 -24
  148. package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
  149. package/dist/shared/analyzers/binaryAcquisition.d.ts +8 -2
  150. package/dist/shared/analyzers/binaryAcquisition.d.ts.map +1 -1
  151. package/dist/shared/analyzers/binaryAcquisition.js +4 -4
  152. package/dist/shared/analyzers/binaryAcquisition.js.map +1 -1
  153. package/dist/shared/constitutionalDocPaths.d.ts.map +1 -1
  154. package/dist/shared/constitutionalDocPaths.js +7 -0
  155. package/dist/shared/constitutionalDocPaths.js.map +1 -1
  156. package/dist/shared/engine/obligationEngine.d.ts +44 -3
  157. package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
  158. package/dist/shared/engine/obligationEngine.js +53 -7
  159. package/dist/shared/engine/obligationEngine.js.map +1 -1
  160. package/dist/shared/findings/dedupe.d.ts.map +1 -1
  161. package/dist/shared/findings/dedupe.js +8 -0
  162. package/dist/shared/findings/dedupe.js.map +1 -1
  163. package/dist/shared/friction/frictionRecord.d.ts +42 -2
  164. package/dist/shared/friction/frictionRecord.d.ts.map +1 -1
  165. package/dist/shared/friction/frictionRecord.js +111 -3
  166. package/dist/shared/friction/frictionRecord.js.map +1 -1
  167. package/dist/shared/friction/triage.d.ts +15 -0
  168. package/dist/shared/friction/triage.d.ts.map +1 -1
  169. package/dist/shared/friction/triage.js +33 -2
  170. package/dist/shared/friction/triage.js.map +1 -1
  171. package/dist/shared/index.d.ts +13 -9
  172. package/dist/shared/index.d.ts.map +1 -1
  173. package/dist/shared/index.js +14 -7
  174. package/dist/shared/index.js.map +1 -1
  175. package/dist/shared/io/fileLock.d.ts.map +1 -1
  176. package/dist/shared/io/fileLock.js +22 -3
  177. package/dist/shared/io/fileLock.js.map +1 -1
  178. package/dist/shared/io/frictionCapture.d.ts +73 -19
  179. package/dist/shared/io/frictionCapture.d.ts.map +1 -1
  180. package/dist/shared/io/frictionCapture.js +21 -30
  181. package/dist/shared/io/frictionCapture.js.map +1 -1
  182. package/dist/shared/io/lockedJsonStore.d.ts +24 -3
  183. package/dist/shared/io/lockedJsonStore.d.ts.map +1 -1
  184. package/dist/shared/io/lockedJsonStore.js +35 -7
  185. package/dist/shared/io/lockedJsonStore.js.map +1 -1
  186. package/dist/shared/io/stepContractWriter.d.ts +3 -3
  187. package/dist/shared/io/stepContractWriter.d.ts.map +1 -1
  188. package/dist/shared/io/stepContractWriter.js +97 -13
  189. package/dist/shared/io/stepContractWriter.js.map +1 -1
  190. package/dist/shared/submission/expectedSubmissions.d.ts +18 -0
  191. package/dist/shared/submission/expectedSubmissions.d.ts.map +1 -1
  192. package/dist/shared/submission/expectedSubmissions.js +17 -0
  193. package/dist/shared/submission/expectedSubmissions.js.map +1 -1
  194. package/dist/shared/submission/handRecovery.d.ts.map +1 -1
  195. package/dist/shared/submission/handRecovery.js +9 -11
  196. package/dist/shared/submission/handRecovery.js.map +1 -1
  197. package/dist/shared/submission/hostHandoffCore.d.ts +155 -0
  198. package/dist/shared/submission/hostHandoffCore.d.ts.map +1 -0
  199. package/dist/shared/submission/hostHandoffCore.js +210 -0
  200. package/dist/shared/submission/hostHandoffCore.js.map +1 -0
  201. package/dist/shared/submission/submissionLedger.d.ts +5 -1
  202. package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
  203. package/dist/shared/submission/submissionLedger.js +33 -3
  204. package/dist/shared/submission/submissionLedger.js.map +1 -1
  205. package/dist/shared/tooling/commandShape.d.ts +29 -0
  206. package/dist/shared/tooling/commandShape.d.ts.map +1 -1
  207. package/dist/shared/tooling/commandShape.js +82 -10
  208. package/dist/shared/tooling/commandShape.js.map +1 -1
  209. package/dist/shared/tooling/exec.d.ts +32 -0
  210. package/dist/shared/tooling/exec.d.ts.map +1 -1
  211. package/dist/shared/tooling/exec.js +189 -0
  212. package/dist/shared/tooling/exec.js.map +1 -1
  213. package/docs/audit-pkg/contracts.md +2 -1
  214. package/docs/audit-pkg/operator-guide.md +5 -2
  215. package/docs/audit-pkg/product.md +22 -6
  216. package/package.json +10 -6
  217. package/schemas/audit-code-v1alpha1.schema.json +297 -0
  218. package/schemas/audit_result.schema.json +4 -0
  219. package/schemas/audit_results.schema.json +4 -0
  220. package/scripts/shared/install-host-assets.mjs +8 -5
  221. package/skills/audit-code/SKILL.md +4 -2
  222. package/skills/audit-code/audit-code.prompt.md +5 -2
  223. package/skills/audit-code/opencode-command-template.txt +1 -1
  224. package/wrapper/audit-code-wrapper-lib.mjs +81 -16
  225. package/wrapper/installer-verb-help.mjs +14 -4
  226. package/docs/audit-pkg/development.md +0 -113
  227. package/docs/audit-pkg/release.md +0 -140
@@ -1,7 +1,6 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { mkdir } from "node:fs/promises";
3
- import { isAbsolute, join, resolve } from "node:path";
4
- import { headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent, assertSubmissionRunId, hashContent, isGitRepo, normalizeRepoPath, readSubmissionDocument, readSubmissionLedger, repoRelativePath, resolveContainedPath, spawnSyncHidden, stableStringify, submissionPathFor, writeJsonFile, } from "audit-tools/shared";
2
+ import { isAbsolute } from "node:path";
3
+ import { AUDIT_TOOLS_DIRNAME, headCommit, commandLeavesDeclaredShape, FindingSchema, SUBMISSION_ISSUE_CODES, SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION, appendSubmissionEvent, compareCodeUnits, contentSha256, hasExactKeys, hostHandoffResultPath, idsAreStrictlyAscending, isCommit, isGitRepo, isRecord, isSha256, normalizeRepoPath, parseAllWorkloadItems, parseCommandString, parseWorkloadEnvelope, promptSha256, readSubmissionDocument, readSubmissionLedger, repoRelativePath, resolveContainedPath, resolveHostHandoffPaths, resultIdentityIsBound, runTrackedAsync, sameStrings, stringArray, spawnSyncHidden, stableStringify, writeJsonFile, } from "audit-tools/shared";
5
4
  import { RemediationHostHandoffRecordSchema, RemediationPlanSchema, isClarificationCategory, } from "../../state/types.js";
6
5
  import { ITEM_STATUSES, isTerminalStatus, isVerifiedCompleteStatus, } from "../../state/itemStatus.js";
7
6
  import { REMEDIATION_HOST_DECISION_CONTRACT_VERSION as DECISION_CONTRACT_VERSION, REMEDIATION_HOST_RESULT_CONTRACT_VERSION as RESULT_CONTRACT_VERSION, REMEDIATION_HOST_WORKLOAD_CONTRACT_VERSION as WORKLOAD_CONTRACT_VERSION, } from "../types.js";
@@ -102,24 +101,6 @@ const CURRENT_ITEM_KEYS = new Set([
102
101
  "started_at",
103
102
  "status",
104
103
  ]);
105
- function compareCodeUnits(left, right) {
106
- return left < right ? -1 : left > right ? 1 : 0;
107
- }
108
- function isRecord(value) {
109
- return value !== null && typeof value === "object" && !Array.isArray(value);
110
- }
111
- function hasExactKeys(value, expected) {
112
- const actual = Object.keys(value).sort(compareCodeUnits);
113
- const canonicalExpected = [...expected].sort(compareCodeUnits);
114
- return (actual.length === canonicalExpected.length &&
115
- actual.every((key, index) => key === canonicalExpected[index]));
116
- }
117
- function isCommit(value) {
118
- return typeof value === "string" && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(value);
119
- }
120
- function isSha256(value) {
121
- return typeof value === "string" && /^[0-9a-f]{64}$/u.test(value);
122
- }
123
104
  function hasOnlyKnownKeys(value, known) {
124
105
  return Object.keys(value).every((key) => known.has(key));
125
106
  }
@@ -325,15 +306,19 @@ function cannotPrepareMessage(root, state, raised) {
325
306
  return `Cannot prepare a remediation host workload: ${messages.join("; ")}`;
326
307
  }
327
308
  function resolveBoundaryPaths(params) {
328
- assertSubmissionRunId(params.runId, "remediation host run id");
329
- const root = resolve(params.root);
330
- const artifactsDir = resolveContainedPath(root, params.artifactsDir, "artifactsDir");
331
- const runDir = resolveContainedPath(artifactsDir, join("runs", params.runId, "implement"), "remediation host run directory");
309
+ // The remediate draw's run dir carries the `implement` lane segment — its
310
+ // runs directory also holds triage/closing lanes — which the core takes as a
311
+ // parameter rather than a fork.
312
+ const core = resolveHostHandoffPaths({
313
+ ...params,
314
+ runDirSegments: ["implement"],
315
+ runIdLabel: "remediation host run id",
316
+ });
332
317
  return {
333
- root,
334
- artifactsDir,
335
- workloadPath: join(runDir, "host-workload.json"),
336
- resultDir: join(runDir, "host-results"),
318
+ root: core.root,
319
+ artifactsDir: core.artifactsDir,
320
+ workloadPath: core.workloadPath,
321
+ resultDir: core.resultDir,
337
322
  };
338
323
  }
339
324
  /**
@@ -342,7 +327,13 @@ function resolveBoundaryPaths(params) {
342
327
  * divergence between them would have been silent on both sides.
343
328
  */
344
329
  function resultPathFor(paths, workItemId) {
345
- return submissionPathFor({ root: paths.root, submissionDir: paths.resultDir }, workItemId);
330
+ return hostHandoffResultPath({
331
+ root: paths.root,
332
+ artifactsDir: paths.artifactsDir,
333
+ runDir: paths.workloadPath.slice(0, paths.workloadPath.length - "host-workload.json".length - 1),
334
+ resultDir: paths.resultDir,
335
+ workloadPath: paths.workloadPath,
336
+ }, workItemId);
346
337
  }
347
338
  /**
348
339
  * Resolve the submission validator the INGEST applies to one work item, plus
@@ -511,15 +502,12 @@ function buildWorkItem(paths, block, baselineCommit, state) {
511
502
  finding_ids: [...block.items],
512
503
  allowed_files: allowedFiles,
513
504
  baseline_commit: baselineCommit,
514
- prompt: { text: promptText, sha256: hashContent(promptText) },
505
+ prompt: { text: promptText, sha256: promptSha256(promptText) },
515
506
  required_tests: requiredTests,
516
507
  result_path: resultPath,
517
508
  token_estimate: block.token_estimate ?? 0,
518
509
  };
519
510
  }
520
- function hostWorkloadSha256(workload) {
521
- return hashContent(stableStringify(workload));
522
- }
523
511
  function buildCanonicalWorkload(params) {
524
512
  const requestedIds = params.workItemIds
525
513
  ? new Set(params.workItemIds)
@@ -566,7 +554,7 @@ function parseWorkItem(value, paths, state, expectedBaselineCommit) {
566
554
  !hasExactKeys(value.prompt, ["sha256", "text"]) ||
567
555
  typeof value.prompt.text !== "string" ||
568
556
  !isSha256(value.prompt.sha256) ||
569
- hashContent(value.prompt.text) !== value.prompt.sha256) {
557
+ promptSha256(value.prompt.text) !== value.prompt.sha256) {
570
558
  return null;
571
559
  }
572
560
  const block = state.plan.blocks.find((candidate) => candidate.block_id === value.id);
@@ -584,40 +572,32 @@ function parseWorkItem(value, paths, state, expectedBaselineCommit) {
584
572
  : null;
585
573
  }
586
574
  function parseWorkload(value, paths, runId, state) {
587
- if (!isRecord(value) ||
588
- !hasExactKeys(value, ["contract_version", "run_id", "work_items"]) ||
589
- value.contract_version !== WORKLOAD_CONTRACT_VERSION ||
590
- value.run_id !== runId ||
591
- !Array.isArray(value.work_items)) {
575
+ // Envelope (keys, contract version, run id, items array) is the CORE's check.
576
+ const envelope = parseWorkloadEnvelope(value, {
577
+ contractVersion: WORKLOAD_CONTRACT_VERSION,
578
+ runId,
579
+ });
580
+ if (!envelope.ok)
592
581
  return null;
593
- }
594
582
  const binding = state.host_handoff;
595
583
  if (binding &&
596
584
  (binding.run_id !== runId ||
597
- hostWorkloadSha256(value) !==
585
+ contentSha256(value) !==
598
586
  binding.workload_sha256)) {
599
587
  return null;
600
588
  }
601
- const workItems = value.work_items.map((item) => parseWorkItem(item, paths, state, binding?.baseline_commit));
602
- if (workItems.some((item) => item === null))
589
+ const workItems = parseAllWorkloadItems(envelope.rawItems, (item) => parseWorkItem(item, paths, state, binding?.baseline_commit));
590
+ if (workItems === null)
603
591
  return null;
604
592
  const ids = workItems.map((item) => item.id);
605
- if (new Set(ids).size !== ids.length ||
606
- ids.some((id, index) => index > 0 && compareCodeUnits(ids[index - 1], id) >= 0) ||
593
+ // Strictly ascending covers BOTH "sorted" and "duplicate-free" — the two
594
+ // properties the byte-for-byte re-derivation comparison needs.
595
+ if (!idsAreStrictlyAscending(ids) ||
607
596
  (binding !== undefined && !sameStrings(ids, binding.work_item_ids))) {
608
597
  return null;
609
598
  }
610
599
  return value;
611
600
  }
612
- function stringArray(value) {
613
- return Array.isArray(value) && value.every((entry) => typeof entry === "string")
614
- ? value
615
- : null;
616
- }
617
- function sameStrings(left, right) {
618
- return (left.length === right.length &&
619
- left.every((entry, index) => entry === right[index]));
620
- }
621
601
  function invalidResult(reason) {
622
602
  return { ok: false, reason };
623
603
  }
@@ -631,11 +611,11 @@ function parseResult(value, runId, workItem) {
631
611
  "prompt_sha256",
632
612
  "outcome",
633
613
  ]) ||
634
- typeof value.result_id !== "string" ||
635
- value.result_id.length === 0 ||
636
- value.run_id !== runId ||
637
- value.work_item_id !== workItem.id ||
638
- value.prompt_sha256 !== workItem.prompt.sha256 ||
614
+ !resultIdentityIsBound(value, {
615
+ runId,
616
+ workItemId: workItem.id,
617
+ promptSha256: workItem.prompt.sha256,
618
+ }) ||
639
619
  !isRecord(value.outcome) ||
640
620
  typeof value.outcome.status !== "string") {
641
621
  return invalidResult("decision must match the exact current run, work-item, and prompt binding");
@@ -690,11 +670,11 @@ function parseResult(value, runId, workItem) {
690
670
  "worktree_evidence",
691
671
  ]) ||
692
672
  value.contract_version !== RESULT_CONTRACT_VERSION ||
693
- typeof value.result_id !== "string" ||
694
- value.result_id.length === 0 ||
695
- value.run_id !== runId ||
696
- value.work_item_id !== workItem.id ||
697
- value.prompt_sha256 !== workItem.prompt.sha256) {
673
+ !resultIdentityIsBound(value, {
674
+ runId,
675
+ workItemId: workItem.id,
676
+ promptSha256: workItem.prompt.sha256,
677
+ })) {
698
678
  return invalidResult("result must match the exact current contract, run, work-item, and prompt binding");
699
679
  }
700
680
  const changedFiles = stringArray(value.changed_files);
@@ -797,6 +777,134 @@ function gitChangedFilesOfCommit(root, commit) {
797
777
  return null;
798
778
  return [...new Set((result.stdout ?? "").split("\0").filter(Boolean))].sort(compareCodeUnits);
799
779
  }
780
+ /**
781
+ * Every repo-relative path the tree shows as touched since `baseline` — the
782
+ * commits baseline→HEAD, the working tree's own deviation from HEAD (staged and
783
+ * unstaged alike), and the untracked files git considers repository content.
784
+ *
785
+ * All three legs are needed to falsify a no-change claim, and they enumerate the
786
+ * three ways a host can have edited: committed (leg 1), edited a TRACKED file
787
+ * and left it uncommitted (leg 2), and CREATED a file (leg 3). A new `src/*.ts`
788
+ * is a real edit and the most natural shape a remediation takes; without leg 3
789
+ * the cheapest way to smuggle one past a no-change claim was simply never to
790
+ * `git add` it. `null` means git could not answer, which callers must treat as
791
+ * "cannot corroborate" rather than as "nothing changed".
792
+ *
793
+ * The untracked leg honours `--exclude-standard`, so it enumerates only what git
794
+ * itself treats as content — a repo's `.gitignore`d build and coverage output is
795
+ * already invisible to it. Two exemptions cover the remainder, both ground
796
+ * truth rather than the host's word:
797
+ *
798
+ * - THIS TOOL'S OWN ARTIFACT TREE ({@link AUDIT_TOOLS_DIRNAME}) is subtracted
799
+ * here, explicitly. In a real repository the tool writes a managed
800
+ * `.gitignore` block covering it, so it never reaches this probe at all; the
801
+ * explicit subtraction is what makes that independent of whether the block
802
+ * has been written yet, so a bare root (a fixture, a first run) cannot
803
+ * manufacture a false refusal out of the tool's own workload, prompt and
804
+ * result documents.
805
+ * - PRE-EXISTING untracked strays are excused by the caller's `excusedPaths`,
806
+ * for free: `run_start_dirty` is captured from `stagedAndUntracked` before
807
+ * any remediation edit exists, so it already enumerates untracked files.
808
+ * What survives both is an untracked file that appeared DURING the run — the
809
+ * only untracked class that can be this host's edit.
810
+ */
811
+ function gitChangedFilesSince(root, baseline) {
812
+ const files = new Set();
813
+ for (const args of [
814
+ // baseline → HEAD: what the host committed.
815
+ ["diff", "--name-only", "-z", baseline, "HEAD"],
816
+ // HEAD → working tree: what the host edited and did not commit.
817
+ ["diff", "--name-only", "-z", "HEAD"],
818
+ // Never added: what the host CREATED. `--exclude-standard` keeps git's own
819
+ // ignore rules authoritative.
820
+ ["ls-files", "--others", "--exclude-standard", "-z"],
821
+ ]) {
822
+ const probe = spawnSyncHidden("git", args, {
823
+ cwd: root,
824
+ encoding: "utf8",
825
+ shell: false,
826
+ });
827
+ if (probe.error || probe.status !== 0)
828
+ return null;
829
+ for (const file of (probe.stdout ?? "").split("\0").filter(Boolean)) {
830
+ if (isAuditToolsArtifactPath(file))
831
+ continue;
832
+ files.add(file);
833
+ }
834
+ }
835
+ return [...files].sort(compareCodeUnits);
836
+ }
837
+ /** Whether a repo-relative path lives inside this tool's own artifact tree. */
838
+ function isAuditToolsArtifactPath(path) {
839
+ const normalized = normalizeRepoPath(path);
840
+ return (normalized === AUDIT_TOOLS_DIRNAME ||
841
+ normalized.startsWith(`${AUDIT_TOOLS_DIRNAME}/`));
842
+ }
843
+ /**
844
+ * Corroborate an explicit `resolved_no_change` decision against the repository.
845
+ *
846
+ * A no-change decision used to be accepted on its evidence STRINGS alone, with
847
+ * only the required tests re-run — so a host that had in fact edited and then
848
+ * declared "nothing to do" was recorded as verified-no-change, and the edit
849
+ * rode into the run unattributed. The claim is mechanically falsifiable, so it
850
+ * is FALSIFIED.
851
+ *
852
+ * The scope of the falsification is the FULL write-scope corroboration every
853
+ * other acceptance path gets, NOT a narrowing to this item's `allowed_files`.
854
+ * `corroborateHostResult` refuses a landed commit that touched anything outside
855
+ * `allowed_files`; a no-change decision that narrowed the check to files INSIDE
856
+ * `allowed_files` would be the inverse rule — the out-of-scope edit, the more
857
+ * serious of the two, would be the one silently admitted. So EVERY path the
858
+ * tree shows as moved since the workload baseline refuses the claim.
859
+ *
860
+ * `excusedPaths` is the only exemption, and it is ground truth rather than the
861
+ * host's word: `run_start_dirty` (already dirty before the run began, so not
862
+ * evidence that this host edited anything — and because it is captured from
863
+ * `stagedAndUntracked`, it excuses pre-existing UNTRACKED strays too) unioned
864
+ * with the accepted edit surface — `applied_edit_surface` plus whatever this
865
+ * same ingest has already corroborated and accepted, so a sibling work item's
866
+ * legitimately landed files do not falsify this item's claim.
867
+ *
868
+ * Fails CLOSED, like every other corroboration here: a git that cannot answer
869
+ * refuses the claim rather than admitting it.
870
+ */
871
+ function corroborateNoChangeClaim(params) {
872
+ const { root, workItem, excusedPaths } = params;
873
+ if (!isGitRepo(root))
874
+ return { ok: true };
875
+ const baseline = workItem.baseline_commit;
876
+ if (!gitCommitExists(root, baseline)) {
877
+ return {
878
+ ok: false,
879
+ code: "commit_missing",
880
+ message: "resolved_no_change cannot be corroborated: baseline_commit does not resolve to a real commit",
881
+ };
882
+ }
883
+ const changed = gitChangedFilesSince(root, baseline);
884
+ if (changed === null) {
885
+ return {
886
+ ok: false,
887
+ code: "commit_missing",
888
+ message: "resolved_no_change cannot be corroborated: git could not enumerate the changes since the workload baseline",
889
+ };
890
+ }
891
+ const violating = changed.filter((path) => !excusedPaths.has(normalizeRepoPath(path)));
892
+ if (violating.length > 0) {
893
+ const inScope = violating.filter((path) => workItem.allowed_files.includes(path));
894
+ const outOfScope = violating.filter((path) => !workItem.allowed_files.includes(path));
895
+ return {
896
+ ok: false,
897
+ code: "changed_files_mismatch",
898
+ message: "resolved_no_change is contradicted by the tree — these files changed since the " +
899
+ `workload baseline: ${violating.join(", ")}` +
900
+ (outOfScope.length > 0
901
+ ? ` (outside the prompt-bound allowed_files: ${outOfScope.join(", ")}` +
902
+ (inScope.length > 0 ? `; inside: ${inScope.join(", ")})` : ")")
903
+ : ""),
904
+ };
905
+ }
906
+ return { ok: true };
907
+ }
800
908
  /**
801
909
  * Per-command deadline. A required test is host-authored and may legitimately be
802
910
  * a full suite, so the bound is generous; what changed is that hitting it is now
@@ -865,10 +973,19 @@ function requiredTestVerdictKey(root, command) {
865
973
  * first-class outcome of this function, and an outcome that can only be reached
866
974
  * by waiting ten real minutes is an outcome nothing ever tests.
867
975
  */
868
- export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT_MS) {
869
- const result = spawnSync(command, {
976
+ export async function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT_MS) {
977
+ // AWAITED, never `spawnSync`: ingestion runs with the remediation state lock
978
+ // held, and a synchronous child blocks the event loop for the whole suite —
979
+ // starving the lock's mtime heartbeat until a LIVE lock is classified stale
980
+ // and stolen mid-ingest.
981
+ //
982
+ // argv + `shell: false`, never a shell string. A required test is a workload
983
+ // command that already cleared the declared-shape gate at the producer, so
984
+ // splitting it is unambiguous, and dropping the shell removes the last place
985
+ // an ingest hands a declared string to `sh`/`cmd.exe`. `resolveExecArgv`
986
+ // inside the runner is what keeps the npm/npx shims resolvable on win32.
987
+ const result = await runTrackedAsync(parseCommandString(command), {
870
988
  cwd: root,
871
- shell: true,
872
989
  // Captured, not discarded: without it a red ingest reports that something
873
990
  // failed and nothing about why.
874
991
  encoding: "utf8",
@@ -920,7 +1037,7 @@ export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT
920
1037
  // `status` with `signal` null — so this branch is unreachable on the platform
921
1038
  // this repo runs its suites on, and no test exercises it. It is kept because
922
1039
  // the runner is OS-agnostic by contract, not because it has been observed.
923
- if (result.signal !== null) {
1040
+ if (result.signal !== null && result.signal !== undefined) {
924
1041
  return {
925
1042
  command,
926
1043
  outcome: "failed",
@@ -941,7 +1058,7 @@ export function runRequiredTest(root, command, timeoutMs = REQUIRED_TEST_TIMEOUT
941
1058
  }
942
1059
  return null;
943
1060
  }
944
- function rerunRequiredTests(root, commands,
1061
+ async function rerunRequiredTests(root, commands,
945
1062
  /** `null` on the normal lane — see {@link RemediationRequiredTestVerdicts}. */
946
1063
  verdicts) {
947
1064
  const failures = [];
@@ -962,7 +1079,7 @@ verdicts) {
962
1079
  }
963
1080
  continue;
964
1081
  }
965
- const failure = runRequiredTest(root, command);
1082
+ const failure = await runRequiredTest(root, command);
966
1083
  if (failure !== null)
967
1084
  failures.push(failure);
968
1085
  }
@@ -1028,11 +1145,11 @@ export async function precomputeRecoveryTestVerdicts(params) {
1028
1145
  }
1029
1146
  }
1030
1147
  for (const command of commands) {
1031
- verdicts.set(requiredTestVerdictKey(paths.root, command), runRequiredTest(paths.root, command));
1148
+ verdicts.set(requiredTestVerdictKey(paths.root, command), await runRequiredTest(paths.root, command));
1032
1149
  }
1033
1150
  return verdicts;
1034
1151
  }
1035
- function corroborateHostResult(params) {
1152
+ async function corroborateHostResult(params) {
1036
1153
  const { root, state, workItem, result, verdicts } = params;
1037
1154
  const baseline = workItem.baseline_commit;
1038
1155
  const landed = result.commit_evidence.after;
@@ -1103,7 +1220,7 @@ function corroborateHostResult(params) {
1103
1220
  message: `landed files overlap pre-existing run-start dirt: ${dirtyOverlap.join(", ")}`,
1104
1221
  };
1105
1222
  }
1106
- const failedTests = rerunRequiredTests(root, workItem.required_tests, verdicts);
1223
+ const failedTests = await rerunRequiredTests(root, workItem.required_tests, verdicts);
1107
1224
  if (failedTests.length > 0) {
1108
1225
  const issue = requiredTestIssue(workItem, failedTests);
1109
1226
  return { ok: false, code: issue.code, message: issue.message };
@@ -1163,7 +1280,7 @@ export async function prepareRemediationHostHandoff(params) {
1163
1280
  .map((issue) => issue.message)
1164
1281
  .join("; ")}`);
1165
1282
  }
1166
- const workloadDigest = hostWorkloadSha256(workload);
1283
+ const workloadDigest = contentSha256(workload);
1167
1284
  if (existingRecord &&
1168
1285
  existingRecord.workload_sha256 !== workloadDigest) {
1169
1286
  throw new Error("Trusted remediation host workload no longer matches the persisted state binding");
@@ -1312,7 +1429,13 @@ export async function ingestRemediationHostResults(params) {
1312
1429
  // the same acceptance.
1313
1430
  let recordedRecoveryMarks = null;
1314
1431
  const landedFiles = new Set(nextState.applied_edit_surface ?? []);
1315
- const requireRepositoryCorroboration = state.host_handoff !== undefined || isGitRepo(paths.root);
1432
+ // Can this ingest be corroborated against ground truth AT ALL? A git root
1433
+ // supplies the tree; a persisted `host_handoff` supplies the trusted binding.
1434
+ // With NEITHER there is nothing to check a host's claim against, and the
1435
+ // remaining evidence is the host's own attestation — which is exactly the
1436
+ // claim under test. That branch is REFUSED for both result and decision
1437
+ // documents rather than admitted on the attestation alone.
1438
+ const canCorroborate = state.host_handoff !== undefined || isGitRepo(paths.root);
1316
1439
  for (const workItem of workload.work_items) {
1317
1440
  const pendingItems = workItem.finding_ids.filter((findingId) => nextState.items[findingId]?.status === "pending");
1318
1441
  if (pendingItems.length === 0)
@@ -1372,7 +1495,36 @@ export async function ingestRemediationHostResults(params) {
1372
1495
  const result = parsed.result;
1373
1496
  const outcome = result.outcome;
1374
1497
  if (outcome.status === "resolved_no_change") {
1375
- const failedTests = rerunRequiredTests(paths.root, workItem.required_tests, requiredTestVerdicts);
1498
+ if (!canCorroborate) {
1499
+ issues.push({
1500
+ code: "trusted_binding_missing",
1501
+ work_item_id: workItem.id,
1502
+ result_path: workItem.result_path,
1503
+ message: "resolved_no_change needs a git root or a persisted host_handoff binding to corroborate the write scope against; attestation-only acceptance is refused",
1504
+ });
1505
+ continue;
1506
+ }
1507
+ const noChange = corroborateNoChangeClaim({
1508
+ root: paths.root,
1509
+ workItem,
1510
+ // Ground truth only: pre-existing dirt plus the edit surface this run
1511
+ // has already corroborated and accepted (`landedFiles` starts from
1512
+ // `applied_edit_surface` and grows as this same ingest accepts).
1513
+ excusedPaths: new Set([
1514
+ ...(state.run_start_dirty ?? []).map(normalizeRepoPath),
1515
+ ...[...landedFiles].map(normalizeRepoPath),
1516
+ ]),
1517
+ });
1518
+ if (!noChange.ok) {
1519
+ issues.push({
1520
+ code: noChange.code,
1521
+ work_item_id: workItem.id,
1522
+ result_path: workItem.result_path,
1523
+ message: noChange.message,
1524
+ });
1525
+ continue;
1526
+ }
1527
+ const failedTests = await rerunRequiredTests(paths.root, workItem.required_tests, requiredTestVerdicts);
1376
1528
  if (failedTests.length > 0) {
1377
1529
  issues.push(requiredTestIssue(workItem, failedTests));
1378
1530
  continue;
@@ -1414,76 +1566,82 @@ export async function ingestRemediationHostResults(params) {
1414
1566
  continue;
1415
1567
  }
1416
1568
  const result = parsed.result;
1417
- let acceptedFiles = result.changed_files;
1418
- if (requireRepositoryCorroboration) {
1419
- const corroborated = corroborateHostResult({
1420
- root: paths.root,
1421
- state,
1422
- workItem,
1423
- result,
1424
- verdicts: requiredTestVerdicts,
1425
- recovery: params.recovery !== undefined,
1569
+ if (!canCorroborate) {
1570
+ issues.push({
1571
+ code: "trusted_binding_missing",
1572
+ work_item_id: workItem.id,
1573
+ result_path: workItem.result_path,
1574
+ message: "a landed result needs a git root or a persisted host_handoff binding to corroborate the write scope against; attestation-only acceptance is refused",
1426
1575
  });
1427
- if (!corroborated.ok) {
1576
+ continue;
1577
+ }
1578
+ const corroborated = await corroborateHostResult({
1579
+ root: paths.root,
1580
+ state,
1581
+ workItem,
1582
+ result,
1583
+ verdicts: requiredTestVerdicts,
1584
+ recovery: params.recovery !== undefined,
1585
+ });
1586
+ if (!corroborated.ok) {
1587
+ issues.push({
1588
+ code: corroborated.code,
1589
+ work_item_id: workItem.id,
1590
+ result_path: workItem.result_path,
1591
+ message: corroborated.message,
1592
+ });
1593
+ continue;
1594
+ }
1595
+ if (corroborated.usedRecovery) {
1596
+ // No acceptance without a record. The mark goes down BEFORE the item is
1597
+ // marked resolved, and an append that throws refuses this item rather
1598
+ // than landing an acceptance the ledger cannot account for — the run
1599
+ // must never read as one that never drifted. The refusal is per item:
1600
+ // an unwritable ledger is not a reason to discard the whole ingest.
1601
+ try {
1602
+ recordedRecoveryMarks ??= (await readSubmissionLedger(paths.artifactsDir)).filter((event) => event.kind === "accepted_via_recovery");
1603
+ // The mark's identity is (run, item, LANDED COMMIT), not just
1604
+ // (run, item): an item re-opened and later re-accepted from a
1605
+ // DIFFERENT landing is a different relaxed acceptance and earns its
1606
+ // own record. Only a retry of the SAME landing is a duplicate. The
1607
+ // landed sha is matched inside the message because the shared event
1608
+ // contract carries no commit field, and a 40-hex sha this writer
1609
+ // itself emitted is an unambiguous token to match on.
1610
+ const landedCommit = result.commit_evidence.after;
1611
+ const alreadyMarked = recordedRecoveryMarks.some((event) => event.run_id === params.runId &&
1612
+ event.submission_id === workItem.id &&
1613
+ (event.message ?? "").includes(landedCommit));
1614
+ if (!alreadyMarked) {
1615
+ const event = {
1616
+ contract_version: SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION,
1617
+ run_id: params.runId,
1618
+ submission_id: workItem.id,
1619
+ lane: workItem.id,
1620
+ kind: "accepted_via_recovery",
1621
+ // Derived from what was actually probed, never asserted: the
1622
+ // baseline was found in no ref and unreachable from HEAD.
1623
+ message: `accepted under recovery: the trusted baseline ${workItem.baseline_commit} is ` +
1624
+ "contained by no ref and unreachable from HEAD, so landed commit " +
1625
+ `${landedCommit} was corroborated against HEAD and this ` +
1626
+ "item's bound scope instead of against baseline ancestry",
1627
+ recorded_at: new Date().toISOString(),
1628
+ };
1629
+ await appendSubmissionEvent(paths.artifactsDir, event);
1630
+ recordedRecoveryMarks.push(event);
1631
+ }
1632
+ }
1633
+ catch (error) {
1428
1634
  issues.push({
1429
- code: corroborated.code,
1635
+ code: "recovery_unrecorded",
1430
1636
  work_item_id: workItem.id,
1431
1637
  result_path: workItem.result_path,
1432
- message: corroborated.message,
1638
+ message: "the recovery acceptance could not be recorded on the submission ledger, so it " +
1639
+ `was refused: ${error instanceof Error ? error.message : String(error)}`,
1433
1640
  });
1434
1641
  continue;
1435
1642
  }
1436
- if (corroborated.usedRecovery) {
1437
- // No acceptance without a record. The mark goes down BEFORE the item is
1438
- // marked resolved, and an append that throws refuses this item rather
1439
- // than landing an acceptance the ledger cannot account for — the run
1440
- // must never read as one that never drifted. The refusal is per item:
1441
- // an unwritable ledger is not a reason to discard the whole ingest.
1442
- try {
1443
- recordedRecoveryMarks ??= (await readSubmissionLedger(paths.artifactsDir)).filter((event) => event.kind === "accepted_via_recovery");
1444
- // The mark's identity is (run, item, LANDED COMMIT), not just
1445
- // (run, item): an item re-opened and later re-accepted from a
1446
- // DIFFERENT landing is a different relaxed acceptance and earns its
1447
- // own record. Only a retry of the SAME landing is a duplicate. The
1448
- // landed sha is matched inside the message because the shared event
1449
- // contract carries no commit field, and a 40-hex sha this writer
1450
- // itself emitted is an unambiguous token to match on.
1451
- const landedCommit = result.commit_evidence.after;
1452
- const alreadyMarked = recordedRecoveryMarks.some((event) => event.run_id === params.runId &&
1453
- event.submission_id === workItem.id &&
1454
- (event.message ?? "").includes(landedCommit));
1455
- if (!alreadyMarked) {
1456
- const event = {
1457
- contract_version: SUBMISSION_LEDGER_EVENT_CONTRACT_VERSION,
1458
- run_id: params.runId,
1459
- submission_id: workItem.id,
1460
- lane: workItem.id,
1461
- kind: "accepted_via_recovery",
1462
- // Derived from what was actually probed, never asserted: the
1463
- // baseline was found in no ref and unreachable from HEAD.
1464
- message: `accepted under recovery: the trusted baseline ${workItem.baseline_commit} is ` +
1465
- "contained by no ref and unreachable from HEAD, so landed commit " +
1466
- `${landedCommit} was corroborated against HEAD and this ` +
1467
- "item's bound scope instead of against baseline ancestry",
1468
- recorded_at: new Date().toISOString(),
1469
- };
1470
- await appendSubmissionEvent(paths.artifactsDir, event);
1471
- recordedRecoveryMarks.push(event);
1472
- }
1473
- }
1474
- catch (error) {
1475
- issues.push({
1476
- code: "recovery_unrecorded",
1477
- work_item_id: workItem.id,
1478
- result_path: workItem.result_path,
1479
- message: "the recovery acceptance could not be recorded on the submission ledger, so it " +
1480
- `was refused: ${error instanceof Error ? error.message : String(error)}`,
1481
- });
1482
- continue;
1483
- }
1484
- }
1485
- acceptedFiles = corroborated.changedFiles;
1486
1643
  }
1644
+ const acceptedFiles = corroborated.changedFiles;
1487
1645
  const completedAt = new Date().toISOString();
1488
1646
  for (const findingId of pendingItems) {
1489
1647
  const item = nextState.items[findingId];