audit-tools 0.52.4 → 0.53.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 (348) hide show
  1. package/dist/audit/cli/auditStep.d.ts +1 -1
  2. package/dist/audit/cli/dispatch/hostHandoff.d.ts +2 -0
  3. package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
  4. package/dist/audit/cli/dispatch/hostHandoff.js +1 -1
  5. package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
  6. package/dist/audit/cli/dispatch/packetFilter.d.ts +1 -1
  7. package/dist/audit/cli/dispatch/packetFilter.d.ts.map +1 -1
  8. package/dist/audit/cli/dispatch/packetFilter.js +1 -1
  9. package/dist/audit/cli/dispatch/packetFilter.js.map +1 -1
  10. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  11. package/dist/audit/cli/nextStepCommand.js +42 -20
  12. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  13. package/dist/audit/cli/nextStepHelpers.d.ts +3 -3
  14. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  15. package/dist/audit/cli/nextStepHelpers.js +50 -75
  16. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  17. package/dist/audit/cli/prompts.d.ts +1 -2
  18. package/dist/audit/cli/prompts.d.ts.map +1 -1
  19. package/dist/audit/cli/prompts.js +2 -12
  20. package/dist/audit/cli/prompts.js.map +1 -1
  21. package/dist/audit/cli/sampleRunCommand.d.ts +2 -2
  22. package/dist/audit/cli/semanticReviewStep.d.ts +11 -2
  23. package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
  24. package/dist/audit/cli/semanticReviewStep.js +14 -10
  25. package/dist/audit/cli/semanticReviewStep.js.map +1 -1
  26. package/dist/audit/contracts/workerSchemas.d.ts +14 -14
  27. package/dist/audit/contracts/wrapperResponse.d.ts +6 -6
  28. package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
  29. package/dist/audit/extractors/graphRoutes.js +1 -3
  30. package/dist/audit/extractors/graphRoutes.js.map +1 -1
  31. package/dist/audit/io/artifacts.d.ts.map +1 -1
  32. package/dist/audit/io/artifacts.js +2 -3
  33. package/dist/audit/io/artifacts.js.map +1 -1
  34. package/dist/audit/orchestrator/architectureDiscovery.d.ts +11 -0
  35. package/dist/audit/orchestrator/architectureDiscovery.d.ts.map +1 -0
  36. package/dist/audit/orchestrator/architectureDiscovery.js +174 -0
  37. package/dist/audit/orchestrator/architectureDiscovery.js.map +1 -0
  38. package/dist/audit/orchestrator/designReviewProjection.d.ts +2 -6
  39. package/dist/audit/orchestrator/designReviewProjection.d.ts.map +1 -1
  40. package/dist/audit/orchestrator/designReviewProjection.js +2 -6
  41. package/dist/audit/orchestrator/designReviewProjection.js.map +1 -1
  42. package/dist/audit/orchestrator/designReviewSnapshot.d.ts +2 -3
  43. package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
  44. package/dist/audit/orchestrator/designReviewSnapshot.js +2 -3
  45. package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
  46. package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
  47. package/dist/audit/orchestrator/executorRunners.js +1 -22
  48. package/dist/audit/orchestrator/executorRunners.js.map +1 -1
  49. package/dist/audit/orchestrator/executors.d.ts.map +1 -1
  50. package/dist/audit/orchestrator/executors.js +1 -13
  51. package/dist/audit/orchestrator/executors.js.map +1 -1
  52. package/dist/audit/orchestrator/nextStep.d.ts +2 -28
  53. package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
  54. package/dist/audit/orchestrator/nextStep.js +5 -37
  55. package/dist/audit/orchestrator/nextStep.js.map +1 -1
  56. package/dist/audit/orchestrator/obligationDerive.d.ts +2 -3
  57. package/dist/audit/orchestrator/obligationDerive.d.ts.map +1 -1
  58. package/dist/audit/orchestrator/obligationDerive.js +2 -3
  59. package/dist/audit/orchestrator/obligationDerive.js.map +1 -1
  60. package/dist/audit/orchestrator/pendingTasks.d.ts +2 -0
  61. package/dist/audit/orchestrator/pendingTasks.d.ts.map +1 -1
  62. package/dist/audit/orchestrator/pendingTasks.js +5 -1
  63. package/dist/audit/orchestrator/pendingTasks.js.map +1 -1
  64. package/dist/audit/orchestrator/planningExecutors.d.ts.map +1 -1
  65. package/dist/audit/orchestrator/planningExecutors.js +9 -0
  66. package/dist/audit/orchestrator/planningExecutors.js.map +1 -1
  67. package/dist/audit/orchestrator/requeueCommand.d.ts +1 -1
  68. package/dist/audit/orchestrator/resultBaseline.d.ts +5 -5
  69. package/dist/audit/orchestrator/resultBaseline.d.ts.map +1 -1
  70. package/dist/audit/orchestrator/resultBaseline.js +24 -47
  71. package/dist/audit/orchestrator/resultBaseline.js.map +1 -1
  72. package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.d.ts.map +1 -1
  73. package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.js +2 -0
  74. package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.js.map +1 -1
  75. package/dist/audit/orchestrator/state.d.ts.map +1 -1
  76. package/dist/audit/orchestrator/state.js +2 -0
  77. package/dist/audit/orchestrator/state.js.map +1 -1
  78. package/dist/audit/orchestrator/synthesisExecutors.d.ts.map +1 -1
  79. package/dist/audit/orchestrator/synthesisExecutors.js +3 -0
  80. package/dist/audit/orchestrator/synthesisExecutors.js.map +1 -1
  81. package/dist/audit/types/auditState.d.ts +6 -6
  82. package/dist/audit/types/flowCoverage.d.ts +6 -6
  83. package/dist/audit/types.d.ts +12 -12
  84. package/dist/remediate/contractPipeline/executionPlan.d.ts +1908 -0
  85. package/dist/remediate/contractPipeline/executionPlan.d.ts.map +1 -0
  86. package/dist/remediate/contractPipeline/executionPlan.js +467 -0
  87. package/dist/remediate/contractPipeline/executionPlan.js.map +1 -0
  88. package/dist/remediate/contractPipeline/runtimePlanAuthority.d.ts +12 -0
  89. package/dist/remediate/contractPipeline/runtimePlanAuthority.d.ts.map +1 -0
  90. package/dist/remediate/contractPipeline/runtimePlanAuthority.js +41 -0
  91. package/dist/remediate/contractPipeline/runtimePlanAuthority.js.map +1 -0
  92. package/dist/remediate/dedup/crossLensDedup.d.ts +3 -16
  93. package/dist/remediate/dedup/crossLensDedup.d.ts.map +1 -1
  94. package/dist/remediate/dedup/crossLensDedup.js +2 -32
  95. package/dist/remediate/dedup/crossLensDedup.js.map +1 -1
  96. package/dist/remediate/index.d.ts +2 -19
  97. package/dist/remediate/index.d.ts.map +1 -1
  98. package/dist/remediate/index.js +22 -138
  99. package/dist/remediate/index.js.map +1 -1
  100. package/dist/remediate/intake.d.ts +10 -10
  101. package/dist/remediate/intent/intentOrdering.d.ts +6 -8
  102. package/dist/remediate/intent/intentOrdering.d.ts.map +1 -1
  103. package/dist/remediate/intent/intentOrdering.js +24 -47
  104. package/dist/remediate/intent/intentOrdering.js.map +1 -1
  105. package/dist/remediate/phases/close.d.ts +25 -17
  106. package/dist/remediate/phases/close.d.ts.map +1 -1
  107. package/dist/remediate/phases/close.js +278 -218
  108. package/dist/remediate/phases/close.js.map +1 -1
  109. package/dist/remediate/phases/closeAcceptance.d.ts +42 -0
  110. package/dist/remediate/phases/closeAcceptance.d.ts.map +1 -0
  111. package/dist/remediate/phases/closeAcceptance.js +66 -0
  112. package/dist/remediate/phases/closeAcceptance.js.map +1 -0
  113. package/dist/remediate/phases/closeReviewProvenance.d.ts +1 -1
  114. package/dist/remediate/phases/closeReviewProvenance.d.ts.map +1 -1
  115. package/dist/remediate/phases/closeReviewProvenance.js +9 -8
  116. package/dist/remediate/phases/closeReviewProvenance.js.map +1 -1
  117. package/dist/remediate/phases/closeVerifyAnalyzerLeads.d.ts.map +1 -1
  118. package/dist/remediate/phases/closeVerifyAnalyzerLeads.js +11 -9
  119. package/dist/remediate/phases/closeVerifyAnalyzerLeads.js.map +1 -1
  120. package/dist/remediate/phases/closeVerifyHeadEvidence.d.ts +21 -86
  121. package/dist/remediate/phases/closeVerifyHeadEvidence.d.ts.map +1 -1
  122. package/dist/remediate/phases/closeVerifyHeadEvidence.js +30 -19
  123. package/dist/remediate/phases/closeVerifyHeadEvidence.js.map +1 -1
  124. package/dist/remediate/phases/plan.d.ts +3 -60
  125. package/dist/remediate/phases/plan.d.ts.map +1 -1
  126. package/dist/remediate/phases/plan.js +3 -227
  127. package/dist/remediate/phases/plan.js.map +1 -1
  128. package/dist/remediate/phases/triage.d.ts.map +1 -1
  129. package/dist/remediate/phases/triage.js +19 -30
  130. package/dist/remediate/phases/triage.js.map +1 -1
  131. package/dist/remediate/riskSignal.d.ts +9 -43
  132. package/dist/remediate/riskSignal.d.ts.map +1 -1
  133. package/dist/remediate/riskSignal.js +7 -28
  134. package/dist/remediate/riskSignal.js.map +1 -1
  135. package/dist/remediate/state/disposition.d.ts +2 -1
  136. package/dist/remediate/state/disposition.d.ts.map +1 -1
  137. package/dist/remediate/state/disposition.js +37 -1
  138. package/dist/remediate/state/disposition.js.map +1 -1
  139. package/dist/remediate/state/runIdentity.d.ts +2 -13
  140. package/dist/remediate/state/runIdentity.d.ts.map +1 -1
  141. package/dist/remediate/state/runIdentity.js +3 -13
  142. package/dist/remediate/state/runIdentity.js.map +1 -1
  143. package/dist/remediate/state/store.d.ts +12 -36
  144. package/dist/remediate/state/store.d.ts.map +1 -1
  145. package/dist/remediate/state/store.js +58 -150
  146. package/dist/remediate/state/store.js.map +1 -1
  147. package/dist/remediate/state/types.d.ts +406 -504
  148. package/dist/remediate/state/types.d.ts.map +1 -1
  149. package/dist/remediate/state/types.js +24 -104
  150. package/dist/remediate/state/types.js.map +1 -1
  151. package/dist/remediate/steps/contractPipeline.d.ts +7 -661
  152. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  153. package/dist/remediate/steps/contractPipeline.js +259 -3854
  154. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  155. package/dist/remediate/steps/contractPipelinePrompts.d.ts +20 -142
  156. package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
  157. package/dist/remediate/steps/contractPipelinePrompts.js +48 -581
  158. package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
  159. package/dist/remediate/steps/dispatch/contractConformanceReview.d.ts +4 -1
  160. package/dist/remediate/steps/dispatch/contractConformanceReview.d.ts.map +1 -1
  161. package/dist/remediate/steps/dispatch/contractConformanceReview.js +6 -3
  162. package/dist/remediate/steps/dispatch/contractConformanceReview.js.map +1 -1
  163. package/dist/remediate/steps/dispatch/hostContracts.d.ts +1 -1
  164. package/dist/remediate/steps/dispatch/hostContracts.d.ts.map +1 -1
  165. package/dist/remediate/steps/dispatch/hostHandoff.d.ts +4 -21
  166. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  167. package/dist/remediate/steps/dispatch/hostHandoff.js +161 -383
  168. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  169. package/dist/remediate/steps/finalGate.d.ts +34 -0
  170. package/dist/remediate/steps/finalGate.d.ts.map +1 -1
  171. package/dist/remediate/steps/finalGate.js +44 -4
  172. package/dist/remediate/steps/finalGate.js.map +1 -1
  173. package/dist/remediate/steps/frictionCloseout.d.ts +1 -30
  174. package/dist/remediate/steps/frictionCloseout.d.ts.map +1 -1
  175. package/dist/remediate/steps/frictionCloseout.js +6 -99
  176. package/dist/remediate/steps/frictionCloseout.js.map +1 -1
  177. package/dist/remediate/steps/nextStep.d.ts +2 -15
  178. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  179. package/dist/remediate/steps/nextStep.js +255 -1028
  180. package/dist/remediate/steps/nextStep.js.map +1 -1
  181. package/dist/remediate/steps/prompts.js +12 -12
  182. package/dist/remediate/steps/prompts.js.map +1 -1
  183. package/dist/remediate/steps/stepUtils.d.ts +1 -1
  184. package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
  185. package/dist/remediate/steps/types.d.ts +1 -1
  186. package/dist/remediate/steps/types.d.ts.map +1 -1
  187. package/dist/remediate/steps/types.js +1 -1
  188. package/dist/remediate/steps/types.js.map +1 -1
  189. package/dist/remediate/types/options.d.ts +9 -0
  190. package/dist/remediate/types/options.d.ts.map +1 -1
  191. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  192. package/dist/remediate/validation/artifacts.js +10 -56
  193. package/dist/remediate/validation/artifacts.js.map +1 -1
  194. package/dist/remediate/validation/remediationState.d.ts +2 -2
  195. package/dist/remediate/validation/remediationState.d.ts.map +1 -1
  196. package/dist/remediate/validation/remediationState.js +17 -77
  197. package/dist/remediate/validation/remediationState.js.map +1 -1
  198. package/dist/remediate/validation/verificationReport.d.ts +3 -0
  199. package/dist/remediate/validation/verificationReport.d.ts.map +1 -0
  200. package/dist/remediate/validation/verificationReport.js +28 -0
  201. package/dist/remediate/validation/verificationReport.js.map +1 -0
  202. package/dist/shared/analyzers/types.d.ts +10 -10
  203. package/dist/shared/engine/obligationEngine.d.ts +2 -2
  204. package/dist/shared/friction/triage.d.ts +7 -28
  205. package/dist/shared/friction/triage.d.ts.map +1 -1
  206. package/dist/shared/friction/triage.js +7 -28
  207. package/dist/shared/friction/triage.js.map +1 -1
  208. package/dist/shared/graph/orderedReachability.js +1 -1
  209. package/dist/shared/graph/orderedReachability.js.map +1 -1
  210. package/dist/shared/index.d.ts +5 -6
  211. package/dist/shared/index.d.ts.map +1 -1
  212. package/dist/shared/index.js +3 -4
  213. package/dist/shared/index.js.map +1 -1
  214. package/dist/shared/loopCorePaths.d.ts.map +1 -1
  215. package/dist/shared/loopCorePaths.js +6 -0
  216. package/dist/shared/loopCorePaths.js.map +1 -1
  217. package/dist/shared/prompts.d.ts +1 -2
  218. package/dist/shared/prompts.d.ts.map +1 -1
  219. package/dist/shared/prompts.js +1 -2
  220. package/dist/shared/prompts.js.map +1 -1
  221. package/dist/shared/reReview/projectionDiff.d.ts.map +1 -1
  222. package/dist/shared/reReview/projectionDiff.js +6 -14
  223. package/dist/shared/reReview/projectionDiff.js.map +1 -1
  224. package/dist/shared/types/executionPlan.d.ts +416 -0
  225. package/dist/shared/types/executionPlan.d.ts.map +1 -0
  226. package/dist/shared/types/executionPlan.js +132 -0
  227. package/dist/shared/types/executionPlan.js.map +1 -0
  228. package/dist/shared/types/remediationOutcome.d.ts +1378 -12
  229. package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
  230. package/dist/shared/types/remediationOutcome.js +29 -1
  231. package/dist/shared/types/remediationOutcome.js.map +1 -1
  232. package/dist/shared/types/{contractPipeline/verification.d.ts → verificationReport.d.ts} +9 -37
  233. package/dist/shared/types/verificationReport.d.ts.map +1 -0
  234. package/dist/shared/types/verificationReport.js +4 -0
  235. package/dist/shared/types/verificationReport.js.map +1 -0
  236. package/dist/shared/validation/findingGrounding.d.ts +3 -5
  237. package/dist/shared/validation/findingGrounding.d.ts.map +1 -1
  238. package/dist/shared/validation/findingGrounding.js +4 -7
  239. package/dist/shared/validation/findingGrounding.js.map +1 -1
  240. package/package.json +1 -1
  241. package/remediate-code.mjs +16 -3
  242. package/dist/remediate/contractPipeline/artifactNames.d.ts +0 -8
  243. package/dist/remediate/contractPipeline/artifactNames.d.ts.map +0 -1
  244. package/dist/remediate/contractPipeline/artifactNames.js +0 -28
  245. package/dist/remediate/contractPipeline/artifactNames.js.map +0 -1
  246. package/dist/remediate/contractPipeline/artifactStore.d.ts +0 -219
  247. package/dist/remediate/contractPipeline/artifactStore.d.ts.map +0 -1
  248. package/dist/remediate/contractPipeline/artifactStore.js +0 -436
  249. package/dist/remediate/contractPipeline/artifactStore.js.map +0 -1
  250. package/dist/remediate/contractPipeline/changeClassification.d.ts +0 -130
  251. package/dist/remediate/contractPipeline/changeClassification.d.ts.map +0 -1
  252. package/dist/remediate/contractPipeline/changeClassification.js +0 -446
  253. package/dist/remediate/contractPipeline/changeClassification.js.map +0 -1
  254. package/dist/remediate/contractPipeline/contractReviewBinding.d.ts +0 -28
  255. package/dist/remediate/contractPipeline/contractReviewBinding.d.ts.map +0 -1
  256. package/dist/remediate/contractPipeline/contractReviewBinding.js +0 -75
  257. package/dist/remediate/contractPipeline/contractReviewBinding.js.map +0 -1
  258. package/dist/remediate/contractPipeline/counterexampleFingerprint.d.ts +0 -27
  259. package/dist/remediate/contractPipeline/counterexampleFingerprint.d.ts.map +0 -1
  260. package/dist/remediate/contractPipeline/counterexampleFingerprint.js +0 -37
  261. package/dist/remediate/contractPipeline/counterexampleFingerprint.js.map +0 -1
  262. package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts +0 -131
  263. package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts.map +0 -1
  264. package/dist/remediate/contractPipeline/cyclicSeamResolution.js +0 -257
  265. package/dist/remediate/contractPipeline/cyclicSeamResolution.js.map +0 -1
  266. package/dist/remediate/contractPipeline/derive.d.ts +0 -173
  267. package/dist/remediate/contractPipeline/derive.d.ts.map +0 -1
  268. package/dist/remediate/contractPipeline/derive.js +0 -477
  269. package/dist/remediate/contractPipeline/derive.js.map +0 -1
  270. package/dist/remediate/contractPipeline/finalizedContractFields.d.ts +0 -50
  271. package/dist/remediate/contractPipeline/finalizedContractFields.d.ts.map +0 -1
  272. package/dist/remediate/contractPipeline/finalizedContractFields.js +0 -77
  273. package/dist/remediate/contractPipeline/finalizedContractFields.js.map +0 -1
  274. package/dist/remediate/contractPipeline/idRegistry.d.ts +0 -144
  275. package/dist/remediate/contractPipeline/idRegistry.d.ts.map +0 -1
  276. package/dist/remediate/contractPipeline/idRegistry.js +0 -214
  277. package/dist/remediate/contractPipeline/idRegistry.js.map +0 -1
  278. package/dist/remediate/contractPipeline/obligationKinds.d.ts +0 -27
  279. package/dist/remediate/contractPipeline/obligationKinds.d.ts.map +0 -1
  280. package/dist/remediate/contractPipeline/obligationKinds.js +0 -18
  281. package/dist/remediate/contractPipeline/obligationKinds.js.map +0 -1
  282. package/dist/remediate/contractPipeline/phaseCut.d.ts +0 -125
  283. package/dist/remediate/contractPipeline/phaseCut.d.ts.map +0 -1
  284. package/dist/remediate/contractPipeline/phaseCut.js +0 -339
  285. package/dist/remediate/contractPipeline/phaseCut.js.map +0 -1
  286. package/dist/remediate/contractPipeline/phaseCutArtifact.d.ts +0 -31
  287. package/dist/remediate/contractPipeline/phaseCutArtifact.d.ts.map +0 -1
  288. package/dist/remediate/contractPipeline/phaseCutArtifact.js +0 -48
  289. package/dist/remediate/contractPipeline/phaseCutArtifact.js.map +0 -1
  290. package/dist/remediate/contractPipeline/repairState.d.ts +0 -110
  291. package/dist/remediate/contractPipeline/repairState.d.ts.map +0 -1
  292. package/dist/remediate/contractPipeline/repairState.js +0 -185
  293. package/dist/remediate/contractPipeline/repairState.js.map +0 -1
  294. package/dist/remediate/contractPipeline/reviewSnapshot.d.ts +0 -79
  295. package/dist/remediate/contractPipeline/reviewSnapshot.d.ts.map +0 -1
  296. package/dist/remediate/contractPipeline/reviewSnapshot.js +0 -140
  297. package/dist/remediate/contractPipeline/reviewSnapshot.js.map +0 -1
  298. package/dist/remediate/contractPipeline/semanticProjection.d.ts +0 -64
  299. package/dist/remediate/contractPipeline/semanticProjection.d.ts.map +0 -1
  300. package/dist/remediate/contractPipeline/semanticProjection.js +0 -136
  301. package/dist/remediate/contractPipeline/semanticProjection.js.map +0 -1
  302. package/dist/remediate/contractPipeline/sketchSource.d.ts +0 -194
  303. package/dist/remediate/contractPipeline/sketchSource.d.ts.map +0 -1
  304. package/dist/remediate/contractPipeline/sketchSource.js +0 -262
  305. package/dist/remediate/contractPipeline/sketchSource.js.map +0 -1
  306. package/dist/remediate/contractPipeline/testPlanCarry.d.ts +0 -17
  307. package/dist/remediate/contractPipeline/testPlanCarry.d.ts.map +0 -1
  308. package/dist/remediate/contractPipeline/testPlanCarry.js +0 -72
  309. package/dist/remediate/contractPipeline/testPlanCarry.js.map +0 -1
  310. package/dist/remediate/steps/dispatch/marshal.d.ts +0 -7
  311. package/dist/remediate/steps/dispatch/marshal.d.ts.map +0 -1
  312. package/dist/remediate/steps/dispatch/marshal.js +0 -11
  313. package/dist/remediate/steps/dispatch/marshal.js.map +0 -1
  314. package/dist/remediate/validation/contractPipeline.d.ts +0 -35
  315. package/dist/remediate/validation/contractPipeline.d.ts.map +0 -1
  316. package/dist/remediate/validation/contractPipeline.js +0 -613
  317. package/dist/remediate/validation/contractPipeline.js.map +0 -1
  318. package/dist/remediate/validation/contractPipelineGates.d.ts +0 -272
  319. package/dist/remediate/validation/contractPipelineGates.d.ts.map +0 -1
  320. package/dist/remediate/validation/contractPipelineGates.js +0 -1397
  321. package/dist/remediate/validation/contractPipelineGates.js.map +0 -1
  322. package/dist/shared/types/contractPipeline/design.d.ts +0 -79
  323. package/dist/shared/types/contractPipeline/design.d.ts.map +0 -1
  324. package/dist/shared/types/contractPipeline/design.js +0 -16
  325. package/dist/shared/types/contractPipeline/design.js.map +0 -1
  326. package/dist/shared/types/contractPipeline/goal.d.ts +0 -41
  327. package/dist/shared/types/contractPipeline/goal.d.ts.map +0 -1
  328. package/dist/shared/types/contractPipeline/goal.js +0 -10
  329. package/dist/shared/types/contractPipeline/goal.js.map +0 -1
  330. package/dist/shared/types/contractPipeline/implementation.d.ts +0 -118
  331. package/dist/shared/types/contractPipeline/implementation.d.ts.map +0 -1
  332. package/dist/shared/types/contractPipeline/implementation.js +0 -13
  333. package/dist/shared/types/contractPipeline/implementation.js.map +0 -1
  334. package/dist/shared/types/contractPipeline/obligations.d.ts +0 -283
  335. package/dist/shared/types/contractPipeline/obligations.d.ts.map +0 -1
  336. package/dist/shared/types/contractPipeline/obligations.js +0 -84
  337. package/dist/shared/types/contractPipeline/obligations.js.map +0 -1
  338. package/dist/shared/types/contractPipeline/verification.d.ts.map +0 -1
  339. package/dist/shared/types/contractPipeline/verification.js +0 -10
  340. package/dist/shared/types/contractPipeline/verification.js.map +0 -1
  341. package/dist/shared/types/contractPipeline.d.ts +0 -48
  342. package/dist/shared/types/contractPipeline.d.ts.map +0 -1
  343. package/dist/shared/types/contractPipeline.js +0 -32
  344. package/dist/shared/types/contractPipeline.js.map +0 -1
  345. package/dist/shared/types/obligationLedger.d.ts +0 -39
  346. package/dist/shared/types/obligationLedger.d.ts.map +0 -1
  347. package/dist/shared/types/obligationLedger.js +0 -49
  348. package/dist/shared/types/obligationLedger.js.map +0 -1
@@ -1,1397 +0,0 @@
1
- // sites-pinned: tests/remediate/step-prompt-sketch-drift.test.ts, tests/remediate/validate-artifact-cross-gates.test.ts
2
- /**
3
- * Structural gates for the DesignSpec contract-pipeline artifact.
4
- *
5
- * Extracted from contractPipeline.ts to keep gate logic (structural checks,
6
- * obligation cross-checks, cycle detection) separate from the per-artifact
7
- * field validators. MNT-86b18f1b.
8
- *
9
- * Re-exported from contractPipeline.ts for backward-compatible imports.
10
- *
11
- * Also contains:
12
- * validateGoalIdConsistency — ARC-86b18f1b: goal_id equality across all
13
- * contract-pipeline artifacts that carry one.
14
- * validateImplementationDAGIntegrity — ARC-86b18f1b-2: referential integrity
15
- * and bidirectional coverage for the implementation DAG.
16
- */
17
- import { readFileSync } from "node:fs";
18
- import { join } from "node:path";
19
- import { WorkBlockSeamSchema, isRecord, pushValidationIssue, groundDesignFinding, normalizeRepoPath, isBareBasename, resolveBasenameToTrackedPath, enumerateTrackedFilePaths, findCyclicComponents, runTrackedAsync, TRACKED_CHILD_DEADLINE_MS, } from "audit-tools/shared";
20
- import { evaluatePairing, obligationScopeAnchors, readObligationChangeClassification, extractSymbolTokens, } from "../contractPipeline/changeClassification.js";
21
- import { TESTABLE_OBLIGATION_KINDS } from "../contractPipeline/obligationKinds.js";
22
- import { coerceGoalId } from "../contractPipeline/idRegistry.js";
23
- // ── DesignSpec structural gates ───────────────────────────────────────────────
24
- /**
25
- * Deterministic structural gates run before the adversarial critic phase.
26
- * Returns applicability and issues — errors block the pipeline (re-emit design phase),
27
- * warnings are advisory (appended to the critic prompt). Circular obligation
28
- * dependency detection yields a warning (not an error): the critic is told to
29
- * break the cycle by re-drafting the interface definitions.
30
- *
31
- * Call this with the design_spec payload and, optionally, the obligation_ledger
32
- * payload for the invariant-coverage cross-check.
33
- */
34
- export function validateDesignSpecGates(designSpec, obligationLedger) {
35
- const issues = [];
36
- if (!isRecord(designSpec)) {
37
- return skippedGate("design_spec", "design payload (finalized_module_contracts) is absent or malformed");
38
- }
39
- const requiredFieldChecks = [
40
- {
41
- collections: ["modules", "module_contracts"],
42
- field: "inputs",
43
- expectedShape: "array",
44
- message: (collection, index) => `${collection}[${index}].inputs must be a non-empty array — every module must declare its inputs.`,
45
- },
46
- {
47
- collections: ["modules", "module_contracts"],
48
- field: "outputs",
49
- expectedShape: "array",
50
- message: (collection, index) => `${collection}[${index}].outputs must be a non-empty array — every module must declare its outputs.`,
51
- },
52
- {
53
- collections: ["side_effects"],
54
- field: "owner",
55
- expectedShape: "string",
56
- message: (_, index) => `side_effects[${index}].owner must be a non-empty string — every side effect must have an owner.`,
57
- },
58
- {
59
- collections: ["external_dependencies"],
60
- field: "failure_semantics",
61
- expectedShape: "string",
62
- message: (collection, index, field) => `${collection}[${index}].${field} must be a non-empty string — every external dependency must declare its failure semantics.`,
63
- },
64
- {
65
- collections: ["trust_boundaries"],
66
- field: "untrusted_inputs",
67
- expectedShape: "array",
68
- message: (collection, index, field) => `${collection}[${index}].${field} must be a non-empty array — every trust boundary must declare its untrusted inputs.`,
69
- },
70
- {
71
- collections: ["trust_boundaries"],
72
- field: "validation_ref",
73
- expectedShape: "string",
74
- message: (collection, index, field) => `${collection}[${index}].${field} must be a non-empty string — every trust boundary must have a validation reference.`,
75
- },
76
- ];
77
- const checksGate1And2 = requiredFieldChecks.slice(0, 3);
78
- const checksGate4And5 = requiredFieldChecks.slice(3);
79
- const specRecord = designSpec;
80
- const resolveCollection = (check) => {
81
- if (check.collections.includes("modules")) {
82
- if (Array.isArray(specRecord.modules))
83
- return "modules";
84
- if (Array.isArray(specRecord.module_contracts))
85
- return "module_contracts";
86
- }
87
- return check.collections.find((collection) => Array.isArray(specRecord[collection]));
88
- };
89
- const runDesignSpecFieldChecks = (checks) => {
90
- const isInvalid = (shape, value) => {
91
- if (shape === "array") {
92
- return !Array.isArray(value) || value.length === 0;
93
- }
94
- return typeof value !== "string" || value.length === 0;
95
- };
96
- const collectionOrder = [];
97
- for (const check of checks) {
98
- const collection = resolveCollection(check);
99
- if (collection && !collectionOrder.includes(collection)) {
100
- collectionOrder.push(collection);
101
- }
102
- }
103
- for (const collection of collectionOrder) {
104
- const maybeEntries = specRecord[collection];
105
- if (!Array.isArray(maybeEntries))
106
- continue;
107
- const checksForCollection = checks.filter((check) => resolveCollection(check) === collection);
108
- for (const [index, entry] of maybeEntries.entries()) {
109
- if (!isRecord(entry))
110
- continue;
111
- for (const check of checksForCollection) {
112
- if (isInvalid(check.expectedShape, entry[check.field])) {
113
- pushValidationIssue(issues, `${collection}[${index}].${check.field}`, check.message(collection, index, check.field));
114
- }
115
- }
116
- }
117
- }
118
- };
119
- runDesignSpecFieldChecks(checksGate1And2);
120
- // Gate 3: invariant/obligation ledger cross-check.
121
- // Every invariant in the design_spec must have at least one obligation in the ledger
122
- // with kind === 'invariant' and whose description or id references the invariant's id.
123
- if (Array.isArray(designSpec.invariants) &&
124
- isRecord(obligationLedger) &&
125
- Array.isArray(obligationLedger.obligations)) {
126
- const obligations = obligationLedger.obligations;
127
- for (const inv of designSpec.invariants) {
128
- if (!isRecord(inv) || typeof inv.id !== "string")
129
- continue;
130
- const invId = inv.id;
131
- const covered = obligations.some((obl) => {
132
- if (!isRecord(obl))
133
- return false;
134
- if (obl.kind !== "invariant")
135
- return false;
136
- const oblId = typeof obl.id === "string" ? obl.id : "";
137
- const oblDesc = typeof obl.description === "string" ? obl.description : "";
138
- // Exact id match or word-boundary containment in description to avoid
139
- // substring false-positives (e.g. "INV-1" ⊂ "INV-10"). COR-cca3801c:
140
- // invId is worker-authored and must be escaped before interpolation —
141
- // unescaped, a metacharacter id (e.g. "INV-(1") threw and aborted the
142
- // whole cross-gate evaluation, losing all eight gate results, not just
143
- // this one. Mirrors the sibling fix
144
- // already applied to fid in validateDigestCoverage below.
145
- return oblId === invId || new RegExp(`(?<![\\w-])${escapeRegExp(invId)}(?![\\w-])`).test(oblDesc);
146
- });
147
- if (!covered) {
148
- pushValidationIssue(issues, `invariants[${invId}]`, `Invariant "${invId}" has no verification obligation in the obligation ledger — add an obligation with kind "invariant" that references "${invId}".`);
149
- }
150
- }
151
- }
152
- runDesignSpecFieldChecks(checksGate4And5);
153
- // Gate 6: circular obligation dependency detection (warning, not error).
154
- // Exact cycle membership from the shared directed-cycle core: the reported
155
- // ids are the strongly connected components' own members, so an obligation
156
- // merely DOWNSTREAM of a cycle is not named (the Kahn drain this replaced
157
- // reported every node it could not drain, tails included). The ledger is
158
- // untrusted host-authored input, so the shape guards stay here at the call
159
- // site — the core takes typed nodes.
160
- if (isRecord(obligationLedger) && Array.isArray(obligationLedger.obligations)) {
161
- const obligations = obligationLedger.obligations;
162
- const nodes = [];
163
- for (const obl of obligations) {
164
- if (!isRecord(obl) || typeof obl.id !== "string")
165
- continue;
166
- nodes.push({
167
- id: obl.id,
168
- depends_on: Array.isArray(obl.depends_on)
169
- ? obl.depends_on.filter((d) => typeof d === "string")
170
- : [],
171
- });
172
- }
173
- const cycleIds = findCyclicComponents(nodes, { includeSelfLoops: true }).flat();
174
- if (cycleIds.length > 0) {
175
- issues.push({
176
- path: "obligation_ledger.obligations",
177
- message: `Circular interface-definition dependency detected among obligations: [${cycleIds.join(", ")}]; break the cycle by re-drafting the interface definitions so the obligations form a DAG`,
178
- severity: "warning",
179
- });
180
- }
181
- }
182
- return evaluatedGate("design_spec", issues);
183
- }
184
- // ── Goal-ID consistency gate ──────────────────────────────────────────────────
185
- /**
186
- * ARC-86b18f1b: validate that every contract-pipeline artifact that carries a
187
- * `goal_id` field contains the SAME value. A mismatch indicates that two
188
- * artifacts were produced for different goals and must not be used together.
189
- *
190
- * Pass in a map of artifact-name → payload. Payloads that are not records, or
191
- * that have no `goal_id` field, are silently skipped (the per-artifact
192
- * validators already flag missing goal_ids). Issues are errors.
193
- *
194
- * Comparison is on the CANONICAL form (`coerceGoalId`), not the literal string.
195
- * Two writers reach these artifacts by different routes — a host-authored
196
- * artifact arrives through ingestion carrying whatever the model typed
197
- * (`Goal-001`), while a TOOL-derived one has already been through the id
198
- * registry's grammar (`goal-001`) — so comparing literals reds on a spelling
199
- * difference that names no real disagreement. Worse, the red is not benign: the
200
- * caller archives the artifact and re-emits the phase, so a `Goal-001`/`goal-001`
201
- * pair would send a green pipeline back to re-author an artifact that was
202
- * already correct. Canonicalizing here makes the equality test mean "the same
203
- * goal", which is what the invariant has always said.
204
- *
205
- * A value the grammar cannot canonicalize at all (empty, a placeholder, prose)
206
- * is skipped exactly as an absent one is: it is not an identity, so it cannot
207
- * participate in an identity comparison. The per-artifact validators are the
208
- * surface that refuses it.
209
- */
210
- export function validateGoalIdConsistency(artifacts) {
211
- const issues = [];
212
- let canonical;
213
- let canonicalSource;
214
- let canonicalRaw;
215
- for (const [name, payload] of Object.entries(artifacts)) {
216
- if (!isRecord(payload))
217
- continue;
218
- // Compare on the canonical form; REPORT the artifact's own literal. The
219
- // operator is being sent back to a specific artifact to fix a specific
220
- // field, so the message must quote the bytes they will find there — echoing
221
- // a normalized value they never wrote would send them looking for text that
222
- // is not in the file.
223
- const raw = payload.goal_id;
224
- const id = coerceGoalId(raw);
225
- if (id.length === 0)
226
- continue;
227
- if (canonical === undefined) {
228
- canonical = id;
229
- canonicalSource = name;
230
- canonicalRaw = typeof raw === "string" ? raw : id;
231
- }
232
- else if (id !== canonical) {
233
- pushValidationIssue(issues, `${name}.goal_id`, `goal_id mismatch: "${name}" has goal_id "${String(raw)}" but "${canonicalSource}" has "${String(canonicalRaw)}". All contract-pipeline artifacts must share the same goal_id.`);
234
- }
235
- }
236
- return issues;
237
- }
238
- // ── Implementation-DAG referential-integrity gate ─────────────────────────────
239
- /**
240
- * ARC-86b18f1b-2: validate the implementation_dag against the obligation_ledger
241
- * and counterexample/judge artifacts for:
242
- *
243
- * 1. Referential integrity — every id referenced in `satisfies_obligations`,
244
- * `verification_obligation_ids`, and `addresses_counterexamples` must exist
245
- * in the obligation_ledger or as an accepted counterexample in the judge
246
- * report (respectively).
247
- *
248
- * 2. Bidirectional coverage — every obligation in the ledger, and every
249
- * accepted counterexample (per the judge report), must be addressed by at
250
- * least one DAG node.
251
- *
252
- * All issues are errors. Accepts `undefined` payloads for missing artifacts —
253
- * referential checks are skipped when the target artifact is absent (the caller
254
- * is responsible for ensuring the artifacts exist before calling this gate).
255
- */
256
- /**
257
- * The non-empty string ids a sibling artifact declares, read defensively from an
258
- * `unknown` payload shaped `{ [arrayKey]: [{ id }] }`.
259
- *
260
- * The obligation ledger and the counterexample artifact were each read by their
261
- * own copy of this loop, identical but for the key they look under.
262
- */
263
- function collectDeclaredIds(payload, arrayKey) {
264
- const ids = new Set();
265
- if (isRecord(payload) && Array.isArray(payload[arrayKey])) {
266
- for (const entry of payload[arrayKey]) {
267
- if (isRecord(entry) && typeof entry.id === "string" && entry.id.length > 0) {
268
- ids.add(entry.id);
269
- }
270
- }
271
- }
272
- return ids;
273
- }
274
- /**
275
- * One node's obligation references, for one of the two fields that carry them.
276
- * Both fields were checked by the same fifteen lines, differing only in the field
277
- * name — which appears in the issue path AND in its message, so a copy that
278
- * drifted would have misreported which field was wrong.
279
- *
280
- * ⚠ An EMPTY `obligationIds` means the ledger declared nothing, which is not the
281
- * same as "this reference is unknown". In that case every referenced id counts as
282
- * covered and NOTHING is reported — the gate refuses to invent a referential
283
- * error against an artifact that stated no ids at all.
284
- */
285
- function checkNodeObligationRefs(issues, node, nodeIndex, field, obligationIds, coveredObligationIds) {
286
- const refs = node[field];
287
- if (!Array.isArray(refs))
288
- return;
289
- for (const oblId of refs) {
290
- if (typeof oblId !== "string")
291
- continue;
292
- if (obligationIds.size > 0 && !obligationIds.has(oblId)) {
293
- pushValidationIssue(issues, `implementation_dag.nodes[${nodeIndex}].${field}`, `Node "${node.id}" references obligation "${oblId}" in ${field}, but no such obligation exists in the obligation_ledger.`);
294
- }
295
- else {
296
- coveredObligationIds.add(oblId);
297
- }
298
- }
299
- }
300
- /** Report every declared id no node covered. An empty set reports nothing. */
301
- function reportUncovered(issues, declared, covered, message) {
302
- for (const id of declared) {
303
- if (!covered.has(id)) {
304
- pushValidationIssue(issues, "implementation_dag.coverage", message(id));
305
- }
306
- }
307
- }
308
- /**
309
- * Phase 1 — referential integrity, node by node: every obligation and
310
- * counterexample a node references must exist in the artifact that declares it.
311
- *
312
- * Returns the covered sets alongside the issues, because phase 2 asks the
313
- * opposite question of the same walk and must not walk the nodes again.
314
- */
315
- function validateDAGReferentialIntegrity(nodes, obligationIds, counterexampleIds, acceptedCounterexampleIds) {
316
- const issues = [];
317
- const coveredObligationIds = new Set();
318
- const coveredCounterexampleIds = new Set();
319
- for (const [i, node] of nodes.entries()) {
320
- if (!isRecord(node))
321
- continue;
322
- checkNodeObligationRefs(issues, node, i, "satisfies_obligations", obligationIds, coveredObligationIds);
323
- checkNodeObligationRefs(issues, node, i, "verification_obligation_ids", obligationIds, coveredObligationIds);
324
- if (Array.isArray(node.addresses_counterexamples)) {
325
- for (const ceId of node.addresses_counterexamples) {
326
- if (typeof ceId !== "string")
327
- continue;
328
- if (!counterexampleIds.has(ceId)) {
329
- pushValidationIssue(issues, `implementation_dag.nodes[${i}].addresses_counterexamples`, `Node "${node.id}" references counterexample "${ceId}" in addresses_counterexamples, but no such counterexample exists in the counterexample artifact.`);
330
- }
331
- if (acceptedCounterexampleIds.has(ceId)) {
332
- coveredCounterexampleIds.add(ceId);
333
- }
334
- }
335
- }
336
- }
337
- return { issues, coveredObligationIds, coveredCounterexampleIds };
338
- }
339
- /**
340
- * Phase 2 — the other direction: every declared obligation and every accepted
341
- * counterexample must be addressed by SOME node. Phase 1 asks whether each
342
- * reference resolves; this asks whether each declaration was reached.
343
- */
344
- function validateDAGCoverageGaps(obligationIds, acceptedCounterexampleIds, coveredObligationIds, coveredCounterexampleIds) {
345
- const issues = [];
346
- reportUncovered(issues, obligationIds, coveredObligationIds, (oblId) => `Obligation "${oblId}" from the obligation_ledger is not addressed by any implementation_dag node (neither in satisfies_obligations nor verification_obligation_ids).`);
347
- reportUncovered(issues, acceptedCounterexampleIds, coveredCounterexampleIds, (ceId) => `Judge-accepted counterexample "${ceId}" is not addressed by any implementation_dag node in addresses_counterexamples.`);
348
- return issues;
349
- }
350
- export function validateImplementationDAGIntegrity(dagPayload, obligationLedgerPayload, counterexamplePayload, judgeReportPayload, waivedCounterexampleIds) {
351
- if (!isRecord(dagPayload) || !Array.isArray(dagPayload.nodes)) {
352
- return skippedGate("implementation_dag_integrity", "implementation_dag payload is absent or malformed (not a record with a nodes array)");
353
- }
354
- const obligationIds = collectDeclaredIds(obligationLedgerPayload, "obligations");
355
- const counterexampleIds = collectDeclaredIds(counterexamplePayload, "counterexamples");
356
- const acceptedCounterexampleIds = collectUnwaivedAcceptedCounterexampleIds(judgeReportPayload, waivedCounterexampleIds);
357
- const referential = validateDAGReferentialIntegrity(dagPayload.nodes, obligationIds, counterexampleIds, acceptedCounterexampleIds);
358
- return evaluatedGate("implementation_dag_integrity", [
359
- ...referential.issues,
360
- ...validateDAGCoverageGaps(obligationIds, acceptedCounterexampleIds, referential.coveredObligationIds, referential.coveredCounterexampleIds),
361
- ]);
362
- }
363
- // ── Contract-obligations gates (CP-BLOCK-N-contract-obligations) ───────────────
364
- //
365
- // The gates below enforce the auditor-agnostic robustness invariants for the
366
- // contract-obligations module. Each is a pure, deterministic function that
367
- // returns its applicability and issues (errors block promotion — fail-closed). None of
368
- // them inspect or select a model identity.
369
- //
370
- // OBL-CO-01 validatePairedObligations — every testable obligation is
371
- // covered by a test spec that asserts BOTH the satisfied path and a
372
- // negative/failure path (paired positive+negative obligation).
373
- // OBL-CO-03 validateEvidenceThreaded — upstream evidence is threaded into
374
- // the artifacts that consume it (no evidence is dropped at a seam).
375
- // OBL-CO-04 validateDigestCoverage — source_type-scoped: for an
376
- // enumerable (structured_audit) intake, every enumerated finding
377
- // maps to at least one obligation; non-enumerable sources pass
378
- // vacuously.
379
- // OBL-CO-12 validateReconciliationDerivation — INV-CO-12 fail-closed
380
- // derivation gate: every reconciled seam mismatch is derived into
381
- // the finalized module contracts.
382
- /**
383
- * Testable obligation kinds — THE single source of truth for whether an
384
- * obligation needs a test-plan spec (derive.ts's buildTestValidatorPlanScaffold)
385
- * and whether the coverage gate below requires that spec to be filled
386
- * (validatePairedObligations). MNT-e10b9d9b: previously duplicated by hand as
387
- * TESTABLE_KINDS in derive.ts and TESTABLE_OBLIGATION_KINDS here, and the two
388
- * copies had already diverged — derive.ts failed OPEN on an unrecognized kind
389
- * (conservatively testable) while this file failed CLOSED (silently skipped,
390
- * i.e. treated as non-testable, so a scaffold-offered spec was never actually
391
- * required). The subset is now derived from the full vocabulary in
392
- * contractPipeline/obligationKinds.ts; this module owns only the fail-open
393
- * classification policy.
394
- */
395
- export { TESTABLE_OBLIGATION_KINDS };
396
- /**
397
- * testable (invariant/behavioral) → true; the structural contract-conformance
398
- * kind → false; an unrecognized/unexpected kind → true (fail-OPEN into the
399
- * paired-test gate rather than silently skipping coverage — matches
400
- * derive.ts's original scaffold semantics, which this predicate now also
401
- * governs on the coverage-gate side, closing the divergence).
402
- */
403
- export function isTestablePhaseObligation(kind) {
404
- if (TESTABLE_OBLIGATION_KINDS.has(kind))
405
- return true;
406
- if (kind === "structural")
407
- return false;
408
- return true;
409
- }
410
- // <!-- comment-symbol-exempt: names deliberately-retired symbols; this block records that history -->
411
- /**
412
- * OBL-CO-01 / DC-5 — paired-obligation gate (fail-closed, change-scoped).
413
- *
414
- * Every TESTABLE (invariant / behavioral) obligation must be covered by at least
415
- * one test_validator_plan spec. Whether that coverage must be a positive+negative
416
- * PAIR depends on the obligation's change-vs-addition classification (CE-013):
417
- *
418
- * - A behavior CHANGE (it touches an existing symbol) requires BOTH a positive
419
- * (satisfied-path) assertion AND a negative (failure-path) assertion, and the
420
- * negative must be SCOPED to the changed symbol/file (CE-006): an unscoped,
421
- * repo-wide negative does not count. A narrow positive-only test, or a negative
422
- * that greps the whole tree, is the exact latent failure mode this gate stops.
423
- * - A pure ADDITION has no prior behavior to regress, so it is NEVER forced to
424
- * pair — coverage by any spec is sufficient.
425
- * - An UNCLASSIFIED testable obligation is treated as a CHANGE (fail-closed): a
426
- * dropped classification can never silently relax the requirement.
427
- *
428
- * The classification is recorded on the ledger by `deriveObligationLedger`
429
- * (deterministic first pass, LLM-confirmable). Pairing/scoping/polarity are all
430
- * evaluated through the single-source `changeClassification` helpers so this gate
431
- * and the `mergeImplementResults` verify gate agree exactly.
432
- *
433
- * An obligation may opt out only via an explicit, falsifiable `inapplicable_claim`
434
- * on a spec that cites that obligation id — bare omission is an error.
435
- *
436
- * Accepts `undefined` for a missing test_validator_plan: with testable
437
- * obligations present and no plan at all, every testable obligation is reported
438
- * uncovered (fail-closed).
439
- */
440
- export function validatePairedObligations(obligationLedgerPayload, testValidatorPlanPayload) {
441
- const issues = [];
442
- if (!isRecord(obligationLedgerPayload) || !Array.isArray(obligationLedgerPayload.obligations)) {
443
- return skippedGate("paired_obligations", "obligation_ledger payload is absent or malformed (not a record with an obligations array)");
444
- }
445
- const coverage = new Map();
446
- const ensure = (id) => {
447
- let entry = coverage.get(id);
448
- if (!entry) {
449
- entry = { covered: false, inapplicable: false, assertions: [] };
450
- coverage.set(id, entry);
451
- }
452
- return entry;
453
- };
454
- const specs = isRecord(testValidatorPlanPayload) && Array.isArray(testValidatorPlanPayload.test_specs)
455
- ? testValidatorPlanPayload.test_specs
456
- : [];
457
- for (const spec of specs) {
458
- if (!isRecord(spec) || typeof spec.obligation_id !== "string")
459
- continue;
460
- const entry = ensure(spec.obligation_id);
461
- entry.covered = true;
462
- // An inapplicable_claim that cites this same obligation id opts it out.
463
- if (isRecord(spec.inapplicable_claim) &&
464
- spec.inapplicable_claim.obligation_id === spec.obligation_id &&
465
- typeof spec.inapplicable_claim.reason === "string" &&
466
- spec.inapplicable_claim.reason.length > 0) {
467
- entry.inapplicable = true;
468
- }
469
- if (Array.isArray(spec.assertions)) {
470
- for (const a of spec.assertions) {
471
- if (typeof a === "string")
472
- entry.assertions.push(a);
473
- }
474
- }
475
- }
476
- for (const obl of obligationLedgerPayload.obligations) {
477
- if (!isRecord(obl) || typeof obl.id !== "string")
478
- continue;
479
- if (typeof obl.kind !== "string" || !isTestablePhaseObligation(obl.kind))
480
- continue;
481
- const id = obl.id;
482
- const entry = coverage.get(id);
483
- if (!entry || !entry.covered) {
484
- pushValidationIssue(issues, `test_validator_plan.coverage[${id}]`, `Testable obligation "${id}" (kind "${obl.kind}") has no test spec — every invariant/behavioral obligation must be covered by a test spec (a paired positive+negative for a behavior change), or declared inapplicable with a falsifiable claim.`);
485
- continue;
486
- }
487
- if (entry.inapplicable)
488
- continue;
489
- // A pure ADDITION is not forced to pair — coverage by any spec is enough.
490
- const classification = readObligationChangeClassification(obl);
491
- if (classification?.change_kind === "addition")
492
- continue;
493
- // CHANGE (or fail-closed unclassified): require the scoped positive+negative
494
- // pair, evaluated by the single-source helper against the change's anchors.
495
- const description = typeof obl.description === "string" ? obl.description : "";
496
- const anchors = obligationScopeAnchors(id, description, classification);
497
- const verdict = evaluatePairing(entry.assertions, anchors);
498
- if (!verdict.hasPositive) {
499
- // Polarity escape-hatch (INV-CVG-3 / CE): the keyword classifier reads a
500
- // satisfied path whose success is a block/`exit 2` action as "no positive"
501
- // (a block/exit reads as a failure). Point the author at the explicit label
502
- // override so a false negative is always recoverable without euphemising.
503
- pushValidationIssue(issues, `test_validator_plan.coverage[${id}].positive`, `Testable obligation "${id}" (behavior change) has no positive (satisfied-path) assertion — a paired obligation must assert the behavior holds in the success case. ` +
504
- `If the success case is a block / \`exit 2\` action the polarity heuristic misreads as "no positive", prefix that assertion with an explicit \`POSITIVE:\` (or \`NEGATIVE:\`) label to override the classifier.`);
505
- }
506
- if (!verdict.hasNegative) {
507
- const detail = verdict.negativeUnscoped
508
- ? `its negative assertion is not scoped to the changed symbol/file (anchors: ${anchors.join(", ") || "none"}) — an unscoped, repo-wide negative is rejected (CE-006)`
509
- : `it has no negative (failure-path) assertion`;
510
- pushValidationIssue(issues, `test_validator_plan.coverage[${id}].negative`, `Testable obligation "${id}" (behavior change) ${detail}. A paired obligation must assert the failure mode is rejected, scoped to the change, not only the positive case.`);
511
- }
512
- }
513
- return evaluatedGate("paired_obligations", issues);
514
- }
515
- /**
516
- * OBL-CO-03 — evidence-threading gate (fail-closed).
517
- *
518
- * Evidence produced upstream must survive every downstream seam:
519
- *
520
- * 1. A contract_assessment_report finding with status "violated" must carry
521
- * non-empty evidence — a violation asserted without evidence is unfalsifiable.
522
- * 2. Every judge-accepted counterexample must be threaded forward into the
523
- * implementation_dag (a node must list it in addresses_counterexamples).
524
- * This is the seam where adversarial evidence is most often dropped.
525
- * 3. Every DAG node that satisfies an obligation must carry obligation-derived
526
- * evidence in its description (the node must not be an empty placeholder).
527
- *
528
- * Accepts `undefined` payloads — a check whose source artifact is absent is
529
- * skipped, except the counterexample-threading check, which is fail-closed when
530
- * accepted counterexamples exist but the DAG is missing.
531
- */
532
- /** The DAG's node array, or `[]` when the payload declares none. */
533
- function dagNodes(dagPayload) {
534
- return isRecord(dagPayload) && Array.isArray(dagPayload.nodes)
535
- ? dagPayload.nodes
536
- : [];
537
- }
538
- /** Check 1: a finding that reports a violation must carry concrete evidence. */
539
- function validateViolatedFindingsEvidence(assessmentReportPayload) {
540
- const issues = [];
541
- if (!isRecord(assessmentReportPayload) || !Array.isArray(assessmentReportPayload.findings)) {
542
- return issues;
543
- }
544
- for (const [i, finding] of assessmentReportPayload.findings.entries()) {
545
- if (!isRecord(finding))
546
- continue;
547
- if (finding.status !== "violated")
548
- continue;
549
- const evidence = Array.isArray(finding.evidence)
550
- ? finding.evidence.filter((e) => typeof e === "string" && e.length > 0)
551
- : [];
552
- if (evidence.length === 0) {
553
- pushValidationIssue(issues, `contract_assessment_report.findings[${i}].evidence`, `Assessment finding for obligation "${typeof finding.obligation_id === "string" ? finding.obligation_id : "?"}" is "violated" but carries no evidence — a violation must thread concrete evidence forward.`);
554
- }
555
- }
556
- return issues;
557
- }
558
- /**
559
- * Check 2: every judge-accepted counterexample must reach implementation.
560
- *
561
- * ⚠ FAIL-CLOSED, and the shape of the code is what makes it so. An ABSENT dag
562
- * yields no nodes, so nothing is threaded, so every accepted id is reported.
563
- * Accepted adversarial evidence with no DAG to reach is the failure this gate
564
- * exists to catch — not a case to skip for want of a payload.
565
- */
566
- function validateCounterexampleThreading(acceptedCounterexampleIds, dagPayload) {
567
- const issues = [];
568
- if (acceptedCounterexampleIds.size === 0)
569
- return issues;
570
- const threaded = new Set();
571
- for (const node of dagNodes(dagPayload)) {
572
- if (!isRecord(node) || !Array.isArray(node.addresses_counterexamples))
573
- continue;
574
- for (const ceId of node.addresses_counterexamples) {
575
- if (typeof ceId === "string")
576
- threaded.add(ceId);
577
- }
578
- }
579
- for (const ceId of acceptedCounterexampleIds) {
580
- if (!threaded.has(ceId)) {
581
- pushValidationIssue(issues, "implementation_dag.evidence_threading", `Judge-accepted counterexample "${ceId}" is not threaded into any implementation_dag node (addresses_counterexamples) — accepted adversarial evidence must reach implementation.`);
582
- }
583
- }
584
- return issues;
585
- }
586
- /** Check 3: a node that claims to satisfy obligations must say what it does. */
587
- function validateSatisfyingNodeDescriptions(dagPayload) {
588
- const issues = [];
589
- for (const [i, node] of dagNodes(dagPayload).entries()) {
590
- if (!isRecord(node))
591
- continue;
592
- const satisfies = Array.isArray(node.satisfies_obligations)
593
- ? node.satisfies_obligations.filter((o) => typeof o === "string")
594
- : [];
595
- if (satisfies.length === 0)
596
- continue;
597
- const description = typeof node.description === "string" ? node.description.trim() : "";
598
- if (description.length === 0) {
599
- pushValidationIssue(issues, `implementation_dag.nodes[${i}].description`, `Node "${typeof node.id === "string" ? node.id : "?"}" satisfies obligations but has an empty description — the evidence of what work satisfies the obligation must not be blank.`);
600
- }
601
- }
602
- return issues;
603
- }
604
- export function validateEvidenceThreaded(assessmentReportPayload, judgeReportPayload, dagPayload, waivedCounterexampleIds) {
605
- if (!isRecord(assessmentReportPayload) && !isRecord(judgeReportPayload) && !isRecord(dagPayload)) {
606
- return skippedGate("evidence_threaded", "all three input payloads (contract_assessment_report, judge_report, implementation_dag) are absent or malformed");
607
- }
608
- return evaluatedGate("evidence_threaded", [
609
- ...validateViolatedFindingsEvidence(assessmentReportPayload),
610
- ...validateCounterexampleThreading(collectUnwaivedAcceptedCounterexampleIds(judgeReportPayload, waivedCounterexampleIds), dagPayload),
611
- ...validateSatisfyingNodeDescriptions(dagPayload),
612
- ]);
613
- }
614
- /**
615
- * OBL-CO-04 — source_type-scoped digest-coverage gate (fail-closed for
616
- * enumerable sources).
617
- *
618
- * For a `structured_audit` (enumerable) intake every enumerated finding must map
619
- * to at least one obligation in the ledger, so no auditor finding silently
620
- * vanishes between intake and the contract. For `conversation` / `document`
621
- * sources — or any finding-enumeration explicitly marked `is_enumerable:false`
622
- * — the gate passes vacuously, because there is no closed finding set to cover.
623
- *
624
- * The mapping is established by finding-id appearing in any obligation's
625
- * `source_finding_ids` (preferred) OR being referenced by id within an
626
- * obligation's `description` (word-boundary match, fallback).
627
- *
628
- * `sourceType` comes from goal_spec.source_type. `findingEnumerationPayload` is
629
- * the intake finding-enumeration.json ({ is_enumerable, findings:[{id}] }).
630
- */
631
- export function validateDigestCoverage(sourceType, findingEnumerationPayload, obligationLedgerPayload) {
632
- const issues = [];
633
- // Non-enumerable sources have no closed finding set: pass vacuously.
634
- if (sourceType !== "structured_audit" && sourceType !== "mixed") {
635
- return skippedGate("digest_coverage", "source not enumerable — source_type is not structured_audit or mixed");
636
- }
637
- if (!isRecord(findingEnumerationPayload)) {
638
- return skippedGate("digest_coverage", "finding-enumeration payload is absent or malformed");
639
- }
640
- if (findingEnumerationPayload.is_enumerable === false) {
641
- return skippedGate("digest_coverage", "source not enumerable — is_enumerable is false");
642
- }
643
- const findingEnumeration = findingEnumerationPayload;
644
- const findingIds = Array.isArray(findingEnumeration.findings)
645
- ? findingEnumeration.findings
646
- .map((f) => (isRecord(f) && typeof f.id === "string" ? f.id : undefined))
647
- .filter((id) => id !== undefined)
648
- : [];
649
- if (findingIds.length === 0)
650
- return evaluatedGate("digest_coverage", issues);
651
- const obligations = isRecord(obligationLedgerPayload) && Array.isArray(obligationLedgerPayload.obligations)
652
- ? obligationLedgerPayload.obligations
653
- : [];
654
- // Build the set of finding ids any obligation maps to.
655
- const mapped = new Set();
656
- for (const obl of obligations) {
657
- if (!isRecord(obl))
658
- continue;
659
- if (Array.isArray(obl.source_finding_ids)) {
660
- for (const fid of obl.source_finding_ids) {
661
- if (typeof fid === "string")
662
- mapped.add(fid);
663
- }
664
- }
665
- }
666
- const descriptions = obligations
667
- .map((obl) => (isRecord(obl) && typeof obl.description === "string" ? obl.description : ""))
668
- .join("\n");
669
- for (const fid of findingIds) {
670
- if (mapped.has(fid))
671
- continue;
672
- // Fallback: a word-boundary mention of the finding id in any description.
673
- const referenced = new RegExp(`(?<![\\w-])${escapeRegExp(fid)}(?![\\w-])`).test(descriptions);
674
- if (!referenced) {
675
- pushValidationIssue(issues, `obligation_ledger.digest_coverage[${fid}]`, `Enumerated finding "${fid}" maps to no obligation (neither via source_finding_ids nor by reference in any obligation description) — an enumerable (${sourceType}) intake must cover every finding.`);
676
- }
677
- }
678
- return evaluatedGate("digest_coverage", issues);
679
- }
680
- /**
681
- * Path-A work-topology gate. Every dangerous audit overlap must have exactly one
682
- * distinct seam-preparation module, and both participating implementation blocks
683
- * must remain represented by implementation modules. This turns the auditor's
684
- * overlap metadata into a mechanically enforced decomposition boundary instead
685
- * of prompt-only advice.
686
- */
687
- export function validateWorkBlockSeamPreparation(pathASeedPayload, moduleDecompositionPayload) {
688
- const issues = [];
689
- if (!isRecord(pathASeedPayload) || !Array.isArray(pathASeedPayload.work_block_seams)) {
690
- return issues;
691
- }
692
- // Parse every seam against the contract BEFORE gating on it. The contested-file
693
- // check used to read `typeof seam.file === "string" ? … : skip`, so an
694
- // old-shape or malformed seed lost the check and the gate still reported green
695
- // — a fail-OPEN on exactly the record that authorizes parallel writes.
696
- const parsedSeams = [];
697
- for (const [index, candidate] of pathASeedPayload.work_block_seams.entries()) {
698
- const parsed = WorkBlockSeamSchema.safeParse(candidate);
699
- if (!parsed.success) {
700
- const seamLabel = isRecord(candidate) && typeof candidate.id === "string"
701
- ? candidate.id
702
- : `#${index}`;
703
- pushValidationIssue(issues, `path_a_seed.work_block_seams[${index}]`, `Seam "${seamLabel}" does not match the work-block seam contract, so its preparation cannot be checked: ${parsed.error.issues
704
- .map((issue) => `${issue.path.map(String).join(".") || "<root>"}: ${issue.message}`)
705
- .join("; ")}`);
706
- continue;
707
- }
708
- parsedSeams.push(parsed.data);
709
- }
710
- const requiredSeams = parsedSeams.filter((seam) => seam.requires_preparation === true);
711
- if (requiredSeams.length === 0)
712
- return issues;
713
- const workBlocks = Array.isArray(pathASeedPayload.work_blocks)
714
- ? pathASeedPayload.work_blocks.filter(isRecord)
715
- : [];
716
- const blockById = new Map();
717
- for (const block of workBlocks) {
718
- if (typeof block.id === "string")
719
- blockById.set(block.id, block);
720
- }
721
- const modules = isRecord(moduleDecompositionPayload) && Array.isArray(moduleDecompositionPayload.modules)
722
- ? moduleDecompositionPayload.modules.filter(isRecord)
723
- : [];
724
- const requiredSeamIds = new Set(requiredSeams.map((seam) => seam.id));
725
- for (const [index, mod] of modules.entries()) {
726
- const sourceIds = Array.isArray(mod.source_work_block_ids)
727
- ? mod.source_work_block_ids.filter((id) => typeof id === "string")
728
- : [];
729
- const preparedIds = Array.isArray(mod.prepares_seam_ids)
730
- ? mod.prepares_seam_ids.filter((id) => typeof id === "string")
731
- : [];
732
- for (const blockId of sourceIds) {
733
- if (!blockById.has(blockId)) {
734
- pushValidationIssue(issues, `module_decomposition.modules[${index}].source_work_block_ids`, `Module references unknown audit work block "${blockId}".`);
735
- }
736
- }
737
- for (const seamId of preparedIds) {
738
- if (!requiredSeamIds.has(seamId)) {
739
- pushValidationIssue(issues, `module_decomposition.modules[${index}].prepares_seam_ids`, `Module references unknown or non-required audit seam "${seamId}".`);
740
- }
741
- }
742
- }
743
- for (const seam of requiredSeams) {
744
- const seamId = seam.id;
745
- const preparers = modules.filter((mod) => Array.isArray(mod.prepares_seam_ids) &&
746
- mod.prepares_seam_ids.includes(seamId));
747
- if (preparers.length !== 1) {
748
- pushValidationIssue(issues, `module_decomposition.seam_preparation[${seamId}]`, `Required audit seam "${seamId}" must be prepared by exactly one module; found ${preparers.length}.`);
749
- continue;
750
- }
751
- const preparer = preparers[0];
752
- const preparerFiles = new Set(Array.isArray(preparer.file_scope)
753
- ? preparer.file_scope.filter((file) => typeof file === "string")
754
- : []);
755
- if (!preparerFiles.has(seam.file)) {
756
- pushValidationIssue(issues, `module_decomposition.seam_preparation[${seamId}].file_scope`, `The module preparing seam "${seamId}" must own its contested file (${seam.file}).`);
757
- }
758
- for (const blockId of seam.block_ids) {
759
- const block = blockById.get(blockId);
760
- if (!block) {
761
- pushValidationIssue(issues, `path_a_seed.work_block_seams[${seamId}].block_ids`, `Seam "${seamId}" references unknown work block "${blockId}".`);
762
- continue;
763
- }
764
- const implementationOwners = modules.filter((mod) => mod !== preparer &&
765
- Array.isArray(mod.source_work_block_ids) &&
766
- mod.source_work_block_ids.includes(blockId));
767
- const preparerOwnsCoordinationBlock = block.role === "coordination" &&
768
- Array.isArray(preparer.source_work_block_ids) &&
769
- preparer.source_work_block_ids.includes(blockId);
770
- if (implementationOwners.length === 0 && !preparerOwnsCoordinationBlock) {
771
- pushValidationIssue(issues, `module_decomposition.work_block_coverage[${blockId}]`, `Work block "${blockId}" participating in required seam "${seamId}" has no distinct implementation module.`);
772
- }
773
- }
774
- }
775
- return issues;
776
- }
777
- function escapeRegExp(value) {
778
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
779
- }
780
- /**
781
- * The judge-accepted counterexample ids this pipeline still DEMANDS coverage
782
- * for: accepted, non-empty, and not resolved by a recorded owner waiver.
783
- *
784
- * Two gates in this file need that set and each built it inline, character for
785
- * character. Every conjunct is load-bearing, which is why they must not drift
786
- * apart: the `length > 0` test keeps an empty id out of a demand nothing can
787
- * satisfy, and the optional chaining on the waiver set is required because most
788
- * callers omit the argument entirely.
789
- *
790
- * Deliberately NOT `acceptedCounterexampleIds` from `contractPipeline/derive.ts`,
791
- * despite the name. That one returns a possibly-duplicated array, filters no
792
- * waiver and admits the empty string — and importing its value here would close
793
- * a module cycle, since `derive.ts` already imports `isTestablePhaseObligation`
794
- * from this file.
795
- *
796
- * A waived counterexample is resolved by a recorded owner decision, so coverage
797
- * is not demanded for it — see the waiver entry in `docs/backlog/open-bugs.md`.
798
- */
799
- function collectUnwaivedAcceptedCounterexampleIds(judgeReportPayload, waivedCounterexampleIds) {
800
- const accepted = new Set();
801
- if (isRecord(judgeReportPayload) && Array.isArray(judgeReportPayload.classifications)) {
802
- for (const cls of judgeReportPayload.classifications) {
803
- if (isRecord(cls) &&
804
- cls.classification === "accepted" &&
805
- typeof cls.counterexample_id === "string" &&
806
- cls.counterexample_id.length > 0 &&
807
- !waivedCounterexampleIds?.has(cls.counterexample_id)) {
808
- accepted.add(cls.counterexample_id);
809
- }
810
- }
811
- }
812
- return accepted;
813
- }
814
- /**
815
- * OBL-CO-12 / INV-CO-12 — reconciliation-derivation gate (fail-closed).
816
- *
817
- * Every mismatch reconciled in the seam_reconciliation_report must be DERIVED
818
- * into the finalized_module_contracts: the report's `agreed_interface` for each
819
- * mismatch must be reflected in the finalized contracts (matched against the
820
- * union of every finalized module's inputs/outputs/invariants/side_effects text).
821
- * A reconciliation decision that never reaches the finalized contract is a
822
- * dropped derivation — the exact failure INV-CO-12 forbids.
823
- *
824
- * Fail-closed: if the report declares mismatches but the finalized contracts
825
- * artifact is absent, every mismatch is reported as underived.
826
- */
827
- export function validateReconciliationDerivation(seamReconciliationReportPayload, finalizedModuleContractsPayload) {
828
- const issues = [];
829
- if (!isRecord(seamReconciliationReportPayload) || !Array.isArray(seamReconciliationReportPayload.mismatches)) {
830
- return skippedGate("reconciliation_derivation", "seam_reconciliation_report payload is absent or malformed (not a record with a mismatches array)");
831
- }
832
- const mismatches = seamReconciliationReportPayload
833
- .mismatches;
834
- if (mismatches.length === 0)
835
- return evaluatedGate("reconciliation_derivation", issues);
836
- // Build a single normalized corpus of all finalized-contract interface text.
837
- const corpusParts = [];
838
- const moduleContracts = isRecord(finalizedModuleContractsPayload) &&
839
- Array.isArray(finalizedModuleContractsPayload.module_contracts)
840
- ? finalizedModuleContractsPayload.module_contracts
841
- : [];
842
- for (const mod of moduleContracts) {
843
- if (!isRecord(mod))
844
- continue;
845
- for (const field of [
846
- "inputs",
847
- "outputs",
848
- "invariants",
849
- "side_effects",
850
- "seam_adjustments",
851
- ]) {
852
- if (Array.isArray(mod[field])) {
853
- for (const entry of mod[field]) {
854
- if (typeof entry === "string")
855
- corpusParts.push(entry);
856
- }
857
- }
858
- }
859
- if (typeof mod.validation_boundary === "string")
860
- corpusParts.push(mod.validation_boundary);
861
- }
862
- const corpus = normalizeToContentTokens(corpusParts.join("\n"));
863
- for (const [i, mismatch] of mismatches.entries()) {
864
- if (!isRecord(mismatch))
865
- continue;
866
- const resolution = isRecord(mismatch.resolution) ? mismatch.resolution : undefined;
867
- const agreed = resolution && typeof resolution.agreed_interface === "string"
868
- ? resolution.agreed_interface
869
- : "";
870
- const seamId = typeof mismatch.seam_id === "string" ? mismatch.seam_id : `#${i}`;
871
- if (agreed.length === 0) {
872
- // No agreed interface text to derive — the reconciliation is incomplete.
873
- pushValidationIssue(issues, `seam_reconciliation_report.mismatches[${i}].resolution.agreed_interface`, `Seam "${seamId}" has no agreed_interface to derive into the finalized contracts (INV-CO-12).`);
874
- continue;
875
- }
876
- if (moduleContracts.length === 0) {
877
- pushValidationIssue(issues, `finalized_module_contracts.derivation[${seamId}]`, `Seam "${seamId}" was reconciled but finalized_module_contracts has no module contracts to carry the agreed interface — reconciliation was not derived (INV-CO-12, fail-closed).`);
878
- continue;
879
- }
880
- if (!corpusContainsAgreedInterface(corpus, agreed)) {
881
- pushValidationIssue(issues, `finalized_module_contracts.derivation[${seamId}]`, `Seam "${seamId}" agreed interface "${agreed}" is not reflected in any finalized module contract — the reconciliation decision was not derived downstream (INV-CO-12).`);
882
- }
883
- }
884
- return evaluatedGate("reconciliation_derivation", issues);
885
- }
886
- /**
887
- * Lossy semantic-token normalization: lowercases and collapses every run of
888
- * non-alphanumeric characters (punctuation, symbols, whitespace) to a single
889
- * space, so a reworded/repunctuated interface still matches by content tokens
890
- * (INV-CO-12). Deliberately lossy — NOT the same contract as shared
891
- * `normalizeForMatch` (`audit-tools/shared`), which is whitespace-only and
892
- * lossless because it backs verbatim quote-and-verify grounding. Kept distinct
893
- * (and distinctly named) so the two contracts can never be confused.
894
- */
895
- function normalizeToContentTokens(value) {
896
- return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
897
- }
898
- /**
899
- * Function words that are length >= 4 but carry no interface meaning. Excluded from
900
- * the salient-token set so a derivation is judged on its CONTENT terms, not on filler
901
- * — otherwise "must"/"than"/"with" force the finalized contract to echo the agreed
902
- * interface near-verbatim (the dogfood: a faithful paraphrase failed INV-CO-12).
903
- */
904
- const DERIVATION_STOPWORDS = new Set([
905
- "must", "shall", "should", "will", "would", "with", "that", "this", "from", "into",
906
- "when", "then", "than", "they", "them", "their", "there", "have", "been", "were",
907
- "what", "which", "while", "your", "here", "where", "also", "only", "such", "each",
908
- "both", "more", "most", "some", "very", "upon", "onto", "does", "done",
909
- ]);
910
- /**
911
- * A derivation is satisfied when a strong majority of the agreed interface's salient
912
- * CONTENT tokens (length >= 4, excluding function-word stopwords) appear in the
913
- * finalized-contract corpus. Substring matching already tolerates morphology
914
- * (`flush` ⊂ `flushes`); dropping stopwords and requiring a majority rather than ALL
915
- * tolerates genuine rewording (a synonym or two), while still failing when the agreed
916
- * interface left little or no trace — the INV-CO-12 fail-closed property.
917
- */
918
- function corpusContainsAgreedInterface(corpus, agreed) {
919
- const tokens = normalizeToContentTokens(agreed)
920
- .split(" ")
921
- .filter((t) => t.length >= 4 && !DERIVATION_STOPWORDS.has(t));
922
- if (tokens.length === 0) {
923
- // No salient content tokens — fall back to a normalized substring check.
924
- const normAgreed = normalizeToContentTokens(agreed);
925
- return normAgreed.length === 0 || corpus.includes(normAgreed);
926
- }
927
- const present = tokens.filter((t) => corpus.includes(t)).length;
928
- // Require ~60% of content tokens — one reworded term in a short interface passes,
929
- // a mostly-absent interface fails. ceil keeps 1–2-token interfaces strict.
930
- const required = Math.max(1, Math.ceil(tokens.length * 0.6));
931
- return present >= required;
932
- }
933
- /**
934
- * Enumerate the working-tree paths at `repoRoot`, normalized through the shared
935
- * `normalizeRepoPath` for membership matching. This is the lowercased *draw* over
936
- * the one shared corpus — `enumerateTrackedFilePaths` owns the git invocation and
937
- * its NUL-delimited parsing, so the two corpora cannot drift on how a path is
938
- * read off the index (they did: both split newline-terminated `ls-files` output,
939
- * so a C-quoted non-ASCII path corrupted both). Returns an empty set when git is
940
- * unavailable or the tree is empty (caller treats empty as the fail-closed
941
- * unreadable-tree signal).
942
- */
943
- export async function enumerateRepoTreePaths(repoRoot) {
944
- return repoTreePathsDraw(await enumerateTrackedFilePaths(repoRoot));
945
- }
946
- /**
947
- * The pure lowercased draw over an already-enumerated tracked corpus. Split
948
- * from {@link enumerateRepoTreePaths} so a gate that needs BOTH corpora (the
949
- * lowered membership set and the case-preserving one) enumerates git ONCE and
950
- * derives the draw locally instead of spawning a second child.
951
- */
952
- export function repoTreePathsDraw(tracked) {
953
- const known = new Set();
954
- for (const path of tracked) {
955
- const normalized = normalizeRepoPath(path);
956
- if (normalized.length > 0)
957
- known.add(normalized);
958
- }
959
- return known;
960
- }
961
- /**
962
- * Whether `repoRoot` is inside a VALID git working tree, via
963
- * `git rev-parse --is-inside-work-tree`. This distinguishes the two reasons
964
- * `enumerateRepoTreePaths` can return empty: (a) git missing / not a repo →
965
- * `false` (genuinely unreadable — fail-closed); (b) a valid git work tree that
966
- * simply has zero tracked files yet (a fresh/never-committed repo) → `true`
967
- * (the citations may be sound; degrade to pass-with-warning, never hard-block).
968
- * NEVER throws — any failure is treated as not-a-tree. ASYNC (INV-SSF): runs
969
- * under the remediation phase lock, so the child must not block the loop.
970
- */
971
- export async function isInsideGitWorkTree(repoRoot) {
972
- const result = await runTrackedAsync(["git", "rev-parse", "--is-inside-work-tree"], {
973
- cwd: repoRoot,
974
- encoding: "utf8",
975
- timeout: TRACKED_CHILD_DEADLINE_MS,
976
- });
977
- return (!result.error && result.status === 0 && result.stdout.trim() === "true");
978
- }
979
- /**
980
- * Build the corpus of pre-existing symbol tokens from the known repo paths, so a
981
- * symbol-shaped citation can be grounded against "a token that actually names a
982
- * real file or a segment of one". Each path is split on path/extension separators
983
- * (`/`, `.`, `_`, `-`) into segments (`src/auth.ts` → {src, auth, ts}), so a bare
984
- * symbol citation like `auth` grounds against the real file `src/auth.ts`. Single-
985
- * letter / very short segments (<3) are dropped as noise.
986
- */
987
- function buildKnownSymbolCorpus(knownPaths) {
988
- const corpus = new Set();
989
- for (const path of knownPaths) {
990
- for (const segment of path.split(/[/._\-]+/)) {
991
- const token = segment.toLowerCase();
992
- if (token.length >= 3)
993
- corpus.add(token);
994
- }
995
- }
996
- return corpus;
997
- }
998
- /**
999
- * Every ancestor directory of a known path (`src/a/b.ts` → {src, src/a}), so a
1000
- * path-shaped citation to a brand-new file that does not exist yet can still
1001
- * ground against a REAL tracked directory. This is the create-file case a module
1002
- * cannot pre-ground any other way (its deliverable does not exist until it runs,
1003
- * so `git ls-files` never lists it) — yet the module is not hallucinating when the
1004
- * file lands in a real location. A fully-invented path under a non-existent
1005
- * directory (`made/up/dir/x.ts`) still fails, so the hallucination signal is kept
1006
- * for the case it can actually catch. Normalized, forward-slash, no trailing slash.
1007
- */
1008
- function buildKnownDirs(knownPaths) {
1009
- const dirs = new Set();
1010
- for (const path of knownPaths) {
1011
- let slash = path.lastIndexOf("/");
1012
- while (slash > 0) {
1013
- const dir = path.slice(0, slash);
1014
- if (dirs.has(dir))
1015
- break;
1016
- dirs.add(dir);
1017
- slash = dir.lastIndexOf("/");
1018
- }
1019
- }
1020
- return dirs;
1021
- }
1022
- /** The parent directory of a normalized path token, or "" for a top-level file. */
1023
- function parentDir(token) {
1024
- const slash = token.lastIndexOf("/");
1025
- return slash > 0 ? token.slice(0, slash) : "";
1026
- }
1027
- /**
1028
- * A token is path-shaped when it contains a path separator or a file-extension
1029
- * dot (`src/a.ts`, `a/b`, `foo.ts`); otherwise it is symbol-shaped (`writeRecord`,
1030
- * `flush_buffer`). The partition decides which grounding set a citation token is
1031
- * checked against (CE: a symbol-only citation to a non-existent symbol is
1032
- * rejected, not excused as "cites no component").
1033
- */
1034
- function isPathShaped(token) {
1035
- return token.includes("/") || /\.[a-z0-9]+$/i.test(token);
1036
- }
1037
- /**
1038
- * M-B3 — source-grounded citation gate (fail-closed only on an unreadable tree).
1039
- *
1040
- * For each finding:
1041
- * 1. If it cites at least one real `affected_files` path → grounded (delegates
1042
- * to the shared `groundDesignFinding` against the repo-tree path set — no
1043
- * re-implementation).
1044
- * 2. Otherwise, partition the citation tokens (affected_files paths that did
1045
- * not resolve + symbol tokens from the description) into path-shaped vs
1046
- * symbol-shaped. A path-shaped token grounds against the known-path set; a
1047
- * symbol-shaped token grounds against the symbol corpus. If ANY token
1048
- * grounds, the finding passes. If NONE grounds — including a finding that
1049
- * cites only non-existent symbols — it is rejected (error).
1050
- *
1051
- * `findings` is the array of finding-shaped citations to ground (each carries
1052
- * `affected_files` and an optional `description`). `repoRoot` is the working-tree
1053
- * root enumerated by `git ls-files`.
1054
- */
1055
- export async function validateContractCitationGrounding(findings, repoRoot) {
1056
- const issues = [];
1057
- const knownPaths = await enumerateRepoTreePaths(repoRoot);
1058
- // An empty path set has two distinct causes — distinguish them so a legitimately
1059
- // new/empty git repo is not hard-blocked (the grounding edge):
1060
- // - git missing / not a repo → genuinely unreadable → ERROR (fail-closed).
1061
- // - valid work tree, 0 tracked → nothing to ground against, but the citations
1062
- // may be sound → WARNING (pass-with-warning; callers block only on errors).
1063
- if (knownPaths.size === 0) {
1064
- if (await isInsideGitWorkTree(repoRoot)) {
1065
- pushValidationIssue(issues, "contract_citation_grounding.repo_tree", `The working tree at "${repoRoot}" is a valid git repo but has no tracked files yet (git ls-files is empty) — citation grounding cannot run, so it is SKIPPED with a warning rather than blocking promotion. Citations were not verified against the tree.`, "warning");
1066
- // treeReadable: the tree IS readable — it is just empty. No error issue, so
1067
- // the gate degrades to pass-with-warning (callers filter on severity:error).
1068
- return { treeReadable: true, issues };
1069
- }
1070
- pushValidationIssue(issues, "contract_citation_grounding.repo_tree", `Could not enumerate the working tree at "${repoRoot}" (git unavailable or not a git work tree) — citation grounding cannot run, so the gate fails closed. Verify repo_root points at a git working tree.`);
1071
- return { treeReadable: false, issues };
1072
- }
1073
- const knownSymbols = buildKnownSymbolCorpus(knownPaths);
1074
- const knownDirs = buildKnownDirs(knownPaths);
1075
- findings.forEach((finding, index) => {
1076
- // 1. Real path citation → grounded by the shared design-finding primitive.
1077
- const pathVerdict = groundDesignFinding(finding, knownPaths);
1078
- if (pathVerdict.status === "grounded")
1079
- return;
1080
- // 2. No real path. Gather every citation token and partition it.
1081
- const tokens = new Set();
1082
- for (const file of finding.affected_files ?? []) {
1083
- const normalized = normalizeRepoPath(file?.path ?? "");
1084
- if (normalized.length > 0)
1085
- tokens.add(normalized);
1086
- }
1087
- // The canonical Finding shape carries no `description`; its prose lives in
1088
- // `summary` (+ title). Pull candidate symbol tokens from both so a symbol-only
1089
- // citation is grounded against the real-symbol corpus.
1090
- for (const token of extractSymbolTokens(`${finding.summary ?? ""} ${finding.title ?? ""}`)) {
1091
- tokens.add(token);
1092
- }
1093
- let grounded = false;
1094
- for (const token of tokens) {
1095
- if (isPathShaped(token)) {
1096
- // A real path grounds directly; a not-yet-tracked path grounds when its
1097
- // parent directory is real (a legitimate brand-new-file deliverable in an
1098
- // existing tracked location — the create-file case, which by definition
1099
- // cannot cite an existing path). A path under a non-existent directory
1100
- // still fails, preserving the hallucination signal.
1101
- if (knownPaths.has(token) || knownDirs.has(parentDir(token))) {
1102
- grounded = true;
1103
- break;
1104
- }
1105
- }
1106
- else if (knownSymbols.has(token)) {
1107
- grounded = true;
1108
- break;
1109
- }
1110
- }
1111
- if (!grounded) {
1112
- const findingId = typeof finding.id === "string" && finding.id.length > 0 ? finding.id : `#${index}`;
1113
- const cited = [...tokens].slice(0, 5).join(", ") || "(no path or symbol citation)";
1114
- pushValidationIssue(issues, `contract_citation_grounding.findings[${findingId}]`, `Finding "${findingId}" cites no real component: no cited path exists in the working tree and no cited symbol (${cited}) names anything in the repository. A finding must point at a real path or a real symbol.`);
1115
- }
1116
- });
1117
- return { treeReadable: true, issues };
1118
- }
1119
- // ── B5: decomposition file_scope points at real logic (not a re-export shim) ──
1120
- //
1121
- // A module_decomposition assigns each module a `file_scope[]` — the files where
1122
- // that module's named responsibility logic is supposed to live. A latent failure
1123
- // mode (B5): a decomposition scopes a module at a thin re-export barrel/shim
1124
- // (`export * from "./real.js"`) instead of the file that actually implements the
1125
- // responsibility. The implement worker then edits a file that holds no logic, or
1126
- // hunts for the real site itself. This gate rejects a module whose file_scope
1127
- // resolves ONLY to re-export shims.
1128
- //
1129
- // The shim signal is STRUCTURAL (a re-export/barrel shape), never a line-count
1130
- // heuristic: a genuinely small real module (a five-line installer) must pass. It
1131
- // is a rebuttable lead — a module that scopes at least one real-logic file passes
1132
- // even if it also lists a barrel.
1133
- //
1134
- // Grounding reuses the SHARED, dotfile-safe resolver (findingGrounding.ts —
1135
- // normalizeRepoPath keeps a dotfile-dir leading dot; resolveBasenameToTrackedPath
1136
- // resolves a bare basename to its unique tracked path), so a scoped `.claude/x`
1137
- // or bare `advance.ts` grounds identically to the M-B3 gate — no private copy of
1138
- // dotfile/basename logic. Fail-closed ONLY on an unreadable git tree (mirrors
1139
- // validateContractCitationGrounding); a valid-but-empty tree degrades to warning.
1140
- /**
1141
- * A file is a re-export shim/barrel when — after stripping comments — every
1142
- * top-level statement is an import or a re-export, and at least one is a
1143
- * re-export (`export … from`, `export *`). A statement that defines logic (a
1144
- * function/class/enum, or a `const/let/var X = <value>` that is not itself a
1145
- * re-export) makes the file real. Structural, not line-count based.
1146
- *
1147
- * A file that cannot be read is treated as NOT a shim (never false-reject on an
1148
- * unreadable file — the tree-readability gate below is the only fail-closed path).
1149
- */
1150
- function isReExportShim(absPath) {
1151
- let content;
1152
- try {
1153
- content = readFileSync(absPath, "utf8");
1154
- }
1155
- catch {
1156
- return false;
1157
- }
1158
- // Strip block + line comments so a doc-comment mentioning code is not counted.
1159
- const stripped = content
1160
- .replace(/\/\*[\s\S]*?\*\//g, "")
1161
- .replace(/(^|[^:])\/\/[^\n]*/g, "$1");
1162
- const statements = stripped
1163
- .split(";")
1164
- .map((s) => s.replace(/\s+/g, " ").trim())
1165
- .filter((s) => s.length > 0);
1166
- if (statements.length === 0)
1167
- return false;
1168
- let hasReExport = false;
1169
- for (const stmt of statements) {
1170
- // `export * from "..."` / `export * as ns from "..."`
1171
- if (/^export\s+\*(?:\s+as\s+[A-Za-z0-9_$]+)?\s+from\s+['"]/.test(stmt)) {
1172
- hasReExport = true;
1173
- continue;
1174
- }
1175
- // `export { a, b } from "..."` / `export type { T } from "..."`
1176
- if (/^export\s+(?:type\s+)?\{[^}]*\}\s*from\s+['"]/.test(stmt)) {
1177
- hasReExport = true;
1178
- continue;
1179
- }
1180
- // `export { a, b }` (bare local re-list — carries no logic of its own)
1181
- if (/^export\s+(?:type\s+)?\{[^}]*\}$/.test(stmt))
1182
- continue;
1183
- // `export { default } from "..."` handled by the from-form above.
1184
- // A plain import (side-effect or named) carries no logic.
1185
- if (/^import\b/.test(stmt))
1186
- continue;
1187
- // Anything else — an `export function`, `export class`, `export const X = …`,
1188
- // or a bare statement — is real logic; the file is not a shim.
1189
- return false;
1190
- }
1191
- return hasReExport;
1192
- }
1193
- /**
1194
- * INV-CVG-4 (B5) — reject a module whose `file_scope` points ONLY at thin
1195
- * re-export shims rather than the file where its named responsibility logic
1196
- * lives. `moduleDecompositionPayload` is the (envelope-unwrapped) module_decomposition
1197
- * payload; `repoRoot` is the working-tree root enumerated via `git ls-files`.
1198
- *
1199
- * Behaviour:
1200
- * - A scoped path that does not resolve to any tracked file is SKIPPED here (the
1201
- * M-B3 citation gate owns path-existence; this gate only judges resolved files).
1202
- * - A module is flagged only when it resolves ≥1 scoped path and EVERY resolved
1203
- * path is a re-export shim (no real-logic file among them).
1204
- * - Fail-closed only on an unreadable git tree (git missing / not a repo → error);
1205
- * a valid-but-empty tree degrades to a warning (never hard-block a fresh repo).
1206
- */
1207
- export async function validateDecompositionFileScope(moduleDecompositionPayload, repoRoot) {
1208
- const issues = [];
1209
- if (!isRecord(moduleDecompositionPayload) || !Array.isArray(moduleDecompositionPayload.modules)) {
1210
- return skippedGate("decomposition_file_scope", "module_decomposition payload is absent or malformed (not a record with a modules array)");
1211
- }
1212
- const modules = moduleDecompositionPayload.modules;
1213
- // Does any module actually declare a file_scope? If not, nothing to ground —
1214
- // do not spawn git or fail-closed on an unrelated empty tree.
1215
- const anyScope = modules.some((mod) => isRecord(mod) &&
1216
- Array.isArray(mod.file_scope) &&
1217
- mod.file_scope.some((p) => typeof p === "string" && p.length > 0));
1218
- if (!anyScope)
1219
- return evaluatedGate("decomposition_file_scope", issues);
1220
- // Tree readability gate (mirror validateContractCitationGrounding): the M-B3
1221
- // gate's lowered draw decides readable-vs-empty; the case-preserving corpus is
1222
- // what we resolve + read files against. ONE git enumeration serves both.
1223
- const caseCorpus = await enumerateTrackedFilePaths(repoRoot);
1224
- const knownLower = repoTreePathsDraw(caseCorpus);
1225
- if (knownLower.size === 0) {
1226
- if (await isInsideGitWorkTree(repoRoot)) {
1227
- pushValidationIssue(issues, "decomposition_file_scope.repo_tree", `The working tree at "${repoRoot}" is a valid git repo but has no tracked files yet (git ls-files is empty) — decomposition file_scope grounding cannot run, so it is SKIPPED with a warning rather than blocking. file_scope was not verified against the tree.`, "warning");
1228
- return evaluatedGate("decomposition_file_scope", issues);
1229
- }
1230
- pushValidationIssue(issues, "decomposition_file_scope.repo_tree", `Could not enumerate the working tree at "${repoRoot}" (git unavailable or not a git work tree) — decomposition file_scope grounding cannot run, so the gate fails closed. Verify repo_root points at a git working tree.`);
1231
- return evaluatedGate("decomposition_file_scope", issues);
1232
- }
1233
- // normalizeRepoPath(tracked) → real on-disk case, so a scoped path grounds
1234
- // through the shared dotfile-safe normalizer without a private copy.
1235
- const byNorm = new Map();
1236
- for (const p of caseCorpus)
1237
- byNorm.set(normalizeRepoPath(p), p);
1238
- const resolveScoped = (scoped) => {
1239
- const norm = normalizeRepoPath(scoped);
1240
- const direct = byNorm.get(norm);
1241
- if (direct)
1242
- return direct;
1243
- if (isBareBasename(scoped))
1244
- return resolveBasenameToTrackedPath(scoped, caseCorpus);
1245
- return undefined;
1246
- };
1247
- for (const [i, mod] of modules.entries()) {
1248
- if (!isRecord(mod))
1249
- continue;
1250
- const fileScope = Array.isArray(mod.file_scope)
1251
- ? mod.file_scope.filter((p) => typeof p === "string" && p.length > 0)
1252
- : [];
1253
- if (fileScope.length === 0)
1254
- continue;
1255
- let anyResolved = false;
1256
- let anyRealLogic = false;
1257
- const shims = [];
1258
- for (const scoped of fileScope) {
1259
- const real = resolveScoped(scoped);
1260
- if (!real)
1261
- continue; // path existence is the M-B3 gate's concern
1262
- anyResolved = true;
1263
- if (isReExportShim(join(repoRoot, real))) {
1264
- shims.push(scoped);
1265
- }
1266
- else {
1267
- anyRealLogic = true;
1268
- }
1269
- }
1270
- if (anyResolved && !anyRealLogic && shims.length > 0) {
1271
- const name = typeof mod.name === "string" && mod.name.length > 0 ? mod.name : `#${i}`;
1272
- pushValidationIssue(issues, `module_decomposition.modules[${i}].file_scope`, `Module "${name}" file_scope points only at thin re-export shim(s) [${shims.join(", ")}] — a barrel/re-export carries no responsibility logic. Scope this module at the file where its named logic ACTUALLY lives, not a re-export shim.`);
1273
- }
1274
- }
1275
- return evaluatedGate("decomposition_file_scope", issues);
1276
- }
1277
- // ── INV-CO-13: the finalized module SET is preserved from the drafts ──────────
1278
- /** Non-empty `name` values of a `{module_contracts: [...]}` payload, in order. */
1279
- function moduleContractNames(payload) {
1280
- const record = isRecord(payload) ? payload : {};
1281
- const modules = Array.isArray(record.module_contracts) ? record.module_contracts : [];
1282
- const names = [];
1283
- for (const mod of modules) {
1284
- if (isRecord(mod) && typeof mod.name === "string" && mod.name.length > 0) {
1285
- names.push(mod.name);
1286
- }
1287
- }
1288
- return names;
1289
- }
1290
- /**
1291
- * INV-CO-13 — `finalized_module_contracts` must carry EXACTLY the module names
1292
- * its drafted `module_contracts` input carries. Finalization is a mechanical
1293
- * merge (`deriveFinalizedModuleContracts` maps the drafts 1:1), and the
1294
- * contract_finalization role text says "for every module contract in
1295
- * module_contracts", so this is a POST-CONDITION on a guarantee the pipeline
1296
- * already states — not a new authoring requirement on any host.
1297
- *
1298
- * It exists because the derive is not the only writer: a judge repair or a
1299
- * critique repair re-emits `contract_finalization` as an LLM step, and that
1300
- * rewrite re-enters through ingestion, whose validator for this artifact
1301
- * (`validateFinalizedModuleContracts`) is shape-only and structurally cannot see
1302
- * the drafts. An LLM rewrite that merges two modules under an invented name and
1303
- * drops a third is therefore shape-valid, and every downstream consumer — the
1304
- * phase cut, the derived obligation ids, and the DAG write-scope prefix join —
1305
- * is then built on a module set that has already silently lost a module. That is
1306
- * not hypothetical: it collapsed 7 modules to 4 in the
1307
- * dispatch-effectiveness-observability run and stranded two DAG nodes with an
1308
- * empty write scope (docs/reviews/observability-dag-scope-join-2026-08-09.md).
1309
- *
1310
- * Ground truth is unambiguous and no host discretion is involved, so this is an
1311
- * exact comparison rather than any form of tolerant matching: a renamed module is
1312
- * reported as BOTH a drop and an invention, which is exactly what it is. It checks
1313
- * multiplicity as well as membership — a repeated name is set-equal to the drafts
1314
- * but still loses content, since consumers keep the first entry per name.
1315
- *
1316
- * Absent-tolerant on both sides (a run that has not reached finalization yet, or
1317
- * a partial single-artifact self-check, must never fabricate an issue).
1318
- */
1319
- export function validateFinalizedModuleSetPreserved(draftedModuleContracts, finalizedModuleContracts) {
1320
- const issues = [];
1321
- if (!isRecord(draftedModuleContracts) || !Array.isArray(draftedModuleContracts.module_contracts) ||
1322
- !isRecord(finalizedModuleContracts) || !Array.isArray(finalizedModuleContracts.module_contracts) ||
1323
- moduleContractNames(draftedModuleContracts).length === 0) {
1324
- return skippedGate("finalized_module_set_preserved", "drafted or finalized module_contracts payload is absent, malformed, or names no modules");
1325
- }
1326
- const drafted = new Set(moduleContractNames(draftedModuleContracts));
1327
- const finalizedNames = moduleContractNames(finalizedModuleContracts);
1328
- const finalized = new Set(finalizedNames);
1329
- const dropped = [...drafted].filter((name) => !finalized.has(name)).sort();
1330
- const invented = [...finalized].filter((name) => !drafted.has(name)).sort();
1331
- // Multiplicity, not just membership: a name repeated in the finalized contracts
1332
- // is set-equal to the drafts and would slip past the comparison above, but every
1333
- // consumer keys modules BY NAME and keeps the first occurrence — so the second
1334
- // entry's interface is silently discarded, which is the same content loss by
1335
- // another route.
1336
- const duplicated = [
1337
- ...new Set(finalizedNames.filter((name, i) => finalizedNames.indexOf(name) !== i)),
1338
- ].sort();
1339
- for (const name of dropped) {
1340
- pushValidationIssue(issues, `finalized_module_contracts.module_contracts[${name}]`, `Drafted module "${name}" is missing from finalized_module_contracts. Finalization carries every drafted module through verbatim — it may adjust a module's interface per the seam reconciliation, but it may never drop, merge, or rename one. Re-emit the finalized contracts with all ${drafted.size} drafted module(s): ${[...drafted].sort().join(", ")}.`);
1341
- }
1342
- for (const name of invented) {
1343
- pushValidationIssue(issues, `finalized_module_contracts.module_contracts[${name}]`, `Module "${name}" appears in finalized_module_contracts but in no drafted module contract. Finalization may not invent or merge module names — downstream obligation ids and the DAG write-scope join are keyed on the drafted names, so an invented name resolves to an empty scope. Use only the drafted module names: ${[...drafted].sort().join(", ")}.`);
1344
- }
1345
- for (const name of duplicated) {
1346
- pushValidationIssue(issues, `finalized_module_contracts.module_contracts[${name}]`, `Module "${name}" appears more than once in finalized_module_contracts. Every consumer keys modules by name and keeps the FIRST entry, so the duplicate's interface is silently discarded. Emit exactly one finalized contract per drafted module.`);
1347
- }
1348
- return evaluatedGate("finalized_module_set_preserved", issues);
1349
- }
1350
- function evaluatedGate(gate, issues) {
1351
- return { gate, evaluated: true, issues };
1352
- }
1353
- function skippedGate(gate, reason) {
1354
- return { gate, evaluated: false, issues: [], reason };
1355
- }
1356
- /** Wire the eight cross-artifact gates in canonical order. Each gate owns its
1357
- * absent-input tolerance and evaluated/skipped result. Callers needing only
1358
- * diagnostics flatten outcome.issues; no runner-level applicability check. */
1359
- export async function evaluateContractPipelineCrossGateOutcomes(inputs) {
1360
- const { payloads, findingEnumeration, root } = inputs;
1361
- const goalSpec = payloads.get("goal_spec");
1362
- const sourceType = isRecord(goalSpec) && typeof goalSpec.source_type === "string"
1363
- ? goalSpec.source_type
1364
- : undefined;
1365
- const obligationLedger = payloads.get("obligation_ledger");
1366
- const testValidatorPlan = payloads.get("test_validator_plan");
1367
- const finalizedContracts = payloads.get("finalized_module_contracts");
1368
- const draftedContracts = payloads.get("module_contracts");
1369
- const seamReport = payloads.get("seam_reconciliation_report");
1370
- const assessment = payloads.get("contract_assessment_report");
1371
- const judge = payloads.get("judge_report");
1372
- const counterexample = payloads.get("counterexample");
1373
- const dag = payloads.get("implementation_dag");
1374
- const moduleDecomposition = payloads.get("module_decomposition");
1375
- return [
1376
- validatePairedObligations(obligationLedger, testValidatorPlan),
1377
- validateEvidenceThreaded(assessment, judge, dag, inputs.waivedCounterexampleIds),
1378
- validateDigestCoverage(sourceType, findingEnumeration, obligationLedger),
1379
- validateReconciliationDerivation(seamReport, finalizedContracts),
1380
- validateDesignSpecGates(finalizedContracts, obligationLedger),
1381
- validateImplementationDAGIntegrity(dag, obligationLedger, counterexample, judge, inputs.waivedCounterexampleIds),
1382
- await validateDecompositionFileScope(moduleDecomposition, root),
1383
- validateFinalizedModuleSetPreserved(draftedContracts, finalizedContracts),
1384
- ];
1385
- }
1386
- // ── (removed) Downstream-only repair propagation — S2, dropped ─────────────────
1387
- // <!-- comment-symbol-exempt: names deliberately-deleted symbols; this block records their removal -->
1388
- // The dead `repairDownstreamPhases` / `CONTRACT_PHASE_SEQUENCE` / `ARTIFACT_NAME_TO_PHASE`
1389
- // were deleted (contract-authoring determinism design, S2). A linear phase-slice
1390
- // ("every phase after the repaired one") is a coarser, AD-HOC re-run authority that
1391
- // would conflict with the project's "dependency DAG is truth, never ad-hoc freshness"
1392
- // invariant. The hash-based DEPENDENCY_MAP staleness DAG (`artifactStore` +
1393
- // `detectStaleArtifacts`, consumed in `buildNextContractPipelineStep`) ALREADY
1394
- // re-derives exactly the genuinely-affected downstream artifacts after a repair, so
1395
- // this function had no correct caller. Verified via the S2/S4 dogfood (2026-06-15);
1396
- // see `spec/contract-authoring-determinism-design.md` S2. Do not re-add it.
1397
- //# sourceMappingURL=contractPipelineGates.js.map