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.
- package/dist/audit/cli/auditStep.d.ts +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.d.ts +2 -0
- package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.js +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
- package/dist/audit/cli/dispatch/packetFilter.d.ts +1 -1
- package/dist/audit/cli/dispatch/packetFilter.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/packetFilter.js +1 -1
- package/dist/audit/cli/dispatch/packetFilter.js.map +1 -1
- package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
- package/dist/audit/cli/nextStepCommand.js +42 -20
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts +3 -3
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +50 -75
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/prompts.d.ts +1 -2
- package/dist/audit/cli/prompts.d.ts.map +1 -1
- package/dist/audit/cli/prompts.js +2 -12
- package/dist/audit/cli/prompts.js.map +1 -1
- package/dist/audit/cli/sampleRunCommand.d.ts +2 -2
- package/dist/audit/cli/semanticReviewStep.d.ts +11 -2
- package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
- package/dist/audit/cli/semanticReviewStep.js +14 -10
- package/dist/audit/cli/semanticReviewStep.js.map +1 -1
- package/dist/audit/contracts/workerSchemas.d.ts +14 -14
- package/dist/audit/contracts/wrapperResponse.d.ts +6 -6
- package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
- package/dist/audit/extractors/graphRoutes.js +1 -3
- package/dist/audit/extractors/graphRoutes.js.map +1 -1
- package/dist/audit/io/artifacts.d.ts.map +1 -1
- package/dist/audit/io/artifacts.js +2 -3
- package/dist/audit/io/artifacts.js.map +1 -1
- package/dist/audit/orchestrator/architectureDiscovery.d.ts +11 -0
- package/dist/audit/orchestrator/architectureDiscovery.d.ts.map +1 -0
- package/dist/audit/orchestrator/architectureDiscovery.js +174 -0
- package/dist/audit/orchestrator/architectureDiscovery.js.map +1 -0
- package/dist/audit/orchestrator/designReviewProjection.d.ts +2 -6
- package/dist/audit/orchestrator/designReviewProjection.d.ts.map +1 -1
- package/dist/audit/orchestrator/designReviewProjection.js +2 -6
- package/dist/audit/orchestrator/designReviewProjection.js.map +1 -1
- package/dist/audit/orchestrator/designReviewSnapshot.d.ts +2 -3
- package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
- package/dist/audit/orchestrator/designReviewSnapshot.js +2 -3
- package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
- package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.js +1 -22
- package/dist/audit/orchestrator/executorRunners.js.map +1 -1
- package/dist/audit/orchestrator/executors.d.ts.map +1 -1
- package/dist/audit/orchestrator/executors.js +1 -13
- package/dist/audit/orchestrator/executors.js.map +1 -1
- package/dist/audit/orchestrator/nextStep.d.ts +2 -28
- package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
- package/dist/audit/orchestrator/nextStep.js +5 -37
- package/dist/audit/orchestrator/nextStep.js.map +1 -1
- package/dist/audit/orchestrator/obligationDerive.d.ts +2 -3
- package/dist/audit/orchestrator/obligationDerive.d.ts.map +1 -1
- package/dist/audit/orchestrator/obligationDerive.js +2 -3
- package/dist/audit/orchestrator/obligationDerive.js.map +1 -1
- package/dist/audit/orchestrator/pendingTasks.d.ts +2 -0
- package/dist/audit/orchestrator/pendingTasks.d.ts.map +1 -1
- package/dist/audit/orchestrator/pendingTasks.js +5 -1
- package/dist/audit/orchestrator/pendingTasks.js.map +1 -1
- package/dist/audit/orchestrator/planningExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/planningExecutors.js +9 -0
- package/dist/audit/orchestrator/planningExecutors.js.map +1 -1
- package/dist/audit/orchestrator/requeueCommand.d.ts +1 -1
- package/dist/audit/orchestrator/resultBaseline.d.ts +5 -5
- package/dist/audit/orchestrator/resultBaseline.d.ts.map +1 -1
- package/dist/audit/orchestrator/resultBaseline.js +24 -47
- package/dist/audit/orchestrator/resultBaseline.js.map +1 -1
- package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.d.ts.map +1 -1
- package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.js +2 -0
- package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.js.map +1 -1
- package/dist/audit/orchestrator/state.d.ts.map +1 -1
- package/dist/audit/orchestrator/state.js +2 -0
- package/dist/audit/orchestrator/state.js.map +1 -1
- package/dist/audit/orchestrator/synthesisExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/synthesisExecutors.js +3 -0
- package/dist/audit/orchestrator/synthesisExecutors.js.map +1 -1
- package/dist/audit/types/auditState.d.ts +6 -6
- package/dist/audit/types/flowCoverage.d.ts +6 -6
- package/dist/audit/types.d.ts +12 -12
- package/dist/remediate/contractPipeline/executionPlan.d.ts +1908 -0
- package/dist/remediate/contractPipeline/executionPlan.d.ts.map +1 -0
- package/dist/remediate/contractPipeline/executionPlan.js +467 -0
- package/dist/remediate/contractPipeline/executionPlan.js.map +1 -0
- package/dist/remediate/contractPipeline/runtimePlanAuthority.d.ts +12 -0
- package/dist/remediate/contractPipeline/runtimePlanAuthority.d.ts.map +1 -0
- package/dist/remediate/contractPipeline/runtimePlanAuthority.js +41 -0
- package/dist/remediate/contractPipeline/runtimePlanAuthority.js.map +1 -0
- package/dist/remediate/dedup/crossLensDedup.d.ts +3 -16
- package/dist/remediate/dedup/crossLensDedup.d.ts.map +1 -1
- package/dist/remediate/dedup/crossLensDedup.js +2 -32
- package/dist/remediate/dedup/crossLensDedup.js.map +1 -1
- package/dist/remediate/index.d.ts +2 -19
- package/dist/remediate/index.d.ts.map +1 -1
- package/dist/remediate/index.js +22 -138
- package/dist/remediate/index.js.map +1 -1
- package/dist/remediate/intake.d.ts +10 -10
- package/dist/remediate/intent/intentOrdering.d.ts +6 -8
- package/dist/remediate/intent/intentOrdering.d.ts.map +1 -1
- package/dist/remediate/intent/intentOrdering.js +24 -47
- package/dist/remediate/intent/intentOrdering.js.map +1 -1
- package/dist/remediate/phases/close.d.ts +25 -17
- package/dist/remediate/phases/close.d.ts.map +1 -1
- package/dist/remediate/phases/close.js +278 -218
- package/dist/remediate/phases/close.js.map +1 -1
- package/dist/remediate/phases/closeAcceptance.d.ts +42 -0
- package/dist/remediate/phases/closeAcceptance.d.ts.map +1 -0
- package/dist/remediate/phases/closeAcceptance.js +66 -0
- package/dist/remediate/phases/closeAcceptance.js.map +1 -0
- package/dist/remediate/phases/closeReviewProvenance.d.ts +1 -1
- package/dist/remediate/phases/closeReviewProvenance.d.ts.map +1 -1
- package/dist/remediate/phases/closeReviewProvenance.js +9 -8
- package/dist/remediate/phases/closeReviewProvenance.js.map +1 -1
- package/dist/remediate/phases/closeVerifyAnalyzerLeads.d.ts.map +1 -1
- package/dist/remediate/phases/closeVerifyAnalyzerLeads.js +11 -9
- package/dist/remediate/phases/closeVerifyAnalyzerLeads.js.map +1 -1
- package/dist/remediate/phases/closeVerifyHeadEvidence.d.ts +21 -86
- package/dist/remediate/phases/closeVerifyHeadEvidence.d.ts.map +1 -1
- package/dist/remediate/phases/closeVerifyHeadEvidence.js +30 -19
- package/dist/remediate/phases/closeVerifyHeadEvidence.js.map +1 -1
- package/dist/remediate/phases/plan.d.ts +3 -60
- package/dist/remediate/phases/plan.d.ts.map +1 -1
- package/dist/remediate/phases/plan.js +3 -227
- package/dist/remediate/phases/plan.js.map +1 -1
- package/dist/remediate/phases/triage.d.ts.map +1 -1
- package/dist/remediate/phases/triage.js +19 -30
- package/dist/remediate/phases/triage.js.map +1 -1
- package/dist/remediate/riskSignal.d.ts +9 -43
- package/dist/remediate/riskSignal.d.ts.map +1 -1
- package/dist/remediate/riskSignal.js +7 -28
- package/dist/remediate/riskSignal.js.map +1 -1
- package/dist/remediate/state/disposition.d.ts +2 -1
- package/dist/remediate/state/disposition.d.ts.map +1 -1
- package/dist/remediate/state/disposition.js +37 -1
- package/dist/remediate/state/disposition.js.map +1 -1
- package/dist/remediate/state/runIdentity.d.ts +2 -13
- package/dist/remediate/state/runIdentity.d.ts.map +1 -1
- package/dist/remediate/state/runIdentity.js +3 -13
- package/dist/remediate/state/runIdentity.js.map +1 -1
- package/dist/remediate/state/store.d.ts +12 -36
- package/dist/remediate/state/store.d.ts.map +1 -1
- package/dist/remediate/state/store.js +58 -150
- package/dist/remediate/state/store.js.map +1 -1
- package/dist/remediate/state/types.d.ts +406 -504
- package/dist/remediate/state/types.d.ts.map +1 -1
- package/dist/remediate/state/types.js +24 -104
- package/dist/remediate/state/types.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts +7 -661
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +259 -3854
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.d.ts +20 -142
- package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.js +48 -581
- package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
- package/dist/remediate/steps/dispatch/contractConformanceReview.d.ts +4 -1
- package/dist/remediate/steps/dispatch/contractConformanceReview.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/contractConformanceReview.js +6 -3
- package/dist/remediate/steps/dispatch/contractConformanceReview.js.map +1 -1
- package/dist/remediate/steps/dispatch/hostContracts.d.ts +1 -1
- package/dist/remediate/steps/dispatch/hostContracts.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts +4 -21
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.js +161 -383
- package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
- package/dist/remediate/steps/finalGate.d.ts +34 -0
- package/dist/remediate/steps/finalGate.d.ts.map +1 -1
- package/dist/remediate/steps/finalGate.js +44 -4
- package/dist/remediate/steps/finalGate.js.map +1 -1
- package/dist/remediate/steps/frictionCloseout.d.ts +1 -30
- package/dist/remediate/steps/frictionCloseout.d.ts.map +1 -1
- package/dist/remediate/steps/frictionCloseout.js +6 -99
- package/dist/remediate/steps/frictionCloseout.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +2 -15
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +255 -1028
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/prompts.js +12 -12
- package/dist/remediate/steps/prompts.js.map +1 -1
- package/dist/remediate/steps/stepUtils.d.ts +1 -1
- package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
- package/dist/remediate/steps/types.d.ts +1 -1
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js +1 -1
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/remediate/types/options.d.ts +9 -0
- package/dist/remediate/types/options.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +10 -56
- package/dist/remediate/validation/artifacts.js.map +1 -1
- package/dist/remediate/validation/remediationState.d.ts +2 -2
- package/dist/remediate/validation/remediationState.d.ts.map +1 -1
- package/dist/remediate/validation/remediationState.js +17 -77
- package/dist/remediate/validation/remediationState.js.map +1 -1
- package/dist/remediate/validation/verificationReport.d.ts +3 -0
- package/dist/remediate/validation/verificationReport.d.ts.map +1 -0
- package/dist/remediate/validation/verificationReport.js +28 -0
- package/dist/remediate/validation/verificationReport.js.map +1 -0
- package/dist/shared/analyzers/types.d.ts +10 -10
- package/dist/shared/engine/obligationEngine.d.ts +2 -2
- package/dist/shared/friction/triage.d.ts +7 -28
- package/dist/shared/friction/triage.d.ts.map +1 -1
- package/dist/shared/friction/triage.js +7 -28
- package/dist/shared/friction/triage.js.map +1 -1
- package/dist/shared/graph/orderedReachability.js +1 -1
- package/dist/shared/graph/orderedReachability.js.map +1 -1
- package/dist/shared/index.d.ts +5 -6
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +3 -4
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/loopCorePaths.d.ts.map +1 -1
- package/dist/shared/loopCorePaths.js +6 -0
- package/dist/shared/loopCorePaths.js.map +1 -1
- package/dist/shared/prompts.d.ts +1 -2
- package/dist/shared/prompts.d.ts.map +1 -1
- package/dist/shared/prompts.js +1 -2
- package/dist/shared/prompts.js.map +1 -1
- package/dist/shared/reReview/projectionDiff.d.ts.map +1 -1
- package/dist/shared/reReview/projectionDiff.js +6 -14
- package/dist/shared/reReview/projectionDiff.js.map +1 -1
- package/dist/shared/types/executionPlan.d.ts +416 -0
- package/dist/shared/types/executionPlan.d.ts.map +1 -0
- package/dist/shared/types/executionPlan.js +132 -0
- package/dist/shared/types/executionPlan.js.map +1 -0
- package/dist/shared/types/remediationOutcome.d.ts +1378 -12
- package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
- package/dist/shared/types/remediationOutcome.js +29 -1
- package/dist/shared/types/remediationOutcome.js.map +1 -1
- package/dist/shared/types/{contractPipeline/verification.d.ts → verificationReport.d.ts} +9 -37
- package/dist/shared/types/verificationReport.d.ts.map +1 -0
- package/dist/shared/types/verificationReport.js +4 -0
- package/dist/shared/types/verificationReport.js.map +1 -0
- package/dist/shared/validation/findingGrounding.d.ts +3 -5
- package/dist/shared/validation/findingGrounding.d.ts.map +1 -1
- package/dist/shared/validation/findingGrounding.js +4 -7
- package/dist/shared/validation/findingGrounding.js.map +1 -1
- package/package.json +1 -1
- package/remediate-code.mjs +16 -3
- package/dist/remediate/contractPipeline/artifactNames.d.ts +0 -8
- package/dist/remediate/contractPipeline/artifactNames.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/artifactNames.js +0 -28
- package/dist/remediate/contractPipeline/artifactNames.js.map +0 -1
- package/dist/remediate/contractPipeline/artifactStore.d.ts +0 -219
- package/dist/remediate/contractPipeline/artifactStore.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/artifactStore.js +0 -436
- package/dist/remediate/contractPipeline/artifactStore.js.map +0 -1
- package/dist/remediate/contractPipeline/changeClassification.d.ts +0 -130
- package/dist/remediate/contractPipeline/changeClassification.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/changeClassification.js +0 -446
- package/dist/remediate/contractPipeline/changeClassification.js.map +0 -1
- package/dist/remediate/contractPipeline/contractReviewBinding.d.ts +0 -28
- package/dist/remediate/contractPipeline/contractReviewBinding.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/contractReviewBinding.js +0 -75
- package/dist/remediate/contractPipeline/contractReviewBinding.js.map +0 -1
- package/dist/remediate/contractPipeline/counterexampleFingerprint.d.ts +0 -27
- package/dist/remediate/contractPipeline/counterexampleFingerprint.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/counterexampleFingerprint.js +0 -37
- package/dist/remediate/contractPipeline/counterexampleFingerprint.js.map +0 -1
- package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts +0 -131
- package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/cyclicSeamResolution.js +0 -257
- package/dist/remediate/contractPipeline/cyclicSeamResolution.js.map +0 -1
- package/dist/remediate/contractPipeline/derive.d.ts +0 -173
- package/dist/remediate/contractPipeline/derive.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/derive.js +0 -477
- package/dist/remediate/contractPipeline/derive.js.map +0 -1
- package/dist/remediate/contractPipeline/finalizedContractFields.d.ts +0 -50
- package/dist/remediate/contractPipeline/finalizedContractFields.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/finalizedContractFields.js +0 -77
- package/dist/remediate/contractPipeline/finalizedContractFields.js.map +0 -1
- package/dist/remediate/contractPipeline/idRegistry.d.ts +0 -144
- package/dist/remediate/contractPipeline/idRegistry.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/idRegistry.js +0 -214
- package/dist/remediate/contractPipeline/idRegistry.js.map +0 -1
- package/dist/remediate/contractPipeline/obligationKinds.d.ts +0 -27
- package/dist/remediate/contractPipeline/obligationKinds.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/obligationKinds.js +0 -18
- package/dist/remediate/contractPipeline/obligationKinds.js.map +0 -1
- package/dist/remediate/contractPipeline/phaseCut.d.ts +0 -125
- package/dist/remediate/contractPipeline/phaseCut.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/phaseCut.js +0 -339
- package/dist/remediate/contractPipeline/phaseCut.js.map +0 -1
- package/dist/remediate/contractPipeline/phaseCutArtifact.d.ts +0 -31
- package/dist/remediate/contractPipeline/phaseCutArtifact.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/phaseCutArtifact.js +0 -48
- package/dist/remediate/contractPipeline/phaseCutArtifact.js.map +0 -1
- package/dist/remediate/contractPipeline/repairState.d.ts +0 -110
- package/dist/remediate/contractPipeline/repairState.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/repairState.js +0 -185
- package/dist/remediate/contractPipeline/repairState.js.map +0 -1
- package/dist/remediate/contractPipeline/reviewSnapshot.d.ts +0 -79
- package/dist/remediate/contractPipeline/reviewSnapshot.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/reviewSnapshot.js +0 -140
- package/dist/remediate/contractPipeline/reviewSnapshot.js.map +0 -1
- package/dist/remediate/contractPipeline/semanticProjection.d.ts +0 -64
- package/dist/remediate/contractPipeline/semanticProjection.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/semanticProjection.js +0 -136
- package/dist/remediate/contractPipeline/semanticProjection.js.map +0 -1
- package/dist/remediate/contractPipeline/sketchSource.d.ts +0 -194
- package/dist/remediate/contractPipeline/sketchSource.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/sketchSource.js +0 -262
- package/dist/remediate/contractPipeline/sketchSource.js.map +0 -1
- package/dist/remediate/contractPipeline/testPlanCarry.d.ts +0 -17
- package/dist/remediate/contractPipeline/testPlanCarry.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/testPlanCarry.js +0 -72
- package/dist/remediate/contractPipeline/testPlanCarry.js.map +0 -1
- package/dist/remediate/steps/dispatch/marshal.d.ts +0 -7
- package/dist/remediate/steps/dispatch/marshal.d.ts.map +0 -1
- package/dist/remediate/steps/dispatch/marshal.js +0 -11
- package/dist/remediate/steps/dispatch/marshal.js.map +0 -1
- package/dist/remediate/validation/contractPipeline.d.ts +0 -35
- package/dist/remediate/validation/contractPipeline.d.ts.map +0 -1
- package/dist/remediate/validation/contractPipeline.js +0 -613
- package/dist/remediate/validation/contractPipeline.js.map +0 -1
- package/dist/remediate/validation/contractPipelineGates.d.ts +0 -272
- package/dist/remediate/validation/contractPipelineGates.d.ts.map +0 -1
- package/dist/remediate/validation/contractPipelineGates.js +0 -1397
- package/dist/remediate/validation/contractPipelineGates.js.map +0 -1
- package/dist/shared/types/contractPipeline/design.d.ts +0 -79
- package/dist/shared/types/contractPipeline/design.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/design.js +0 -16
- package/dist/shared/types/contractPipeline/design.js.map +0 -1
- package/dist/shared/types/contractPipeline/goal.d.ts +0 -41
- package/dist/shared/types/contractPipeline/goal.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/goal.js +0 -10
- package/dist/shared/types/contractPipeline/goal.js.map +0 -1
- package/dist/shared/types/contractPipeline/implementation.d.ts +0 -118
- package/dist/shared/types/contractPipeline/implementation.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/implementation.js +0 -13
- package/dist/shared/types/contractPipeline/implementation.js.map +0 -1
- package/dist/shared/types/contractPipeline/obligations.d.ts +0 -283
- package/dist/shared/types/contractPipeline/obligations.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/obligations.js +0 -84
- package/dist/shared/types/contractPipeline/obligations.js.map +0 -1
- package/dist/shared/types/contractPipeline/verification.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/verification.js +0 -10
- package/dist/shared/types/contractPipeline/verification.js.map +0 -1
- package/dist/shared/types/contractPipeline.d.ts +0 -48
- package/dist/shared/types/contractPipeline.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline.js +0 -32
- package/dist/shared/types/contractPipeline.js.map +0 -1
- package/dist/shared/types/obligationLedger.d.ts +0 -39
- package/dist/shared/types/obligationLedger.d.ts.map +0 -1
- package/dist/shared/types/obligationLedger.js +0 -49
- package/dist/shared/types/obligationLedger.js.map +0 -1
|
@@ -1,3892 +1,297 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import { ImplementationContextSchema } from "../../shared/types/contractPipeline/implementation.js";
|
|
4
|
-
import { CounterexampleSchema } from "../../shared/types/contractPipeline/obligations.js";
|
|
5
|
-
// sites-pinned: tests/remediate/contract-pipeline.test.ts, tests/remediate/dc3.test.ts, tests/remediate/contract-pipeline-adversarial.test.ts, tests/remediate/step-prompt-sketch-drift.test.ts
|
|
6
|
-
// contract-pipeline: the promotion computes no finding field FindingSchema would drop.
|
|
7
|
-
// dc3: the fan-out wording (needs, not mechanism) and the TRANSPORT report for a
|
|
8
|
-
// partially returned wave.
|
|
9
|
-
/**
|
|
10
|
-
* Contract-pipeline gate for ALL remediation starts (both paths).
|
|
11
|
-
*
|
|
12
|
-
* When intake is ready, next-step routes through the resumable
|
|
13
|
-
* contract_goal → context → design → critique → obligations → assessment →
|
|
14
|
-
* critic → judge → implementation DAG pipeline before producing an extracted
|
|
15
|
-
* plan that feeds the document/implement/close flow.
|
|
16
|
-
*
|
|
17
|
-
* Path A (structured audit-findings.json): a path_a_seed.json is written to
|
|
18
|
-
* the contract directory before the first phase step, so goal_normalization
|
|
19
|
-
* and context_collection prompts can reference the auditor findings directly.
|
|
20
|
-
* Path B (document/conversation): enters the pipeline directly from intake.
|
|
21
|
-
*
|
|
22
|
-
* Worker outputs are untrusted until validated: each invocation first ingests
|
|
23
|
-
* raw worker-written payloads into validated envelopes (recording dependency
|
|
24
|
-
* content hashes), then archives stale artifacts so the staleness DAG
|
|
25
|
-
* re-derives everything downstream of a repair. The adversarial critic →
|
|
26
|
-
* judge → repair loop lives across next-step invocations with its state in
|
|
27
|
-
* the contract artifacts plus repair-state.json; repairs are capped so a
|
|
28
|
-
* non-converging judge can never oscillate forever.
|
|
29
|
-
*/
|
|
1
|
+
// sites-pinned: tests/remediate/executable-plan-identity.test.ts, tests/remediate/executable-plan-safety.test.ts, tests/remediate/contract-review-independence.test.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
30
3
|
import { existsSync } from "node:fs";
|
|
31
|
-
import {
|
|
32
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import { readIntakeRiskSignal,
|
|
38
|
-
import { phaseOrdinalForObligations, moduleSlug, moduleSlugForObligationId, renderPhaseCutSection, detectContractTokenCycles, } from "../contractPipeline/phaseCut.js";
|
|
39
|
-
import { ensurePhaseCutArtifact, readPhaseCutArtifact } from "../contractPipeline/phaseCutArtifact.js";
|
|
40
|
-
import { detectCyclicSeamObligations, validateAuthoredCycleBreak, } from "../contractPipeline/cyclicSeamResolution.js";
|
|
41
|
-
import { deriveObligationLedger, deriveFinalizedModuleContracts, buildTestValidatorPlanScaffold, buildImplementationDagScaffold, acceptedCounterexampleIds, advisoryCritiqueItems, } from "../contractPipeline/derive.js";
|
|
42
|
-
import { ensureNodeId, toBlockId } from "../contractPipeline/idRegistry.js";
|
|
43
|
-
import { captureReviewSnapshot, computeReReviewDelta, isReviewArtifact, readReviewSnapshot, renderReReviewSection, reviewSnapshotExists, } from "../contractPipeline/reviewSnapshot.js";
|
|
44
|
-
import { captureTestPlanCarry, readTestPlanCarry, } from "../contractPipeline/testPlanCarry.js";
|
|
45
|
-
import { CYCLIC_SEAM_BREAK_STRATEGIES, CYCLIC_SEAM_RESOLUTION_STATUSES_OFFERED, isCyclicSeamBreakStrategy, sketchValues, } from "../contractPipeline/sketchSource.js";
|
|
46
|
-
import { readRepairState, writeRepairState, counterexamplesByIdOf, counterexampleKeyOf, counterexampleWaiversPath, foldCounterexampleWaivers, waivedAcceptedIds, waivedJudgeAcceptedIds, } from "../contractPipeline/repairState.js";
|
|
47
|
-
import { renderContractPipelinePrompt, renderContractRepairPrompt, CONTRACT_PIPELINE_PHASE_ORDER, PHASE_TO_ARTIFACT, reviewRequirementForRole, } from "./contractPipelinePrompts.js";
|
|
48
|
-
// The seven cross-artifact validators this module used to call one by one are
|
|
49
|
-
// gone from this list on purpose: every one of them is now reached through
|
|
50
|
-
// `evaluateContractPipelineCrossGateOutcomes`, so a call site cannot read a
|
|
51
|
-
// gate's issue array without also seeing whether the gate RAN
|
|
52
|
-
// (the branch-on-evaluated rule). What remains here are the checks that are not
|
|
53
|
-
// part of that eight-gate set.
|
|
54
|
-
import { CONTRACT_PIPELINE_VALIDATORS, CP_MODULE_CONTRACTS_VERSION, validateGoalIdConsistency, validateWorkBlockSeamPreparation, validateContractCitationGrounding, validateDecompositionFileScope, } from "../validation/contractPipeline.js";
|
|
55
|
-
// Imported from the owning gate module directly (as derive.ts does): this
|
|
56
|
-
// loop-core path consumes the single outcome-based entry point and its
|
|
57
|
-
// evaluated/skipped vocabulary together.
|
|
58
|
-
import { evaluateContractPipelineCrossGateOutcomes, enumerateRepoTreePaths, isInsideGitWorkTree, isTestablePhaseObligation, } from "../validation/contractPipelineGates.js";
|
|
59
|
-
import { compareCodeUnits } from "../../shared/compareCodeUnits.js";
|
|
4
|
+
import { readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { hashContent, stableStringify, readOptionalJsonFile, writeJsonFile, repoRelativePath, toPosixPath, FindingSchema, AuditReadSchema } from "audit-tools/shared";
|
|
8
|
+
import { bindWorkerPrompt } from "../../shared/submission/workerPromptBinding.js";
|
|
9
|
+
import { parseReviewSubmissionEnvelope, reviewIndependenceIssue } from "../../shared/types/reviewIndependence.js";
|
|
10
|
+
import { readIntakeRiskSignal, adversarialDepthForTier, escalateRiskSignal, writeIntakeRiskSignal, decompositionRiskEvidence } from "../riskSignal.js";
|
|
60
11
|
import { writeCurrentStep } from "./stepWriter.js";
|
|
61
12
|
import { loaderCommand } from "./prompts.js";
|
|
62
|
-
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* sharpest case: the judge verdict is the SOLE admission to implementation
|
|
93
|
-
* planning, so one worker emitting both artifacts could write zero
|
|
94
|
-
* counterexamples plus `approved` and let the loop certify its own exit.
|
|
95
|
-
* - `implementation_planning` is the phase `judgeRepairGate` protects, and its
|
|
96
|
-
* scaffold is built from the judge's accepted counterexamples, which do not
|
|
97
|
-
* exist at judge-render time.
|
|
98
|
-
*
|
|
99
|
-
* A collapsed section carries exactly what its fine-grained step would have
|
|
100
|
-
* carried — see `collapsedSectionExtra`.
|
|
101
|
-
*/
|
|
102
|
-
const COLLAPSE_GROUPS = [
|
|
103
|
-
// Framing: scope the change top-down. One coherent authoring act, no
|
|
104
|
-
// adversarial judgment, no deterministic derivation interleaved.
|
|
105
|
-
["goal_normalization", "context_collection", "decomposition"],
|
|
106
|
-
// Authoring tail: the test/validator plan and the author's OWN coverage
|
|
107
|
-
// self-assessment. `assessment` is deliberately not an independent-critic
|
|
108
|
-
// phase, and `contract_assessment_report` already depends on
|
|
109
|
-
// `test_validator_plan` — the same later-reads-earlier shape the framing
|
|
110
|
-
// group relies on. The critic reviews both afterwards, unchanged.
|
|
111
|
-
["test_validator_plan", "assessment"],
|
|
112
|
-
];
|
|
113
|
-
// ── Bounded-loop caps ─────────────────────────────────────────────────────────
|
|
114
|
-
/**
|
|
115
|
-
* Runaway backstop for the judge↔repair loop — the LOUD exception path, NOT the
|
|
116
|
-
* normal terminator. The loop normally terminates by *convergence*: it keeps
|
|
117
|
-
* repairing only while each round surfaces a genuinely NEW accepted counterexample
|
|
118
|
-
* (real progress), reaches a fixpoint when the judge approves, and escalates to the
|
|
119
|
-
* user the moment a round re-accepts an already-addressed counterexample without
|
|
120
|
-
* progress (a stall/oscillation). This ceiling exists only so a pathological run
|
|
121
|
-
* that keeps minting brand-new accepted counterexamples forever cannot loop without
|
|
122
|
-
* bound; hitting it is itself an escalation (loud), never a silent proceed. It is
|
|
123
|
-
* deliberately generous — a genuinely deep but converging design (each round a new
|
|
124
|
-
* real defect) must not be cut mid-convergence (the failure mode of the former N=2).
|
|
125
|
-
*/
|
|
126
|
-
export const MAX_CONTRACT_REPAIR_ITERATIONS = 8;
|
|
127
|
-
/** Maximum implementation_dag regenerations after traceability rejections. */
|
|
128
|
-
export const MAX_DAG_REGENERATION_ATTEMPTS = 2;
|
|
129
|
-
/**
|
|
130
|
-
* Maximum LLM cycle-break resolution attempts before routing to user-decision
|
|
131
|
-
* (and, if that also fails, to `blocked`).
|
|
132
|
-
*/
|
|
133
|
-
export const MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS = 2;
|
|
134
|
-
function cyclicSeamRepairStatePath(artifactsDir) {
|
|
135
|
-
return join(contractPipelineDir(artifactsDir), "cyclic-seam-repair-state.json");
|
|
136
|
-
}
|
|
137
|
-
export async function readCyclicSeamRepairState(artifactsDir) {
|
|
138
|
-
const state = await readOptionalJsonFile(cyclicSeamRepairStatePath(artifactsDir));
|
|
139
|
-
return (state ?? {
|
|
140
|
-
schema_version: "remediate-code-contract-pipeline/cyclic-seam-repair-state/v1alpha1",
|
|
141
|
-
attempts: [],
|
|
142
|
-
user_decision_emitted: false,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
export async function writeCyclicSeamRepairState(artifactsDir, state) {
|
|
146
|
-
await mkdir(contractPipelineDir(artifactsDir), { recursive: true });
|
|
147
|
-
await writeJsonFile(cyclicSeamRepairStatePath(artifactsDir), state);
|
|
148
|
-
}
|
|
149
|
-
// ── Envelope handling ─────────────────────────────────────────────────────────
|
|
150
|
-
/**
|
|
151
|
-
* Render a pre-filled skeleton section (S3 scaffold) for the partially-derivable
|
|
152
|
-
* phases. The tool derives the structure/ids/cross-refs from the already-present
|
|
153
|
-
* obligation ledger and leaves only the judgment slots blank, so the worker fills
|
|
154
|
-
* sentences/commands rather than emitting a whole artifact from scratch. Returns
|
|
155
|
-
* undefined when there is nothing to scaffold (no testable obligations / no nodes).
|
|
156
|
-
*/
|
|
157
|
-
async function buildScaffoldSection(phase, artifactsDir) {
|
|
158
|
-
const ledger = envelopePayload(await readContractArtifact(artifactsDir, "obligation_ledger"));
|
|
159
|
-
if (phase === "test_validator_plan") {
|
|
160
|
-
const prior = await readTestPlanCarry(artifactsDir);
|
|
161
|
-
const scaffold = buildTestValidatorPlanScaffold(ledger, prior);
|
|
162
|
-
if (scaffold.test_specs.length === 0)
|
|
163
|
-
return undefined;
|
|
164
|
-
const carriedCount = scaffold.test_specs.filter((s) => s.assertions.length > 0).length;
|
|
165
|
-
const path = contractInputFilePath(artifactsDir, "test_validator_plan");
|
|
166
|
-
const carryNote = carriedCount > 0
|
|
167
|
-
? `\n\n**Carried from the prior round (C3):** ${carriedCount} spec(s) already have assertions — their obligation premise is unchanged, so keep them as-is unless you intend to revise. Only the specs with an EMPTY \`assertions\` array need authoring.`
|
|
168
|
-
: "";
|
|
169
|
-
return `## Pre-filled Skeleton — fill only the blank slots
|
|
170
|
-
|
|
171
|
-
The obligation ledger was derived deterministically. Below is the test-plan skeleton: one spec per testable obligation, with \`obligation_id\`, \`name\`, \`kind\`, and \`scope_anchors\` already filled. Fill ONLY each \`assertions\` array — every spec needs at least one positive (satisfied-path) assertion AND one negative (failure-path) assertion. The negative assertion MUST name one of the spec's \`scope_anchors\` (the touched symbol/file) and must not be an unscoped repo-wide scan, or it fails the negative-scoping gate. Do not add, remove, or rename specs. If an obligation is genuinely untestable, keep the spec's \`obligation_id\` and \`name\`, and replace its \`kind\`, \`scope_anchors\` and \`assertions\` with \`"inapplicable_claim": { "obligation_id": "<the same obligation_id>", "reason": "<a reason the ledger can disprove>" }\`.${carryNote}
|
|
172
|
-
|
|
173
|
-
\`\`\`json
|
|
174
|
-
${JSON.stringify(scaffold, null, 2)}
|
|
175
|
-
\`\`\`
|
|
176
|
-
|
|
177
|
-
Self-check before next-step: \`${loaderCommand(`validate-artifact --name test_validator_plan --file ${path}`)}\``;
|
|
178
|
-
}
|
|
179
|
-
if (phase === "implementation_planning") {
|
|
180
|
-
const judge = envelopePayload(await readContractArtifact(artifactsDir, "judge_report"));
|
|
181
|
-
const finalized = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
182
|
-
const scaffold = buildImplementationDagScaffold(ledger, acceptedCounterexampleIds(judge), finalized);
|
|
183
|
-
if (scaffold.nodes.length === 0)
|
|
184
|
-
return undefined;
|
|
185
|
-
const advisory = advisoryCritiqueItems(envelopePayload(await readContractArtifact(artifactsDir, "conceptual_design_critique")));
|
|
186
|
-
const advisoryBlock = advisory.length > 0
|
|
187
|
-
? `\n\nAdvisory conceptual-critique items (no obligation/counterexample of their own — give each a home in some node's \`addressed_critique_items\` and let it shape that node's implementation; do NOT smuggle them into test assertions):\n${advisory
|
|
188
|
-
.map((a) => `- \`${a.id}\`: ${a.description}`)
|
|
189
|
-
.join("\n")}`
|
|
190
|
-
: "";
|
|
191
|
-
const path = contractInputFilePath(artifactsDir, "implementation_dag");
|
|
192
|
-
return `## Pre-filled Skeleton — fill only the blank slots
|
|
193
|
-
|
|
194
|
-
Below is the implementation-DAG skeleton: ONE node per module (its obligations already grouped), covering every obligation and accepted counterexample. Each node's \`depends_on\` is already DERIVED from the finalized contracts' data-flow (a node depends on the modules whose \`artifact:<name>\` outputs it consumes) — keep it unless you know an ordering is wrong. Fill ONLY each node's \`title\`, \`description\`, and \`targeted_commands\`. You MAY further merge or split nodes and refine \`depends_on\`/\`edges\` ordering, as long as every obligation stays covered (in \`satisfies_obligations\` or \`verification_obligation_ids\`) and every accepted counterexample stays in some node's \`addresses_counterexamples\`.${advisoryBlock}
|
|
195
|
-
|
|
196
|
-
\`\`\`json
|
|
197
|
-
${JSON.stringify(scaffold, null, 2)}
|
|
198
|
-
\`\`\`
|
|
199
|
-
|
|
200
|
-
Self-check before next-step: \`${loaderCommand(`validate-artifact --name implementation_dag --file ${path}`)}\``;
|
|
201
|
-
}
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Archive an artifact into `<contract>/history/` instead of deleting it, so a
|
|
206
|
-
* repair loop never silently destroys an LLM output. Two disjoint files exist
|
|
207
|
-
* per artifact (D3): the host's plain INPUT (`<name>.input.json` — the LLM
|
|
208
|
-
* emission) and the tool's canonical envelope (`<name>.json` — regenerable
|
|
209
|
-
* bookkeeping). On a stale/invalid re-emit BOTH are moved to history: the input
|
|
210
|
-
* to preserve the LLM output AND free its path for a fresh host Write, the
|
|
211
|
-
* canonical so the completion gate (`contractArtifactExists`) re-fires and the
|
|
212
|
-
* producing phase re-emits. The returned `archivedPath` references the input
|
|
213
|
-
* archive when present (what the host re-authors), else the canonical archive.
|
|
214
|
-
* A tool-derived artifact with no input file (e.g. a merged-shard artifact)
|
|
215
|
-
* archives only its canonical envelope. If any move throws, the rest are left
|
|
216
|
-
* in place (`originalFree: false`) rather than silently dropped. `renameFn` is a
|
|
217
|
-
* DI seam so a failed history move is testable.
|
|
218
|
-
*/
|
|
219
|
-
export async function archiveContractArtifact(artifactsDir, name, label, renameFn = rename) {
|
|
220
|
-
const inputSource = contractInputFilePath(artifactsDir, name);
|
|
221
|
-
const canonicalSource = contractArtifactFilePath(artifactsDir, name);
|
|
222
|
-
const hasInput = existsSync(inputSource);
|
|
223
|
-
const hasCanonical = existsSync(canonicalSource);
|
|
224
|
-
// The `invalid` label rejects the artifact this snapshot is the verdict FOR, so
|
|
225
|
-
// the snapshot dies with it — unconditionally, and before the nothing-to-
|
|
226
|
-
// archive short-circuit below, so an orphaned snapshot is cleaned up too. A
|
|
227
|
-
// surviving copy is worse than an absent one: ingest captures a fresh snapshot
|
|
228
|
-
// only AFTER its own staleness pass, so the NEXT invocation would diff the new
|
|
229
|
-
// payload against a verdict about content the reviewer never saw. Dropped even
|
|
230
|
-
// when the history move itself fails, because the re-emit that follows tells
|
|
231
|
-
// the worker to overwrite the input in place — the old verdict is void either
|
|
232
|
-
// way.
|
|
233
|
-
//
|
|
234
|
-
// The `stale` label must NOT drop it, and that asymmetry is the whole point:
|
|
235
|
-
// staleness is what re-opens a review phase, and the diff-based re-review only
|
|
236
|
-
// fires when a prior snapshot is there to diff against. Dropping on the stale
|
|
237
|
-
// path would silently turn every staleness re-emit back into the blind full
|
|
238
|
-
// review this mechanism exists to avoid.
|
|
239
|
-
if (label === "invalid") {
|
|
240
|
-
await dropReviewSnapshot(artifactsDir, name);
|
|
241
|
-
}
|
|
242
|
-
if (!hasInput && !hasCanonical)
|
|
243
|
-
return { originalFree: true };
|
|
244
|
-
const historyDir = join(contractPipelineDir(artifactsDir), "history");
|
|
245
|
-
await mkdir(historyDir, { recursive: true });
|
|
246
|
-
const stamp = Date.now();
|
|
247
|
-
let archivedPath;
|
|
248
|
-
// Preserve the host's plain output (the LLM emission) first, freeing the input
|
|
249
|
-
// path so the rewrite signpost's fresh Write lands cleanly.
|
|
250
|
-
if (hasInput) {
|
|
251
|
-
const dest = join(historyDir, `${name}.${label}-${stamp}.input.json`);
|
|
252
|
-
archivedPath = dest;
|
|
253
|
-
try {
|
|
254
|
-
await withFsRetry(() => renameFn(inputSource, dest));
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
return { archivedPath, originalFree: false };
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// Clear the tool-derived canonical envelope so the completion gate re-fires.
|
|
261
|
-
if (hasCanonical) {
|
|
262
|
-
const dest = join(historyDir, `${name}.${label}-${stamp}.json`);
|
|
263
|
-
try {
|
|
264
|
-
await withFsRetry(() => renameFn(canonicalSource, dest));
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return { archivedPath: archivedPath ?? dest, originalFree: false };
|
|
268
|
-
}
|
|
269
|
-
archivedPath = archivedPath ?? dest;
|
|
270
|
-
}
|
|
271
|
-
return { archivedPath, originalFree: true };
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* The explicit re-author signpost appended to every inline rejection re-emit:
|
|
275
|
-
* the prior output was archived, so the worker must Write a fresh complete
|
|
276
|
-
* artifact at the ORIGINAL path — never Edit the previous (now-archived) file.
|
|
277
|
-
*/
|
|
278
|
-
export function rejectionRewriteInstruction(archived) {
|
|
279
|
-
// Back-compat: a bare path argument behaves as a successful (originalFree) archive.
|
|
280
|
-
const outcome = typeof archived === "string" || archived === undefined
|
|
281
|
-
? { archivedPath: archived, originalFree: true }
|
|
282
|
-
: archived;
|
|
283
|
-
const where = outcome.archivedPath
|
|
284
|
-
? `\`${outcome.archivedPath}\``
|
|
285
|
-
: "the contract history directory";
|
|
286
|
-
if (outcome.originalFree === false) {
|
|
287
|
-
// Honor archiveContractArtifact's originalFree signal: the history move failed,
|
|
288
|
-
// so the rejected file is STILL at its original path. Tell the host to
|
|
289
|
-
// overwrite it in place — a fresh Write that replaces the stale content is the
|
|
290
|
-
// only way the re-emit lands (the path is not free).
|
|
291
|
-
return `\n\n> The previous output could not be archived and REMAINS at its original path; overwrite it with a fresh complete artifact (a full Write that replaces the file) — do NOT Edit incrementally.`;
|
|
292
|
-
}
|
|
293
|
-
return `\n\n> Prior output archived to ${where}; Write a fresh complete artifact at its original path — do NOT Edit the previous file.`;
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Derive validated canonical envelopes from the host's plain INPUT files (D3).
|
|
297
|
-
* The host writes the bare payload the role schema describes to
|
|
298
|
-
* `<name>.input.json`; the tool reads it here, validates it, and writes the
|
|
299
|
-
* content-hash envelope to the canonical `<name>.json` — the host's input file
|
|
300
|
-
* is never mutated in place. CP_ARTIFACT_NAMES is dependency-ordered, so
|
|
301
|
-
* dependencies are enveloped before their dependents and dependency hashes are
|
|
302
|
-
* always available.
|
|
303
|
-
*/
|
|
304
|
-
export async function ingestContractArtifacts(artifactsDir, adversarialDepth) {
|
|
305
|
-
const ingested = [];
|
|
306
|
-
const invalid = [];
|
|
307
|
-
const stale = [];
|
|
308
|
-
const depth = adversarialDepth ?? (await resolveAdversarialDepth(artifactsDir)).adversarialDepth;
|
|
309
|
-
for (const name of CP_ARTIFACT_NAMES) {
|
|
310
|
-
const raw = await readOptionalJsonFile(contractInputFilePath(artifactsDir, name));
|
|
311
|
-
if (raw === undefined || raw === null)
|
|
312
|
-
continue;
|
|
313
|
-
// The host writes a plain payload; defensively unwrap if an envelope slipped
|
|
314
|
-
// into the input path so ingest and the validate-artifact self-check agree.
|
|
315
|
-
const role = ARTIFACT_TO_PHASE[name] ?? "";
|
|
316
|
-
const requirement = reviewRequirementForRole(role, depth);
|
|
317
|
-
const reviewed = await validateContractReviewInput({
|
|
318
|
-
artifactsDir, artifact: name, role, requirement,
|
|
319
|
-
raw: requirement === "ordinary" && isEnvelope(raw) ? raw.payload : raw,
|
|
320
|
-
});
|
|
321
|
-
if (!reviewed.ok) {
|
|
322
|
-
if (reviewed.code === "dependency_stale") {
|
|
323
|
-
stale.push(name);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
invalid.push({ name, issues: [{ severity: "error", path: `${name}.review`, message: reviewed.issue }], ...(reviewed.code === "review_unavailable" ? { unavailable: true } : {}) });
|
|
327
|
-
continue;
|
|
328
|
-
}
|
|
329
|
-
const bare = reviewed.payload;
|
|
330
|
-
// The host has no clock: stamp the tool-owned `created_at` before validation
|
|
331
|
-
// so the host never has to invent a timestamp (B4). No-op when already present.
|
|
332
|
-
const payload = stampToolCreatedAt(bare, new Date().toISOString());
|
|
333
|
-
// Idempotency: the input file persists across next-step calls, so skip
|
|
334
|
-
// re-ingesting an input whose canonical envelope already reflects it. The
|
|
335
|
-
// semantic projection strips the tool-stamped `created_at`, so a no-op
|
|
336
|
-
// re-ingest is stable (it does NOT re-fire snapshots or rewrite the
|
|
337
|
-
// envelope); only a genuine host edit re-derives.
|
|
338
|
-
const requiredIssues = reviewed.provenance
|
|
339
|
-
? CONTRACT_PIPELINE_VALIDATORS[name](payload, name).filter(issue => issue.severity === "error")
|
|
340
|
-
: undefined;
|
|
341
|
-
if (requiredIssues && requiredIssues.length > 0) {
|
|
342
|
-
invalid.push({ name, issues: requiredIssues });
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
const existing = await readContractArtifact(artifactsDir, name);
|
|
346
|
-
const sameReview = !reviewed.provenance || (existing?.review_provenance?.prompt_sha256 === reviewed.provenance.prompt_sha256 &&
|
|
347
|
-
existing.review_provenance.requirement === reviewed.provenance.requirement &&
|
|
348
|
-
stableStringify(existing.review_provenance.review) === stableStringify(reviewed.provenance.review));
|
|
349
|
-
if (existing && sameReview && envelopeSemanticHash(existing) === payloadSemanticHash(name, payload)) {
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
const issues = requiredIssues ?? CONTRACT_PIPELINE_VALIDATORS[name](payload, name).filter((issue) => issue.severity === "error");
|
|
353
|
-
if (issues.length > 0) {
|
|
354
|
-
invalid.push({ name, issues });
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
await writeContractArtifact(artifactsDir, name, payload, reviewed.provenance);
|
|
358
|
-
ingested.push(name);
|
|
359
|
-
// Repair-revert fix: an ingested aggregated `module_contracts` payload (a
|
|
360
|
-
// degenerate single-agent draft, or a direct edit) is written back through to
|
|
361
|
-
// the per-module shards so shards ≡ aggregate stays an invariant — otherwise a
|
|
362
|
-
// later upstream cascade (e.g. a module_decomposition edit) re-merges the STALE
|
|
363
|
-
// shards and silently reverts the change. No-op for every non-sharded artifact
|
|
364
|
-
// (`finalized_module_contracts` is deterministically derived, never sharded).
|
|
365
|
-
await propagateAggregateToShards(artifactsDir, name, payload);
|
|
366
|
-
// Snapshot a freshly-produced review verdict + the upstreams it reviewed, so
|
|
367
|
-
// a later staleness re-emit can be diff-based (B2). No-op for non-review
|
|
368
|
-
// artifacts. Captured at ingest, when the upstreams are in the exact state
|
|
369
|
-
// the worker reviewed.
|
|
370
|
-
if (isReviewArtifact(name)) {
|
|
371
|
-
await captureReviewSnapshot(artifactsDir, name, payload, new Date().toISOString());
|
|
372
|
-
}
|
|
373
|
-
// C3: snapshot the authored test-plan so a later re-emit can diff-carry the
|
|
374
|
-
// assertions of unchanged obligations instead of forcing a full re-author.
|
|
375
|
-
if (name === "test_validator_plan") {
|
|
376
|
-
await captureTestPlanCarry(artifactsDir, payload, new Date().toISOString());
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return { ingested, invalid, ...(stale.length > 0 ? { stale } : {}) };
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Determine whether the contract pipeline should be entered for this run.
|
|
383
|
-
* The pipeline is entered for ALL intake source types (structured_audit,
|
|
384
|
-
* document, conversation) when an extracted-plan.json has not yet been
|
|
385
|
-
* produced. Path A (structured_audit) seeds the pipeline via a path_a_seed.json
|
|
386
|
-
* before the first phase step, so goal_normalization and context_collection
|
|
387
|
-
* prompts can reference the auditor findings.
|
|
388
|
-
*/
|
|
389
|
-
export function shouldEnterContractPipeline(artifactsDir, _intakeSourceType) {
|
|
390
|
-
const paths = intakePaths(artifactsDir);
|
|
391
|
-
// If an extracted plan already exists, the pipeline has completed.
|
|
392
|
-
if (existsSync(paths.extractedPlan)) {
|
|
393
|
-
return { shouldHandleContractPipeline: false, pipelineComplete: true };
|
|
394
|
-
}
|
|
395
|
-
// Check whether the implementation_dag exists (pipeline complete, awaiting extraction).
|
|
396
|
-
if (contractArtifactExists(artifactsDir, "implementation_dag")) {
|
|
397
|
-
return { shouldHandleContractPipeline: true, pipelineComplete: true };
|
|
398
|
-
}
|
|
399
|
-
return { shouldHandleContractPipeline: true, pipelineComplete: false };
|
|
400
|
-
}
|
|
401
|
-
/** Return the first pipeline phase whose output artifact does not exist. */
|
|
402
|
-
export function nextMissingContractPhase(artifactsDir) {
|
|
403
|
-
for (const phase of CONTRACT_PIPELINE_PHASE_ORDER) {
|
|
404
|
-
const artifactName = PHASE_TO_ARTIFACT[phase];
|
|
405
|
-
if (!artifactName)
|
|
406
|
-
continue;
|
|
407
|
-
if (!contractArtifactExists(artifactsDir, artifactName)) {
|
|
408
|
-
return phase;
|
|
409
|
-
}
|
|
13
|
+
import { renderPlanAuthorPrompt, renderPlanReviewPrompt, reviewRequirementForRole } from "./contractPipelinePrompts.js";
|
|
14
|
+
import { executionPlanPaths, PlanSourceSchema, readPlanSource, readCanonicalPlan, ingestExecutionPlan, readPlanReviewHistory, readPlanReview, readApprovedExecutionPlan, CritiqueSchema, CriticSchema, PlanJudgeSchema, PlanRiskDecisionSchema, PLAN_REVIEW_ROLES, executionPlanContextIssues, PlanReviewReceiptSchema, readExecutionIntent, } from "../contractPipeline/executionPlan.js";
|
|
15
|
+
export { readApprovedExecutionPlan } from "../contractPipeline/executionPlan.js";
|
|
16
|
+
export async function resolveAdversarialDepth(artifactsDir) {
|
|
17
|
+
let riskSignal = await readIntakeRiskSignal(artifactsDir);
|
|
18
|
+
const canonical = await readCanonicalPlan(artifactsDir);
|
|
19
|
+
if (riskSignal && canonical) {
|
|
20
|
+
const files = [...new Set(canonical.plan.units.flatMap(unit => unit.allowed_files))];
|
|
21
|
+
// The run's existing shared classifier remains the policy authority.
|
|
22
|
+
const evidence = decompositionRiskEvidence({ moduleCount: canonical.plan.units.length, fileScopes: files });
|
|
23
|
+
const raised = evidence ? escalateRiskSignal(riskSignal, evidence) : riskSignal;
|
|
24
|
+
if (raised !== riskSignal) {
|
|
25
|
+
riskSignal = raised;
|
|
26
|
+
await writeIntakeRiskSignal(artifactsDir, raised);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { riskSignal, adversarialDepth: adversarialDepthForTier(riskSignal?.tier) };
|
|
30
|
+
}
|
|
31
|
+
async function sourceDigests(paths) {
|
|
32
|
+
return Promise.all([...new Set(paths)].sort().map(async (path) => ({ path, sha256: hashContent(await readFile(path, "utf8")) })));
|
|
33
|
+
}
|
|
34
|
+
export async function writePathASeedFromFindings(artifactsDir, sourcePath, payload) {
|
|
35
|
+
const raw = payload;
|
|
36
|
+
const findings = (raw.findings ?? []).map(finding => FindingSchema.parse(finding));
|
|
37
|
+
const paths = executionPlanPaths(artifactsDir);
|
|
38
|
+
const existing = await readPlanSource(artifactsDir);
|
|
39
|
+
if (existing) {
|
|
40
|
+
if (stableStringify(existing.findings) !== stableStringify(findings))
|
|
41
|
+
throw new Error("Approved source findings changed during this run; preserve the current run and explicitly start a new source selection.");
|
|
42
|
+
return;
|
|
410
43
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
* The seed is written once (idempotent: skipped when it already exists).
|
|
416
|
-
* goal_normalization and context_collection prompts detect the seed and
|
|
417
|
-
* include its contents so every pipeline node traces to an auditor finding.
|
|
418
|
-
*/
|
|
419
|
-
export async function writePathASeedFromFindings(artifactsDir, auditFindingsPath, auditFindings) {
|
|
420
|
-
// Contract-claiming input must pass the strict shared validator before even
|
|
421
|
-
// the idempotence shortcut. No seed/state/plan artifact may be derived from a
|
|
422
|
-
// partially parsed or permissively defaulted report.
|
|
423
|
-
const approved = projectApprovedFindings(auditFindings);
|
|
424
|
-
const seedPath = pathASeedFilePath(artifactsDir);
|
|
425
|
-
if (existsSync(seedPath))
|
|
426
|
-
return; // idempotent
|
|
427
|
-
const findings = [...approved.findings].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
428
|
-
const affectedFilesSet = new Set();
|
|
429
|
-
const findingsSummary = findings.map((finding) => ({
|
|
430
|
-
id: finding.id,
|
|
431
|
-
title: finding.title,
|
|
432
|
-
lens: finding.lens,
|
|
44
|
+
const audit = AuditReadSchema.safeParse(raw.audit_read);
|
|
45
|
+
await writeJsonFile(paths.source, PlanSourceSchema.parse({
|
|
46
|
+
plan_id: `plan-${randomUUID()}`, findings, audit_read: audit.success ? audit.data : null,
|
|
47
|
+
sources: await sourceDigests([sourcePath]), intent: await readExecutionIntent(artifactsDir),
|
|
433
48
|
}));
|
|
434
|
-
for (const finding of findings) {
|
|
435
|
-
for (const affectedFile of finding.affected_files) {
|
|
436
|
-
affectedFilesSet.add(affectedFile.path);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
const workBlocks = approved.workBlocks
|
|
440
|
-
.map((block) => ({
|
|
441
|
-
...block,
|
|
442
|
-
finding_ids: [...block.finding_ids].sort(),
|
|
443
|
-
unit_ids: [...block.unit_ids].sort(),
|
|
444
|
-
owned_files: [...block.owned_files].sort(),
|
|
445
|
-
depends_on: [...block.depends_on].sort(),
|
|
446
|
-
}))
|
|
447
|
-
.sort((a, b) => compareCodeUnits(a.id, b.id));
|
|
448
|
-
// Code-unit order, not ICU collation, on EVERY persisted seed array: the
|
|
449
|
-
// seed order must not depend on the host's ICU collation, and seam ids are
|
|
450
|
-
// hex now — a locale that orders digits against letters differently would
|
|
451
|
-
// reshuffle the file.
|
|
452
|
-
const workBlockSeams = approved.workBlockSeams
|
|
453
|
-
.map((seam) => ({
|
|
454
|
-
...seam,
|
|
455
|
-
block_ids: [...seam.block_ids].sort(compareCodeUnits),
|
|
456
|
-
}))
|
|
457
|
-
.sort((a, b) => compareCodeUnits(a.id, b.id));
|
|
458
|
-
const affectedFiles = [...affectedFilesSet].sort();
|
|
459
|
-
const seed = {
|
|
460
|
-
schema_version: "remediate-code-contract-pipeline/path-a-seed/v1alpha2",
|
|
461
|
-
audit_findings_path: auditFindingsPath,
|
|
462
|
-
finding_count: findings.length,
|
|
463
|
-
findings_summary: findingsSummary,
|
|
464
|
-
affected_files: affectedFiles,
|
|
465
|
-
work_blocks: workBlocks,
|
|
466
|
-
work_block_seams: workBlockSeams,
|
|
467
|
-
source_digests: await hashSeedSourcePaths(seedRepoRoot(artifactsDir), auditFindingsPath, affectedFiles),
|
|
468
|
-
created_at: new Date().toISOString(),
|
|
469
|
-
};
|
|
470
|
-
await mkdir(contractPipelineDir(artifactsDir), { recursive: true });
|
|
471
|
-
await writeJsonFile(seedPath, seed);
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* The repository root that owns `artifactsDir`, for resolving the seed's
|
|
475
|
-
* repo-relative `affected_files`. Derived through the shared
|
|
476
|
-
* `climbOutOfAuditTools` rather than a hand-rolled `../..`, so the one
|
|
477
|
-
* `.audit-tools` layout rule stays single-sourced (and a caller that hands us a
|
|
478
|
-
* dir outside the tree simply gets that dir back, which resolves relative paths
|
|
479
|
-
* against it — the same thing every other artifact path in this module does).
|
|
480
|
-
*/
|
|
481
|
-
function seedRepoRoot(artifactsDir) {
|
|
482
|
-
return climbOutOfAuditTools(artifactsDir);
|
|
483
|
-
}
|
|
484
|
-
/** Absolute form of a seed-recorded path (absolute entries pass through). */
|
|
485
|
-
function resolveSeedSourcePath(root, path) {
|
|
486
|
-
return isAbsolute(path) ? path : resolve(root, path);
|
|
487
|
-
}
|
|
488
|
-
/**
|
|
489
|
-
* sha256 every seed source path that EXISTS at seed-build time. A path that is
|
|
490
|
-
* absent is not recorded at all — the seed binds what it actually read, and a
|
|
491
|
-
* finding citing a file that does not exist yet (a new-file remediation) must
|
|
492
|
-
* not mint a digest that can never match.
|
|
493
|
-
*/
|
|
494
|
-
async function hashSeedSourcePaths(root, auditFindingsPath, affectedFiles) {
|
|
495
|
-
const digests = [];
|
|
496
|
-
// Content-derived order (path-sorted, deduped): an incidentally-ordered array
|
|
497
|
-
// would churn the seed's content hash on every re-derivation.
|
|
498
|
-
const candidates = [...new Set([auditFindingsPath, ...affectedFiles])].sort((left, right) => compareCodeUnits(left, right));
|
|
499
|
-
for (const path of candidates) {
|
|
500
|
-
const absolute = resolveSeedSourcePath(root, path);
|
|
501
|
-
let content;
|
|
502
|
-
try {
|
|
503
|
-
content = await readFile(absolute);
|
|
504
|
-
}
|
|
505
|
-
catch {
|
|
506
|
-
continue; // Not readable at seed time — nothing to bind.
|
|
507
|
-
}
|
|
508
|
-
digests.push({ path, sha256: hashContent(content) });
|
|
509
|
-
}
|
|
510
|
-
return digests;
|
|
511
|
-
}
|
|
512
|
-
/** The tool-named host lane: the operator's drift acceptances land here. */
|
|
513
|
-
export function seedSourceAcceptancesPath(artifactsDir) {
|
|
514
|
-
return join(contractPipelineDir(artifactsDir), "seed-source-acceptances.json");
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Read the operator's acceptances, keyed by the same path normalization the
|
|
518
|
-
* mismatch side uses. A malformed entry is DROPPED, never widened: an acceptance
|
|
519
|
-
* missing its rationale or its decider records no decision, and treating it as
|
|
520
|
-
* one would turn a shape defect into a silent bypass of the alarm.
|
|
521
|
-
*/
|
|
522
|
-
export async function readSeedSourceAcceptances(artifactsDir) {
|
|
523
|
-
const raw = await readOptionalJsonFile(seedSourceAcceptancesPath(artifactsDir));
|
|
524
|
-
const byPath = new Map();
|
|
525
|
-
if (!isRecord(raw) || !Array.isArray(raw.acceptances))
|
|
526
|
-
return byPath;
|
|
527
|
-
for (const entry of raw.acceptances) {
|
|
528
|
-
if (!isRecord(entry))
|
|
529
|
-
continue;
|
|
530
|
-
const path = entry.path;
|
|
531
|
-
const rationale = entry.rationale;
|
|
532
|
-
const acceptedBy = entry.accepted_by;
|
|
533
|
-
if (typeof path !== "string" ||
|
|
534
|
-
path.trim().length === 0 ||
|
|
535
|
-
typeof rationale !== "string" ||
|
|
536
|
-
rationale.trim().length === 0 ||
|
|
537
|
-
typeof acceptedBy !== "string" ||
|
|
538
|
-
acceptedBy.trim().length === 0) {
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
byPath.set(normalizeRepoPath(path), {
|
|
542
|
-
path: path.trim(),
|
|
543
|
-
rationale: rationale.trim(),
|
|
544
|
-
accepted_by: acceptedBy.trim(),
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
return byPath;
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* The seed paths the operator's acceptance file covers, applied to the drifted
|
|
551
|
-
* set under the ONE path normalization both sides share; everything else blocks.
|
|
552
|
-
*
|
|
553
|
-
* This is the accept path and it is deliberately the ONLY automatic one: the
|
|
554
|
-
* gate cannot tell a benign drift from a falsifying one, and the one predicate
|
|
555
|
-
* that claimed to (cited line counts) was unsound — see {@link SeedSourceAcceptance}.
|
|
556
|
-
*/
|
|
557
|
-
export async function partitionSeedSourceDrift(artifactsDir, mismatches) {
|
|
558
|
-
if (mismatches.length === 0)
|
|
559
|
-
return { accepted: [], blocking: [] };
|
|
560
|
-
const acceptances = await readSeedSourceAcceptances(artifactsDir);
|
|
561
|
-
if (acceptances.size === 0)
|
|
562
|
-
return { accepted: [], blocking: [...mismatches] };
|
|
563
|
-
const accepted = [];
|
|
564
|
-
const blocking = [];
|
|
565
|
-
for (const mismatch of mismatches) {
|
|
566
|
-
const acceptance = acceptances.get(normalizeRepoPath(mismatch.path));
|
|
567
|
-
if (acceptance) {
|
|
568
|
-
accepted.push({ ...acceptance, mismatch });
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
blocking.push(mismatch);
|
|
572
|
-
}
|
|
573
|
-
return { accepted, blocking };
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Seed source-digest binding — re-hash every path the path_a seed recorded and
|
|
577
|
-
* report the ones that moved. Pure over (root, seed): the caller decides what a
|
|
578
|
-
* mismatch means, so this is directly red-green testable without a pipeline.
|
|
579
|
-
*
|
|
580
|
-
* A seed with no `source_digests` (written before the field existed) binds
|
|
581
|
-
* nothing and yields no mismatches.
|
|
582
|
-
*/
|
|
583
|
-
export async function detectSeedSourceDigestMismatches(root, seed) {
|
|
584
|
-
const mismatches = [];
|
|
585
|
-
for (const entry of seed?.source_digests ?? []) {
|
|
586
|
-
if (typeof entry?.path !== "string" || typeof entry?.sha256 !== "string")
|
|
587
|
-
continue;
|
|
588
|
-
const absolute = resolveSeedSourcePath(root, entry.path);
|
|
589
|
-
let actual = null;
|
|
590
|
-
try {
|
|
591
|
-
actual = hashContent(await readFile(absolute));
|
|
592
|
-
}
|
|
593
|
-
catch {
|
|
594
|
-
actual = null;
|
|
595
|
-
}
|
|
596
|
-
if (actual !== entry.sha256) {
|
|
597
|
-
mismatches.push({ path: entry.path, expected: entry.sha256, actual });
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return mismatches;
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* Infer the most appropriate repair target from judge classifications when no
|
|
604
|
-
* explicit repair_directive is provided. Examines only accepted classifications
|
|
605
|
-
* and keyword-matches their rationale text.
|
|
606
|
-
*
|
|
607
|
-
* Priority (first match wins):
|
|
608
|
-
* obligation/ledger/invariant/constraint keywords → obligation_ledger
|
|
609
|
-
* assessment/finding/gap keywords → contract_assessment_report
|
|
610
|
-
* fallback → finalized_module_contracts
|
|
611
|
-
*/
|
|
612
|
-
export function inferRepairTarget(
|
|
613
|
-
// Widened to match the guard below rather than the other way round: judge
|
|
614
|
-
// reports are read back from artifact JSON, so `classifications` genuinely
|
|
615
|
-
// arrives absent on a malformed/partial report. The declared-required type
|
|
616
|
-
// said that could not happen while the body defended against it — and a
|
|
617
|
-
// signature that disagrees with its own null-guard makes one of them dead.
|
|
618
|
-
classifications) {
|
|
619
|
-
const accepted = (classifications ?? []).filter((c) => c.classification === "accepted");
|
|
620
|
-
const text = accepted.map((c) => c.rationale).join(" ").toLowerCase();
|
|
621
|
-
if (/obligation|ledger|invariant violated|constraint/.test(text)) {
|
|
622
|
-
return "obligation_ledger";
|
|
623
|
-
}
|
|
624
|
-
if (/assessment|contract finding|gap identified/.test(text)) {
|
|
625
|
-
return "contract_assessment_report";
|
|
626
|
-
}
|
|
627
|
-
return "finalized_module_contracts";
|
|
628
|
-
}
|
|
629
|
-
function inferRepairDirective(judge) {
|
|
630
|
-
return {
|
|
631
|
-
target: inferRepairTarget(judge.classifications),
|
|
632
|
-
instruction: "Address every judge-accepted counterexample in the judge report's classifications.",
|
|
633
|
-
};
|
|
634
49
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
* - a needs_repair verdict that surfaces a NEW accepted counterexample (one not
|
|
646
|
-
* already addressed by a prior repair) ⇒ repair (genuine progress);
|
|
647
|
-
* - a needs_repair verdict whose accepted counterexamples were ALL already
|
|
648
|
-
* addressed ⇒ escalate (stall/oscillation — the repair loop is not converging,
|
|
649
|
-
* surface the outstanding counterexamples to the user instead of silently
|
|
650
|
-
* shipping residual risk or looping);
|
|
651
|
-
* - the runaway backstop (MAX_CONTRACT_REPAIR_ITERATIONS) ⇒ escalate (loud).
|
|
652
|
-
* The former fixed N=2 cap that proceeded-with-residual-risk at an arbitrary count
|
|
653
|
-
* is gone: a deep-but-converging run is no longer cut mid-convergence, and a
|
|
654
|
-
* genuinely non-converging run is surfaced rather than buried.
|
|
655
|
-
*/
|
|
656
|
-
export async function evaluateJudgeGate(artifactsDir) {
|
|
657
|
-
const judgeEnvelope = await readContractArtifact(artifactsDir, "judge_report");
|
|
658
|
-
if (!judgeEnvelope)
|
|
659
|
-
return { kind: "proceed" };
|
|
660
|
-
const judge = envelopePayload(judgeEnvelope);
|
|
661
|
-
if (!judge || judge.verdict === "approved")
|
|
662
|
-
return { kind: "proceed" };
|
|
663
|
-
// Content-fingerprint keying (not raw id): two independent adversarial
|
|
664
|
-
// rounds may each label their genuinely-distinct top counterexample with
|
|
665
|
-
// the SAME reviewer id string (e.g. "CE-001", the prompt schema's own
|
|
666
|
-
// example value). Keying convergence on the raw id would then read "same CE
|
|
667
|
-
// re-accepted after a repair" and falsely escalate while a real new defect
|
|
668
|
-
// is being correctly repaired. Resolve each accepted id against the live
|
|
669
|
-
// counterexample artifact and key on content instead; an id with no
|
|
670
|
-
// matching counterexample falls back to raw-id keying — today's behavior —
|
|
671
|
-
// so nothing regresses when content can't be resolved. The keying is
|
|
672
|
-
// single-sourced with the waiver ledger (counterexampleKeyOf).
|
|
673
|
-
const cePayload = envelopePayload(await readContractArtifact(artifactsDir, "counterexample"));
|
|
674
|
-
const ceById = counterexamplesByIdOf(cePayload);
|
|
675
|
-
const keyOf = (rawId) => counterexampleKeyOf(ceById, rawId);
|
|
676
|
-
const acceptedIds = acceptedCeIdsOf(judge);
|
|
677
|
-
// Owner waivers (open-bugs.md:108, the recorded resolution verb): fold the
|
|
678
|
-
// host-written waiver file BEFORE any convergence math, so a waiver recorded
|
|
679
|
-
// against a blocked escalation unblocks this same invocation — including one
|
|
680
|
-
// recorded after a repair for this judge hash was already dispatched. An
|
|
681
|
-
// invalid file escalates loudly and applies NOTHING (never half-applied).
|
|
682
|
-
const fold = await foldCounterexampleWaivers(artifactsDir, {
|
|
683
|
-
counterexamplesById: ceById,
|
|
684
|
-
judgeAcceptedIds: new Set(acceptedIds),
|
|
50
|
+
async function ensureSource(options) {
|
|
51
|
+
const existing = await readPlanSource(options.artifactsDir);
|
|
52
|
+
if (existing)
|
|
53
|
+
return existing;
|
|
54
|
+
const sources = await sourceDigests(options.sourcePaths ?? []);
|
|
55
|
+
const requestText = (await Promise.all(sources.map(source => readFile(source.path, "utf8")))).join("\n\n");
|
|
56
|
+
const id = `plan-${randomUUID()}`;
|
|
57
|
+
const source = PlanSourceSchema.parse({
|
|
58
|
+
plan_id: id, findings: [], audit_read: null, sources, intent: await readExecutionIntent(options.artifactsDir),
|
|
59
|
+
request: { id: `request-${randomUUID()}`, text: requestText || "Implement the operator's confirmed request.", source_paths: sources.map(entry => entry.path) },
|
|
685
60
|
});
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
kind: "escalate",
|
|
689
|
-
reason: "invalid_waivers",
|
|
690
|
-
outstanding: acceptedIds,
|
|
691
|
-
waiverIssues: fold.issues,
|
|
692
|
-
note: "The counterexample waiver file was refused and nothing was applied. " +
|
|
693
|
-
"Fix or delete it, then re-run next-step.",
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
const repairState = await readRepairState(artifactsDir);
|
|
697
|
-
const waived = waivedAcceptedIds(repairState, ceById, acceptedIds);
|
|
698
|
-
const unwaivedAccepted = acceptedIds.filter((id) => !waived.has(id));
|
|
699
|
-
// Every accepted counterexample carries a recorded owner waiver → the
|
|
700
|
-
// needs_repair verdict is resolved by decision: proceed.
|
|
701
|
-
if (acceptedIds.length > 0 && unwaivedAccepted.length === 0) {
|
|
702
|
-
return { kind: "proceed" };
|
|
703
|
-
}
|
|
704
|
-
const judgeHash = judgeEnvelope.content_hash;
|
|
705
|
-
// ONE-TIME UPGRADE EFFECT: `computeHash` now serializes with
|
|
706
|
-
// `stableStringify` (artifactStore.ts), so a repair-ledger entry recorded
|
|
707
|
-
// before that change stores a key-order-sensitive `judge_hash` the recomputed
|
|
708
|
-
// value can never equal. A run sitting mid-repair-loop across the upgrade
|
|
709
|
-
// therefore misses on this compare and escalates ONCE as "not converging".
|
|
710
|
-
// Deliberate: the escalation is fail-safe (the operator resolves it and the
|
|
711
|
-
// loop re-enters on the freshly hashed report), and back-compat code for a
|
|
712
|
-
// value the next read recomputes would be carried forever for one run.
|
|
713
|
-
const alreadyHandled = repairState.repairs.some((repair) => repair.judge_hash === judgeHash);
|
|
714
|
-
// Map judge.repair_directive.target if present; if absent, infer from classifications.
|
|
715
|
-
//
|
|
716
|
-
// The validator admits only the declared owning artifacts plus the legacy
|
|
717
|
-
// `design_spec` alias (a report from an older release); `counterexample` is
|
|
718
|
-
// refused at ingestion with its reason, never swapped here. The legacy alias
|
|
719
|
-
// falls through to the inferred target, as it always has.
|
|
720
|
-
const rawDirective = judge.repair_directive;
|
|
721
|
-
const directive = rawDirective
|
|
722
|
-
? {
|
|
723
|
-
target: rawDirective.target === "design_spec"
|
|
724
|
-
? inferRepairDirective(judge).target
|
|
725
|
-
: rawDirective.target,
|
|
726
|
-
instruction: rawDirective.instruction,
|
|
727
|
-
}
|
|
728
|
-
: inferRepairDirective(judge);
|
|
729
|
-
const addressed = new Set(repairState.repairs.filter((repair) => (repair.target === "design_spec" ? "finalized_module_contracts" : repair.target) === directive.target).flatMap((r) => r.addressed_ce_fingerprints ??
|
|
730
|
-
(r.accepted_ce_ids ?? []).map((id) => `id:${id}`)));
|
|
731
|
-
const newAccepted = unwaivedAccepted.filter((id) => !addressed.has(keyOf(id)));
|
|
732
|
-
const newAcceptedFingerprints = newAccepted.map(keyOf);
|
|
733
|
-
// Idempotent re-entry: this exact judge report already drove a repair (its hash
|
|
734
|
-
// is recorded). Re-emit the same repair directive; do not re-evaluate convergence
|
|
735
|
-
// (the repair has not yet produced a fresh judge report).
|
|
736
|
-
if (alreadyHandled) {
|
|
737
|
-
return {
|
|
738
|
-
kind: "repair",
|
|
739
|
-
directive,
|
|
740
|
-
judgeHash,
|
|
741
|
-
acceptedCeIds: newAccepted,
|
|
742
|
-
addressedCeFingerprints: newAcceptedFingerprints,
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
// Runaway backstop (loud) — the exception path, not the normal terminator.
|
|
746
|
-
if (repairState.repairs.length >= MAX_CONTRACT_REPAIR_ITERATIONS) {
|
|
747
|
-
return {
|
|
748
|
-
kind: "escalate",
|
|
749
|
-
reason: "runaway",
|
|
750
|
-
outstanding: unwaivedAccepted,
|
|
751
|
-
note: `The judge↔repair loop reached its runaway backstop (${repairState.repairs.length} repair rounds) without converging. Each round was still surfacing accepted counterexamples. This is pathological non-convergence — review the outstanding counterexamples and the contract design with the user before proceeding.`,
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
// Progress: a new accepted counterexample (or the first round) ⇒ repair.
|
|
755
|
-
if (repairState.repairs.length === 0 || newAccepted.length > 0) {
|
|
756
|
-
return {
|
|
757
|
-
kind: "repair",
|
|
758
|
-
directive,
|
|
759
|
-
judgeHash,
|
|
760
|
-
acceptedCeIds: newAccepted,
|
|
761
|
-
addressedCeFingerprints: newAcceptedFingerprints,
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
// Stall: a needs_repair verdict whose every accepted counterexample was already
|
|
765
|
-
// addressed by a prior repair ⇒ the loop is not converging ⇒ escalate.
|
|
766
|
-
return {
|
|
767
|
-
kind: "escalate",
|
|
768
|
-
reason: "stall",
|
|
769
|
-
outstanding: unwaivedAccepted,
|
|
770
|
-
note: `The judge re-accepted counterexample(s) that a prior repair already addressed (${unwaivedAccepted.join(", ") || "none newly accepted"}), with no new accepted counterexample this round. The repair loop is not converging on these items. Resolve them with the user — adjust the contract design, or record an owner waiver accepting them as known limitations — before the plan can be promoted.`,
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
/** Blocking-severity critique item ids from a conceptual_design_critique payload. */
|
|
774
|
-
function blockingCritiqueIds(critique) {
|
|
775
|
-
const items = isRecord(critique) && Array.isArray(critique.items) ? critique.items : [];
|
|
776
|
-
return items
|
|
777
|
-
.filter((item) => isRecord(item))
|
|
778
|
-
.filter((item) => item.severity === "blocking")
|
|
779
|
-
.map((item) => (typeof item.id === "string" ? item.id : ""))
|
|
780
|
-
.filter((id) => id.length > 0);
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Decide whether the pipeline may advance past the conceptual-design critique.
|
|
784
|
-
*
|
|
785
|
-
* The routing signal is MECHANICAL and derived only from the critique items: a
|
|
786
|
-
* critique carrying ANY `severity: "blocking"` item means the design is not
|
|
787
|
-
* approved and must be repaired — regardless of the author-stated `verdict`
|
|
788
|
-
* string. This closes the contradictory-combo gap: a critique that marks items
|
|
789
|
-
* `blocking` while declaring `approved` / `approved_with_concerns` (which the
|
|
790
|
-
* pipeline previously waved through, since only a judge verdict ever gated
|
|
791
|
-
* anything) no longer silently proceeds. Enforce-in-tooling: the verdict label
|
|
792
|
-
* is advisory display; the blocking-item set is the contract.
|
|
793
|
-
*
|
|
794
|
-
* Convergence-terminated, mirroring {@link evaluateJudgeGate}: the first blocking
|
|
795
|
-
* critique ⇒ repair its declared owner (legacy: `finalized_module_contracts`); repairing it
|
|
796
|
-
* re-stales and re-emits the critique (it depends on the finalized contracts), so
|
|
797
|
-
* a clean re-critique ⇒ proceed (the fixpoint). A fresh critique whose blocking
|
|
798
|
-
* ids were ALL already addressed by a prior repair, with none new ⇒ escalate
|
|
799
|
-
* (stall — the design loop is not converging) rather than repair forever; the
|
|
800
|
-
* runaway backstop also escalates (loud).
|
|
801
|
-
*/
|
|
802
|
-
export async function evaluateCritiqueGate(artifactsDir) {
|
|
803
|
-
const env = await readContractArtifact(artifactsDir, "conceptual_design_critique");
|
|
804
|
-
if (!env)
|
|
805
|
-
return { kind: "proceed" };
|
|
806
|
-
const critique = envelopePayload(env);
|
|
807
|
-
const blockingIds = blockingCritiqueIds(critique);
|
|
808
|
-
if (blockingIds.length === 0)
|
|
809
|
-
return { kind: "proceed" };
|
|
810
|
-
const requestedTarget = isRecord(critique) ? critique.repair_target : undefined;
|
|
811
|
-
if (requestedTarget !== undefined &&
|
|
812
|
-
!CONCEPTUAL_CRITIQUE_REPAIR_TARGETS.includes(requestedTarget)) {
|
|
813
|
-
return { kind: "escalate", reason: "unsupported_target", blocking: blockingIds,
|
|
814
|
-
note: `Unsupported critique repair_target ${JSON.stringify(requestedTarget)}; correct the critique to name finalized_module_contracts or module_decomposition.` };
|
|
815
|
-
}
|
|
816
|
-
const target = requestedTarget === "module_decomposition"
|
|
817
|
-
? "module_decomposition" : "finalized_module_contracts";
|
|
818
|
-
const repairState = await readRepairState(artifactsDir);
|
|
819
|
-
const critiqueRepairs = repairState.critique_repairs ?? [];
|
|
820
|
-
const critiqueHash = env.content_hash;
|
|
821
|
-
// ONE-TIME UPGRADE EFFECT, the critique gate's twin of the judge site above:
|
|
822
|
-
// a `critique_hash` recorded before `computeHash` moved to `stableStringify`
|
|
823
|
-
// cannot equal the recomputed value, so an in-flight design-repair loop
|
|
824
|
-
// crossing the upgrade escalates once as a stall instead of re-emitting.
|
|
825
|
-
// Fail-safe and deliberately not mitigated — see that site for the reasoning.
|
|
826
|
-
const alreadyHandled = critiqueRepairs.some((r) => r.critique_hash === critiqueHash);
|
|
827
|
-
// Idempotent re-entry: this exact critique already drove a repair (its design
|
|
828
|
-
// repair has not yet produced a fresh critique). Re-emit the same repair.
|
|
829
|
-
if (alreadyHandled) {
|
|
830
|
-
return { kind: "repair", critiqueHash, blockingIds, target };
|
|
831
|
-
}
|
|
832
|
-
const addressed = new Set(critiqueRepairs
|
|
833
|
-
.filter((repair) => (repair.target ?? "finalized_module_contracts") === target)
|
|
834
|
-
.flatMap((repair) => repair.blocking_ids ?? []));
|
|
835
|
-
const newBlocking = blockingIds.filter((id) => !addressed.has(id));
|
|
836
|
-
// Runaway backstop (loud) — pathological non-convergence.
|
|
837
|
-
if (critiqueRepairs.length >= MAX_CONTRACT_REPAIR_ITERATIONS) {
|
|
838
|
-
return {
|
|
839
|
-
kind: "escalate",
|
|
840
|
-
reason: "runaway",
|
|
841
|
-
blocking: blockingIds,
|
|
842
|
-
note: `The conceptual-design critique↔repair loop reached its runaway backstop (${critiqueRepairs.length} repair rounds) while still raising blocking concerns. Review the critique and contract design with the user before proceeding.`,
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
// Progress: a new blocking concern (or the first round) ⇒ repair the design.
|
|
846
|
-
if (critiqueRepairs.length === 0 || newBlocking.length > 0) {
|
|
847
|
-
return { kind: "repair", critiqueHash, blockingIds, target };
|
|
848
|
-
}
|
|
849
|
-
// Stall: every blocking concern was already addressed by a prior repair, none
|
|
850
|
-
// new ⇒ the design loop is not converging ⇒ escalate to the user.
|
|
851
|
-
return {
|
|
852
|
-
kind: "escalate",
|
|
853
|
-
reason: "stall",
|
|
854
|
-
blocking: blockingIds,
|
|
855
|
-
note: `The conceptual-design critique re-raised blocking concern(s) that a prior design repair already addressed (${blockingIds.join(", ")}), with none newly raised. The design is not converging on these concerns. Resolve them with the user — revise the contract design or downgrade the concerns to advisory — before the pipeline can proceed.`,
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
/**
|
|
859
|
-
* The traceability invariant: no implementation_dag node may exist without
|
|
860
|
-
* tracing to an obligation from the ledger (satisfies_obligations or
|
|
861
|
-
* verification_obligation_ids) or to a judge-accepted counterexample
|
|
862
|
-
* (addresses_counterexamples). Untraceable nodes are unattributable work — the
|
|
863
|
-
* exact thing the contract pipeline exists to prevent.
|
|
864
|
-
*/
|
|
865
|
-
export async function validateImplementationDagTraceability(artifactsDir) {
|
|
866
|
-
const dag = envelopePayload(await readContractArtifact(artifactsDir, "implementation_dag"));
|
|
867
|
-
if (!dag) {
|
|
868
|
-
return { ok: false, violations: ["implementation_dag is missing."] };
|
|
869
|
-
}
|
|
870
|
-
const ledger = envelopePayload(await readContractArtifact(artifactsDir, "obligation_ledger"));
|
|
871
|
-
const judge = envelopePayload(await readContractArtifact(artifactsDir, "judge_report"));
|
|
872
|
-
const obligationIds = new Set((ledger?.obligations ?? []).map((obligation) => obligation.id));
|
|
873
|
-
const acceptedCounterexampleIds = new Set((judge?.classifications ?? [])
|
|
874
|
-
.filter((entry) => entry.classification === "accepted")
|
|
875
|
-
.map((entry) => entry.counterexample_id));
|
|
876
|
-
const violations = [];
|
|
877
|
-
const nodes = Array.isArray(dag.nodes) ? dag.nodes : [];
|
|
878
|
-
if (nodes.length === 0) {
|
|
879
|
-
violations.push("implementation_dag has no nodes; nothing would be implemented.");
|
|
880
|
-
}
|
|
881
|
-
// A node that resolves to no write scope is undispatchable: no worktree seed, no
|
|
882
|
-
// write boundary, no paths to inline for a single-shot worker. It used to reach
|
|
883
|
-
// the dispatch boundary anyway and fail there with "there is nothing a worker
|
|
884
|
-
// could be scoped to" — cascade-blocking its dependents, three steps from the
|
|
885
|
-
// cause. Refusing HERE puts it in front of the regeneration loop this validator
|
|
886
|
-
// already feeds, and names the slug that failed to join.
|
|
887
|
-
const { resolve: resolveWriteScope, availableSlugs } = await buildNodeWriteScopeResolver(artifactsDir);
|
|
888
|
-
for (const node of nodes) {
|
|
889
|
-
const tracedObligations = [
|
|
890
|
-
...(node.satisfies_obligations ?? []),
|
|
891
|
-
...(node.verification_obligation_ids ?? []),
|
|
892
|
-
].filter((id) => obligationIds.has(id));
|
|
893
|
-
const tracedCounterexamples = (node.addresses_counterexamples ?? []).filter((id) => acceptedCounterexampleIds.has(id));
|
|
894
|
-
if (tracedObligations.length === 0 && tracedCounterexamples.length === 0) {
|
|
895
|
-
violations.push(`Node "${node.id}" traces to no obligation from the obligation ledger and no judge-accepted counterexample.`);
|
|
896
|
-
}
|
|
897
|
-
if (resolveWriteScope(node).length === 0) {
|
|
898
|
-
const carried = [
|
|
899
|
-
...(node.satisfies_obligations ?? []),
|
|
900
|
-
...(node.verification_obligation_ids ?? []),
|
|
901
|
-
];
|
|
902
|
-
violations.push(`Node "${node.id}" resolves to an EMPTY write scope, so nothing could be dispatched for it. ` +
|
|
903
|
-
`It declares no output_files/files_likely_touched, and none of its obligation ids ` +
|
|
904
|
-
`(${carried.join(", ") || "none"}) begins with "OBL-<module>-" for any module in ` +
|
|
905
|
-
`module_decomposition (${availableSlugs.join(", ") || "no modules declared"}). ` +
|
|
906
|
-
`Declare the node's output_files, or name its obligations after a decomposed module.`);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
return { ok: violations.length === 0, violations };
|
|
61
|
+
await writeJsonFile(executionPlanPaths(options.artifactsDir).source, source);
|
|
62
|
+
return source;
|
|
910
63
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
* {@link consumeGateOutcomes} for the per-boundary `required` policy and the one
|
|
922
|
-
* declared exception (`digest_coverage`).
|
|
923
|
-
*/
|
|
924
|
-
export async function evaluateContractObligationsPromotionGate(artifactsDir, root = climbOutOfAuditTools(artifactsDir), inputs) {
|
|
925
|
-
const outcomes = await evaluateContractPipelineCrossGateOutcomes(inputs ?? (await readCrossGateInputs(artifactsDir, root)));
|
|
926
|
-
return consumeGateOutcomes(outcomes, PROMOTION_GATES, PROMOTION_REQUIRED_GATES);
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* Pre-adversarial structural floor (S5). The subset of the contract-obligation
|
|
930
|
-
* gates whose inputs all exist by the time the critic phase is reached
|
|
931
|
-
* (paired-obligation coverage, source-scoped digest coverage, and seam
|
|
932
|
-
* reconciliation derivation — none of which need the judge verdict or the
|
|
933
|
-
* implementation_dag). Running them BEFORE the expensive critic/judge loop means
|
|
934
|
-
* the adversarial phases only ever see structurally-sound obligations, tests, and
|
|
935
|
-
* contracts, and a structural gap is re-emitted to the precise responsible phase
|
|
936
|
-
* instead of being discovered at promotion (after the adversarial budget is spent)
|
|
937
|
-
* and re-emitted to the wrong phase. The full {@link evaluateContractObligationsPromotionGate}
|
|
938
|
-
* — including the evidence-threading check that needs the judge + DAG — still runs
|
|
939
|
-
* at promotion as the fail-closed backstop; this gate never replaces it.
|
|
940
|
-
*
|
|
941
|
-
* Returns the first failing gate's responsible phase + rendered error lines, or
|
|
942
|
-
* null when the structural floor is clean. Branches on each outcome's
|
|
943
|
-
* `evaluated` before its empty issue list is allowed to mean clean
|
|
944
|
-
* (the branch-on-evaluated rule); `contract_finalization`, `seam_reconciliation`
|
|
945
|
-
* and `test_validator_plan` all precede `critic` in the phase order, so a
|
|
946
|
-
* skipped gate here is a malformed payload rather than an absent one.
|
|
947
|
-
*/
|
|
948
|
-
export async function evaluatePreCriticStructuralGate(artifactsDir, root = climbOutOfAuditTools(artifactsDir), inputs) {
|
|
949
|
-
const outcomes = await evaluateContractPipelineCrossGateOutcomes(inputs ?? (await readCrossGateInputs(artifactsDir, root)));
|
|
950
|
-
// Upstream-owned checks first: a derivation/coverage gap is fixed in the
|
|
951
|
-
// finalized contracts (the obligation ledger is derived from them).
|
|
952
|
-
const design = consumeGateOutcomes(outcomes, ["reconciliation_derivation", "digest_coverage"], PRE_CRITIC_REQUIRED_GATES);
|
|
953
|
-
if (!design.ok) {
|
|
954
|
-
return {
|
|
955
|
-
phase: "contract_finalization",
|
|
956
|
-
errorLines: design.violations.map((violation) => `- ${violation}`),
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
// A testable obligation without a paired spec is fixed in the test plan
|
|
960
|
-
// (skeleton-scaffolded from the derived ledger).
|
|
961
|
-
const tests = consumeGateOutcomes(outcomes, ["paired_obligations"], PRE_CRITIC_REQUIRED_GATES);
|
|
962
|
-
if (!tests.ok) {
|
|
963
|
-
return {
|
|
964
|
-
phase: "test_validator_plan",
|
|
965
|
-
errorLines: tests.violations.map((violation) => `- ${violation}`),
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
// ── M-B3: source-grounded citation gate (repo-tree knownPaths) ────────────────
|
|
971
|
-
//
|
|
972
|
-
// A contract finding that cites a file path or a code symbol must point at
|
|
973
|
-
// something REAL in the working tree. The gate runs at two boundaries:
|
|
974
|
-
//
|
|
975
|
-
// 1. PRE-CRITIC: ground the module_decomposition's `file_scope` citations
|
|
976
|
-
// (each module declares the files it owns; file_scope lives in the
|
|
977
|
-
// decomposition — the finalized contracts carry interface fields, not
|
|
978
|
-
// paths). A module that cites only a path that does not exist AND no real
|
|
979
|
-
// symbol is hallucinating its scope before the adversarial budget is ever
|
|
980
|
-
// spent — re-emit the `decomposition` phase (the phase that OWNS file_scope,
|
|
981
|
-
// so re-authoring it can actually fix the bad path; re-emitting a downstream
|
|
982
|
-
// phase like contract_finalization could never change file_scope → loops).
|
|
983
|
-
// 2. PROMOTION BACKSTOP: ground every promoted extracted-plan finding's
|
|
984
|
-
// citations before the plan is handed to the document/implement flow.
|
|
985
|
-
//
|
|
986
|
-
// Fail-closed ONLY when the working tree itself is unreadable (git ls-files
|
|
987
|
-
// returns nothing) — a normal run with legitimately new-file scopes is not
|
|
988
|
-
// bricked, because a finding grounds if ANY cited path OR symbol is real.
|
|
989
|
-
/**
|
|
990
|
-
* Map module_decomposition modules to Finding-shaped citations the shared
|
|
991
|
-
* grounding gate consumes: each module's `file_scope` → affected_files (the
|
|
992
|
-
* declared paths it owns), its name + responsibilities → summary (for the
|
|
993
|
-
* symbol-shaped grounding fallback). The decomposition is where file_scope
|
|
994
|
-
* lives — the finalized contracts carry interface fields (inputs/outputs/
|
|
995
|
-
* invariants), not paths — so a module that declares only a non-existent
|
|
996
|
-
* file_scope path is the pre-critic hallucination this catches.
|
|
997
|
-
*
|
|
998
|
-
* A module that declares NO file_scope at all contributes no citation (there is
|
|
999
|
-
* nothing to ground) — it is not a hallucination, just an undeclared scope.
|
|
1000
|
-
*/
|
|
1001
|
-
function decompositionModulesToCitations(decompositionPayload) {
|
|
1002
|
-
const modules = isRecord(decompositionPayload) && Array.isArray(decompositionPayload.modules)
|
|
1003
|
-
? decompositionPayload.modules
|
|
1004
|
-
: [];
|
|
1005
|
-
const citations = [];
|
|
1006
|
-
for (const [i, mod] of modules.entries()) {
|
|
1007
|
-
if (!isRecord(mod))
|
|
1008
|
-
continue;
|
|
1009
|
-
const fileScope = Array.isArray(mod.file_scope)
|
|
1010
|
-
? mod.file_scope.filter((p) => typeof p === "string")
|
|
1011
|
-
: [];
|
|
1012
|
-
if (fileScope.length === 0)
|
|
1013
|
-
continue;
|
|
1014
|
-
const name = typeof mod.name === "string" ? mod.name : `module-${i}`;
|
|
1015
|
-
const responsibilities = typeof mod.responsibilities === "string" ? mod.responsibilities : "";
|
|
1016
|
-
citations.push({
|
|
1017
|
-
id: name,
|
|
1018
|
-
title: name,
|
|
1019
|
-
category: "module_contract",
|
|
1020
|
-
severity: "medium",
|
|
1021
|
-
confidence: "high",
|
|
1022
|
-
lens: "architecture",
|
|
1023
|
-
summary: `${name} ${responsibilities}`,
|
|
1024
|
-
affected_files: fileScope.map((path) => ({ path })),
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
return citations;
|
|
1028
|
-
}
|
|
1029
|
-
/**
|
|
1030
|
-
* Pre-critic citation grounding over the module decomposition's file scope.
|
|
1031
|
-
* Returns rendered error lines (re-emit contract_finalization) or null when clean
|
|
1032
|
-
* — including a clean fail-closed pass (the gate's own repo-tree issue is surfaced
|
|
1033
|
-
* as an error line so an unreadable tree is loud, never silent).
|
|
1034
|
-
*/
|
|
1035
|
-
async function evaluatePreCriticCitationGrounding(artifactsDir, repoRoot) {
|
|
1036
|
-
const decomposition = envelopePayload(await readContractArtifact(artifactsDir, "module_decomposition"));
|
|
1037
|
-
const citations = decompositionModulesToCitations(decomposition);
|
|
1038
|
-
if (citations.length === 0)
|
|
1039
|
-
return null;
|
|
1040
|
-
const result = await validateContractCitationGrounding(citations, repoRoot);
|
|
1041
|
-
// The same boundary owns the re-export-shim rule: a file_scope that grounds
|
|
1042
|
-
// only at a barrel is as unfixable downstream as one that does not ground at
|
|
1043
|
-
// all, and the decomposition prompt states the rule as binding.
|
|
1044
|
-
// Its tree-readability issue repeats the citation gate's own, so it is dropped.
|
|
1045
|
-
const shimIssues = (await validateDecompositionFileScope(decomposition, repoRoot)).issues.filter((issue) => issue.path !== "decomposition_file_scope.repo_tree");
|
|
1046
|
-
const errors = [...result.issues, ...shimIssues].filter((issue) => issue.severity === "error");
|
|
1047
|
-
if (errors.length === 0)
|
|
1048
|
-
return null;
|
|
1049
|
-
return { errorLines: errors.map((issue) => `- [${issue.path}] ${issue.message}`) };
|
|
64
|
+
async function emit(options, prompt, status = "ready") {
|
|
65
|
+
const paths = executionPlanPaths(options.artifactsDir);
|
|
66
|
+
return writeCurrentStep({
|
|
67
|
+
stepKind: "contract_pipeline", status, runId: options.runId, repoRoot: options.root,
|
|
68
|
+
artifactsDir: options.artifactsDir,
|
|
69
|
+
prompt: `${prompt}\n\nAfter writing the requested response, run \`${loaderCommand("next-step")}\`.`,
|
|
70
|
+
allowedCommands: [loaderCommand("next-step")],
|
|
71
|
+
stopCondition: "Complete only the bound planning/review assignment, then call next-step.",
|
|
72
|
+
artifactPaths: { source: paths.source, execution_plan: paths.canonical, plan_submission: paths.submission },
|
|
73
|
+
});
|
|
1050
74
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const plan = await readOptionalJsonFile(intakePaths(artifactsDir).extractedPlan);
|
|
1057
|
-
const findings = isRecord(plan) && Array.isArray(plan.findings)
|
|
1058
|
-
? plan.findings
|
|
1059
|
-
: [];
|
|
1060
|
-
if (findings.length === 0)
|
|
1061
|
-
return null;
|
|
1062
|
-
const result = await validateContractCitationGrounding(findings, repoRoot);
|
|
1063
|
-
const errors = result.issues.filter((issue) => issue.severity === "error");
|
|
1064
|
-
if (errors.length === 0)
|
|
1065
|
-
return null;
|
|
1066
|
-
return { violations: errors.map((issue) => `[${issue.path}] ${issue.message}`) };
|
|
75
|
+
async function authorStep(options, source, canonical, reason) {
|
|
76
|
+
const paths = executionPlanPaths(options.artifactsDir);
|
|
77
|
+
return emit(options, renderPlanAuthorPrompt({ root: options.root, source, canonical,
|
|
78
|
+
history: await readPlanReviewHistory(options.artifactsDir), sourcePath: paths.source,
|
|
79
|
+
planPath: paths.canonical, outputPath: paths.submission, reason }));
|
|
1067
80
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
// array keyed by module name. DC-3 exposes one bounded host workload per module,
|
|
1072
|
-
// replacing the former single sequential workload — each agent reads its own
|
|
1073
|
-
// module's file scope, so no single agent owns both sides of a seam. Each agent
|
|
1074
|
-
// writes a per-module SHARD; the orchestrator merges all shards into the
|
|
1075
|
-
// aggregated artifact — byte-identical in shape to the single-agent output — and
|
|
1076
|
-
// guarantees the merge is COMPLETE (every decomposed module present) before
|
|
1077
|
-
// downstream derivation runs. A missing shard re-emits the wave (never a partial
|
|
1078
|
-
// aggregate). `contract_finalization` is NOT a parallel wave: it is derived
|
|
1079
|
-
// deterministically from the drafts + seam report (see the deterministic
|
|
1080
|
-
// contract_finalization fast path), no fresh source read.
|
|
1081
|
-
/** The phase(s) that fan out per module, and the artifact each produces. */
|
|
1082
|
-
const PARALLEL_MODULE_PHASES = {
|
|
1083
|
-
module_contract_drafting: "module_contracts",
|
|
81
|
+
/** Review dependencies are declarations over one revision, never a second artifact-state graph. */
|
|
82
|
+
const REVIEW_DEPENDENCIES = {
|
|
83
|
+
critique: [], critic: ["critique"], judge: ["critique", "critic"],
|
|
1084
84
|
};
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
const
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}
|
|
1099
|
-
result
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
const NON_WRITE_TARGET_CHARS = /[\s:*?"<>|]/u;
|
|
1116
|
-
/**
|
|
1117
|
-
* A finalized module contract's `outputs` / `side_effects` are FREE PROSE that
|
|
1118
|
-
* may name a file ("src/foo.ts") or may describe an effect ("writes the run
|
|
1119
|
-
* ledger under .audit-tools") or carry an ordering token
|
|
1120
|
-
* ("artifact:validated-roster" — see ARTIFACT_TOKEN_PATTERN in phaseCut.ts).
|
|
1121
|
-
* Only the first kind is a write target, so this is a deliberately CONSERVATIVE
|
|
1122
|
-
* parse: an entry qualifies only when it reads unambiguously as a repo-relative
|
|
1123
|
-
* path, and everything else is silently dropped — prose stays prose. A false
|
|
1124
|
-
* positive here would widen a worker's write scope on the strength of a
|
|
1125
|
-
* sentence, which is strictly worse than the manual widening this replaces.
|
|
1126
|
-
*
|
|
1127
|
-
* Returns the forward-slashed path, or null when the entry is not one.
|
|
1128
|
-
*/
|
|
1129
|
-
function contractDeclaredWriteTarget(entry) {
|
|
1130
|
-
if (typeof entry !== "string")
|
|
1131
|
-
return null;
|
|
1132
|
-
const trimmed = entry.trim();
|
|
1133
|
-
if (trimmed.length === 0)
|
|
1134
|
-
return null;
|
|
1135
|
-
if (NON_WRITE_TARGET_CHARS.test(trimmed))
|
|
1136
|
-
return null;
|
|
1137
|
-
// Absolute (POSIX or Windows-UNC) forms are not repo-relative.
|
|
1138
|
-
if (trimmed.startsWith("/") || trimmed.startsWith("\\"))
|
|
1139
|
-
return null;
|
|
1140
|
-
const normalized = trimmed.replace(/\\/gu, "/");
|
|
1141
|
-
if (normalized.split("/").includes(".."))
|
|
1142
|
-
return null;
|
|
1143
|
-
// A bare word ("session") is an interface name, not a path. Require either a
|
|
1144
|
-
// path separator or a file extension.
|
|
1145
|
-
if (!normalized.includes("/") && !/\.[a-z0-9]{1,6}$/iu.test(normalized))
|
|
1146
|
-
return null;
|
|
1147
|
-
return normalized;
|
|
1148
|
-
}
|
|
1149
|
-
/**
|
|
1150
|
-
* The path-parseable write targets each finalized module contract declares,
|
|
1151
|
-
* keyed by `moduleSlug(name)` — the SAME identity the obligation ids encode, so
|
|
1152
|
-
* this map joins to the decomposition's modules without a second name space.
|
|
1153
|
-
*
|
|
1154
|
-
* Degrades to an empty map when the artifact is absent or malformed: this
|
|
1155
|
-
* resolver runs on the VALIDATOR's refusal path, so a bad contracts file must
|
|
1156
|
-
* cost the widening, never wedge every subsequent next-step with a throw.
|
|
1157
|
-
*/
|
|
1158
|
-
async function readModuleContractWriteTargets(artifactsDir) {
|
|
1159
|
-
let finalized;
|
|
1160
|
-
try {
|
|
1161
|
-
finalized = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
1162
|
-
}
|
|
1163
|
-
catch {
|
|
1164
|
-
return new Map();
|
|
1165
|
-
}
|
|
1166
|
-
const entries = isRecord(finalized) && Array.isArray(finalized.module_contracts)
|
|
1167
|
-
? finalized.module_contracts
|
|
1168
|
-
: [];
|
|
1169
|
-
const bySlug = new Map();
|
|
1170
|
-
for (const entry of entries) {
|
|
1171
|
-
if (!isRecord(entry) || typeof entry.name !== "string")
|
|
1172
|
-
continue;
|
|
1173
|
-
const slug = moduleSlug(entry.name);
|
|
1174
|
-
if (slug.length === 0)
|
|
1175
|
-
continue;
|
|
1176
|
-
const targets = bySlug.get(slug) ?? [];
|
|
1177
|
-
const declared = [
|
|
1178
|
-
...(Array.isArray(entry.outputs) ? entry.outputs : []),
|
|
1179
|
-
...(Array.isArray(entry.side_effects) ? entry.side_effects : []),
|
|
1180
|
-
];
|
|
1181
|
-
for (const raw of declared) {
|
|
1182
|
-
const target = contractDeclaredWriteTarget(raw);
|
|
1183
|
-
if (target !== null && !targets.includes(target))
|
|
1184
|
-
targets.push(target);
|
|
1185
|
-
}
|
|
1186
|
-
bySlug.set(slug, targets);
|
|
1187
|
-
}
|
|
1188
|
-
return bySlug;
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Single source for "which files may this DAG node write". The scope is the
|
|
1192
|
-
* UNION of two declarations, never one overriding the other:
|
|
1193
|
-
*
|
|
1194
|
-
* - the node's own declared files (`output_files`, else `files_likely_touched`);
|
|
1195
|
-
* - the path-parseable write targets (`outputs` + `side_effects`) declared by
|
|
1196
|
-
* the finalized contract of the module(s) the node's obligations belong to,
|
|
1197
|
-
* resolved by longest-`OBL-<slug>-` prefix so a short slug never mis-claims a
|
|
1198
|
-
* longer module's targets.
|
|
1199
|
-
*
|
|
1200
|
-
* A node that declared NO files of its own additionally inherits the
|
|
1201
|
-
* `file_scope` of those same modules — that inheritance is the scope-less
|
|
1202
|
-
* FALLBACK only, and is deliberately not unioned into a node that did declare.
|
|
1203
|
-
*
|
|
1204
|
-
* ⚠ The declared-files-win EARLY RETURN this used to perform is deliberately
|
|
1205
|
-
* superseded (owner decision, nightly ledger 2026-08-20). The module contract is
|
|
1206
|
-
* where a module's write targets are declared, and they never reached the node
|
|
1207
|
-
* scope — so an implementer was handed an obligation whose declared target file
|
|
1208
|
-
* was missing from `allowed_files`, and a human widened it by hand (four
|
|
1209
|
-
* recoveries in one wave). Union, not precedence.
|
|
1210
|
-
*
|
|
1211
|
-
* ⚠ Shared by the PROMOTER (which derives the scope) and the VALIDATOR (which
|
|
1212
|
-
* refuses when it resolves to nothing) on purpose. Two copies of this resolution
|
|
1213
|
-
* would drift, and the drift is invisible: the validator would pass a node the
|
|
1214
|
-
* promoter then writes scope-less.
|
|
1215
|
-
*
|
|
1216
|
-
* The join it performs is between two INDEPENDENTLY AUTHORED name spaces — the
|
|
1217
|
-
* obligation id's slug and the decomposition's module names — which is exactly
|
|
1218
|
-
* where it fails. Observed 2026-08-09: nodes carrying `OBL-attribution-capture-…`
|
|
1219
|
-
* and `OBL-verdict-capture-…` matched no module, because the decomposition had
|
|
1220
|
-
* named them `dispatch-attribution-capture` and `verdict-capture-audit` /
|
|
1221
|
-
* `verdict-capture-remediate`. Their siblings matched and dispatched; these two
|
|
1222
|
-
* resolved to nothing and died later at the dispatch boundary with "there is
|
|
1223
|
-
* nothing a worker could be scoped to", three steps from the cause.
|
|
1224
|
-
*/
|
|
1225
|
-
async function buildNodeWriteScopeResolver(artifactsDir) {
|
|
1226
|
-
const decomposedModules = await readDecomposedModules(artifactsDir);
|
|
1227
|
-
const contractTargetsBySlug = await readModuleContractWriteTargets(artifactsDir);
|
|
1228
|
-
const moduleScopesBySlug = decomposedModules.map((m) => ({
|
|
1229
|
-
slug: moduleSlug(m.name),
|
|
1230
|
-
files: m.file_scope,
|
|
1231
|
-
targets: contractTargetsBySlug.get(moduleSlug(m.name)) ?? [],
|
|
1232
|
-
}));
|
|
1233
|
-
// The join is the EXACT one (`idRegistry.moduleSlugForObligationId`), not a
|
|
1234
|
-
// longest-prefix match. Longest-prefix guessed whenever one module slug
|
|
1235
|
-
// prefixed another: with `auth` and `auth-service` both decomposed the guess
|
|
1236
|
-
// resolved correctly by luck of the sort, but with `auth-service` out of
|
|
1237
|
-
// scope, `OBL-auth-service-contract` matched `auth` and the node silently
|
|
1238
|
-
// took `auth`'s targets — another module's write boundary, granted without
|
|
1239
|
-
// anyone choosing it. The suffix grammar makes the unresolvable case
|
|
1240
|
-
// UNRESOLVABLE, which the empty-scope refusal below already reports loudly.
|
|
1241
|
-
const scopeBySlug = new Map(moduleScopesBySlug.map((m) => [m.slug, m]));
|
|
1242
|
-
const knownSlugs = new Set(scopeBySlug.keys());
|
|
1243
|
-
const resolve = (node) => {
|
|
1244
|
-
const declared = [...new Set(node.output_files ?? node.files_likely_touched ?? [])];
|
|
1245
|
-
const obligationIds = [
|
|
1246
|
-
...(node.satisfies_obligations ?? []),
|
|
1247
|
-
...(node.verification_obligation_ids ?? []),
|
|
1248
|
-
];
|
|
1249
|
-
const inherited = new Set();
|
|
1250
|
-
const ownedTargets = new Set();
|
|
1251
|
-
for (const id of obligationIds) {
|
|
1252
|
-
const slug = moduleSlugForObligationId(id, knownSlugs);
|
|
1253
|
-
const owner = slug === null ? undefined : scopeBySlug.get(slug);
|
|
1254
|
-
if (!owner)
|
|
1255
|
-
continue;
|
|
1256
|
-
// file_scope inheritance is the scope-less fallback ONLY; the contract's
|
|
1257
|
-
// declared targets are unioned in either way.
|
|
1258
|
-
if (declared.length === 0)
|
|
1259
|
-
for (const f of owner.files)
|
|
1260
|
-
inherited.add(f);
|
|
1261
|
-
for (const t of owner.targets)
|
|
1262
|
-
ownedTargets.add(t);
|
|
1263
|
-
}
|
|
1264
|
-
// Content-derived order: the node's own declarations first, in the order
|
|
1265
|
-
// they were declared, then the added targets path-sorted — so the resolved
|
|
1266
|
-
// scope (which reaches the plan's content hash through affected_files)
|
|
1267
|
-
// never churns on module ordering.
|
|
1268
|
-
const base = declared.length > 0 ? declared : [...inherited];
|
|
1269
|
-
const added = [...ownedTargets]
|
|
1270
|
-
.filter((t) => !base.includes(t))
|
|
1271
|
-
.sort((left, right) => compareCodeUnits(left, right));
|
|
1272
|
-
return [...base, ...added];
|
|
1273
|
-
};
|
|
1274
|
-
return { resolve, availableSlugs: moduleScopesBySlug.map((m) => m.slug) };
|
|
1275
|
-
}
|
|
1276
|
-
/** The goal_id carried by module_decomposition (authoritative for the merge). */
|
|
1277
|
-
async function readDecompositionGoalId(artifactsDir) {
|
|
1278
|
-
const decomposition = envelopePayload(await readContractArtifact(artifactsDir, "module_decomposition"));
|
|
1279
|
-
return isRecord(decomposition) && typeof decomposition.goal_id === "string"
|
|
1280
|
-
? decomposition.goal_id
|
|
1281
|
-
: "";
|
|
1282
|
-
}
|
|
1283
|
-
/** Filesystem-safe shard id for a module name (the merge re-keys by name, not id). */
|
|
1284
|
-
function moduleShardId(moduleName) {
|
|
1285
|
-
const slug = moduleName.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1286
|
-
// Keep names disjoint even after slugging by appending a short content hash.
|
|
1287
|
-
return `${slug || "module"}-${hashContent(moduleName, { length: 8 })}`;
|
|
1288
|
-
}
|
|
1289
|
-
/** Directory holding one per-module shard for a given parallel phase. */
|
|
1290
|
-
function moduleWaveDir(artifactsDir, phase) {
|
|
1291
|
-
return join(contractPipelineDir(artifactsDir), "module-waves", phase);
|
|
1292
|
-
}
|
|
1293
|
-
function moduleShardPath(artifactsDir, phase, moduleName) {
|
|
1294
|
-
return join(moduleWaveDir(artifactsDir, phase), `${moduleShardId(moduleName)}.json`);
|
|
1295
|
-
}
|
|
1296
|
-
/**
|
|
1297
|
-
* Scan the per-module shards for a phase against the decomposed module set. A
|
|
1298
|
-
* shard counts as present only when it parses to an object whose module-contract
|
|
1299
|
-
* `name` matches the decomposed module it is filed under — a stray/mismatched
|
|
1300
|
-
* shard never satisfies completeness.
|
|
1301
|
-
*/
|
|
1302
|
-
async function scanModuleShards(artifactsDir, phase, modules) {
|
|
1303
|
-
const present = new Map();
|
|
1304
|
-
const missing = [];
|
|
1305
|
-
for (const mod of modules) {
|
|
1306
|
-
const shard = await readOptionalJsonFile(moduleShardPath(artifactsDir, phase, mod.name));
|
|
1307
|
-
const contract = extractShardContract(shard, mod.name);
|
|
1308
|
-
if (contract) {
|
|
1309
|
-
present.set(mod.name, contract);
|
|
1310
|
-
}
|
|
85
|
+
async function reviewStep(options, canonical, role) {
|
|
86
|
+
const paths = executionPlanPaths(options.artifactsDir), rolePaths = paths.review(role);
|
|
87
|
+
const history = await readPlanReviewHistory(options.artifactsDir);
|
|
88
|
+
const dependencies = await Promise.all(REVIEW_DEPENDENCIES[role].map(dependency => readPlanReview(options.artifactsDir, dependency, canonical.revision_sha256)));
|
|
89
|
+
const ownerDecision = await readOptionalJsonFile(join(paths.directory, "owner-decision.json"));
|
|
90
|
+
const inputSha = hashContent(stableStringify({ revision: canonical.revision_sha256, role, dependencies, history, ownerDecision }));
|
|
91
|
+
const prior = await readPlanReview(options.artifactsDir, role, canonical.revision_sha256);
|
|
92
|
+
const { adversarialDepth } = await resolveAdversarialDepth(options.artifactsDir);
|
|
93
|
+
const requirement = reviewRequirementForRole(role, adversarialDepth);
|
|
94
|
+
const body = renderPlanReviewPrompt({ role, root: options.root, sourcePath: paths.source, planPath: paths.canonical,
|
|
95
|
+
canonical, history, priorReviewPaths: REVIEW_DEPENDENCIES[role].map(dependency => paths.review(dependency).accepted), requirement });
|
|
96
|
+
const bound = bindWorkerPrompt(body + `\nInput binding: ${inputSha}`, prompt_sha256 => JSON.stringify({
|
|
97
|
+
contract_version: "review-submission/v1", prompt_sha256,
|
|
98
|
+
review: { mode: requirement === "independent" ? "independent" : "degraded", reason: "<how the context meets the review policy>" },
|
|
99
|
+
result: "<the role's result object>",
|
|
100
|
+
}, null, 2));
|
|
101
|
+
if (prior?.input_sha256 === inputSha &&
|
|
102
|
+
prior.provenance.reviewed_content_hash === canonical.revision_sha256 &&
|
|
103
|
+
prior.provenance.requirement === requirement &&
|
|
104
|
+
prior.provenance.prompt_sha256 === bound.sha256 &&
|
|
105
|
+
!reviewIndependenceIssue(requirement, prior.provenance.review))
|
|
106
|
+
return prior;
|
|
107
|
+
const request = { role, revision_sha256: canonical.revision_sha256, input_sha256: inputSha, prompt_sha256: bound.sha256, requirement };
|
|
108
|
+
const issued = await readOptionalJsonFile(rolePaths.request);
|
|
109
|
+
const raw = await readOptionalJsonFile(rolePaths.submission);
|
|
110
|
+
let errors = [];
|
|
111
|
+
if (raw !== undefined && stableStringify(issued) === stableStringify(request)) {
|
|
112
|
+
const result = parseReviewSubmissionEnvelope(raw, { requirement, promptSha256: bound.sha256 });
|
|
113
|
+
if (!result.ok)
|
|
114
|
+
errors = [result.issue];
|
|
1311
115
|
else {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
* shards. No-op for any artifact that is not a sharded module-phase artifact
|
|
1361
|
-
* (`finalized_module_contracts` is deterministically derived, never sharded), or a
|
|
1362
|
-
* payload lacking a `module_contracts[]` array.
|
|
1363
|
-
*/
|
|
1364
|
-
async function propagateAggregateToShards(artifactsDir, name, payload) {
|
|
1365
|
-
const phase = (Object.entries(PARALLEL_MODULE_PHASES).find(([, artifact]) => artifact === name)?.[0]);
|
|
1366
|
-
if (!phase)
|
|
1367
|
-
return;
|
|
1368
|
-
if (!isRecord(payload) || !Array.isArray(payload.module_contracts))
|
|
1369
|
-
return;
|
|
1370
|
-
const contracts = payload.module_contracts;
|
|
1371
|
-
const modules = await readDecomposedModules(artifactsDir);
|
|
1372
|
-
for (const mod of modules) {
|
|
1373
|
-
const entry = contracts.find((c) => isRecord(c) && c.name === mod.name);
|
|
1374
|
-
if (entry) {
|
|
1375
|
-
await writeJsonFile(moduleShardPath(artifactsDir, phase, mod.name), entry);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
// ── Step builder ──────────────────────────────────────────────────────────────
|
|
1380
|
-
/**
|
|
1381
|
-
* Resolve the adversarial-depth dial for a run (extracted from
|
|
1382
|
-
* buildNextContractPipelineStep for testability; behavior-preserving).
|
|
1383
|
-
*
|
|
1384
|
-
* The depth derives from the intake risk signal (the slice-2 shared signal).
|
|
1385
|
-
* Escalate-on-evidence (optimistic-start): the run begins at the cheap intake
|
|
1386
|
-
* tier; once decomposition reveals the work's actual shape, the tier is raised
|
|
1387
|
-
* for THIS and every subsequent next-step. The raise is idempotent + convergent
|
|
1388
|
-
* (escalateRiskSignal no-ops once the tier already covers the evidence), and the
|
|
1389
|
-
* signal is rewritten only on a real raise. Absent signal ⇒ undefined ⇒ the
|
|
1390
|
-
* renderer applies its fail-safe full depth (floor is `light`, never off).
|
|
1391
|
-
*/
|
|
1392
|
-
export async function resolveAdversarialDepth(artifactsDir) {
|
|
1393
|
-
let riskSignal = await readIntakeRiskSignal(artifactsDir);
|
|
1394
|
-
if (riskSignal) {
|
|
1395
|
-
const modules = await readDecomposedModules(artifactsDir);
|
|
1396
|
-
if (modules.length > 0) {
|
|
1397
|
-
const evidence = decompositionRiskEvidence({
|
|
1398
|
-
moduleCount: modules.length,
|
|
1399
|
-
fileScopes: modules.flatMap((m) => m.file_scope),
|
|
1400
|
-
});
|
|
1401
|
-
if (evidence) {
|
|
1402
|
-
const raised = escalateRiskSignal(riskSignal, evidence);
|
|
1403
|
-
if (raised !== riskSignal) {
|
|
1404
|
-
await writeIntakeRiskSignal(artifactsDir, raised);
|
|
1405
|
-
riskSignal = raised;
|
|
116
|
+
const schema = role === "critique" ? CritiqueSchema : role === "critic" ? CriticSchema : PlanJudgeSchema;
|
|
117
|
+
const parsed = schema.safeParse(result.result);
|
|
118
|
+
if (!parsed.success)
|
|
119
|
+
errors = parsed.error.issues.map(issue => `${issue.path.join(".")}: ${issue.message}`);
|
|
120
|
+
else {
|
|
121
|
+
const payload = parsed.data;
|
|
122
|
+
const reqIds = new Set(canonical.plan.requirements.map(entry => entry.id));
|
|
123
|
+
const unitIds = new Set(canonical.plan.units.map(entry => entry.id));
|
|
124
|
+
if (role === "critique" || role === "critic") {
|
|
125
|
+
const references = role === "critique" ? CritiqueSchema.parse(payload).issues : CriticSchema.parse(payload).counterexamples;
|
|
126
|
+
if (new Set(references.map(entry => entry.id)).size !== references.length)
|
|
127
|
+
errors.push("Review issue identities must be unique.");
|
|
128
|
+
for (const entry of references) {
|
|
129
|
+
if (entry.requirement_ids.some(id => !reqIds.has(id)) || entry.unit_ids.some(id => !unitIds.has(id)))
|
|
130
|
+
errors.push(`Review issue ${entry.id} references missing plan entities.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (role === "judge") {
|
|
134
|
+
const judge = PlanJudgeSchema.parse(payload);
|
|
135
|
+
const critic = CriticSchema.parse(dependencies[1]?.result);
|
|
136
|
+
const ids = new Set([...history.accepted_ids, ...critic.counterexamples.map(entry => entry.id)]);
|
|
137
|
+
const given = judge.classifications.map(entry => entry.counterexample_id);
|
|
138
|
+
if (given.length !== ids.size || new Set(given).size !== given.length || given.some(id => !ids.has(id)))
|
|
139
|
+
errors.push("Judge must classify every current/previously accepted counterexample exactly once.");
|
|
140
|
+
const assessed = judge.requirement_assessments.map(entry => entry.requirement_id);
|
|
141
|
+
if (assessed.length !== reqIds.size || new Set(assessed).size !== assessed.length || assessed.some(id => !reqIds.has(id)))
|
|
142
|
+
errors.push("Judge must assess every requirement exactly once.");
|
|
143
|
+
const disposed = new Set(canonical.plan.source_dispositions.map(entry => entry.finding_id));
|
|
144
|
+
const reviewed = judge.disposition_assessments.map(entry => entry.finding_id);
|
|
145
|
+
if (reviewed.length !== disposed.size || new Set(reviewed).size !== reviewed.length || reviewed.some(id => !disposed.has(id)))
|
|
146
|
+
errors.push("Judge must assess every source disposition exactly once.");
|
|
147
|
+
if (canonical.plan.request_disposition && !judge.request_disposition_assessment)
|
|
148
|
+
errors.push("Judge must assess the evidence-backed request disposition.");
|
|
149
|
+
if (judge.verdict === "approved" && (judge.request_disposition_assessment?.verdict === "insufficient" || judge.classifications.some(entry => entry.classification === "accepted") || judge.requirement_assessments.some(entry => entry.verdict !== "satisfied") || judge.disposition_assessments.some(entry => entry.verdict !== "satisfied")))
|
|
150
|
+
errors.push("An approval cannot contain unresolved counterexamples or insufficient evidence.");
|
|
151
|
+
}
|
|
152
|
+
if (!errors.length) {
|
|
153
|
+
const receipt = { ...request, provenance: { requirement, prompt_sha256: bound.sha256, review: result.review, reviewed_content_hash: canonical.revision_sha256 }, result: payload };
|
|
154
|
+
// Only canonical accepted receipts are downstream inputs.
|
|
155
|
+
const { requirement: _requirement, prompt_sha256: _prompt, ...accepted } = receipt;
|
|
156
|
+
const previousReceipt = PlanReviewReceiptSchema.safeParse(await readOptionalJsonFile(rolePaths.accepted));
|
|
157
|
+
if (previousReceipt.success) {
|
|
158
|
+
const old = previousReceipt.data;
|
|
159
|
+
await writeJsonFile(join(paths.directory, "history", "reviews", `${old.revision_sha256}-${role}-${old.input_sha256}.json`), old);
|
|
160
|
+
}
|
|
161
|
+
await writeJsonFile(join(paths.directory, "submissions", `${canonical.revision_sha256}-${role}-${bound.sha256}.json`), raw);
|
|
162
|
+
await writeJsonFile(rolePaths.accepted, accepted);
|
|
163
|
+
return accepted;
|
|
1406
164
|
}
|
|
1407
165
|
}
|
|
1408
166
|
}
|
|
1409
167
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
};
|
|
1414
|
-
}
|
|
1415
|
-
// ── Writers ───────────────────────────────────────────────────────────────────
|
|
1416
|
-
/** The artifact-path map every emitted step carries (existing artifacts only). */
|
|
1417
|
-
function contractStepArtifactPaths(ctx, outputPath) {
|
|
1418
|
-
const stepArtifactPaths = {};
|
|
1419
|
-
if (outputPath)
|
|
1420
|
-
stepArtifactPaths.output = outputPath;
|
|
1421
|
-
for (const [key, value] of Object.entries(ctx.artifactPaths)) {
|
|
1422
|
-
if (value && existsSync(value))
|
|
1423
|
-
stepArtifactPaths[key] = value;
|
|
1424
|
-
}
|
|
1425
|
-
if (ctx.sourcePaths) {
|
|
1426
|
-
stepArtifactPaths.source_manifest = ctx.paths.sourceManifest;
|
|
1427
|
-
stepArtifactPaths.remediation_brief = ctx.paths.brief;
|
|
1428
|
-
}
|
|
1429
|
-
return stepArtifactPaths;
|
|
1430
|
-
}
|
|
1431
|
-
function writeContractPromptStep(ctx, params) {
|
|
1432
|
-
const nextCommand = loaderCommand("next-step");
|
|
1433
|
-
const prompt = `${params.prompt}
|
|
1434
|
-
|
|
1435
|
-
After writing the output file, run:
|
|
1436
|
-
|
|
1437
|
-
\`${nextCommand}\`
|
|
1438
|
-
`;
|
|
1439
|
-
return writeCurrentStep({
|
|
1440
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1441
|
-
status: "ready",
|
|
1442
|
-
runId: ctx.runId,
|
|
1443
|
-
repoRoot: ctx.root,
|
|
1444
|
-
artifactsDir: ctx.artifactsDir,
|
|
1445
|
-
prompt,
|
|
1446
|
-
allowedCommands: [nextCommand],
|
|
1447
|
-
stopCondition: params.stopCondition,
|
|
1448
|
-
artifactPaths: contractStepArtifactPaths(ctx, params.outputPath),
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
async function writeContractPhaseStep(ctx, phase, extraSection) {
|
|
1452
|
-
if (phase === "cyclic_seam_resolution") {
|
|
1453
|
-
// One text for this phase: a generic re-emit carries the same ledger-rewrite
|
|
1454
|
-
// instructions as the gate's own attempt (see renderCyclicSeamResolutionPrompt).
|
|
1455
|
-
const graph = await readSeamObligationGraph(ctx.artifactsDir);
|
|
1456
|
-
const outputPath = contractInputFilePath(ctx.artifactsDir, "cyclic_seam_resolution");
|
|
1457
|
-
return writeContractPromptStep(ctx, {
|
|
1458
|
-
prompt: renderCyclicSeamResolutionPrompt({
|
|
1459
|
-
cycleDescriptions: renderCycleDescriptions(detectCyclicSeamObligations(graph.nodes)),
|
|
1460
|
-
ledgerInputPath: contractInputFilePath(ctx.artifactsDir, "obligation_ledger"),
|
|
1461
|
-
outputPath,
|
|
1462
|
-
extraSection: extraSection ? `\n${extraSection}` : undefined,
|
|
1463
|
-
}),
|
|
1464
|
-
outputPath,
|
|
1465
|
-
stopCondition: CYCLIC_SEAM_RESOLUTION_STOP,
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
const rendered = renderContractPipelinePrompt({
|
|
1469
|
-
role: phase,
|
|
1470
|
-
artifactPaths: ctx.artifactPaths,
|
|
1471
|
-
artifactReadPaths: ctx.artifactReadPaths,
|
|
1472
|
-
sourcePaths: ctx.sourcePaths,
|
|
1473
|
-
repoRoot: ctx.root,
|
|
1474
|
-
pathASeedPath: ctx.pathASeedPath,
|
|
1475
|
-
adversarialDepth: ctx.adversarialDepth,
|
|
1476
|
-
});
|
|
1477
|
-
const prompt = await bindContractReviewPrompt({
|
|
1478
|
-
artifactsDir: ctx.artifactsDir, artifact: PHASE_TO_ARTIFACT[phase], role: phase,
|
|
1479
|
-
emissionId: ctx.runId, requirement: reviewRequirementForRole(phase, ctx.adversarialDepth),
|
|
1480
|
-
prompt: extraSection ? `${rendered.prompt}\n${extraSection}` : rendered.prompt,
|
|
1481
|
-
});
|
|
1482
|
-
return writeContractPromptStep(ctx, {
|
|
1483
|
-
prompt,
|
|
1484
|
-
outputPath: rendered.outputPath,
|
|
1485
|
-
stopCondition: `Stop after writing the contract-pipeline output for phase "${phase}" and running next-step.`,
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
function writeContractBlockedStep(ctx, params) {
|
|
1489
|
-
return writeCurrentStep({
|
|
1490
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1491
|
-
status: "blocked",
|
|
1492
|
-
runId: ctx.runId,
|
|
1493
|
-
repoRoot: ctx.root,
|
|
1494
|
-
artifactsDir: ctx.artifactsDir,
|
|
1495
|
-
prompt: params.prompt,
|
|
1496
|
-
allowedCommands: [],
|
|
1497
|
-
stopCondition: params.stopCondition,
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1500
|
-
/**
|
|
1501
|
-
* T1 slice 4b: ONE round-trip whose prompt concatenates the rendered specs of
|
|
1502
|
-
* several consecutive authoring phases. The worker writes every named artifact
|
|
1503
|
-
* top-down (each later phase's inputs are the files it wrote in the earlier
|
|
1504
|
-
* sections of the same round-trip), then runs next-step once. The group header
|
|
1505
|
-
* overrides the per-section "stop after writing" lines so they are not read as
|
|
1506
|
-
* three separate stop points.
|
|
1507
|
-
*/
|
|
1508
|
-
/**
|
|
1509
|
-
* The extra section a collapsed member carries, mirroring the gate that would
|
|
1510
|
-
* have claimed that phase on its own — so collapsing changes the number of
|
|
1511
|
-
* round-trips and nothing else about what the worker is told.
|
|
1512
|
-
*
|
|
1513
|
-
* This is load-bearing, not tidiness. `collapsedRoundTripGate` is registered
|
|
1514
|
-
* BEFORE `scaffoldedPhaseGate`, so without this a group containing
|
|
1515
|
-
* `test_validator_plan` would silently drop the S3 skeleton the worker is
|
|
1516
|
-
* supposed to fill in, and the collapse would quietly make that phase HARDER
|
|
1517
|
-
* rather than cheaper.
|
|
1518
|
-
*/
|
|
1519
|
-
async function collapsedSectionExtra(phase, artifactsDir) {
|
|
1520
|
-
if (phase === "test_validator_plan" || phase === "implementation_planning") {
|
|
1521
|
-
return await buildScaffoldSection(phase, artifactsDir);
|
|
1522
|
-
}
|
|
1523
|
-
return await buildReReviewSection(phase, artifactsDir);
|
|
1524
|
-
}
|
|
1525
|
-
async function writeCollapsedRoundTripStep(ctx, phases) {
|
|
1526
|
-
const sections = await Promise.all(phases.map(async (phase, index) => ({
|
|
1527
|
-
phase,
|
|
1528
|
-
rendered: renderContractPipelinePrompt({
|
|
1529
|
-
role: phase,
|
|
1530
|
-
artifactPaths: ctx.artifactPaths,
|
|
1531
|
-
artifactReadPaths: Object.fromEntries(Object.entries(ctx.artifactReadPaths).filter(([name]) => !phases.slice(0, index).some(earlier => PHASE_TO_ARTIFACT[earlier] === name))),
|
|
1532
|
-
sourcePaths: ctx.sourcePaths,
|
|
1533
|
-
repoRoot: ctx.root,
|
|
1534
|
-
pathASeedPath: ctx.pathASeedPath,
|
|
1535
|
-
adversarialDepth: ctx.adversarialDepth,
|
|
1536
|
-
}),
|
|
1537
|
-
extra: await collapsedSectionExtra(phase, ctx.artifactsDir),
|
|
1538
|
-
})));
|
|
1539
|
-
const outputPaths = sections.map((s) => s.rendered.outputPath);
|
|
1540
|
-
const header = `# Collapsed Authoring Round-Trip — ${phases.length} Phases
|
|
1541
|
-
|
|
1542
|
-
This is a low-complexity change, so these ${phases.length} coherent authoring phases are combined into a SINGLE round-trip. Complete EVERY section below — author them top-down, writing each artifact to its named path (each later section's inputs are the files you write in the earlier sections of this same round-trip). Then run next-step ONCE.
|
|
1543
|
-
|
|
1544
|
-
Treat any per-section "Stop after you write the output file" / "Do not start the next phase" instruction as scoped to that section only — it does NOT mean stop the round-trip. Finish all sections first.
|
|
1545
|
-
|
|
1546
|
-
If you cannot complete a section (an artifact would be malformed), write the ones you can and run next-step: the pipeline re-emits any missing or invalid artifact as its own fine-grained step, so no work is lost.
|
|
1547
|
-
|
|
1548
|
-
Artifacts to produce (in order):
|
|
1549
|
-
${outputPaths.map((p, i) => `${i + 1}. \`${p}\` (${phases[i]})`).join("\n")}`;
|
|
1550
|
-
const boundSections = await Promise.all(sections.map(async (section) => bindContractReviewPrompt({
|
|
1551
|
-
artifactsDir: ctx.artifactsDir, artifact: PHASE_TO_ARTIFACT[section.phase], role: section.phase,
|
|
1552
|
-
emissionId: ctx.runId, requirement: reviewRequirementForRole(section.phase, ctx.adversarialDepth),
|
|
1553
|
-
prompt: section.extra ? `${section.rendered.prompt}\n${section.extra}` : section.rendered.prompt,
|
|
1554
|
-
})));
|
|
1555
|
-
const body = boundSections.map(prompt => `\n---\n\n${prompt}`).join("\n");
|
|
1556
|
-
return writeContractPromptStep(ctx, {
|
|
1557
|
-
prompt: `${header}\n${body}`,
|
|
1558
|
-
outputPath: outputPaths[outputPaths.length - 1],
|
|
1559
|
-
stopCondition: `Stop after writing all ${phases.length} collapsed artifacts (${phases.join(", ")}) and running next-step once.`,
|
|
1560
|
-
});
|
|
1561
|
-
}
|
|
1562
|
-
/**
|
|
1563
|
-
* The TRANSPORT-failure report for a re-emitted module wave, or `""` on a first
|
|
1564
|
-
* dispatch.
|
|
1565
|
-
*
|
|
1566
|
-
* A shard absent after dispatch is NOT a refusal of the work and NOT a contract
|
|
1567
|
-
* the worker got wrong — it is an item that was published and did not come back.
|
|
1568
|
-
* The two used to be indistinguishable: the re-emitted wave was byte-identical
|
|
1569
|
-
* to the first one, so a host that lost items mid-output saw only "run this
|
|
1570
|
-
* wave again", and nothing named the loss. Naming it is what lets a host
|
|
1571
|
-
* respond to the right thing (re-deliver the missing items, or report that it
|
|
1572
|
-
* cannot run them) instead of re-running a wave whose other shards already
|
|
1573
|
-
* exist.
|
|
1574
|
-
*
|
|
1575
|
-
* The classification is stated in the prompt because that is the channel the
|
|
1576
|
-
* host actually reads — the re-emission IS the delivery. It is a RE-EMISSION
|
|
1577
|
-
* report, so the absence is derived from the shards on disk, never from a
|
|
1578
|
-
* worker's claim that it wrote one.
|
|
1579
|
-
*/
|
|
1580
|
-
function transportReport(absent, total) {
|
|
1581
|
-
// A PARTIAL return is the transport fact. All-absent is not reported, and the
|
|
1582
|
-
// reason is that NOTHING PERSISTED CAN TELL THE TWO APART. Searched, so the
|
|
1583
|
-
// claim is not an assumption: no dispatch marker exists under the artifacts
|
|
1584
|
-
// dir (the wave directory holds only the shards `scanModuleShards` reads, so a
|
|
1585
|
-
// wave emitted-and-lost leaves the same entries as one never emitted); the
|
|
1586
|
-
// rejected-payload archive covers AGGREGATED artifacts, never a shard, so an
|
|
1587
|
-
// unparseable shard is simply absent; `current-step.json` is overwritten by
|
|
1588
|
-
// EVERY emission and is a request, not a receipt — the tool cannot observe
|
|
1589
|
-
// that a host read it; and this writer has no `runLogger` seam at all, so no
|
|
1590
|
-
// emission is recorded in the run log either. Inventing the distinction would
|
|
1591
|
-
// mean guessing, and guessing here fires a transport failure on every ordinary
|
|
1592
|
-
// first emission — a report that is wrong on the normal case is one a host
|
|
1593
|
-
// learns to ignore, which costs more than the distinguishable half is worth.
|
|
1594
|
-
//
|
|
1595
|
-
// So the report is scoped to what it can actually ASSERT: some items came back
|
|
1596
|
-
// and some did not. That case needs no dispatch record, because the returned
|
|
1597
|
-
// shards ARE the record that a wave was published.
|
|
1598
|
-
if (absent.length === 0 || absent.length === total)
|
|
1599
|
-
return "";
|
|
1600
|
-
const list = absent.map((name) => `\`${name}\``).join(", ");
|
|
1601
|
-
return `> **TRANSPORT failure — ${absent.length} of ${total} items did not return.** The previous wave published ${list}, and no valid shard exists for ${absent.length === 1 ? "it" : "them"} on disk. This is a DELIVERY failure, not a refusal: the item was published and did not come back, which says nothing about whether the work is right. Re-deliver ${absent.length === 1 ? "that item" : "those items"} — the other ${total - absent.length} shard(s) are already present and must NOT be re-run. If the host cannot deliver ${absent.length === 1 ? "it" : "them"} independently, say so rather than serializing the whole wave through one context.
|
|
1602
|
-
`;
|
|
1603
|
-
}
|
|
1604
|
-
/**
|
|
1605
|
-
* DC-3: fan a parallel phase out to one bounded item per module. The host owns
|
|
1606
|
-
* grouping, concurrency, and execution choices; this tool supplies only the
|
|
1607
|
-
* complete coherent workload. Each item writes a per-module shard, and the next
|
|
1608
|
-
* next-step merges every shard into the aggregated artifact before any
|
|
1609
|
-
* downstream derivation. A degenerate decomposition (zero or one module) falls
|
|
1610
|
-
* back to the single aggregated step.
|
|
1611
|
-
*/
|
|
1612
|
-
async function writeParallelModuleWaveStep(ctx, phase) {
|
|
1613
|
-
const modules = await readDecomposedModules(ctx.artifactsDir);
|
|
1614
|
-
if (modules.length <= 1) {
|
|
1615
|
-
return writeContractPhaseStep(ctx, phase);
|
|
1616
|
-
}
|
|
1617
|
-
// THE TRANSPORT REPORT. This wave is emitted either as the FIRST dispatch (no
|
|
1618
|
-
// shard on disk yet — an empty missing set) or as a RE-emission after a
|
|
1619
|
-
// previous wave came back with shards absent. The two are different facts and
|
|
1620
|
-
// used to render identically: a host that lost two of nine items mid-output
|
|
1621
|
-
// got the same undiagnosed "run this wave" it got the first time, so the only
|
|
1622
|
-
// thing that ever noticed the loss was a repeat of the same prompt. The set is
|
|
1623
|
-
// read from disk here rather than threaded through the plan, because the plan
|
|
1624
|
-
// is a pure classifier and the shard scan is a filesystem fact.
|
|
1625
|
-
const absent = (await scanModuleShards(ctx.artifactsDir, phase, modules)).missing;
|
|
1626
|
-
const inputArtifact = "module_decomposition";
|
|
1627
|
-
const inputPaths = ["goal_spec", "context_bundle", "module_decomposition"].map((key) => `- \`${ctx.artifactPaths[key]}\` (${key})`);
|
|
1628
|
-
const moduleLines = modules
|
|
1629
|
-
.map((mod, i) => {
|
|
1630
|
-
const shardPath = moduleShardPath(ctx.artifactsDir, phase, mod.name);
|
|
1631
|
-
const scope = mod.file_scope.length > 0
|
|
1632
|
-
? mod.file_scope.map((p) => `\`${p}\``).join(", ")
|
|
1633
|
-
: "_(no declared file scope)_";
|
|
1634
|
-
return `${i + 1}. **${mod.name}** — file scope: ${scope}\n - Write this module's contract to exactly: \`${shardPath}\``;
|
|
1635
|
-
})
|
|
1636
|
-
.join("\n");
|
|
1637
|
-
const perModuleSchema = `{
|
|
1638
|
-
"name": "<module-name — must equal the assigned module>",
|
|
1639
|
-
"inputs": ["<what this module receives>"],
|
|
1640
|
-
"outputs": ["<what this module produces>"],
|
|
1641
|
-
"invariants": ["<invariant that must hold — include a verification_obligation note>"],
|
|
1642
|
-
"side_effects": ["<observable side-effects with owner>"],
|
|
1643
|
-
"validation_boundary": "<what this module validates vs. what callers must guarantee>",
|
|
1644
|
-
"failure_modes": ["<ways this module can fail and how callers should handle them>"],
|
|
1645
|
-
"neighbor_needs": [{ "neighbor": "<module-name>", "needs": "<what this module needs>" }]
|
|
1646
|
-
}`;
|
|
1647
|
-
const taskVerb = "draft its module contract";
|
|
1648
|
-
const cwdNote = `\n> Set the shell/tool working directory to \`${ctx.root}\` before running any commands.\n`;
|
|
1649
|
-
const nextCommand = loaderCommand("next-step");
|
|
1650
|
-
// THE STEP STATES WHAT IT NEEDS, NOT A MECHANISM. It used to say "dispatch one
|
|
1651
|
-
// sub-agent PER MODULE" — a mechanism the host may not have (in-process
|
|
1652
|
-
// subagents are not universal, and the fallback is a shell-out lane this tool
|
|
1653
|
-
// neither knows nor sizes for). Two of nine such dispatches died mid-output
|
|
1654
|
-
// and only the step's presence check noticed. What the work actually requires
|
|
1655
|
-
// is stated instead: N INDEPENDENT CONTEXTS with no shared authorship. A host
|
|
1656
|
-
// with subagents dispatches N of them; a host without runs the items however
|
|
1657
|
-
// it can, as long as no single context drafts both sides of a seam — which is
|
|
1658
|
-
// the property the seam-reconciliation gate downstream depends on.
|
|
1659
|
-
const prompt = `# Per-Module Contract Drafting (${modules.length} modules)
|
|
1660
|
-
|
|
1661
|
-
This phase publishes one bounded item per module. Complete all ${modules.length} items below; the host owns how they are grouped or executed.
|
|
1662
|
-
${transportReport(absent, modules.length)}
|
|
1663
|
-
|
|
1664
|
-
**What this work needs:** ${modules.length} independent contexts, one per module — no shared authorship. Each module's contract must be drafted by a context that has NOT drafted any module it seams against. That independence is the input the seam reconciliation relies on: a single context drafting both sides of a seam reconciles the seam against itself and reports no mismatch, however mismatched the interfaces are. The host chooses the mechanism; if it has no way to run ${modules.length} independent contexts, say so rather than serializing them through one — a serialized draft is worse than a re-emitted wave, because it produces a wrong aggregate that nothing downstream can detect.
|
|
1665
|
-
|
|
1666
|
-
Each item reads only its module's file scope, then writes ONLY that module's contract shard — no item owns both sides of a seam, and no item writes the aggregated artifact.
|
|
1667
|
-
${cwdNote}
|
|
1668
|
-
## Shared Inputs (every item may read these)
|
|
1669
|
-
|
|
1670
|
-
${inputPaths.join("\n")}
|
|
1671
|
-
|
|
1672
|
-
## Per-Module Assignments — one independent context each
|
|
1673
|
-
|
|
1674
|
-
For each module, an independent context reads its file scope from \`${inputArtifact}\` and ${taskVerb}, writing the result to the module's shard path:
|
|
1675
|
-
|
|
1676
|
-
${moduleLines}
|
|
1677
|
-
|
|
1678
|
-
Each shard must be a single JSON object of this shape (the orchestrator merges all shards into the aggregated \`${PHASE_TO_ARTIFACT[phase]}\` artifact — do NOT write that file yourself):
|
|
1679
|
-
|
|
1680
|
-
\`\`\`json
|
|
1681
|
-
${perModuleSchema}
|
|
1682
|
-
\`\`\`
|
|
1683
|
-
|
|
1684
|
-
## After Every Item Finishes
|
|
1685
|
-
|
|
1686
|
-
Once every module's shard above has been written (all ${modules.length}), run:
|
|
1687
|
-
|
|
1688
|
-
\`${nextCommand}\`
|
|
1689
|
-
|
|
1690
|
-
The orchestrator verifies every module shard is present, merges them into \`${PHASE_TO_ARTIFACT[phase]}\`, and advances. If any shard is missing, this same wave is re-emitted for the missing modules — never a partial aggregate. Do not re-run items whose shard is already present.
|
|
1691
|
-
|
|
1692
|
-
**Stop after the per-module shards are written and you run next-step.** Do not edit source files. Do not write the aggregated artifact. Do not advance further.
|
|
1693
|
-
`;
|
|
1694
|
-
return writeCurrentStep({
|
|
1695
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1696
|
-
status: "ready",
|
|
1697
|
-
runId: ctx.runId,
|
|
1698
|
-
repoRoot: ctx.root,
|
|
1699
|
-
artifactsDir: ctx.artifactsDir,
|
|
1700
|
-
prompt,
|
|
1701
|
-
allowedCommands: [nextCommand],
|
|
1702
|
-
stopCondition: `Stop after writing every per-module shard for phase "${phase}" and running next-step.`,
|
|
1703
|
-
artifactPaths: contractStepArtifactPaths(ctx),
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
/**
|
|
1707
|
-
* The ONE writer dispatch behind the scaffold's single emission call site. Each
|
|
1708
|
-
* underlying writer is reached from exactly here.
|
|
1709
|
-
*/
|
|
1710
|
-
async function writeContractStepPlan(ctx, plan) {
|
|
1711
|
-
switch (plan.via) {
|
|
1712
|
-
case "phase":
|
|
1713
|
-
return await writeContractPhaseStep(ctx, plan.phase, plan.extraSection);
|
|
1714
|
-
case "step":
|
|
1715
|
-
return await writeContractPromptStep(ctx, plan);
|
|
1716
|
-
case "blocked":
|
|
1717
|
-
return await writeContractBlockedStep(ctx, plan);
|
|
1718
|
-
case "module_wave":
|
|
1719
|
-
return await writeParallelModuleWaveStep(ctx, plan.phase);
|
|
1720
|
-
case "collapsed_round_trip":
|
|
1721
|
-
return await writeCollapsedRoundTripStep(ctx, plan.phases);
|
|
1722
|
-
case "rederive":
|
|
1723
|
-
// A deterministic artifact was just written; the frontier moved, so the
|
|
1724
|
-
// whole walk re-runs against the new state rather than guessing the phase.
|
|
1725
|
-
return await buildNextContractPipelineStep(ctx.options);
|
|
1726
|
-
case "pipeline_complete":
|
|
1727
|
-
return null;
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
// ── Branch on `evaluated`: consuming the shared gate-outcome record ─────
|
|
1731
|
-
/**
|
|
1732
|
-
* Read every contract-pipeline payload from disk, plus the intake
|
|
1733
|
-
* finding-enumeration, in the shape the shared cross-gate evaluator consumes.
|
|
1734
|
-
* Always a fresh read — there is no payload cache to go stale.
|
|
1735
|
-
*/
|
|
1736
|
-
async function readCrossGateInputs(artifactsDir, root) {
|
|
1737
|
-
const payloads = new Map();
|
|
1738
|
-
for (const name of CP_ARTIFACT_NAMES) {
|
|
1739
|
-
const envelope = await readContractArtifact(artifactsDir, name);
|
|
1740
|
-
if (envelope)
|
|
1741
|
-
payloads.set(name, envelopePayload(envelope));
|
|
1742
|
-
}
|
|
1743
|
-
const findingEnumeration = await readOptionalJsonFile(intakePaths(artifactsDir).findingEnumeration);
|
|
1744
|
-
// Waived counterexamples (open-bugs.md:108): the coverage gates must not
|
|
1745
|
-
// demand DAG nodes for a counterexample the owner recorded as an accepted
|
|
1746
|
-
// limitation — that would recreate the judge-gate wedge one gate later.
|
|
1747
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1748
|
-
return {
|
|
1749
|
-
payloads,
|
|
1750
|
-
findingEnumeration,
|
|
1751
|
-
root,
|
|
1752
|
-
waivedCounterexampleIds: waivedJudgeAcceptedIds(repairState, payloads.get("judge_report"), payloads.get("counterexample")),
|
|
1753
|
-
};
|
|
168
|
+
else if (raw !== undefined)
|
|
169
|
+
errors = ["A current tool-issued request is required; a stale response cannot authorize this revision."];
|
|
170
|
+
await writeJsonFile(rolePaths.request, request);
|
|
171
|
+
return emit(options, `${bound.text}\n\nWrite the complete bound response to ${rolePaths.submission}.${errors.length ? `\nRefused response:\n${errors.join("\n")}` : ""}`);
|
|
1754
172
|
}
|
|
1755
|
-
/**
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
* exported helpers, which take the payloads this reader produced rather than
|
|
1761
|
-
* reading again — so there is no in-pipeline path to a payload that skipped the
|
|
1762
|
-
* check.
|
|
1763
|
-
* That is the freshness half of The branch-on-evaluated freshness rule, made mechanical: a
|
|
1764
|
-
* gate cannot be handed a payload snapshot taken before its own step archived
|
|
1765
|
-
* the stale copy, because the only way to obtain payloads declines to produce
|
|
1766
|
-
* them until `artifactsSettled` is set.
|
|
1767
|
-
*/
|
|
1768
|
-
async function readCrossGatePayloads(ctx) {
|
|
1769
|
-
if (!ctx.artifactsSettled) {
|
|
1770
|
-
throw new Error("contract pipeline: cross-gate payloads were requested before this invocation's " +
|
|
1771
|
-
"ingestion + staleness-archive pass ran. A gate must read artifact payloads AFTER " +
|
|
1772
|
-
"the archive pass, never from a snapshot taken before it.");
|
|
173
|
+
/** One author/revision loop, with independent review over the executable plan itself. */
|
|
174
|
+
export async function buildNextContractPipelineStep(options) {
|
|
175
|
+
const source = await ensureSource(options);
|
|
176
|
+
if (stableStringify(source.intent ?? null) !== stableStringify(await readExecutionIntent(options.artifactsDir) ?? null)) {
|
|
177
|
+
return emit(options, "The confirmed scope or intent changed after this plan's input was captured. The saved plan and execution evidence are preserved. Restore the agreed checkpoint or explicitly start a new run with the changed scope; the previous review cannot authorize it.", "blocked");
|
|
1773
178
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
* BEFORE an empty `issues` array is allowed to mean "clean".
|
|
1779
|
-
*
|
|
1780
|
-
* `required` is DECLARED PER CALL SITE, as data, because "did not run" means
|
|
1781
|
-
* different things at different boundaries. At a boundary whose upstream phase
|
|
1782
|
-
* order guarantees the gate's input exists, a skip is a refusal — its empty
|
|
1783
|
-
* issue list is proof of nothing. Earlier in the pipeline the same skip means
|
|
1784
|
-
* "not applicable yet", and the gate is simply not required there.
|
|
1785
|
-
*
|
|
1786
|
-
* THE UNCOVERED HALF, stated rather than implied: `digest_coverage` is the one
|
|
1787
|
-
* gate of the eight whose skip is a DOMAIN non-applicability (a source that is
|
|
1788
|
-
* not finding-enumerable) rather than a missing payload, so no boundary lists
|
|
1789
|
-
* it as required and a genuinely absent finding-enumeration file for an
|
|
1790
|
-
* enumerable source still skips silently. Closing that needs the gate module to
|
|
1791
|
-
* expose its enumerability predicate — an edit outside this work item's write
|
|
1792
|
-
* scope.
|
|
1793
|
-
*/
|
|
1794
|
-
export function consumeGateOutcomes(outcomes, selected, required) {
|
|
1795
|
-
const violations = [];
|
|
1796
|
-
for (const gate of selected) {
|
|
1797
|
-
const outcome = outcomes.find((candidate) => candidate.gate === gate);
|
|
1798
|
-
if (!outcome) {
|
|
1799
|
-
violations.push(`[${gate}] produced no outcome record; the gate set changed without this call site.`);
|
|
1800
|
-
continue;
|
|
179
|
+
for (const entry of source.sources) {
|
|
180
|
+
let actual;
|
|
181
|
+
try {
|
|
182
|
+
actual = hashContent(await readFile(entry.path, "utf8"));
|
|
1801
183
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
184
|
+
catch {
|
|
185
|
+
actual = "missing";
|
|
186
|
+
}
|
|
187
|
+
if (actual !== entry.sha256)
|
|
188
|
+
return emit(options, `The agreed input ${entry.path} changed or disappeared. The current plan is preserved. Restore the agreed source or explicitly start a new run; an old review cannot authorize changed source evidence.`, "blocked");
|
|
189
|
+
}
|
|
190
|
+
const state = await readOptionalJsonFile(join(options.artifactsDir, "state.json"));
|
|
191
|
+
const acceptedUnits = new Map();
|
|
192
|
+
for (const unit of state?.plan?.units ?? []) {
|
|
193
|
+
const entry = unit;
|
|
194
|
+
if (["resolved", "resolved_no_change"].includes(state?.items?.[entry.id]?.status ?? ""))
|
|
195
|
+
acceptedUnits.set(entry.id, unit);
|
|
196
|
+
}
|
|
197
|
+
const ingestion = await ingestExecutionPlan({ ...options, acceptedUnits });
|
|
198
|
+
const canonical = await readCanonicalPlan(options.artifactsDir);
|
|
199
|
+
if (ingestion.issues.length || !canonical)
|
|
200
|
+
return authorStep(options, source, canonical, ingestion.issues.join("\n"));
|
|
201
|
+
if (await readApprovedExecutionPlan(options.artifactsDir))
|
|
202
|
+
return options.forceRevision && !ingestion.changed ? authorStep(options, source, canonical, "The operator requested a revised plan. Preserve accepted execution units and add explicit follow-up work where needed.") : null;
|
|
203
|
+
const contextIssues = await executionPlanContextIssues(options.root, canonical);
|
|
204
|
+
if (contextIssues.length)
|
|
205
|
+
return authorStep(options, source, canonical, contextIssues.join("\n"));
|
|
206
|
+
if (source.request || canonical.plan.source_dispositions.some(entry => entry.status === "deferred")) {
|
|
207
|
+
const decisionPath = join(executionPlanPaths(options.artifactsDir).directory, "owner-decision.json");
|
|
208
|
+
const rawDecision = await readOptionalJsonFile(decisionPath);
|
|
209
|
+
const decision = z.object({ revision_sha256: z.string(), confirmed_by: z.literal("host"), approved_unit_ids: z.array(z.string()), declined_units: z.array(z.object({ id: z.string(), reason: z.string().min(1) }).strict()), deferred_findings: z.array(z.object({ finding_id: z.string(), reason: z.string().min(1) }).strict()).default([]) }).strict().safeParse(rawDecision);
|
|
210
|
+
const ids = canonical.plan.units.map(unit => unit.id);
|
|
211
|
+
const covered = decision.success ? [...decision.data.approved_unit_ids, ...decision.data.declined_units.map(unit => unit.id)] : [];
|
|
212
|
+
if (!decision.success || decision.data.revision_sha256 !== canonical.revision_sha256 || new Set(covered).size !== ids.length || covered.length !== ids.length || covered.some(id => !ids.includes(id)) || canonical.plan.source_dispositions.filter(entry => entry.status === "deferred").some(entry => !decision.data.deferred_findings.some(chosen => chosen.finding_id === entry.finding_id))) {
|
|
213
|
+
return emit(options, `# Confirm the requested change plan\n\nPresent the concrete execution units, affected boundaries and unresolved choices in ${executionPlanPaths(options.artifactsDir).canonical}. Batch scope/behavior questions now. Preserve the operator's existing intent and permissions; never silently discard requested work. Record the operator's unit choices in ${decisionPath}:\n${JSON.stringify({ revision_sha256: canonical.revision_sha256, confirmed_by: "host", approved_unit_ids: ids, declined_units: [], deferred_findings: canonical.plan.source_dispositions.filter(entry => entry.status === "deferred").map(entry => ({ finding_id: entry.finding_id, reason: entry.reason })) }, null, 2)}\nA declined unit requires {id,reason}. Proposed finding deferrals require explicit operator confirmation in deferred_findings; an author or judge cannot silently opt out of approved work. For an empty plan, explicitly confirm its evidence-backed request disposition. A material plan revision requires a new decision.`, "blocked");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const pendingRepair = await readPlanReviewHistory(options.artifactsDir);
|
|
217
|
+
if (pendingRepair.repair_revision === canonical.revision_sha256)
|
|
218
|
+
return authorStep(options, source, canonical, pendingRepair.repair_reason);
|
|
219
|
+
for (const role of PLAN_REVIEW_ROLES) {
|
|
220
|
+
const result = await reviewStep(options, canonical, role);
|
|
221
|
+
if ("step_kind" in result)
|
|
222
|
+
return result;
|
|
223
|
+
if (role === "critique") {
|
|
224
|
+
const critique = CritiqueSchema.parse(result.result);
|
|
225
|
+
if (critique.verdict !== "approved" || critique.issues.some(issue => issue.blocking)) {
|
|
226
|
+
const history = await readPlanReviewHistory(options.artifactsDir);
|
|
227
|
+
if (history.repair_rounds >= 8)
|
|
228
|
+
return emit(options, `Conceptual review has not converged after eight repair decisions. Ask the operator to resolve these remaining design issues: ${JSON.stringify(critique.issues)}`, "blocked");
|
|
229
|
+
await writeJsonFile(executionPlanPaths(options.artifactsDir).history, { ...history, repair_rounds: history.repair_rounds + 1, repair_revision: canonical.revision_sha256, repair_reason: JSON.stringify(critique) });
|
|
230
|
+
return authorStep(options, source, canonical, JSON.stringify(critique, null, 2));
|
|
1806
231
|
}
|
|
1807
|
-
continue;
|
|
1808
|
-
}
|
|
1809
|
-
for (const issue of outcome.issues) {
|
|
1810
|
-
if (issue.severity === "error")
|
|
1811
|
-
violations.push(`[${issue.path}] ${issue.message}`);
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
return { ok: violations.length === 0, violations };
|
|
1815
|
-
}
|
|
1816
|
-
/** Locate one gate's outcome in the canonical-order outcome list. */
|
|
1817
|
-
function gateOutcomeOf(outcomes, gate) {
|
|
1818
|
-
return outcomes.find((candidate) => candidate.gate === gate);
|
|
1819
|
-
}
|
|
1820
|
-
/**
|
|
1821
|
-
* The gates the PROMOTION boundary requires to have actually run. Every phase
|
|
1822
|
-
* artifact exists by the time `nextPhase` is null, so a skip here can only mean
|
|
1823
|
-
* a payload went missing or malformed — never "too early".
|
|
1824
|
-
*/
|
|
1825
|
-
const PROMOTION_REQUIRED_GATES = new Set([
|
|
1826
|
-
"paired_obligations",
|
|
1827
|
-
"evidence_threaded",
|
|
1828
|
-
"reconciliation_derivation",
|
|
1829
|
-
]);
|
|
1830
|
-
/** The subset of gates the promotion boundary consumes. */
|
|
1831
|
-
const PROMOTION_GATES = [
|
|
1832
|
-
"paired_obligations",
|
|
1833
|
-
"evidence_threaded",
|
|
1834
|
-
"digest_coverage",
|
|
1835
|
-
"reconciliation_derivation",
|
|
1836
|
-
];
|
|
1837
|
-
/**
|
|
1838
|
-
* The gates the PRE-CRITIC structural floor requires. `contract_finalization`,
|
|
1839
|
-
* `seam_reconciliation` and `test_validator_plan` all precede `critic` in the
|
|
1840
|
-
* phase order, so their artifacts exist by the time this boundary is reached.
|
|
1841
|
-
*/
|
|
1842
|
-
const PRE_CRITIC_REQUIRED_GATES = new Set([
|
|
1843
|
-
"paired_obligations",
|
|
1844
|
-
"reconciliation_derivation",
|
|
1845
|
-
]);
|
|
1846
|
-
// ── Gates, in execution order ─────────────────────────────────────────────────
|
|
1847
|
-
/**
|
|
1848
|
-
* Seed source-digest binding. Re-hash every source path the path_a seed
|
|
1849
|
-
* recorded, against the digest it recorded at seed-build time, and refuse with
|
|
1850
|
-
* a classified blocked step on a mismatch — rather than spending the whole
|
|
1851
|
-
* design pipeline on content that no longer holds the findings the seed
|
|
1852
|
-
* enumerates. Runs first, before anything is ingested or derived.
|
|
1853
|
-
*
|
|
1854
|
-
* The refusal names the operator's ACCEPT lane: a drift the operator has
|
|
1855
|
-
* reviewed and judged not to invalidate the findings is recorded in
|
|
1856
|
-
* `seed-source-acceptances.json` and stops blocking. That lane exists because
|
|
1857
|
-
* the gate cannot tell a version bump from a behaviour change, and the honest
|
|
1858
|
-
* answer to "can the tool tell?" is no — see `SeedSourceAcceptance`.
|
|
1859
|
-
*/
|
|
1860
|
-
const seedSourceDigestGate = async (ctx) => {
|
|
1861
|
-
if (!ctx.pathASeedPath)
|
|
1862
|
-
return null;
|
|
1863
|
-
const seed = await readOptionalJsonFile(ctx.pathASeedPath);
|
|
1864
|
-
const allMismatches = await detectSeedSourceDigestMismatches(ctx.root, seed);
|
|
1865
|
-
if (allMismatches.length === 0)
|
|
1866
|
-
return null;
|
|
1867
|
-
// A digest is a WHOLE-FILE byte binding, so it moves for changes no finding is
|
|
1868
|
-
// about — a release `version` bump in `package.json` moved the digest of every
|
|
1869
|
-
// finding that cited it and blocked the run. The operator's acceptance file is
|
|
1870
|
-
// the way to say "reviewed, and the findings still hold"; anything it does not
|
|
1871
|
-
// cover still blocks. The tool cannot infer this (see `SeedSourceAcceptance`),
|
|
1872
|
-
// so it enforces the record's shape and the join, never the judgment.
|
|
1873
|
-
const { accepted, blocking } = await partitionSeedSourceDrift(ctx.artifactsDir, allMismatches);
|
|
1874
|
-
if (blocking.length === 0)
|
|
1875
|
-
return null;
|
|
1876
|
-
const acceptedLines = accepted
|
|
1877
|
-
.map((entry) => `- \`${entry.path}\` — accepted by ${entry.accepted_by}: ${entry.rationale}`)
|
|
1878
|
-
.join("\n");
|
|
1879
|
-
const lines = blocking
|
|
1880
|
-
.map((mismatch) => `- \`${mismatch.path}\` — recorded \`${mismatch.expected.slice(0, 12)}…\`, ` +
|
|
1881
|
-
`now ${mismatch.actual ? `\`${mismatch.actual.slice(0, 12)}…\`` : "**unreadable**"}`)
|
|
1882
|
-
.join("\n");
|
|
1883
|
-
return {
|
|
1884
|
-
via: "blocked",
|
|
1885
|
-
prompt: `# Source Content Changed Since the Audit Seed Was Built
|
|
1886
|
-
|
|
1887
|
-
The path-A seed records a sha256 for every source it was built from. The following no longer match, so the findings this pipeline is designing against may no longer describe the code:
|
|
1888
|
-
|
|
1889
|
-
${lines}
|
|
1890
|
-
|
|
1891
|
-
The findings this pipeline is designing against were derived from the recorded content, so re-deriving them is the only thing that makes the design sound again. Decide with the user:
|
|
1892
|
-
|
|
1893
|
-
1. **Re-run the audit extraction** against the current tree, so the findings describe the code as it now stands; or
|
|
1894
|
-
2. **Restore the drifted sources** to the content the audit read, if the change was unintended; or
|
|
1895
|
-
3. **Accept the drift**, when the operator has reviewed it and the findings still hold — a release version bump touches no finding the seed enumerates. Write to \`${seedSourceAcceptancesPath(ctx.artifactsDir)}\`:
|
|
1896
|
-
|
|
1897
|
-
\`\`\`json
|
|
1898
|
-
{
|
|
1899
|
-
"acceptances": [
|
|
1900
|
-
{ "path": "<the path above, repo-relative>", "rationale": "<why the findings still hold>", "accepted_by": "<who decided>" }
|
|
1901
|
-
]
|
|
1902
|
-
}
|
|
1903
|
-
\`\`\`
|
|
1904
|
-
|
|
1905
|
-
The tool does not and cannot infer this: it can see that bytes moved and cannot see whether a finding citing those bytes is still true. Record an acceptance ONLY for a decision the operator actually made — the record names its decider.
|
|
1906
|
-
${acceptedLines ? `\nAlready accepted this run (not blocking):\n\n${acceptedLines}\n` : ""}
|
|
1907
|
-
Only as an explicit LAST resort — an accepted, recorded decision to design against findings that no longer match the code — delete \`${ctx.pathASeedPath}\` and re-run next-step. That rebuilds the seed from the CURRENT sources while keeping the OLD findings, which clears this alarm without re-deriving anything.`,
|
|
1908
|
-
stopCondition: "Stop — the contract pipeline is blocked on a source whose content no longer matches the audit seed.",
|
|
1909
|
-
};
|
|
1910
|
-
};
|
|
1911
|
-
/**
|
|
1912
|
-
* Ingest raw worker outputs into validated envelopes. An output that fails
|
|
1913
|
-
* validation is archived and its producing phase re-emitted with the validation
|
|
1914
|
-
* errors — LLM output is untrusted until validated.
|
|
1915
|
-
*/
|
|
1916
|
-
const invalidIngestionGate = async (ctx) => {
|
|
1917
|
-
const ingestion = await ingestContractArtifacts(ctx.artifactsDir, ctx.adversarialDepth);
|
|
1918
|
-
for (const name of ingestion.stale ?? []) {
|
|
1919
|
-
const archived = await archiveContractArtifact(ctx.artifactsDir, name, "stale", ctx.options.renameFn);
|
|
1920
|
-
if (!archived.originalFree) {
|
|
1921
|
-
return { via: "blocked", prompt: `# Stale review context could not be archived\n\nThe inputs to ${name} changed. Its old review cannot be accepted. Unlock or remove the stale submission at \`${contractInputFilePath(ctx.artifactsDir, name)}\` and its canonical artifact, then run next-step.`, stopCondition: "Stop until the stale review artifacts can be archived; never accept the old review." };
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
// Continue the existing staleness/consistency walk. Re-emitting this downstream
|
|
1925
|
-
// reviewer now would skip upstream repairs and ask it to review invalid inputs.
|
|
1926
|
-
if (ingestion.invalid.length === 0)
|
|
1927
|
-
return null;
|
|
1928
|
-
const first = ingestion.invalid[0];
|
|
1929
|
-
if (first.unavailable) {
|
|
1930
|
-
return { via: "blocked", prompt: `# Required review context unavailable
|
|
1931
|
-
|
|
1932
|
-
${formatValidationIssues(first.issues)}
|
|
1933
|
-
|
|
1934
|
-
Keep the submitted declaration for diagnosis. When an independent context is available, replace the response at \`${contractInputFilePath(ctx.artifactsDir, first.name)}\` using the current bound prompt, then run \`${loaderCommand("next-step")}\`. Do not substitute self-review or remove the required review.`, stopCondition: "Stop until a review context satisfying the required policy is available." };
|
|
1935
|
-
}
|
|
1936
|
-
const archived = await archiveContractArtifact(ctx.artifactsDir, first.name, "invalid", ctx.options.renameFn);
|
|
1937
|
-
return {
|
|
1938
|
-
via: "phase",
|
|
1939
|
-
phase: ARTIFACT_TO_PHASE[first.name] ?? "goal_normalization",
|
|
1940
|
-
extraSection: `## Validation Errors From the Previous Attempt
|
|
1941
|
-
|
|
1942
|
-
The previous \`${first.name}\` output failed validation and was archived. Fix every issue below in the rewritten output:
|
|
1943
|
-
|
|
1944
|
-
${formatValidationIssues(first.issues)}
|
|
1945
|
-
${rejectionRewriteInstruction(archived)}`,
|
|
1946
|
-
};
|
|
1947
|
-
};
|
|
1948
|
-
/**
|
|
1949
|
-
* Archive stale artifacts so the staleness DAG re-derives everything downstream
|
|
1950
|
-
* of a repaired (re-ingested) upstream artifact — and ABORT when an archive
|
|
1951
|
-
* fails.
|
|
1952
|
-
*
|
|
1953
|
-
* COR-114e4941: the returned ArchiveOutcome used to be discarded here, alone
|
|
1954
|
-
* among the four archive call sites. `originalFree: false` means the move
|
|
1955
|
-
* failed and the stale file is STILL at its canonical path, where
|
|
1956
|
-
* `contractArtifactExists` (a bare `existsSync`) reports it as present — so the
|
|
1957
|
-
* producing phase was never re-emitted and every downstream derivation (the
|
|
1958
|
-
* obligation ledger, the phase cut, the DAG) was built on content the staleness
|
|
1959
|
-
* DAG had already declared invalid. Refusing here is the only ordering that
|
|
1960
|
-
* keeps that impossible: the frontier is not resolved until every stale
|
|
1961
|
-
* artifact is genuinely out of the way.
|
|
1962
|
-
*/
|
|
1963
|
-
const staleArchiveGate = async (ctx) => {
|
|
1964
|
-
const staleness = await detectStaleArtifacts(ctx.artifactsDir);
|
|
1965
|
-
for (const name of staleness.stale) {
|
|
1966
|
-
const archived = await archiveContractArtifact(ctx.artifactsDir, name, "stale", ctx.options.renameFn);
|
|
1967
|
-
if (archived.originalFree)
|
|
1968
|
-
continue;
|
|
1969
|
-
const phase = ARTIFACT_TO_PHASE[name];
|
|
1970
|
-
if (phase) {
|
|
1971
|
-
return {
|
|
1972
|
-
via: "phase",
|
|
1973
|
-
phase,
|
|
1974
|
-
extraSection: `## A Stale \`${name}\` Could Not Be Archived
|
|
1975
|
-
|
|
1976
|
-
\`${name}\` is stale (an upstream it depends on changed) but the tool could not move it into the contract history directory, so the stale content is still at its canonical path. The pipeline will not derive anything downstream of it.
|
|
1977
|
-
|
|
1978
|
-
Rewrite \`${name}\` from its current upstreams.
|
|
1979
|
-
${rejectionRewriteInstruction(archived)}`,
|
|
1980
|
-
};
|
|
1981
|
-
}
|
|
1982
|
-
return {
|
|
1983
|
-
via: "blocked",
|
|
1984
|
-
prompt: `# A Stale Derived Artifact Could Not Be Archived
|
|
1985
|
-
|
|
1986
|
-
\`${name}\` is stale but could not be moved into the contract history directory, and it is tool-derived — no authoring phase owns it, so it cannot simply be re-emitted.
|
|
1987
|
-
|
|
1988
|
-
Remove or unlock \`${contractArtifactFilePath(ctx.artifactsDir, name)}\` (and its \`.input.json\` sibling if present), then re-run next-step so the pipeline re-derives it from the current upstreams. Proceeding on the stale copy would build the obligation ledger, phase cut and implementation DAG on content the staleness DAG has already declared invalid.`,
|
|
1989
|
-
stopCondition: "Stop — the contract pipeline is blocked on a stale artifact that could not be archived.",
|
|
1990
|
-
};
|
|
1991
|
-
}
|
|
1992
|
-
// OBL-m-friction-inv-5 (post_repair_rederive): when a judge needs_repair →
|
|
1993
|
-
// regenerate-target landed, the re-ingested target makes its downstream
|
|
1994
|
-
// artifacts stale and they are archived above — the REAL remediate
|
|
1995
|
-
// post-repair re-derive site. Route this backend-observed step-boundary fact
|
|
1996
|
-
// through the single CE-005 chokepoint. Discriminator = repair target
|
|
1997
|
-
// artifact id + repair iteration count, so re-recording the same re-derive is
|
|
1998
|
-
// a collision-free no-op (CE-006).
|
|
1999
|
-
if (staleness.stale.length > 0) {
|
|
2000
|
-
const repairState = await readRepairState(ctx.artifactsDir);
|
|
2001
|
-
const lastRepair = repairState.repairs[repairState.repairs.length - 1];
|
|
2002
|
-
if (lastRepair) {
|
|
2003
|
-
const iteration = repairState.repairs.length;
|
|
2004
|
-
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
2005
|
-
eventType: "post_repair_rederive",
|
|
2006
|
-
discriminator: `${lastRepair.target}:${iteration}`,
|
|
2007
|
-
note: `Post-repair re-derive: repair iteration ${iteration} of "${lastRepair.target}" ` +
|
|
2008
|
-
`made ${staleness.stale.length} downstream artifact(s) stale; they were archived so ` +
|
|
2009
|
-
`the staleness DAG re-derives the back half.`,
|
|
2010
|
-
category: "trap",
|
|
2011
|
-
}, "remediate-code");
|
|
2012
232
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
// issue.path is "<artifact_name>.goal_id"; extract the artifact name.
|
|
2046
|
-
const firstPath = goalIdErrors[0]?.path ?? "";
|
|
2047
|
-
const mismatchedArtifact = firstPath.replace(/\.goal_id$/, "");
|
|
2048
|
-
const archived = await archiveContractArtifact(ctx.artifactsDir, mismatchedArtifact, "invalid", ctx.options.renameFn);
|
|
2049
|
-
return {
|
|
2050
|
-
via: "phase",
|
|
2051
|
-
phase: ARTIFACT_TO_PHASE[mismatchedArtifact] ?? "goal_normalization",
|
|
2052
|
-
extraSection: `## Goal-ID Consistency Error
|
|
2053
|
-
|
|
2054
|
-
Every contract-pipeline artifact must share the same goal_id. The following mismatch was detected:
|
|
2055
|
-
|
|
2056
|
-
${goalIdErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2057
|
-
|
|
2058
|
-
Rewrite the output so its goal_id matches the goal_id established in goal_spec.json.
|
|
2059
|
-
${rejectionRewriteInstruction(archived)}`,
|
|
2060
|
-
};
|
|
2061
|
-
};
|
|
2062
|
-
/**
|
|
2063
|
-
* Finalized-module-SET gate (INV-CO-13). `deriveFinalizedModuleContracts` maps
|
|
2064
|
-
* the drafts 1:1, so the deterministic path can never violate this — but it is
|
|
2065
|
-
* not the only writer: a judge or critique repair re-emits contract_finalization
|
|
2066
|
-
* as an LLM step, ingested under a SHAPE-ONLY validator that structurally cannot
|
|
2067
|
-
* see the drafts. A rewrite that merges modules under an invented name and drops
|
|
2068
|
-
* another would otherwise be accepted, and the phase cut, the derived obligation
|
|
2069
|
-
* ids and the DAG write-scope join would all then be built on a module set that
|
|
2070
|
-
* has already lost a module.
|
|
2071
|
-
*
|
|
2072
|
-
* DELIBERATELY PHASE-INDEPENDENT, like the goal-ID gate: rewriting
|
|
2073
|
-
* finalized_module_contracts stales its declared dependent
|
|
2074
|
-
* conceptual_design_critique, which the staleness gate archives BEFORE the
|
|
2075
|
-
* frontier is resolved — so the phase right after a corrupting rewrite is
|
|
2076
|
-
* `critique`, not `critic`. Gating at the critic boundary would not fire until
|
|
2077
|
-
* critique, obligation_ledger, cyclic_seam_resolution, test_validator_plan and
|
|
2078
|
-
* assessment had all been re-spent on the collapsed set.
|
|
2079
|
-
*
|
|
2080
|
-
* The gate is NOT in `PRE_CRITIC_REQUIRED_GATES` / `PROMOTION_REQUIRED_GATES`
|
|
2081
|
-
* because at this phase-independent position a not-evaluated outcome genuinely
|
|
2082
|
-
* means "the drafted or finalized contracts do not exist yet" — the branch on
|
|
2083
|
-
* `evaluated` is taken, and its declared meaning here is "not yet applicable".
|
|
2084
|
-
*/
|
|
2085
|
-
const finalizedModuleSetGate = async (ctx) => {
|
|
2086
|
-
const outcomes = await evaluateContractPipelineCrossGateOutcomes(await readCrossGatePayloads(ctx));
|
|
2087
|
-
const outcome = gateOutcomeOf(outcomes, "finalized_module_set_preserved");
|
|
2088
|
-
if (!outcome?.evaluated)
|
|
2089
|
-
return null;
|
|
2090
|
-
const moduleSetErrors = outcome.issues.filter((issue) => issue.severity === "error");
|
|
2091
|
-
if (moduleSetErrors.length === 0)
|
|
2092
|
-
return null;
|
|
2093
|
-
const archived = await archiveContractArtifact(ctx.artifactsDir, "finalized_module_contracts", "invalid", ctx.options.renameFn);
|
|
2094
|
-
return {
|
|
2095
|
-
via: "phase",
|
|
2096
|
-
phase: "contract_finalization",
|
|
2097
|
-
extraSection: `## Finalized Module Set Does Not Match the Drafted Contracts
|
|
2098
|
-
|
|
2099
|
-
Finalization carries every drafted module contract through — it may incorporate seam-reconciliation decisions into a module's interface, but it may never drop, merge, rename, or invent a module. The following mismatches were detected:
|
|
2100
|
-
|
|
2101
|
-
${moduleSetErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2102
|
-
${rejectionRewriteInstruction(archived)}`,
|
|
2103
|
-
};
|
|
2104
|
-
};
|
|
2105
|
-
/**
|
|
2106
|
-
* Path-A overlap topology gate. A required audit seam is not advisory:
|
|
2107
|
-
* decomposition must name exactly one seam-preparation module and keep distinct
|
|
2108
|
-
* implementation modules for the participating work blocks. Checked before
|
|
2109
|
-
* module contracts fan out, so the seam is shaped once and downstream authors
|
|
2110
|
-
* can work in parallel against it.
|
|
2111
|
-
*/
|
|
2112
|
-
const workBlockSeamGate = async (ctx) => {
|
|
2113
|
-
if (!contractArtifactExists(ctx.artifactsDir, "module_decomposition"))
|
|
2114
|
-
return null;
|
|
2115
|
-
const seed = await readOptionalJsonFile(pathASeedFilePath(ctx.artifactsDir));
|
|
2116
|
-
if (!seed)
|
|
2117
|
-
return null;
|
|
2118
|
-
const decomposition = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_decomposition"));
|
|
2119
|
-
const seamIssues = validateWorkBlockSeamPreparation(seed, decomposition).filter((issue) => issue.severity === "error");
|
|
2120
|
-
if (seamIssues.length === 0)
|
|
2121
|
-
return null;
|
|
2122
|
-
const archived = await archiveContractArtifact(ctx.artifactsDir, "module_decomposition", "invalid", ctx.options.renameFn);
|
|
2123
|
-
return {
|
|
2124
|
-
via: "phase",
|
|
2125
|
-
phase: "decomposition",
|
|
2126
|
-
extraSection: `## Audit Work-Block Seam Errors
|
|
2127
|
-
|
|
2128
|
-
The module decomposition dropped or blurred required audit work-block seams. Fix every issue below. Keep implementation work blocks distinct, add exactly one seam-preparation module per required seam (one module may prepare several seams), and list the corresponding source_work_block_ids / prepares_seam_ids:
|
|
2129
|
-
|
|
2130
|
-
${seamIssues.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2131
|
-
${rejectionRewriteInstruction(archived)}`,
|
|
2132
|
-
};
|
|
2133
|
-
};
|
|
2134
|
-
/**
|
|
2135
|
-
* Conceptual-design-critique gate (A1). Once the critique exists, a blocking
|
|
2136
|
-
* concern routes a design repair BEFORE any downstream artifact is derived. The
|
|
2137
|
-
* signal is mechanical (any blocking item), so the author's verdict label can't
|
|
2138
|
-
* wave a blocking concern through. Convergence-terminated: repairing the
|
|
2139
|
-
* finalized contracts re-stales + re-emits the critique, a clean re-critique
|
|
2140
|
-
* proceeds, a stalled loop escalates to the user.
|
|
2141
|
-
*/
|
|
2142
|
-
const conceptualCritiqueGate = async (ctx) => {
|
|
2143
|
-
if (!contractArtifactExists(ctx.artifactsDir, "conceptual_design_critique"))
|
|
2144
|
-
return null;
|
|
2145
|
-
const gate = await evaluateCritiqueGate(ctx.artifactsDir);
|
|
2146
|
-
if (gate.kind === "repair") {
|
|
2147
|
-
const repairState = await readRepairState(ctx.artifactsDir);
|
|
2148
|
-
const critiqueRepairs = repairState.critique_repairs ?? [];
|
|
2149
|
-
if (!critiqueRepairs.some((repair) => repair.critique_hash === gate.critiqueHash)) {
|
|
2150
|
-
critiqueRepairs.push({
|
|
2151
|
-
critique_hash: gate.critiqueHash,
|
|
2152
|
-
target: gate.target,
|
|
2153
|
-
at: new Date().toISOString(),
|
|
2154
|
-
blocking_ids: gate.blockingIds,
|
|
2155
|
-
});
|
|
2156
|
-
repairState.critique_repairs = critiqueRepairs;
|
|
2157
|
-
await writeRepairState(ctx.artifactsDir, repairState);
|
|
2158
|
-
}
|
|
2159
|
-
const rendered = renderContractRepairPrompt({
|
|
2160
|
-
trigger: "critique",
|
|
2161
|
-
target: gate.target,
|
|
2162
|
-
instruction: "Revise the design to resolve every BLOCKING concern in the conceptual design critique " +
|
|
2163
|
-
`(${gate.blockingIds.join(", ")}). Read conceptual_design_critique.json for each concern's ` +
|
|
2164
|
-
`description, then apply targeted edits to ${gate.target} so the blocking concerns no longer apply.`,
|
|
2165
|
-
artifactPaths: ctx.artifactPaths,
|
|
2166
|
-
artifactReadPaths: ctx.artifactReadPaths,
|
|
2167
|
-
repoRoot: ctx.root,
|
|
2168
|
-
});
|
|
2169
|
-
return {
|
|
2170
|
-
via: "step",
|
|
2171
|
-
prompt: rendered.prompt,
|
|
2172
|
-
outputPath: rendered.outputPath,
|
|
2173
|
-
stopCondition: `Stop after repairing ${gate.target} to resolve the blocking critique concerns and running next-step.`,
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
if (gate.kind === "escalate") {
|
|
2177
|
-
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
2178
|
-
eventType: "repair_round",
|
|
2179
|
-
discriminator: `critique_nonconvergence:${gate.reason}`,
|
|
2180
|
-
note: `Conceptual-design critique↔repair loop escalated (${gate.reason}): ${gate.note}`,
|
|
2181
|
-
category: "trap",
|
|
2182
|
-
}, "remediate-code");
|
|
2183
|
-
return {
|
|
2184
|
-
via: "blocked",
|
|
2185
|
-
prompt: `# Conceptual-Design Critique Did Not Converge
|
|
2186
|
-
|
|
2187
|
-
${gate.note}
|
|
2188
|
-
|
|
2189
|
-
## Outstanding blocking concerns
|
|
2190
|
-
|
|
2191
|
-
${gate.blocking.map((id) => `- ${id}`).join("\n")}
|
|
2192
|
-
|
|
2193
|
-
Read conceptual_design_critique.json, decide with the user how to resolve each blocking concern (revise the contract design and re-run, or downgrade it to advisory), then re-run next-step.`,
|
|
2194
|
-
stopCondition: "Stop — the contract pipeline is blocked on a non-converging conceptual-design critique pending a user decision.",
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
return null;
|
|
2198
|
-
};
|
|
2199
|
-
/**
|
|
2200
|
-
* Deterministic obligation-ledger derivation (S1). The ledger is a pure function
|
|
2201
|
-
* of the finalized module contracts (every invariant/failure mode/module → an
|
|
2202
|
-
* obligation), so the tool generates it rather than an LLM phase: the structure
|
|
2203
|
-
* can never be malformed, no judgment is spent on a mechanical restructuring,
|
|
2204
|
-
* and a weak model is never asked to emit it from scratch.
|
|
2205
|
-
*/
|
|
2206
|
-
const obligationLedgerDerivationGate = async (ctx) => {
|
|
2207
|
-
if (ctx.nextPhase !== "obligation_ledger")
|
|
2208
|
-
return null;
|
|
2209
|
-
const finalizedPayload = envelopePayload(await readContractArtifact(ctx.artifactsDir, "finalized_module_contracts"));
|
|
2210
|
-
await writeDerivedContractArtifact(ctx.artifactsDir, "obligation_ledger", deriveObligationLedger(finalizedPayload));
|
|
2211
|
-
return { via: "rederive" };
|
|
2212
|
-
};
|
|
2213
|
-
/**
|
|
2214
|
-
* Degenerate seam_reconciliation collapse. A single-module decomposition has NO
|
|
2215
|
-
* inter-module seams, so seam_reconciliation is a structural no-op: write an
|
|
2216
|
-
* empty seam report deterministically (no host round-trip). The empty report
|
|
2217
|
-
* makes validateReconciliationDerivation pass vacuously. A multi-module
|
|
2218
|
-
* decomposition falls through to the LLM seam_reconciliation step (which
|
|
2219
|
-
* mismatches exist is a judgment call).
|
|
2220
|
-
*/
|
|
2221
|
-
const degenerateSeamReconciliationGate = async (ctx) => {
|
|
2222
|
-
if (ctx.nextPhase !== "seam_reconciliation")
|
|
2223
|
-
return null;
|
|
2224
|
-
const modules = await readDecomposedModules(ctx.artifactsDir);
|
|
2225
|
-
if (modules.length > 1)
|
|
2226
|
-
return null;
|
|
2227
|
-
const drafted = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_contracts"));
|
|
2228
|
-
const goalId = isRecord(drafted) && typeof drafted.goal_id === "string" ? drafted.goal_id : "";
|
|
2229
|
-
await writeDerivedContractArtifact(ctx.artifactsDir, "seam_reconciliation_report", {
|
|
2230
|
-
contract_version: "remediate-code-contract-pipeline/seam-reconciliation-report/v1alpha1",
|
|
2231
|
-
goal_id: goalId,
|
|
2232
|
-
mismatches: [],
|
|
2233
|
-
created_at: new Date().toISOString(),
|
|
2234
|
-
});
|
|
2235
|
-
return { via: "rederive" };
|
|
2236
|
-
};
|
|
2237
|
-
/** Render the cycle section for the LLM finalization step: every declared-graph
|
|
2238
|
-
* cycle, its members, and the exact artifact tokens forming each edge. */
|
|
2239
|
-
function renderTokenCycleSection(cycles) {
|
|
2240
|
-
const parts = cycles.map((cycle, i) => {
|
|
2241
|
-
const edges = cycle.edges
|
|
2242
|
-
.map((e) => `- \`${e.consumer}\` depends on \`${e.producer}\` via \`artifact:${e.artifact}\` ` +
|
|
2243
|
-
`(${e.consumer} consumes it; ${e.producer} produces it)`)
|
|
2244
|
-
.join("\n");
|
|
2245
|
-
return `### Cycle ${i + 1}: [${cycle.members.join(", ")}]\n\n${edges}`;
|
|
2246
|
-
});
|
|
2247
|
-
return `## Cyclic Artifact-Token Dependencies — Resolve These In Your Output
|
|
2248
|
-
|
|
2249
|
-
The drafted contracts declare a CYCLIC artifact-token flow. Implementation ordering derives from
|
|
2250
|
-
producer/consumer \`artifact:<name>\` tokens ALONE, so the declared flow must be acyclic — a cycle
|
|
2251
|
-
cannot be phased, and the finalized contracts are REJECTED at validation while one remains.
|
|
2252
|
-
|
|
2253
|
-
${parts.join("\n\n")}
|
|
2254
|
-
|
|
2255
|
-
Rewrite the finalized \`inputs\`/\`outputs\` so one direction owns each flow: move an artifact token
|
|
2256
|
-
to the module that genuinely produces it, split a shared primitive into an earlier module's output,
|
|
2257
|
-
or drop a token that does not describe a real data handoff. Keep the module SET unchanged — do not
|
|
2258
|
-
add, drop, rename, or merge modules.`;
|
|
2259
|
-
}
|
|
2260
|
-
/**
|
|
2261
|
-
* Deterministic contract_finalization (all module counts). Finalization is a
|
|
2262
|
-
* mechanical merge, not fresh authoring: carry each drafted module contract's
|
|
2263
|
-
* interface fields verbatim (dropping neighbor_needs — ordering derives from
|
|
2264
|
-
* the artifact-token graph alone, open-bugs.md:106) and attach
|
|
2265
|
-
* the agreed_interface of every seam that touches the module as a
|
|
2266
|
-
* seam_adjustment. The judgment already happened at seam_reconciliation.
|
|
2267
|
-
* Attaching each agreed interface verbatim guarantees the INV-CO-12
|
|
2268
|
-
* reconciliation-derivation gate passes. A downstream gate that still finds the
|
|
2269
|
-
* merge inadequate re-emits contract_finalization as an LLM step — the only path
|
|
2270
|
-
* that still needs judgment. A CYCLIC declared token graph takes that LLM path
|
|
2271
|
-
* up front: the mechanical merge would carry the cycle verbatim into an
|
|
2272
|
-
* artifact validation refuses, so the gate emits the finalization step with the
|
|
2273
|
-
* cycle named instead of deriving.
|
|
2274
|
-
*/
|
|
2275
|
-
const contractFinalizationDerivationGate = async (ctx) => {
|
|
2276
|
-
if (ctx.nextPhase !== "contract_finalization")
|
|
2277
|
-
return null;
|
|
2278
|
-
const drafted = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_contracts"));
|
|
2279
|
-
const cycles = detectContractTokenCycles(drafted);
|
|
2280
|
-
if (cycles.length > 0) {
|
|
2281
|
-
return {
|
|
2282
|
-
via: "phase",
|
|
2283
|
-
phase: "contract_finalization",
|
|
2284
|
-
extraSection: renderTokenCycleSection(cycles),
|
|
2285
|
-
};
|
|
2286
|
-
}
|
|
2287
|
-
const seamReport = envelopePayload(await readContractArtifact(ctx.artifactsDir, "seam_reconciliation_report"));
|
|
2288
|
-
await writeDerivedContractArtifact(ctx.artifactsDir, "finalized_module_contracts", deriveFinalizedModuleContracts(drafted, seamReport));
|
|
2289
|
-
return { via: "rederive" };
|
|
2290
|
-
};
|
|
2291
|
-
/**
|
|
2292
|
-
* Judge gate: implementation planning is reachable only through an approved
|
|
2293
|
-
* verdict (the fixpoint) or a convergent targeted repair. A stalled /
|
|
2294
|
-
* non-converging repair loop escalates to the user (blocked) instead of silently
|
|
2295
|
-
* proceeding with residual risk.
|
|
2296
|
-
*/
|
|
2297
|
-
const judgeRepairGate = async (ctx) => {
|
|
2298
|
-
if (ctx.nextPhase !== "implementation_planning")
|
|
2299
|
-
return null;
|
|
2300
|
-
const gate = await evaluateJudgeGate(ctx.artifactsDir);
|
|
2301
|
-
if (gate.kind === "repair") {
|
|
2302
|
-
const repairTarget = gate.directive.target;
|
|
2303
|
-
const repairState = await readRepairState(ctx.artifactsDir);
|
|
2304
|
-
if (!repairState.repairs.some((repair) => repair.judge_hash === gate.judgeHash)) {
|
|
2305
|
-
repairState.repairs.push({
|
|
2306
|
-
judge_hash: gate.judgeHash,
|
|
2307
|
-
target: repairTarget,
|
|
2308
|
-
at: new Date().toISOString(),
|
|
2309
|
-
accepted_ce_ids: gate.acceptedCeIds,
|
|
2310
|
-
addressed_ce_fingerprints: gate.addressedCeFingerprints,
|
|
2311
|
-
});
|
|
2312
|
-
await writeRepairState(ctx.artifactsDir, repairState);
|
|
2313
|
-
}
|
|
2314
|
-
const rendered = renderContractRepairPrompt({
|
|
2315
|
-
trigger: "judge",
|
|
2316
|
-
target: repairTarget,
|
|
2317
|
-
instruction: gate.directive.instruction,
|
|
2318
|
-
artifactPaths: ctx.artifactPaths,
|
|
2319
|
-
artifactReadPaths: ctx.artifactReadPaths,
|
|
2320
|
-
repoRoot: ctx.root,
|
|
2321
|
-
});
|
|
2322
|
-
return {
|
|
2323
|
-
via: "step",
|
|
2324
|
-
prompt: rendered.prompt,
|
|
2325
|
-
outputPath: rendered.outputPath,
|
|
2326
|
-
stopCondition: `Stop after rewriting "${repairTarget}" per the judge repair directive and running next-step.`,
|
|
2327
|
-
};
|
|
2328
|
-
}
|
|
2329
|
-
if (gate.kind === "escalate") {
|
|
2330
|
-
// Non-convergence (stall or runaway backstop): surface it to the user loudly
|
|
2331
|
-
// rather than promoting a plan over an un-converged contract. The
|
|
2332
|
-
// outstanding accepted counterexamples are named so the user can resolve
|
|
2333
|
-
// them (revise the contract design or accept them as known limitations).
|
|
2334
|
-
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
2335
|
-
eventType: "repair_round",
|
|
2336
|
-
discriminator: `judge_nonconvergence:${gate.reason}`,
|
|
2337
|
-
note: `Judge↔repair loop escalated (${gate.reason}): ${gate.note}`,
|
|
2338
|
-
category: "trap",
|
|
2339
|
-
}, "remediate-code");
|
|
2340
|
-
const waiversPath = counterexampleWaiversPath(ctx.artifactsDir);
|
|
2341
|
-
const waiverIssuesSection = gate.waiverIssues && gate.waiverIssues.length > 0
|
|
2342
|
-
? `\n\n## Waiver file refused — fix these first\n\n${gate.waiverIssues
|
|
2343
|
-
.map((issue) => `- ${issue}`)
|
|
2344
|
-
.join("\n")}`
|
|
2345
|
-
: "";
|
|
2346
|
-
const heading = gate.reason === "invalid_waivers"
|
|
2347
|
-
? "# The Counterexample Waiver File Was Refused"
|
|
2348
|
-
: "# Judge↔Repair Loop Did Not Converge";
|
|
2349
|
-
return {
|
|
2350
|
-
via: "blocked",
|
|
2351
|
-
prompt: `${heading}
|
|
2352
|
-
|
|
2353
|
-
${gate.note}${waiverIssuesSection}
|
|
2354
|
-
|
|
2355
|
-
## Outstanding accepted counterexamples (unwaived)
|
|
2356
|
-
|
|
2357
|
-
${gate.outstanding.length > 0
|
|
2358
|
-
? gate.outstanding.map((id) => `- ${id}`).join("\n")
|
|
2359
|
-
: "_(none newly accepted this round)_"}
|
|
2360
|
-
|
|
2361
|
-
## Record an owner waiver (the recorded resolution verb)
|
|
2362
|
-
|
|
2363
|
-
To accept an outstanding counterexample as a KNOWN LIMITATION of this run, write the operator's decision to:
|
|
2364
|
-
|
|
2365
|
-
\`${waiversPath}\`
|
|
2366
|
-
|
|
2367
|
-
\`\`\`json
|
|
2368
|
-
{
|
|
2369
|
-
"waivers": [
|
|
2370
|
-
{ "ce_id": "<id from the list above>", "rationale": "<why this is acceptable>", "waived_by": "<who decided>" }
|
|
2371
|
-
]
|
|
2372
|
-
}
|
|
2373
|
-
\`\`\`
|
|
2374
|
-
|
|
2375
|
-
The next next-step validates the file, records each waiver in repair-state.json (attributable, content-fingerprint-keyed), consumes the file, and proceeds once every outstanding counterexample is repaired or waived. Record a waiver ONLY for a decision the operator actually made — the record names its decider.
|
|
2376
|
-
|
|
2377
|
-
Read the judge_report and counterexample artifacts, decide with the user how to resolve each outstanding counterexample (revise the contract design and re-run, or record a waiver as above), then re-run next-step.`,
|
|
2378
|
-
stopCondition: "Stop — the contract pipeline is blocked on a non-converging judge↔repair loop pending a user decision.",
|
|
2379
|
-
};
|
|
2380
|
-
}
|
|
2381
|
-
return null;
|
|
2382
|
-
};
|
|
2383
|
-
/** Bounded re-emit of implementation_planning, then blocked — the shared shape
|
|
2384
|
-
* of the four promotion rejections (integrity, traceability, obligation gates,
|
|
2385
|
-
* citation grounding). Single-sourced so the four cannot drift into four
|
|
2386
|
-
* different recovery contracts. */
|
|
2387
|
-
async function dagRegenerationPlan(ctx, params) {
|
|
2388
|
-
const repairState = await readRepairState(ctx.artifactsDir);
|
|
2389
|
-
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
2390
|
-
return {
|
|
2391
|
-
via: "blocked",
|
|
2392
|
-
prompt: `# ${params.heading} ${repairState.dag_regenerations.length + 1} Times
|
|
2393
|
-
|
|
2394
|
-
${params.blockedBody}
|
|
2395
|
-
|
|
2396
|
-
${params.violations.map((violation) => `- ${violation}`).join("\n")}
|
|
2397
|
-
`,
|
|
2398
|
-
stopCondition: `Stop after reporting the failure to the user.`,
|
|
2399
|
-
};
|
|
2400
|
-
}
|
|
2401
|
-
repairState.dag_regenerations.push({
|
|
2402
|
-
violations: params.violations,
|
|
2403
|
-
at: new Date().toISOString(),
|
|
2404
|
-
});
|
|
2405
|
-
await writeRepairState(ctx.artifactsDir, repairState);
|
|
2406
|
-
const archived = await archiveContractArtifact(ctx.artifactsDir, "implementation_dag", "invalid", ctx.options.renameFn);
|
|
2407
|
-
return {
|
|
2408
|
-
via: "phase",
|
|
2409
|
-
phase: "implementation_planning",
|
|
2410
|
-
extraSection: `${params.reEmitBody}
|
|
2411
|
-
|
|
2412
|
-
${params.violations.map((violation) => `- ${violation}`).join("\n")}
|
|
2413
|
-
${rejectionRewriteInstruction(archived)}`,
|
|
2414
|
-
};
|
|
2415
|
-
}
|
|
2416
|
-
/**
|
|
2417
|
-
* All phases exist: enforce referential integrity, traceability, the
|
|
2418
|
-
* fail-closed contract-obligation gates, and the Path-A canonical-block join,
|
|
2419
|
-
* then convert the implementation_dag into an extracted plan and ground its citations.
|
|
2420
|
-
*/
|
|
2421
|
-
const implementationPlanPromotionGate = async (ctx) => {
|
|
2422
|
-
if (ctx.nextPhase)
|
|
2423
|
-
return null;
|
|
2424
|
-
// Path-A canonical-block membership (inv-2), FIRST in this walk so a late
|
|
2425
|
-
// source_finding_ids violation fails before any other gate executes instead
|
|
2426
|
-
// of throwing out of the promoter below and wedging every subsequent
|
|
2427
|
-
// next-step (COR-114e4941). Same bounded re-emit as every other promotion
|
|
2428
|
-
// rejection; no gate has executed past it.
|
|
2429
|
-
const pathARefusals = await collectPathARefusals(ctx.artifactsDir);
|
|
2430
|
-
if (pathARefusals.length > 0) {
|
|
2431
|
-
return await dagRegenerationPlan(ctx, {
|
|
2432
|
-
heading: "Path-A Canonical Block Join Failed",
|
|
2433
|
-
blockedBody: "The implementation_dag repeatedly declares source_finding_ids that cannot be joined to a canonical audit work block:",
|
|
2434
|
-
reEmitBody: `## Path-A Canonical Block Errors From the Previous Attempt
|
|
2435
|
-
|
|
2436
|
-
Each node's source_finding_ids must name exactly one canonical audit work block, and together the nodes must cover every block exactly once. Fix every entry below:`,
|
|
2437
|
-
violations: pathARefusals,
|
|
2438
|
-
});
|
|
2439
|
-
}
|
|
2440
|
-
// ONE payload read for the whole promotion boundary, shared by the integrity
|
|
2441
|
-
// gate and the contract-obligation gates below. Both consume the SAME
|
|
2442
|
-
// post-archive payloads, and nothing between these two consumers writes an
|
|
2443
|
-
// artifact — so a second read bought no freshness and re-paid the whole
|
|
2444
|
-
// read-and-parse of every contract-pipeline artifact (plus the repair-state
|
|
2445
|
-
// and finding-enumeration reads it carries), on the path every plan promotion
|
|
2446
|
-
// walks. The freshness RULE is unchanged and still enforced: the read below is
|
|
2447
|
-
// `readCrossGatePayloads`, which refuses before this invocation's ingestion +
|
|
2448
|
-
// staleness-archive pass, so neither consumer can be handed a pre-archive
|
|
2449
|
-
// snapshot.
|
|
2450
|
-
const crossGatePayloads = await readCrossGatePayloads(ctx);
|
|
2451
|
-
// DAG referential integrity + bidirectional coverage (ARC-86b18f1b-2), run
|
|
2452
|
-
// before the traceability check so specific referential violations are
|
|
2453
|
-
// reported first (traceability is a superset check).
|
|
2454
|
-
const outcomes = await evaluateContractPipelineCrossGateOutcomes(crossGatePayloads);
|
|
2455
|
-
const integrity = gateOutcomeOf(outcomes, "implementation_dag_integrity");
|
|
2456
|
-
if (!integrity?.evaluated) {
|
|
2457
|
-
return await dagRegenerationPlan(ctx, {
|
|
2458
|
-
heading: "Implementation DAG Could Not Be Checked",
|
|
2459
|
-
blockedBody: "The implementation_dag integrity gate could not run, so its empty issue list proves nothing:",
|
|
2460
|
-
reEmitBody: `## The Implementation DAG Could Not Be Checked
|
|
2461
|
-
|
|
2462
|
-
The referential-integrity gate could not run against the previous output, so it was never shown to be sound. Rewrite a complete implementation_dag:`,
|
|
2463
|
-
violations: [integrity?.reason ?? "no outcome record was produced for the gate"],
|
|
2464
|
-
});
|
|
2465
|
-
}
|
|
2466
|
-
const integrityErrors = integrity.issues.filter((issue) => issue.severity === "error");
|
|
2467
|
-
if (integrityErrors.length > 0) {
|
|
2468
|
-
return await dagRegenerationPlan(ctx, {
|
|
2469
|
-
heading: "Implementation DAG Failed Referential Integrity",
|
|
2470
|
-
blockedBody: "The implementation_dag repeatedly contains referential integrity or coverage violations:",
|
|
2471
|
-
reEmitBody: `## Referential Integrity Errors From the Previous Attempt
|
|
2472
|
-
|
|
2473
|
-
The previous implementation_dag was rejected and archived due to referential integrity violations. Fix every issue below:`,
|
|
2474
|
-
violations: integrityErrors.map((issue) => `[${issue.path}] ${issue.message}`),
|
|
2475
|
-
});
|
|
2476
|
-
}
|
|
2477
|
-
const traceability = await validateImplementationDagTraceability(ctx.artifactsDir);
|
|
2478
|
-
if (!traceability.ok) {
|
|
2479
|
-
return await dagRegenerationPlan(ctx, {
|
|
2480
|
-
heading: "Implementation DAG Failed Traceability",
|
|
2481
|
-
blockedBody: "The implementation_dag repeatedly contains nodes that trace to no obligation and no judge-accepted counterexample:",
|
|
2482
|
-
reEmitBody: `## Traceability Errors From the Previous Attempt
|
|
2483
|
-
|
|
2484
|
-
The previous implementation_dag was rejected and archived. Every node must trace to at least one obligation from the obligation ledger or one judge-accepted counterexample:`,
|
|
2485
|
-
violations: traceability.violations,
|
|
2486
|
-
});
|
|
2487
|
-
}
|
|
2488
|
-
// Contract-obligations promotion gates: fail-closed cross-artifact checks that
|
|
2489
|
-
// must pass before a plan is promoted. These are the invariants that keep the
|
|
2490
|
-
// workflow correct regardless of host strength, so they are enforced here,
|
|
2491
|
-
// never left to host discretion.
|
|
2492
|
-
const obligationGate = await evaluateContractObligationsPromotionGate(ctx.artifactsDir, ctx.root, crossGatePayloads);
|
|
2493
|
-
if (!obligationGate.ok) {
|
|
2494
|
-
return await dagRegenerationPlan(ctx, {
|
|
2495
|
-
heading: "Contract-Obligation Gates Failed",
|
|
2496
|
-
blockedBody: "The contract-obligation promotion gates repeatedly failed and the plan cannot be promoted:",
|
|
2497
|
-
reEmitBody: `## Contract-Obligation Gate Errors From the Previous Attempt
|
|
2498
|
-
|
|
2499
|
-
The previous implementation_dag (and/or upstream contract artifacts) failed the fail-closed contract-obligation gates. Fix every issue below before the plan can be promoted:`,
|
|
2500
|
-
violations: obligationGate.violations,
|
|
2501
|
-
});
|
|
2502
|
-
}
|
|
2503
|
-
// Write-scope + command SHAPE, before anything is promoted. These refusals
|
|
2504
|
-
// used to throw out of the promoter — an unclassified stack that wedged every
|
|
2505
|
-
// subsequent next-step, reachable from an LLM form as ordinary as a
|
|
2506
|
-
// leading-slash "repo-relative" path. They take the same bounded re-emit as
|
|
2507
|
-
// every other promotion rejection now.
|
|
2508
|
-
const scopeRefusals = await collectDagWriteScopeRefusals(ctx.artifactsDir, ctx.root);
|
|
2509
|
-
if (scopeRefusals.length > 0) {
|
|
2510
|
-
return await dagRegenerationPlan(ctx, {
|
|
2511
|
-
heading: "Block Write Scope Failed",
|
|
2512
|
-
blockedBody: "The implementation_dag repeatedly declares a write scope or targeted command the plan cannot carry:",
|
|
2513
|
-
reEmitBody: `## Write-Scope and Command Errors From the Previous Attempt
|
|
2514
|
-
|
|
2515
|
-
Each node's declared write scope becomes the block \`touched_files\` a host binds an implementer to and re-checks against the landed diff, and each targeted command is executed verbatim through a shell. Fix every entry below:`,
|
|
2516
|
-
violations: scopeRefusals,
|
|
2517
|
-
});
|
|
2518
|
-
}
|
|
2519
|
-
await promoteImplementationDagToExtractedPlan(ctx.artifactsDir, ctx.root);
|
|
2520
|
-
// M-B3 source-grounded citation gate (promotion backstop): ground every
|
|
2521
|
-
// promoted extracted-plan finding's citations against the working tree.
|
|
2522
|
-
const citationGate = await evaluatePromotedPlanCitationGrounding(ctx.artifactsDir, ctx.root);
|
|
2523
|
-
if (citationGate) {
|
|
2524
|
-
// The plan was promoted to extracted-plan.json BEFORE this gate ran, so the
|
|
2525
|
-
// ungrounded marker is now on disk. Remove it before any return — otherwise
|
|
2526
|
-
// a subsequent next-step reads the promoted plan and hands it straight to
|
|
2527
|
-
// handlePendingExtractedPlan, bypassing the re-emit and completing the
|
|
2528
|
-
// pipeline on hallucinated citations.
|
|
2529
|
-
await rm(ctx.paths.extractedPlan, { force: true });
|
|
2530
|
-
// The grounding-driven re-emit is a backend-observed step-boundary fact:
|
|
2531
|
-
// route it through the single CE-005 chokepoint as phase_reemit.
|
|
2532
|
-
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
2533
|
-
eventType: "phase_reemit",
|
|
2534
|
-
discriminator: "implementation_planning:citation_grounding:promotion",
|
|
2535
|
-
note: "implementation_planning re-emitted: a promoted plan finding cited a " +
|
|
2536
|
-
"component that does not exist in the working tree (M-B3 citation grounding).",
|
|
2537
|
-
category: "trap",
|
|
2538
|
-
}, "remediate-code");
|
|
2539
|
-
return await dagRegenerationPlan(ctx, {
|
|
2540
|
-
heading: "Citation Grounding Failed",
|
|
2541
|
-
blockedBody: "The promoted plan repeatedly cites components that do not exist in the working tree:",
|
|
2542
|
-
reEmitBody: `## Source-Grounded Citation Gate Errors From the Previous Attempt
|
|
2543
|
-
|
|
2544
|
-
The previous implementation_dag produced findings that cite components not present in the working tree. Every cited path or symbol must point at something real:`,
|
|
2545
|
-
violations: citationGate.violations,
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
|
-
// Normalized block write scope, tracked-tree half. Runs AFTER the
|
|
2549
|
-
// citation gate because the two overlap but neither contains the other: a
|
|
2550
|
-
// finding grounds on any real path OR symbol, so a node with plausible prose
|
|
2551
|
-
// can ground while the write scope a host would bind a worker to is still
|
|
2552
|
-
// fabricated. Same bounded recovery, same plan removal.
|
|
2553
|
-
const writeScopeGate = await evaluatePromotedPlanWriteScope(ctx.artifactsDir, ctx.root);
|
|
2554
|
-
if (writeScopeGate) {
|
|
2555
|
-
await rm(ctx.paths.extractedPlan, { force: true });
|
|
2556
|
-
return await dagRegenerationPlan(ctx, {
|
|
2557
|
-
heading: "Block Write Scope Failed",
|
|
2558
|
-
blockedBody: "The promoted plan repeatedly declares a block write scope that does not exist in the working tree:",
|
|
2559
|
-
reEmitBody: `## Block Write-Scope Errors From the Previous Attempt
|
|
2560
|
-
|
|
2561
|
-
Each node's declared write scope becomes the block \`touched_files\` a host binds an implementer to and re-checks against the landed diff. The following entries name a directory that does not exist:`,
|
|
2562
|
-
violations: writeScopeGate.violations,
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
2565
|
-
return { via: "pipeline_complete" };
|
|
2566
|
-
};
|
|
2567
|
-
/** Render the detected cycles for a prompt. */
|
|
2568
|
-
function renderCycleDescriptions(cycles) {
|
|
2569
|
-
return cycles
|
|
2570
|
-
.map((cycle, index) => `Cycle ${index + 1}: [${cycle.members.join(", ")}]`)
|
|
2571
|
-
.join("\n");
|
|
2572
|
-
}
|
|
2573
|
-
/** Build the seam-obligation graph from the obligation ledger AS IT STANDS NOW. */
|
|
2574
|
-
async function readSeamObligationGraph(artifactsDir) {
|
|
2575
|
-
const envelope = await readContractArtifact(artifactsDir, "obligation_ledger");
|
|
2576
|
-
const ledger = envelopePayload(envelope);
|
|
2577
|
-
const obligationIds = new Set((ledger?.obligations ?? []).map((o) => o.id));
|
|
2578
|
-
return {
|
|
2579
|
-
nodes: (ledger?.obligations ?? []).map((obligation) => ({
|
|
2580
|
-
id: obligation.id,
|
|
2581
|
-
needs: (obligation.depends_on ?? []).filter((dep) => obligationIds.has(dep)),
|
|
2582
|
-
})),
|
|
2583
|
-
goalId: ledger?.goal_id ?? "",
|
|
2584
|
-
ledgerHash: envelope?.content_hash ?? "unknown",
|
|
2585
|
-
};
|
|
2586
|
-
}
|
|
2587
|
-
/**
|
|
2588
|
-
* Cyclic-seam resolution gate: runs after obligation_ledger is present and
|
|
2589
|
-
* before assessment. Detects circular interface-definition obligations, then
|
|
2590
|
-
* routes to an LLM resolution step when cycles are found. Cap:
|
|
2591
|
-
* MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS; on exhaustion, route to a user-decision
|
|
2592
|
-
* step (then blocked if still unresolved).
|
|
2593
|
-
*/
|
|
2594
|
-
const cyclicSeamResolutionGate = async (ctx) => {
|
|
2595
|
-
if (ctx.nextPhase !== "cyclic_seam_resolution")
|
|
2596
|
-
return null;
|
|
2597
|
-
const graph = await readSeamObligationGraph(ctx.artifactsDir);
|
|
2598
|
-
const detectedCycles = detectCyclicSeamObligations(graph.nodes);
|
|
2599
|
-
if (detectedCycles.length === 0) {
|
|
2600
|
-
await writeDerivedContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution", {
|
|
2601
|
-
contract_version: "remediate-code-contract-pipeline/cyclic-seam-resolution/v1alpha1",
|
|
2602
|
-
goal_id: graph.goalId,
|
|
2603
|
-
cycles: [],
|
|
2604
|
-
status: "no_cycles",
|
|
2605
|
-
created_at: new Date().toISOString(),
|
|
2606
|
-
});
|
|
2607
|
-
return { via: "rederive" };
|
|
2608
|
-
}
|
|
2609
|
-
const repairState = await readCyclicSeamRepairState(ctx.artifactsDir);
|
|
2610
|
-
const attemptsForLedger = repairState.attempts.filter((attempt) => attempt.ledger_hash === graph.ledgerHash);
|
|
2611
|
-
// Guard: the artifact exists and is already marked resolved/no_cycles. This
|
|
2612
|
-
// branch should not normally be reached (the artifact exists, so the frontier
|
|
2613
|
-
// skips it), but re-deriving is the safe answer.
|
|
2614
|
-
const existingResolution = envelopePayload(await readContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution"));
|
|
2615
|
-
if (existingResolution &&
|
|
2616
|
-
(existingResolution.status === "resolved" ||
|
|
2617
|
-
existingResolution.status === "no_cycles")) {
|
|
2618
|
-
return { via: "rederive" };
|
|
2619
|
-
}
|
|
2620
|
-
const cycleDescriptions = renderCycleDescriptions(detectedCycles);
|
|
2621
|
-
if (attemptsForLedger.length >= MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS) {
|
|
2622
|
-
if (!repairState.user_decision_emitted) {
|
|
2623
|
-
repairState.user_decision_emitted = true;
|
|
2624
|
-
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2625
|
-
return {
|
|
2626
|
-
via: "blocked",
|
|
2627
|
-
prompt: `# Cyclic Seam Resolution — User Decision Required
|
|
2628
|
-
|
|
2629
|
-
The automatic cycle-break resolution reached its cap (${MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS} attempt(s)) without producing a valid cycle-free obligation graph. The following obligation cycles remain unresolved:
|
|
2630
|
-
|
|
2631
|
-
${cycleDescriptions}
|
|
2632
|
-
|
|
2633
|
-
**Choose one of the two sanctioned break strategies per cycle:**
|
|
2634
|
-
|
|
2635
|
-
1. **Mediator module** — Introduce a third obligation/module that both sides depend on. The mediator owns the shared primitive; neither original module defines an interface for the other.
|
|
2636
|
-
2. **Single authority** — Designate one obligation/module as the definitive owner of the co-defined interface. The other becomes a consumer only. This is recorded as a named, scoped exception.
|
|
2637
|
-
|
|
2638
|
-
To proceed, manually rewrite \`${contractInputFilePath(ctx.artifactsDir, "obligation_ledger")}\` so that no circular \`depends_on\` references exist, then delete \`${contractInputFilePath(ctx.artifactsDir, "cyclic_seam_resolution")}\` and \`${cyclicSeamRepairStatePath(ctx.artifactsDir)}\` and re-run next-step.
|
|
2639
|
-
|
|
2640
|
-
If you choose to stop instead, this run will remain blocked.
|
|
2641
|
-
`,
|
|
2642
|
-
stopCondition: "Stop after presenting the user-decision prompt. Do not attempt further resolution.",
|
|
2643
|
-
};
|
|
2644
|
-
}
|
|
2645
|
-
// The user decision was emitted and cycles are still present — blocked.
|
|
2646
|
-
return {
|
|
2647
|
-
via: "blocked",
|
|
2648
|
-
prompt: `# Cyclic Seam Resolution — Blocked
|
|
2649
|
-
|
|
2650
|
-
Cycles in the obligation graph remain unresolved after the automatic cap and a user-decision step. The run cannot proceed without manual intervention.
|
|
2651
|
-
|
|
2652
|
-
${cycleDescriptions}
|
|
2653
|
-
|
|
2654
|
-
Manually rewrite the obligation_ledger to remove circular depends_on references, delete the cyclic_seam_resolution artifact and cyclic-seam-repair-state.json, and re-run next-step.
|
|
2655
|
-
`,
|
|
2656
|
-
stopCondition: "Stop — the run is blocked on cyclic seam resolution.",
|
|
2657
|
-
};
|
|
2658
|
-
}
|
|
2659
|
-
// Emit the LLM cyclic-seam-resolution step.
|
|
2660
|
-
const outputPath = contractInputFilePath(ctx.artifactsDir, "cyclic_seam_resolution");
|
|
2661
|
-
const ledgerInputPath = contractInputFilePath(ctx.artifactsDir, "obligation_ledger");
|
|
2662
|
-
const priorRejection = [...repairState.attempts]
|
|
2663
|
-
.reverse()
|
|
2664
|
-
.find((attempt) => attempt.ledger_hash === graph.ledgerHash && attempt.recheck_reason)?.recheck_reason;
|
|
2665
|
-
const rejectionSection = priorRejection
|
|
2666
|
-
? `\n## Why the Previous Attempt Was Rejected\n\n${priorRejection}\n`
|
|
2667
|
-
: "";
|
|
2668
|
-
repairState.attempts.push({
|
|
2669
|
-
ledger_hash: graph.ledgerHash,
|
|
2670
|
-
at: new Date().toISOString(),
|
|
2671
|
-
recheck_passed: false,
|
|
2672
|
-
});
|
|
2673
|
-
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2674
|
-
return {
|
|
2675
|
-
via: "step",
|
|
2676
|
-
prompt: renderCyclicSeamResolutionPrompt({
|
|
2677
|
-
cycleDescriptions,
|
|
2678
|
-
ledgerInputPath,
|
|
2679
|
-
outputPath,
|
|
2680
|
-
extraSection: rejectionSection,
|
|
2681
|
-
}),
|
|
2682
|
-
outputPath,
|
|
2683
|
-
stopCondition: CYCLIC_SEAM_RESOLUTION_STOP,
|
|
2684
|
-
};
|
|
2685
|
-
};
|
|
2686
|
-
const CYCLIC_SEAM_RESOLUTION_STOP = "Stop after rewriting the obligation_ledger, writing the cyclic_seam_resolution output file, and running next-step.";
|
|
2687
|
-
/**
|
|
2688
|
-
* The ONE worker prompt for cyclic-seam resolution. The gate emits it for each
|
|
2689
|
-
* attempt, and every generic re-emit of the phase (a refused ingestion, a stale
|
|
2690
|
-
* archive, a goal-id mismatch) emits it too — a second, shorter text used to
|
|
2691
|
-
* reach the worker on those re-emits and left out the ledger rewrite that the
|
|
2692
|
-
* re-check requires, so the retry was refused for a rule it was never told.
|
|
2693
|
-
*
|
|
2694
|
-
* The worker writes only `resolved`: `no_cycles` is the tool's own record, and
|
|
2695
|
-
* the re-check refuses any record while the ledger still has a cycle.
|
|
2696
|
-
*/
|
|
2697
|
-
export function renderCyclicSeamResolutionPrompt(params) {
|
|
2698
|
-
const { cycleDescriptions, ledgerInputPath, outputPath } = params;
|
|
2699
|
-
return `# Cyclic Seam Resolution
|
|
2700
|
-
|
|
2701
|
-
The obligation ledger has circular interface-definition obligations. Break each cycle with one of the two strategies below. Then rewrite the ledger and write the resolution record.
|
|
2702
|
-
|
|
2703
|
-
## Detected Cycles
|
|
2704
|
-
|
|
2705
|
-
${cycleDescriptions}
|
|
2706
|
-
${params.extraSection ?? ""}
|
|
2707
|
-
## Break Strategies
|
|
2708
|
-
|
|
2709
|
-
For each cycle, choose one:
|
|
2710
|
-
|
|
2711
|
-
1. **Mediator** — Designate a third obligation that both sides depend on. The mediator owns the shared primitive; neither original obligation defines an interface for the other. The mediator must exist in the ledger and must not be a member of the cycle.
|
|
2712
|
-
2. **Single authority** — Designate one of the cycle's own obligations as the owner of the interface. The others become consumers only. Name the scoped exception in \`exception_registration\`.
|
|
2713
|
-
|
|
2714
|
-
## Required Inputs
|
|
2715
|
-
|
|
2716
|
-
- \`${ledgerInputPath}\` (obligation_ledger)
|
|
2717
|
-
|
|
2718
|
-
## Your Task
|
|
2719
|
-
|
|
2720
|
-
Write two files. The record alone does not break a cycle:
|
|
2721
|
-
|
|
2722
|
-
1. **Rewrite \`${ledgerInputPath}\`** so each cycle's \`depends_on\` edges route through the obligation you designate. The tool runs cycle detection again on the ledger you leave; it refuses the record while any cycle remains.
|
|
2723
|
-
2. **Write the resolution record** to exactly \`${outputPath}\`, with one entry per cycle:
|
|
2724
|
-
|
|
2725
|
-
\`\`\`json
|
|
2726
|
-
{
|
|
2727
|
-
"contract_version": "remediate-code-contract-pipeline/cyclic-seam-resolution/v1alpha1",
|
|
2728
|
-
"goal_id": "<from obligation_ledger>",
|
|
2729
|
-
"cycles": [
|
|
2730
|
-
{
|
|
2731
|
-
"members": ["<obligation-id>", "..."],
|
|
2732
|
-
"break_strategy": "${sketchValues(CYCLIC_SEAM_BREAK_STRATEGIES)}",
|
|
2733
|
-
"designated_obligation_id": "<the mediator, or the single authority — must exist in the rewritten ledger>",
|
|
2734
|
-
"resolution_description": "<what was changed and why>",
|
|
2735
|
-
"exception_registration": "<if single_authority: the named scoped exception; otherwise null>"
|
|
2736
|
-
}
|
|
2737
|
-
],
|
|
2738
|
-
"status": "${sketchValues(CYCLIC_SEAM_RESOLUTION_STATUSES_OFFERED)}"
|
|
2739
|
-
}
|
|
2740
|
-
\`\`\`
|
|
2741
|
-
|
|
2742
|
-
**Stop after you write the two files.** Do not edit source files. Do not start the next phase.
|
|
2743
|
-
`;
|
|
2744
|
-
}
|
|
2745
|
-
/**
|
|
2746
|
-
* Cyclic-seam RE-CHECK. The worker has written a `resolved` record; verify the
|
|
2747
|
-
* break it actually authored against the obligation graph as it actually
|
|
2748
|
-
* stands, and archive + loop back when it does not hold.
|
|
2749
|
-
*
|
|
2750
|
-
* TST-61cff370 / TST-114e4941: this check used to be vacuous. It fabricated a
|
|
2751
|
-
* synthetic node per cycle — `{ id: "_mediator_A_B", needs: [] }` or
|
|
2752
|
-
* `{ id: "_authority_A_B", needs: [] }` — and asked whether redirecting the
|
|
2753
|
-
* cycle's edges at that edge-free sink would be acyclic, against the SAME
|
|
2754
|
-
* unmodified ledger. For any single detected cycle the answer is yes by
|
|
2755
|
-
* construction, so the re-check could never reject: a worker could claim
|
|
2756
|
-
* `status: "resolved"` while changing nothing, and the pipeline advanced. It
|
|
2757
|
-
* now reads the designated obligation off the record and validates it against
|
|
2758
|
-
* the live graph — see `validateAuthoredCycleBreak`.
|
|
2759
|
-
*/
|
|
2760
|
-
const cyclicSeamRecheckGate = async (ctx) => {
|
|
2761
|
-
const resolutionEnvelope = await readContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution");
|
|
2762
|
-
if (!resolutionEnvelope)
|
|
2763
|
-
return null;
|
|
2764
|
-
const resolution = envelopePayload(resolutionEnvelope);
|
|
2765
|
-
if (!resolution)
|
|
2766
|
-
return null;
|
|
2767
|
-
// The record never decides whether cycles remain — the live ledger does. A
|
|
2768
|
-
// `no_cycles` record (the tool's own, or a worker's) passes only while the
|
|
2769
|
-
// ledger is acyclic, and a `resolved` record passes only when every per-cycle
|
|
2770
|
-
// break holds AND no cycle is left anywhere. Before, any status other than
|
|
2771
|
-
// `resolved`, or a `resolved` record with an empty `cycles` list, advanced the
|
|
2772
|
-
// pipeline with the cycles still in the ledger.
|
|
2773
|
-
const graph = await readSeamObligationGraph(ctx.artifactsDir);
|
|
2774
|
-
const remaining = detectCyclicSeamObligations(graph.nodes);
|
|
2775
|
-
let rejection;
|
|
2776
|
-
if (resolution.status !== "resolved") {
|
|
2777
|
-
if (remaining.length === 0)
|
|
2778
|
-
return null;
|
|
2779
|
-
rejection =
|
|
2780
|
-
`The resolution record says status ${JSON.stringify(resolution.status ?? null)}, but the ` +
|
|
2781
|
-
`obligation ledger still has ${remaining.length} cycle(s):\n\n${renderCycleDescriptions(remaining)}\n\n` +
|
|
2782
|
-
`Break each cycle in the ledger, then write status "resolved" with one entry per cycle.`;
|
|
2783
|
-
}
|
|
2784
|
-
const cycleRecords = !rejection && Array.isArray(resolution.cycles)
|
|
2785
|
-
? resolution.cycles
|
|
2786
|
-
: [];
|
|
2787
|
-
for (const cycleRecord of cycleRecords) {
|
|
2788
|
-
if (!Array.isArray(cycleRecord.members))
|
|
2789
|
-
continue;
|
|
2790
|
-
const members = cycleRecord.members.filter((member) => typeof member === "string");
|
|
2791
|
-
const strategy = cycleRecord.break_strategy;
|
|
2792
|
-
// The vocabulary is the shared declaration the prompt sketch also renders
|
|
2793
|
-
// (`contractPipeline/sketchSource.ts`). It was two inline literals here and
|
|
2794
|
-
// a hand-written alternation in the prompt — three statements of one rule,
|
|
2795
|
-
// in two modules, none of which the contract validator checked at all.
|
|
2796
|
-
if (!isCyclicSeamBreakStrategy(strategy)) {
|
|
2797
|
-
rejection =
|
|
2798
|
-
`Cycle [${members.join(", ")}] declares break_strategy ` +
|
|
2799
|
-
`${JSON.stringify(strategy ?? null)}, which is not one of: ${CYCLIC_SEAM_BREAK_STRATEGIES.join(", ")}.`;
|
|
2800
|
-
break;
|
|
2801
|
-
}
|
|
2802
|
-
const authored = {
|
|
2803
|
-
strategy,
|
|
2804
|
-
designatedId: typeof cycleRecord.designated_obligation_id === "string"
|
|
2805
|
-
? cycleRecord.designated_obligation_id
|
|
2806
|
-
: undefined,
|
|
2807
|
-
};
|
|
2808
|
-
const validation = validateAuthoredCycleBreak({ members }, graph.nodes, authored);
|
|
2809
|
-
if (!validation.accepted) {
|
|
2810
|
-
rejection = validation.reason ?? `Cycle [${members.join(", ")}] was not resolved.`;
|
|
2811
|
-
break;
|
|
233
|
+
if (role === "judge") {
|
|
234
|
+
const judge = PlanJudgeSchema.parse(result.result);
|
|
235
|
+
const history = await readPlanReviewHistory(options.artifactsDir);
|
|
236
|
+
if (judge.verdict !== "approved") {
|
|
237
|
+
const critic = CriticSchema.parse((await readPlanReview(options.artifactsDir, "critic", canonical.revision_sha256))?.result);
|
|
238
|
+
const counterexamples = new Map(history.counterexamples.map(entry => [entry.id, entry]));
|
|
239
|
+
for (const example of critic.counterexamples)
|
|
240
|
+
counterexamples.set(example.id, example);
|
|
241
|
+
const acceptedIds = judge.classifications.filter(entry => entry.classification === "accepted").map(entry => entry.counterexample_id);
|
|
242
|
+
if (history.repair_rounds >= 8)
|
|
243
|
+
return emit(options, "The reviewed plan has not converged after eight repair decisions. Ask the operator to resolve the named remaining counterexamples; do not implement an unapproved plan.", "blocked");
|
|
244
|
+
await writeJsonFile(executionPlanPaths(options.artifactsDir).history, { counterexamples: [...counterexamples.values()], accepted_ids: acceptedIds, repair_rounds: history.repair_rounds + 1, repair_revision: canonical.revision_sha256, repair_reason: JSON.stringify(judge) });
|
|
245
|
+
return authorStep(options, source, canonical, JSON.stringify(judge, null, 2));
|
|
246
|
+
}
|
|
247
|
+
const residuals = judge.classifications.filter(entry => entry.classification === "residual_risk");
|
|
248
|
+
if (residuals.length) {
|
|
249
|
+
const riskPath = join(executionPlanPaths(options.artifactsDir).directory, "risk-decisions.json");
|
|
250
|
+
const risk = PlanRiskDecisionSchema.safeParse(await readOptionalJsonFile(riskPath));
|
|
251
|
+
if (!risk.success || risk.data.revision_sha256 !== canonical.revision_sha256 || residuals.some(entry => !risk.data.accepted_counterexample_ids.includes(entry.counterexample_id)))
|
|
252
|
+
return emit(options, `# Accept or repair remaining risks\n\nThe independent judge proposes these residual risks, which remain the operator's choice:\n${JSON.stringify(residuals, null, 2)}\nAsk the operator to accept them or revise the plan. Explicit acceptance is recorded at ${riskPath}: ${JSON.stringify({ revision_sha256: canonical.revision_sha256, confirmed_by: "host", accepted_counterexample_ids: residuals.map(entry => entry.counterexample_id) })}. Never infer risk acceptance from the judge's classification.`, "blocked");
|
|
253
|
+
}
|
|
254
|
+
const finalCritic = CriticSchema.parse((await readPlanReview(options.artifactsDir, "critic", canonical.revision_sha256))?.result);
|
|
255
|
+
const finalExamples = new Map(history.counterexamples.map(entry => [entry.id, entry]));
|
|
256
|
+
for (const entry of finalCritic.counterexamples)
|
|
257
|
+
finalExamples.set(entry.id, entry);
|
|
258
|
+
await writeJsonFile(executionPlanPaths(options.artifactsDir).history, { counterexamples: [...finalExamples.values()], accepted_ids: [], repair_rounds: history.repair_rounds });
|
|
259
|
+
const paths = executionPlanPaths(options.artifactsDir);
|
|
260
|
+
const reviews = Object.fromEntries(await Promise.all(PLAN_REVIEW_ROLES.map(async (role) => [role, hashContent(stableStringify(await readPlanReview(options.artifactsDir, role, canonical.revision_sha256)))])));
|
|
261
|
+
await writeJsonFile(paths.approval, { revision_sha256: canonical.revision_sha256, judge_input_sha256: result.input_sha256,
|
|
262
|
+
owner_decision_sha256: hashContent(stableStringify(await readOptionalJsonFile(join(paths.directory, "owner-decision.json")) ?? null)),
|
|
263
|
+
risk_decision_sha256: hashContent(stableStringify(await readOptionalJsonFile(join(paths.directory, "risk-decisions.json")) ?? null)),
|
|
264
|
+
history_sha256: hashContent(stableStringify(await readPlanReviewHistory(options.artifactsDir))), review_sha256: reviews });
|
|
2812
265
|
}
|
|
2813
266
|
}
|
|
2814
|
-
if (!rejection && remaining.length > 0) {
|
|
2815
|
-
rejection =
|
|
2816
|
-
`The obligation ledger still has ${remaining.length} cycle(s) that no accepted break ` +
|
|
2817
|
-
`removed:\n\n${renderCycleDescriptions(remaining)}\n\nBreak each of them in the ledger, ` +
|
|
2818
|
-
`and record one entry per cycle.`;
|
|
2819
|
-
}
|
|
2820
|
-
if (!rejection)
|
|
2821
|
-
return null;
|
|
2822
|
-
const repairState = await readCyclicSeamRepairState(ctx.artifactsDir);
|
|
2823
|
-
const last = repairState.attempts.at(-1);
|
|
2824
|
-
// Carry the reason forward so the NEXT resolution prompt says what failed,
|
|
2825
|
-
// instead of re-asking for the same claim and burning the attempt cap on an
|
|
2826
|
-
// unexplained retry. A record that appeared without a matching emitted
|
|
2827
|
-
// attempt (a resumed run, a hand-written artifact) still gets its rejection
|
|
2828
|
-
// recorded — the outcome is the attempt.
|
|
2829
|
-
if (last && last.ledger_hash === graph.ledgerHash) {
|
|
2830
|
-
last.recheck_passed = false;
|
|
2831
|
-
last.recheck_reason = rejection;
|
|
2832
|
-
}
|
|
2833
|
-
else {
|
|
2834
|
-
repairState.attempts.push({
|
|
2835
|
-
ledger_hash: graph.ledgerHash,
|
|
2836
|
-
at: new Date().toISOString(),
|
|
2837
|
-
recheck_passed: false,
|
|
2838
|
-
recheck_reason: rejection,
|
|
2839
|
-
});
|
|
2840
|
-
}
|
|
2841
|
-
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2842
|
-
const archived = await archiveContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution", "invalid", ctx.options.renameFn);
|
|
2843
|
-
if (!archived.originalFree) {
|
|
2844
|
-
// The rejected record is STILL at its canonical path, and re-deriving would
|
|
2845
|
-
// read the same record, reject it again, fail to archive it again — an
|
|
2846
|
-
// unbounded loop with no cap to stop it: the attempt ledger updates the
|
|
2847
|
-
// same entry in place (one ledger hash), and `rederive` carries no depth
|
|
2848
|
-
// bound. This branch is what makes the re-check's new ability to REJECT
|
|
2849
|
-
// safe; before the re-check could reject, the failure was unreachable.
|
|
2850
|
-
return {
|
|
2851
|
-
via: "blocked",
|
|
2852
|
-
prompt: `# A Rejected Cyclic-Seam Resolution Could Not Be Archived
|
|
2853
|
-
|
|
2854
|
-
The cycle-break re-check rejected the resolution record:
|
|
2855
|
-
|
|
2856
|
-
${rejection}
|
|
2857
|
-
|
|
2858
|
-
The record could not be moved into the contract history directory, so it is still at its canonical path. Re-running would read the same rejected record and loop without bound, so the run stops here instead.
|
|
2859
|
-
|
|
2860
|
-
Remove or unlock \`${contractArtifactFilePath(ctx.artifactsDir, "cyclic_seam_resolution")}\` (and its \`.input.json\` sibling if present), then re-run next-step so the resolution phase is re-emitted with the rejection above.`,
|
|
2861
|
-
stopCondition: "Stop — a rejected cyclic-seam resolution could not be archived and would otherwise loop.",
|
|
2862
|
-
};
|
|
2863
|
-
}
|
|
2864
|
-
// Re-enter to emit the next attempt or the cap.
|
|
2865
|
-
return { via: "rederive" };
|
|
2866
|
-
};
|
|
2867
|
-
/**
|
|
2868
|
-
* Design-spec structural gates before the adversarial critic phase, in the order
|
|
2869
|
-
* they run: the design artifact's own structure, then the cheap cross-artifact
|
|
2870
|
-
* floor, then citation grounding. Error-severity gate failures re-emit the
|
|
2871
|
-
* responsible phase; warning-only results (e.g. circular obligation
|
|
2872
|
-
* dependencies) ride the critic prompt as advisory.
|
|
2873
|
-
*/
|
|
2874
|
-
const preCriticStructuralGate = async (ctx) => {
|
|
2875
|
-
if (ctx.nextPhase !== "critic")
|
|
2876
|
-
return null;
|
|
2877
|
-
const outcomes = await evaluateContractPipelineCrossGateOutcomes(await readCrossGatePayloads(ctx));
|
|
2878
|
-
// (a) The design artifact itself. `contract_finalization` precedes `critic`,
|
|
2879
|
-
// so a not-evaluated outcome here means the payload is malformed, not
|
|
2880
|
-
// absent — its empty issue list is not proof of a clean design.
|
|
2881
|
-
const designSpec = gateOutcomeOf(outcomes, "design_spec");
|
|
2882
|
-
if (!designSpec?.evaluated) {
|
|
2883
|
-
return {
|
|
2884
|
-
via: "phase",
|
|
2885
|
-
phase: "contract_finalization",
|
|
2886
|
-
extraSection: `## Design Structural Gates Could Not Run
|
|
2887
|
-
|
|
2888
|
-
The finalized module contracts could not be checked before adversarial review: ${designSpec?.reason ?? "no outcome record was produced for the design gate"}. Rewrite a complete, well-formed finalized_module_contracts artifact.
|
|
2889
|
-
`,
|
|
2890
|
-
};
|
|
2891
|
-
}
|
|
2892
|
-
const gateErrors = designSpec.issues.filter((issue) => issue.severity === "error");
|
|
2893
|
-
if (gateErrors.length > 0) {
|
|
2894
|
-
return {
|
|
2895
|
-
via: "phase",
|
|
2896
|
-
phase: "contract_finalization",
|
|
2897
|
-
extraSection: `## Design Structural Gate Errors
|
|
2898
|
-
|
|
2899
|
-
The contract_finalization output failed deterministic structural gates. Fix every issue below before adversarial review can begin:
|
|
2900
|
-
|
|
2901
|
-
${gateErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2902
|
-
`,
|
|
2903
|
-
};
|
|
2904
|
-
}
|
|
2905
|
-
const gateWarnings = designSpec.issues.filter((issue) => issue.severity === "warning");
|
|
2906
|
-
if (gateWarnings.length > 0) {
|
|
2907
|
-
return {
|
|
2908
|
-
via: "phase",
|
|
2909
|
-
phase: "critic",
|
|
2910
|
-
extraSection: `## Advisory: Design Structural Warnings
|
|
2911
|
-
|
|
2912
|
-
The following structural issues were detected and should inform your adversarial review. They do not block the pipeline but may indicate areas of design fragility:
|
|
2913
|
-
|
|
2914
|
-
${gateWarnings.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2915
|
-
`,
|
|
2916
|
-
};
|
|
2917
|
-
}
|
|
2918
|
-
// (b) Pre-adversarial structural floor (S5): the cheap cross-artifact checks
|
|
2919
|
-
// whose inputs all exist by the critic phase, so the adversarial loop only
|
|
2920
|
-
// ever sees structurally-sound obligations/tests/contracts and a gap is
|
|
2921
|
-
// re-emitted to the precise responsible phase instead of being discovered
|
|
2922
|
-
// at promotion after the adversarial budget is spent.
|
|
2923
|
-
const preCriticGate = await evaluatePreCriticStructuralGate(ctx.artifactsDir, ctx.root, await readCrossGatePayloads(ctx));
|
|
2924
|
-
if (preCriticGate) {
|
|
2925
|
-
return {
|
|
2926
|
-
via: "phase",
|
|
2927
|
-
phase: preCriticGate.phase,
|
|
2928
|
-
extraSection: `## Pre-Adversarial Structural Gate Errors
|
|
2929
|
-
|
|
2930
|
-
The ${preCriticGate.phase} output failed deterministic structural gates. Fix every issue below before adversarial review begins:
|
|
2931
|
-
|
|
2932
|
-
${preCriticGate.errorLines.join("\n")}
|
|
2933
|
-
`,
|
|
2934
|
-
};
|
|
2935
|
-
}
|
|
2936
|
-
// (c) M-B3 source-grounded citation gate at the pre-critic boundary: ground
|
|
2937
|
-
// the module_decomposition's file_scope citations against the working tree
|
|
2938
|
-
// before the adversarial loop. A module citing only a non-existent path and
|
|
2939
|
-
// no real symbol is re-emitted to the `decomposition` phase — the phase that
|
|
2940
|
-
// OWNS file_scope (the finalized contracts carry interface fields, not
|
|
2941
|
-
// paths, so re-emitting contract_finalization could never change file_scope
|
|
2942
|
-
// and an ungrounded scope would loop forever).
|
|
2943
|
-
const preCriticCitationGate = await evaluatePreCriticCitationGrounding(ctx.artifactsDir, ctx.root);
|
|
2944
|
-
if (preCriticCitationGate) {
|
|
2945
|
-
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
2946
|
-
eventType: "phase_reemit",
|
|
2947
|
-
discriminator: "decomposition:citation_grounding:pre_critic",
|
|
2948
|
-
note: "decomposition re-emitted: a module's file_scope cited a component " +
|
|
2949
|
-
"that does not exist in the working tree, or only re-export shims.",
|
|
2950
|
-
category: "trap",
|
|
2951
|
-
}, "remediate-code");
|
|
2952
|
-
return {
|
|
2953
|
-
via: "phase",
|
|
2954
|
-
phase: "decomposition",
|
|
2955
|
-
extraSection: `## Module File Scope Errors
|
|
2956
|
-
|
|
2957
|
-
A module's file_scope does not point at real logic in the working tree. Fix each path below in the decomposition: every path must exist, and each module must own at least one file that holds its logic, not only files that re-export:
|
|
2958
|
-
|
|
2959
|
-
${preCriticCitationGate.errorLines.join("\n")}
|
|
2960
|
-
`,
|
|
2961
|
-
};
|
|
2962
|
-
}
|
|
2963
267
|
return null;
|
|
2964
|
-
};
|
|
2965
|
-
/**
|
|
2966
|
-
* DC-3 merge intercept: when a parallel phase's aggregated artifact is still
|
|
2967
|
-
* missing, merge the per-module shards into it once they are ALL present.
|
|
2968
|
-
* Returns true when the aggregate was written. A missing shard (or a degenerate
|
|
2969
|
-
* ≤1-module decomposition, which never used the shard path) returns false, and
|
|
2970
|
-
* the caller re-emits the wave — never a partial aggregate. After a complete
|
|
2971
|
-
* merge the artifact is written enveloped and the pipeline re-derives; the
|
|
2972
|
-
* seam_reconciliation / critique pass downstream stays the consistency gate over
|
|
2973
|
-
* the merged contracts.
|
|
2974
|
-
*/
|
|
2975
|
-
async function tryMergeModuleShards(artifactsDir, phase) {
|
|
2976
|
-
const modules = await readDecomposedModules(artifactsDir);
|
|
2977
|
-
if (modules.length <= 1)
|
|
2978
|
-
return false;
|
|
2979
|
-
const scan = await scanModuleShards(artifactsDir, phase, modules);
|
|
2980
|
-
if (scan.missing.length > 0)
|
|
2981
|
-
return false;
|
|
2982
|
-
// goal_id: the upstream module_decomposition is authoritative (every artifact
|
|
2983
|
-
// shares one goal_id; the goal-ID consistency gate enforces it). Fall back to
|
|
2984
|
-
// a shard's goal_id only if the decomposition somehow lacks one.
|
|
2985
|
-
const decompositionGoalId = await readDecompositionGoalId(artifactsDir);
|
|
2986
|
-
const goalId = decompositionGoalId ||
|
|
2987
|
-
[...scan.present.values()]
|
|
2988
|
-
.map((contract) => (typeof contract.goal_id === "string" ? contract.goal_id : undefined))
|
|
2989
|
-
.find((candidate) => Boolean(candidate)) ||
|
|
2990
|
-
"";
|
|
2991
|
-
await writeDerivedContractArtifact(artifactsDir, PARALLEL_MODULE_PHASES[phase], mergeModuleShards(modules, scan.present, goalId));
|
|
2992
|
-
return true;
|
|
2993
|
-
}
|
|
2994
|
-
/**
|
|
2995
|
-
* Parallel-capable phase (DC-3): `module_contract_drafting` fans out to one
|
|
2996
|
-
* agent per module. The aggregated `module_contracts` artifact is missing here,
|
|
2997
|
-
* so first try to merge per-module shards (the worker may have just written
|
|
2998
|
-
* them) — a COMPLETE shard set merges into the aggregated artifact and the
|
|
2999
|
-
* pipeline re-derives; anything else re-emits the wave (which itself falls back
|
|
3000
|
-
* to the single aggregated step for a degenerate ≤1-module decomposition).
|
|
3001
|
-
*/
|
|
3002
|
-
const parallelModuleWaveGate = async (ctx) => {
|
|
3003
|
-
const phase = ctx.nextPhase;
|
|
3004
|
-
if (phase === null || !isParallelModulePhase(phase))
|
|
3005
|
-
return null;
|
|
3006
|
-
const merged = await tryMergeModuleShards(ctx.artifactsDir, phase);
|
|
3007
|
-
return merged ? { via: "rederive" } : { via: "module_wave", phase };
|
|
3008
|
-
};
|
|
3009
|
-
/**
|
|
3010
|
-
* Auto-phasing (T3): at the conceptual-design critique, hand the critic the
|
|
3011
|
-
* tool-DERIVED phase cut so it assesses design quality WITHIN a mechanically
|
|
3012
|
-
* dependency-ordered foundations→consumers phasing, instead of rejecting an
|
|
3013
|
-
* arbitrary N-goal change as "over-scoped" and forcing the host to re-scope by
|
|
3014
|
-
* hand at intake. The cut is derived from the finalized module contracts'
|
|
3015
|
-
* producer/consumer artifact-token edges and PERSISTED as `phase_cut.json`, so the cut
|
|
3016
|
-
* the critic sees and the cut the implementation-DAG promotion enforces are one
|
|
3017
|
-
* source. Only injected when there is a genuine multi-phase cut to communicate.
|
|
3018
|
-
*/
|
|
3019
|
-
const phaseCutCritiqueGate = async (ctx) => {
|
|
3020
|
-
if (ctx.nextPhase !== "critique")
|
|
3021
|
-
return null;
|
|
3022
|
-
const cut = await ensurePhaseCutArtifact(ctx.artifactsDir);
|
|
3023
|
-
if (!cut || cut.phases.length <= 1)
|
|
3024
|
-
return null;
|
|
3025
|
-
const reReview = await buildReReviewSection("critique", ctx.artifactsDir);
|
|
3026
|
-
const phaseCutSection = renderPhaseCutSection(cut);
|
|
3027
|
-
return {
|
|
3028
|
-
via: "phase",
|
|
3029
|
-
phase: "critique",
|
|
3030
|
-
extraSection: reReview ? `${phaseCutSection}\n${reReview}` : phaseCutSection,
|
|
3031
|
-
};
|
|
3032
|
-
};
|
|
3033
|
-
/**
|
|
3034
|
-
* Granularity collapse (T1 slice 4b): for low-complexity work, fold the suffix
|
|
3035
|
-
* [nextPhase .. end of its group] into ONE round-trip producing several
|
|
3036
|
-
* artifacts, instead of one gated step per phase. Reads the POST-escalation
|
|
3037
|
-
* riskSignal (the escalate-on-evidence intercept may have already raised the
|
|
3038
|
-
* tier), so the dial is never frozen at run start — `fine` for medium/high keeps
|
|
3039
|
-
* full per-phase isolation. Only collapses a genuine multi-phase suffix, so a
|
|
3040
|
-
* lone trailing member falls through to its ordinary per-phase step.
|
|
3041
|
-
*/
|
|
3042
|
-
const collapsedRoundTripGate = (ctx) => {
|
|
3043
|
-
const phase = ctx.nextPhase;
|
|
3044
|
-
if (phase === null ||
|
|
3045
|
-
roundTripGranularityForTier(ctx.riskSignal?.tier) !== "collapsed") {
|
|
3046
|
-
return null;
|
|
3047
|
-
}
|
|
3048
|
-
const group = COLLAPSE_GROUPS.find((g) => g.includes(phase));
|
|
3049
|
-
if (!group)
|
|
3050
|
-
return null;
|
|
3051
|
-
const suffix = group.slice(group.indexOf(phase));
|
|
3052
|
-
if (suffix.length <= 1)
|
|
3053
|
-
return null;
|
|
3054
|
-
return { via: "collapsed_round_trip", phases: [...suffix] };
|
|
3055
|
-
};
|
|
3056
|
-
/**
|
|
3057
|
-
* Skeleton-scaffolded phases (S3): the tool pre-fills structure/ids from the
|
|
3058
|
-
* derived obligation ledger so the worker fills only the judgment slots.
|
|
3059
|
-
*/
|
|
3060
|
-
const scaffoldedPhaseGate = async (ctx) => {
|
|
3061
|
-
const phase = ctx.nextPhase;
|
|
3062
|
-
if (phase !== "test_validator_plan" && phase !== "implementation_planning") {
|
|
3063
|
-
return null;
|
|
3064
|
-
}
|
|
3065
|
-
return {
|
|
3066
|
-
via: "phase",
|
|
3067
|
-
phase,
|
|
3068
|
-
extraSection: await buildScaffoldSection(phase, ctx.artifactsDir),
|
|
3069
|
-
};
|
|
3070
|
-
};
|
|
3071
|
-
/**
|
|
3072
|
-
* The fallback: the ordinary per-phase step. Diff-based re-review (B2) rides it
|
|
3073
|
-
* — when a verdict-bearing review phase is re-emitted because an upstream
|
|
3074
|
-
* changed, the worker gets its prior verdict plus the precise
|
|
3075
|
-
* changed-since-last-review delta, so it re-affirms cheaply or revises only the
|
|
3076
|
-
* affected items rather than running blind.
|
|
3077
|
-
*
|
|
3078
|
-
* Reached only when every gate declined, which by construction means
|
|
3079
|
-
* `nextPhase` is a real phase: the promotion gate above never declines when the
|
|
3080
|
-
* frontier is null.
|
|
3081
|
-
*/
|
|
3082
|
-
const ordinaryPhaseStep = async (ctx) => {
|
|
3083
|
-
const phase = ctx.nextPhase;
|
|
3084
|
-
if (phase === null) {
|
|
3085
|
-
throw new Error("contract pipeline: the gate walk reached the fallback with no next phase — " +
|
|
3086
|
-
"the promotion gate must handle a null frontier.");
|
|
3087
|
-
}
|
|
3088
|
-
return {
|
|
3089
|
-
via: "phase",
|
|
3090
|
-
phase,
|
|
3091
|
-
extraSection: await buildReReviewSection(phase, ctx.artifactsDir),
|
|
3092
|
-
};
|
|
3093
|
-
};
|
|
3094
|
-
/**
|
|
3095
|
-
* THE ORDERED GATE TABLE. Insertion order IS execution order (the walk consumes
|
|
3096
|
-
* the scaffold's derived `handledKeys`), names are unique by construction (a
|
|
3097
|
-
* duplicate object key is a compile error), and no gate can emit a step of its
|
|
3098
|
-
* own — the scaffold owns the single emission site.
|
|
3099
|
-
*/
|
|
3100
|
-
const CONTRACT_PIPELINE_GATES = {
|
|
3101
|
-
seed_source_digest_bound: seedSourceDigestGate,
|
|
3102
|
-
ingested_artifact_invalid: invalidIngestionGate,
|
|
3103
|
-
stale_artifact_archived: staleArchiveGate,
|
|
3104
|
-
phase_frontier_resolved: phaseFrontierGate,
|
|
3105
|
-
goal_id_consistent: goalIdConsistencyGate,
|
|
3106
|
-
finalized_module_set_preserved: finalizedModuleSetGate,
|
|
3107
|
-
work_block_seam_prepared: workBlockSeamGate,
|
|
3108
|
-
conceptual_critique_converged: conceptualCritiqueGate,
|
|
3109
|
-
obligation_ledger_derived: obligationLedgerDerivationGate,
|
|
3110
|
-
degenerate_seam_reconciliation_collapsed: degenerateSeamReconciliationGate,
|
|
3111
|
-
contract_finalization_derived: contractFinalizationDerivationGate,
|
|
3112
|
-
judge_repair_converged: judgeRepairGate,
|
|
3113
|
-
implementation_plan_promoted: implementationPlanPromotionGate,
|
|
3114
|
-
cyclic_seam_resolved: cyclicSeamResolutionGate,
|
|
3115
|
-
cyclic_seam_rechecked: cyclicSeamRecheckGate,
|
|
3116
|
-
pre_critic_structural: preCriticStructuralGate,
|
|
3117
|
-
parallel_module_wave: parallelModuleWaveGate,
|
|
3118
|
-
phase_cut_critique: phaseCutCritiqueGate,
|
|
3119
|
-
collapsed_round_trip: collapsedRoundTripGate,
|
|
3120
|
-
scaffolded_phase: scaffoldedPhaseGate,
|
|
3121
|
-
};
|
|
3122
|
-
/**
|
|
3123
|
-
* Bind the ONE shared step-emission scaffold to an invocation's context.
|
|
3124
|
-
*
|
|
3125
|
-
* Scaffold ADOPTER, never a second scaffold: this consumes `createStepEmissionScaffold` from
|
|
3126
|
-
* `audit-tools/shared` — the same scaffold the audit orchestrator entry point
|
|
3127
|
-
* drives — rather than a second one of this module's own. The pipeline's
|
|
3128
|
-
* numbered early-return-and-re-emit shape is `emitFirstApplicable`, a row shape
|
|
3129
|
-
* in that table, not a fork of it.
|
|
3130
|
-
*/
|
|
3131
|
-
function createContractPipelineEmission(ctx) {
|
|
3132
|
-
return createStepEmissionScaffold({
|
|
3133
|
-
table: CONTRACT_PIPELINE_GATES,
|
|
3134
|
-
fallback: ordinaryPhaseStep,
|
|
3135
|
-
write: (plan) => writeContractStepPlan(ctx, plan),
|
|
3136
|
-
// The pipeline's externally-observable emission is the PERSISTED step
|
|
3137
|
-
// contract, which `write` has just produced; the CLI renders it to the host.
|
|
3138
|
-
// There is deliberately no second stdout announcement here.
|
|
3139
|
-
log: () => { },
|
|
3140
|
-
});
|
|
3141
268
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
* reconstructing one by reflecting over a chain of `if` statements.
|
|
3145
|
-
*
|
|
3146
|
-
* This and the scaffold's `handledKeys` are BOTH `Object.keys` of the SAME
|
|
3147
|
-
* object literal, and the walk consumes `handledKeys` directly — so the two
|
|
3148
|
-
* agree by construction, not by a test that compares them. No such test exists,
|
|
3149
|
-
* and none is needed: there is no second list to drift from.
|
|
3150
|
-
*/
|
|
3151
|
-
export const CONTRACT_PIPELINE_GATE_ORDER = Object.freeze(Object.keys(CONTRACT_PIPELINE_GATES));
|
|
3152
|
-
/**
|
|
3153
|
-
* Build and write the next contract-pipeline step.
|
|
3154
|
-
* Returns null when the pipeline is complete and the extracted plan is ready.
|
|
3155
|
-
*/
|
|
3156
|
-
export async function buildNextContractPipelineStep(options) {
|
|
3157
|
-
const { root, artifactsDir, runId, sourcePaths } = options;
|
|
3158
|
-
// Adversarial-depth dial (T1 slices 3/4): derive the depth for the critique /
|
|
3159
|
-
// critic phases from the intake risk signal, escalating on decomposition
|
|
3160
|
-
// evidence. The (possibly raised) riskSignal is also consumed by the
|
|
3161
|
-
// granularity-collapse gate, so it is carried on the context alongside it.
|
|
3162
|
-
const { riskSignal, adversarialDepth } = await resolveAdversarialDepth(artifactsDir);
|
|
3163
|
-
// Writes and reads are distinct: hosts submit to *.input.json; accepted
|
|
3164
|
-
// upstreams are canonical envelopes whose payload field holds domain data.
|
|
3165
|
-
// Collapsed ordinary authoring sections may read staged raw outputs earlier
|
|
3166
|
-
// in that same round trip, before they have been ingested.
|
|
3167
|
-
const artifactPaths = {};
|
|
3168
|
-
const artifactReadPaths = {};
|
|
3169
|
-
for (const name of CP_ARTIFACT_NAMES) {
|
|
3170
|
-
artifactPaths[name] = contractInputFilePath(artifactsDir, name);
|
|
3171
|
-
artifactReadPaths[name] = contractArtifactFilePath(artifactsDir, name);
|
|
3172
|
-
}
|
|
3173
|
-
const seedPath = pathASeedFilePath(artifactsDir);
|
|
3174
|
-
const ctx = {
|
|
3175
|
-
options,
|
|
3176
|
-
root,
|
|
3177
|
-
artifactsDir,
|
|
3178
|
-
runId,
|
|
3179
|
-
sourcePaths,
|
|
3180
|
-
paths: intakePaths(artifactsDir),
|
|
3181
|
-
artifactPaths,
|
|
3182
|
-
artifactReadPaths,
|
|
3183
|
-
// Present only for structured_audit runs.
|
|
3184
|
-
pathASeedPath: existsSync(seedPath) ? seedPath : undefined,
|
|
3185
|
-
riskSignal,
|
|
3186
|
-
adversarialDepth,
|
|
3187
|
-
artifactsSettled: false,
|
|
3188
|
-
nextPhase: null,
|
|
3189
|
-
};
|
|
3190
|
-
const emission = createContractPipelineEmission(ctx);
|
|
3191
|
-
return await emission.emitFirstApplicable([...emission.handledKeys], ctx);
|
|
3192
|
-
}
|
|
3193
|
-
/**
|
|
3194
|
-
* Build the diff-based re-review section for a review phase being re-emitted after
|
|
3195
|
-
* staleness, or undefined when this is not a re-review (non-review phase, or no
|
|
3196
|
-
* prior snapshot). See `reviewSnapshot.ts`.
|
|
3197
|
-
*/
|
|
3198
|
-
async function buildReReviewSection(phase, artifactsDir) {
|
|
3199
|
-
const artifact = PHASE_TO_ARTIFACT[phase];
|
|
3200
|
-
if (!artifact || !isReviewArtifact(artifact))
|
|
3201
|
-
return undefined;
|
|
3202
|
-
if (!reviewSnapshotExists(artifactsDir, artifact))
|
|
3203
|
-
return undefined;
|
|
3204
|
-
const snapshot = await readReviewSnapshot(artifactsDir, artifact);
|
|
3205
|
-
if (!snapshot)
|
|
3206
|
-
return undefined;
|
|
3207
|
-
const delta = await computeReReviewDelta(artifactsDir, artifact, snapshot);
|
|
3208
|
-
return renderReReviewSection(artifact, snapshot, delta);
|
|
3209
|
-
}
|
|
3210
|
-
// ── DAG → extracted plan conversion ──────────────────────────────────────────
|
|
3211
|
-
// ── Obligation-kind → lens/severity mappings ──────────────────────────────────
|
|
3212
|
-
// <!-- comment-symbol-exempt: names deliberately-retired symbols; this block records that history -->
|
|
3213
|
-
/**
|
|
3214
|
-
* The obligation-kind vocabulary, in priority order (higher index = higher
|
|
3215
|
-
* priority; `invariant` is highest).
|
|
3216
|
-
*
|
|
3217
|
-
* MNT-114e4941-3: this used to be a THIRD independent copy of the vocabulary —
|
|
3218
|
-
* a local `type ObligationKind` union beside derive.ts's `TESTABLE_KINDS` and
|
|
3219
|
-
* contractPipelineGates.ts's `TESTABLE_OBLIGATION_KINDS`, with nothing forcing
|
|
3220
|
-
* the three to agree, while the ledger's own `obligation.kind` is typed as a
|
|
3221
|
-
* bare `string`. The consequence was not theoretical: an unrecognized kind was
|
|
3222
|
-
* CAST to this union, scored -1 by `indexOf`, and then indexed the lens map to
|
|
3223
|
-
* `undefined` — so a ledger kind outside these four promoted a finding with
|
|
3224
|
-
* `lens: undefined`.
|
|
3225
|
-
*
|
|
3226
|
-
* Membership and priority are now derived from the single definition list in
|
|
3227
|
-
* `contractPipeline/obligationKinds.ts`. An unrecognized kind is not dropped or
|
|
3228
|
-
* cast: it is routed through the shared `isTestablePhaseObligation` predicate.
|
|
3229
|
-
*/
|
|
3230
|
-
export { OBLIGATION_KIND_PRIORITY };
|
|
3231
|
-
const OBLIGATION_KIND_SET = new Set(OBLIGATION_KIND_PRIORITY);
|
|
3232
|
-
/**
|
|
3233
|
-
* Classify a raw ledger `kind` string (which the ledger types as a bare
|
|
3234
|
-
* `string`) into this module's vocabulary. A recognized kind maps to itself; an
|
|
3235
|
-
* unrecognized one is classified by the SHARED testability predicate rather
|
|
3236
|
-
* than guessed here — testable ⇒ `behavioral` (the testable default, so it
|
|
3237
|
-
* carries a real lens and a mid severity), otherwise ⇒ `structural`.
|
|
3238
|
-
*/
|
|
3239
|
-
export function classifyObligationKind(kind) {
|
|
3240
|
-
if (OBLIGATION_KIND_SET.has(kind))
|
|
3241
|
-
return kind;
|
|
3242
|
-
return isTestablePhaseObligation(kind) ? "behavioral" : "structural";
|
|
3243
|
-
}
|
|
3244
|
-
function deriveObligationLensAndSeverity(kinds) {
|
|
3245
|
-
if (kinds.length === 0) {
|
|
3246
|
-
return { lens: "correctness", severity: "medium" };
|
|
3247
|
-
}
|
|
3248
|
-
// Pick the highest-priority kind.
|
|
3249
|
-
let topKind = kinds[0];
|
|
3250
|
-
for (const kind of kinds) {
|
|
3251
|
-
if (OBLIGATION_KIND_PRIORITY.indexOf(kind) >
|
|
3252
|
-
OBLIGATION_KIND_PRIORITY.indexOf(topKind)) {
|
|
3253
|
-
topKind = kind;
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
|
-
const lensMap = {
|
|
3257
|
-
invariant: "security",
|
|
3258
|
-
behavioral: "correctness",
|
|
3259
|
-
structural: "architecture",
|
|
3260
|
-
test: "tests",
|
|
3261
|
-
};
|
|
3262
|
-
const severityMap = {
|
|
3263
|
-
invariant: "high",
|
|
3264
|
-
behavioral: "medium",
|
|
3265
|
-
structural: "low",
|
|
3266
|
-
test: "low",
|
|
3267
|
-
};
|
|
3268
|
-
return { lens: lensMap[topKind], severity: severityMap[topKind] };
|
|
3269
|
-
}
|
|
3270
|
-
// ── Normalized block write scope + declared command shape ─────────────────────────────────────
|
|
3271
|
-
//
|
|
3272
|
-
// `touched_files` is the PROMPT-BOUND WRITE SCOPE the host-handoff substrate
|
|
3273
|
-
// enforces against the landed diff, and `targeted_commands` are executed
|
|
3274
|
-
// verbatim through a shell in the repository root. That consumer can check the
|
|
3275
|
-
// SHAPE of what it is handed; it can never check whether the shape is CORRECT
|
|
3276
|
-
// for this repository. So the producer normalizes here: an absolute or
|
|
3277
|
-
// separator-inconsistent path becomes one canonical repo-relative form, a path
|
|
3278
|
-
// that escapes the repository is refused outright, and a command carrying shell
|
|
3279
|
-
// chaining or substitution is refused rather than handed to a shell.
|
|
3280
|
-
/**
|
|
3281
|
-
* The tracked-path corpus for write-scope checking, or null when the tree
|
|
3282
|
-
* cannot be read. Null degrades to "shape-only normalization" exactly as the
|
|
3283
|
-
* M-B3 citation gate degrades on an unreadable tree — a fixture directory or a
|
|
3284
|
-
* fresh checkout must not be bricked, only an unsound path in a REAL tree is
|
|
3285
|
-
* refused.
|
|
3286
|
-
*/
|
|
3287
|
-
async function readTrackedWriteScopeCorpus(root) {
|
|
3288
|
-
if (!(await isInsideGitWorkTree(root)))
|
|
3289
|
-
return null;
|
|
3290
|
-
const files = await enumerateRepoTreePaths(root);
|
|
3291
|
-
if (files.size === 0)
|
|
3292
|
-
return null;
|
|
3293
|
-
const directories = new Set();
|
|
269
|
+
export function normalizeBlockTouchedFiles(root, files, id) {
|
|
270
|
+
const normalized = new Set(), refusals = [];
|
|
3294
271
|
for (const path of files) {
|
|
3295
|
-
const segments = path.split("/");
|
|
3296
|
-
for (let i = 1; i < segments.length; i += 1) {
|
|
3297
|
-
directories.add(segments.slice(0, i).join("/"));
|
|
3298
|
-
}
|
|
3299
|
-
}
|
|
3300
|
-
return { files, directories };
|
|
3301
|
-
}
|
|
3302
|
-
export function normalizeBlockTouchedFiles(root, files, blockId) {
|
|
3303
|
-
const normalized = new Set();
|
|
3304
|
-
const refusals = [];
|
|
3305
|
-
for (const raw of files) {
|
|
3306
|
-
const candidate = typeof raw === "string" ? raw.trim() : "";
|
|
3307
|
-
if (candidate.length === 0) {
|
|
3308
|
-
refusals.push(`Block "${blockId}" declares an empty touched_files entry.`);
|
|
3309
|
-
continue;
|
|
3310
|
-
}
|
|
3311
|
-
const directoryIntent = /[\\/]$/u.test(candidate);
|
|
3312
|
-
const portableCandidate = toPosixPath(candidate);
|
|
3313
|
-
const absolute = isAbsolute(portableCandidate)
|
|
3314
|
-
? portableCandidate
|
|
3315
|
-
: resolve(root, portableCandidate);
|
|
3316
272
|
try {
|
|
3317
|
-
const
|
|
3318
|
-
normalized.add(
|
|
273
|
+
const relative = repoRelativePath(root, isAbsolute(toPosixPath(path)) ? toPosixPath(path) : resolve(root, toPosixPath(path)), `Unit ${id} write scope`);
|
|
274
|
+
normalized.add(/[\\/]$/u.test(path) ? `${relative}/` : relative);
|
|
3319
275
|
}
|
|
3320
276
|
catch {
|
|
3321
|
-
refusals.push(`
|
|
3322
|
-
`does not resolve to a path beneath the repository root. A POSIX-absolute form ` +
|
|
3323
|
-
`("/src/x.ts") is read as absolute, not repo-relative — drop the leading slash. The ` +
|
|
3324
|
-
`write scope is re-checked against the landed diff, so it may only name paths ` +
|
|
3325
|
-
`beneath ${root}.`);
|
|
277
|
+
refusals.push(`Unit ${id} write scope ${path} must remain beneath ${root}.`);
|
|
3326
278
|
}
|
|
3327
279
|
}
|
|
3328
|
-
|
|
3329
|
-
// plan's content hash on every re-promotion.
|
|
3330
|
-
return {
|
|
3331
|
-
touched_files: [...normalized].sort((left, right) => compareCodeUnits(left, right)),
|
|
3332
|
-
refusals,
|
|
3333
|
-
};
|
|
280
|
+
return { touched_files: [...normalized].sort(), refusals };
|
|
3334
281
|
}
|
|
3335
|
-
/**
|
|
3336
|
-
* The tracked-tree half of The normalized-write-scope invariant, run against
|
|
3337
|
-
* the PROMOTED plan so a violation takes the same bounded re-emit path the M-B3
|
|
3338
|
-
* citation gate takes, rather than throwing out of the promotion.
|
|
3339
|
-
*
|
|
3340
|
-
* It exists because the citation gate is NOT a superset: a finding grounds if
|
|
3341
|
-
* ANY cited path OR SYMBOL is real, so a node whose prose names a real symbol
|
|
3342
|
-
* can ground while its declared write scope is still fabricated — and the write
|
|
3343
|
-
* scope is what a host binds a worker to.
|
|
3344
|
-
*
|
|
3345
|
-
* A path that is not tracked but whose parent directory IS stays legal: a
|
|
3346
|
-
* remediation block legitimately creates new files, and dropping a declared
|
|
3347
|
-
* write target is the failure mode that strands an implementer with an
|
|
3348
|
-
* obligation it has no scope to discharge. Fail-open on an unreadable tree, as
|
|
3349
|
-
* the citation gate does.
|
|
3350
|
-
*/
|
|
3351
|
-
export async function evaluatePromotedPlanWriteScope(artifactsDir, root) {
|
|
3352
|
-
const corpus = await readTrackedWriteScopeCorpus(root);
|
|
3353
|
-
if (!corpus)
|
|
3354
|
-
return null;
|
|
3355
|
-
const plan = await readOptionalJsonFile(intakePaths(artifactsDir).extractedPlan);
|
|
3356
|
-
const violations = [];
|
|
3357
|
-
for (const block of Array.isArray(plan?.blocks) ? plan.blocks : []) {
|
|
3358
|
-
const blockId = typeof block.block_id === "string" ? block.block_id : "(unnamed block)";
|
|
3359
|
-
const touched = Array.isArray(block.touched_files) ? block.touched_files : [];
|
|
3360
|
-
violations.push(...writeScopeCorpusViolations(corpus, touched.filter((path) => typeof path === "string"), `Block "${blockId}"`));
|
|
3361
|
-
}
|
|
3362
|
-
return violations.length > 0 ? { violations } : null;
|
|
3363
|
-
}
|
|
3364
|
-
/**
|
|
3365
|
-
* The ONE tracked-tree membership rule for a declared write-scope path: legal
|
|
3366
|
-
* when the file is tracked, sits at the repo root, or its parent directory
|
|
3367
|
-
* exists in the tracked tree (a block legitimately creates NEW files in
|
|
3368
|
-
* existing directories). Shared by the promotion gate above and the
|
|
3369
|
-
* clarification scope-delta validation, so the two cannot drift.
|
|
3370
|
-
*/
|
|
3371
|
-
function writeScopeCorpusViolations(corpus, paths, label) {
|
|
3372
|
-
const violations = [];
|
|
3373
|
-
for (const path of paths) {
|
|
3374
|
-
const key = normalizeRepoPath(path);
|
|
3375
|
-
const parent = key.includes("/") ? key.slice(0, key.lastIndexOf("/")) : "";
|
|
3376
|
-
if (corpus.files.has(key) || parent === "" || corpus.directories.has(parent)) {
|
|
3377
|
-
continue;
|
|
3378
|
-
}
|
|
3379
|
-
violations.push(`${label} declares the write-scope path "${path}", whose directory does ` +
|
|
3380
|
-
`not exist in the tracked tree.`);
|
|
3381
|
-
}
|
|
3382
|
-
return violations;
|
|
3383
|
-
}
|
|
3384
|
-
/**
|
|
3385
|
-
* Tracked-tree parity for a POST-promotion write-scope widening
|
|
3386
|
-
* (open-bugs.md:110): a clarification scope delta must clear the same rule the
|
|
3387
|
-
* promotion gate enforced, or the delta lane becomes a bypass of it. Returns
|
|
3388
|
-
* violation lines; [] on an unreadable tree (fail-open, exactly as the
|
|
3389
|
-
* promotion gate degrades).
|
|
3390
|
-
*/
|
|
3391
282
|
export async function checkWriteScopePathsAgainstTrackedTree(root, paths, label) {
|
|
3392
|
-
const
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
// invocations, in order", which is what two entries mean, so it is split here
|
|
3400
|
-
// rather than spending a whole DAG regeneration on it (open-bugs: one dispatch
|
|
3401
|
-
// emitted `npm run build && npm run check` on 23 nodes and the DAG was
|
|
3402
|
-
// regenerated twice for a defect with a mechanical answer). Everything the
|
|
3403
|
-
// split cannot faithfully restate — a pipe, a redirect, `;`, substitution, an
|
|
3404
|
-
// inadmissible half — still takes the bounded re-emit below.
|
|
3405
|
-
const partitioned = partitionCommandsByDeclaredShape(commands, (kind, raw) => kind === "empty"
|
|
3406
|
-
? `Block "${blockId}" declares an empty targeted_commands entry.`
|
|
3407
|
-
: `Block "${blockId}" declares the targeted_commands entry ${JSON.stringify(raw)}, ` +
|
|
3408
|
-
`which carries shell chaining, substitution or redirection. A targeted command is ` +
|
|
3409
|
-
`executed verbatim through a shell, so it must be one invocation — split it into ` +
|
|
3410
|
-
`separate entries.`, splitSequentialCommandChain);
|
|
3411
|
-
return { targeted_commands: partitioned.commands, refusals: partitioned.refusals };
|
|
3412
|
-
}
|
|
3413
|
-
/**
|
|
3414
|
-
* Collect every write-scope and command refusal the promotion WOULD hit, before
|
|
3415
|
-
* a plan is written. Runs the same two normalizers over the same derived node
|
|
3416
|
-
* scope the promoter uses, so this pre-check and the promotion cannot disagree
|
|
3417
|
-
* about what is refusable — and the refusal reaches the host as the bounded
|
|
3418
|
-
* `implementation_planning` re-emit every other promotion rejection takes,
|
|
3419
|
-
* rather than as a thrown stack that wedges every subsequent next-step.
|
|
3420
|
-
*/
|
|
3421
|
-
export async function collectDagWriteScopeRefusals(artifactsDir, root) {
|
|
3422
|
-
const dag = envelopePayload(await readContractArtifact(artifactsDir, "implementation_dag"));
|
|
3423
|
-
const nodes = Array.isArray(dag?.nodes) ? dag.nodes : [];
|
|
3424
|
-
if (nodes.length === 0)
|
|
3425
|
-
return [];
|
|
3426
|
-
const { resolve: deriveNodeFiles } = await buildNodeWriteScopeResolver(artifactsDir);
|
|
3427
|
-
const refusals = [];
|
|
3428
|
-
for (const [index, node] of nodes.entries()) {
|
|
3429
|
-
const blockId = toBlockId(ensureNodeId(node.id, index));
|
|
3430
|
-
refusals.push(...normalizeBlockTouchedFiles(root, deriveNodeFiles(node), blockId).refusals, ...normalizeBlockTargetedCommands(node.targeted_commands ?? [], blockId).refusals);
|
|
3431
|
-
}
|
|
3432
|
-
return refusals;
|
|
3433
|
-
}
|
|
3434
|
-
/**
|
|
3435
|
-
* The ONE Path-A canonical-group membership evaluator, in ONE body.
|
|
3436
|
-
*
|
|
3437
|
-
* The promoter and the pre-promotion gate ask the byte-identical question — is
|
|
3438
|
-
* each DAG node's `source_finding_ids` declaration a canonical audit work block,
|
|
3439
|
-
* exactly once each? — and they used to answer it with two hand-mirrored copies
|
|
3440
|
-
* of the same forty lines: one returning refusal lines, one throwing. Two copies
|
|
3441
|
-
* of an identity rule drift, and a drift between THESE two is the worst kind:
|
|
3442
|
-
* the gate would certify a DAG promotable and the promoter would then throw on
|
|
3443
|
-
* it, or (worse) the gate would refuse a DAG the promoter would have accepted,
|
|
3444
|
-
* wedging a run behind a rule nothing in the emitter can satisfy.
|
|
3445
|
-
*
|
|
3446
|
-
* So the body is `ok`/`refusals`, plus the node→canonical-group map the promoter
|
|
3447
|
-
* needs for its projection, and each caller renders what it needs from the SAME
|
|
3448
|
-
* result. `refusals` is empty exactly when the DAG is promotable on this axis;
|
|
3449
|
-
* `byNodeId` is populated exactly on that path.
|
|
3450
|
-
*
|
|
3451
|
-
* The caller supplies `nodes` and `approvedSource` because they read them for
|
|
3452
|
-
* their own purposes too (the gate must not read the DAG twice; the promoter
|
|
3453
|
-
* filters the canonical group map through its own sort).
|
|
3454
|
-
*/
|
|
3455
|
-
function evaluatePathACanonicalGroups(params) {
|
|
3456
|
-
const { nodes, approvedSource, seedPresent } = params;
|
|
3457
|
-
const byNodeId = new Map();
|
|
3458
|
-
if (nodes.length === 0)
|
|
3459
|
-
return { refusals: [], byNodeId };
|
|
3460
|
-
if (!nodes.some((node) => Array.isArray(node.source_finding_ids))) {
|
|
3461
|
-
return { refusals: [], byNodeId };
|
|
3462
|
-
}
|
|
3463
|
-
if (!approvedSource) {
|
|
3464
|
-
return {
|
|
3465
|
-
refusals: [
|
|
3466
|
-
"implementation_dag declares source_finding_ids but no Path-A seed is present, so the ids cannot be joined to a canonical audit work block.",
|
|
3467
|
-
],
|
|
3468
|
-
byNodeId,
|
|
3469
|
-
};
|
|
3470
|
-
}
|
|
3471
|
-
if (!seedPresent) {
|
|
3472
|
-
return {
|
|
3473
|
-
refusals: [
|
|
3474
|
-
"implementation_dag declares source_finding_ids but no Path-A seed is present, so the ids cannot be joined to a canonical audit work block.",
|
|
3475
|
-
],
|
|
3476
|
-
byNodeId,
|
|
3477
|
-
};
|
|
3478
|
-
}
|
|
3479
|
-
const signature = (ids) => JSON.stringify([...ids].sort((left, right) => compareCodeUnits(left, right)));
|
|
3480
|
-
const canonicalGroups = new Map(approvedSource.workBlocks.map((block) => [
|
|
3481
|
-
signature(block.finding_ids),
|
|
3482
|
-
[...block.finding_ids].sort((left, right) => compareCodeUnits(left, right)),
|
|
3483
|
-
]));
|
|
3484
|
-
const usedGroups = new Set();
|
|
3485
|
-
const refusals = [];
|
|
3486
|
-
for (const [index, node] of nodes.entries()) {
|
|
3487
|
-
const nodeId = ensureNodeId(node.id, index);
|
|
3488
|
-
const sourceIds = node.source_finding_ids;
|
|
3489
|
-
if (!Array.isArray(sourceIds) || sourceIds.length === 0) {
|
|
3490
|
-
refusals.push(`implementation_dag node "${nodeId}" must declare source_finding_ids for Path-A promotion.`);
|
|
3491
|
-
continue;
|
|
3492
|
-
}
|
|
3493
|
-
const uniqueIds = [...new Set(sourceIds)];
|
|
3494
|
-
if (uniqueIds.length !== sourceIds.length) {
|
|
3495
|
-
refusals.push(`implementation_dag node "${nodeId}" repeats a source_finding_ids member.`);
|
|
3496
|
-
}
|
|
3497
|
-
const groupSignature = signature(uniqueIds);
|
|
3498
|
-
const canonicalGroup = canonicalGroups.get(groupSignature);
|
|
3499
|
-
if (!canonicalGroup) {
|
|
3500
|
-
refusals.push(`implementation_dag node "${nodeId}" source_finding_ids do not match a canonical audit work block.`);
|
|
3501
|
-
continue;
|
|
3502
|
-
}
|
|
3503
|
-
if (usedGroups.has(groupSignature)) {
|
|
3504
|
-
refusals.push(`implementation_dag node "${nodeId}" duplicates a canonical audit work block.`);
|
|
3505
|
-
}
|
|
3506
|
-
usedGroups.add(groupSignature);
|
|
3507
|
-
byNodeId.set(nodeId, canonicalGroup);
|
|
3508
|
-
}
|
|
3509
|
-
if (refusals.length === 0 && usedGroups.size !== canonicalGroups.size) {
|
|
3510
|
-
refusals.push("implementation_dag source_finding_ids do not cover every canonical audit work block exactly once.");
|
|
3511
|
-
}
|
|
3512
|
-
return { refusals, byNodeId };
|
|
3513
|
-
}
|
|
3514
|
-
/**
|
|
3515
|
-
* Path-A canonical-block membership validation, run BEFORE anything is promoted
|
|
3516
|
-
* (OBL-seam-prep-remediate-core-inv-2 / COR-114e4941). These are exactly the
|
|
3517
|
-
* checks the promoter itself performs while building its node→canonical-group
|
|
3518
|
-
* map — but there they THROW out of `promoteImplementationDagToExtractedPlan`,
|
|
3519
|
-
* an unclassified stack that wedged every subsequent next-step. Hoisted here so
|
|
3520
|
-
* an invalid `source_finding_ids` declaration takes the same bounded re-emit as
|
|
3521
|
-
* every other promotion rejection, with no gate having executed past it.
|
|
3522
|
-
*
|
|
3523
|
-
* The checks themselves are {@link evaluatePathACanonicalGroups} — the ONE body
|
|
3524
|
-
* both this gate and the promoter call, so the gate can never certify a DAG the
|
|
3525
|
-
* promoter then refuses.
|
|
3526
|
-
*
|
|
3527
|
-
* Returns one line per violation; empty means the DAG is promotable on this
|
|
3528
|
-
* axis (or Path A is not in play at all).
|
|
3529
|
-
*/
|
|
3530
|
-
export async function collectPathARefusals(artifactsDir) {
|
|
3531
|
-
const dag = envelopePayload(await readContractArtifact(artifactsDir, "implementation_dag"));
|
|
3532
|
-
const nodes = Array.isArray(dag?.nodes) ? dag.nodes : [];
|
|
3533
|
-
if (nodes.length === 0)
|
|
3534
|
-
return [];
|
|
3535
|
-
if (!nodes.some((node) => Array.isArray(node.source_finding_ids)))
|
|
3536
|
-
return [];
|
|
3537
|
-
const pathASeed = await readOptionalJsonFile(pathASeedFilePath(artifactsDir));
|
|
3538
|
-
const approvedSource = pathASeed
|
|
3539
|
-
? projectApprovedFindings(await readOptionalJsonFile(pathASeed.audit_findings_path))
|
|
3540
|
-
: undefined;
|
|
3541
|
-
return evaluatePathACanonicalGroups({
|
|
3542
|
-
nodes,
|
|
3543
|
-
approvedSource,
|
|
3544
|
-
seedPresent: pathASeed !== undefined,
|
|
3545
|
-
}).refusals;
|
|
3546
|
-
}
|
|
3547
|
-
/**
|
|
3548
|
-
* What the AUDIT read, for the run whose Path-A seed lives in `artifactsDir` —
|
|
3549
|
-
* the value plan application stamps onto `state.plan.audit_read`.
|
|
3550
|
-
*
|
|
3551
|
-
* Read by the TOOL from the seed's own source report, which passed the strict
|
|
3552
|
-
* shared validator (`projectApprovedFindings`) before it may answer. It never
|
|
3553
|
-
* rides `extracted-plan.json`: that file is host-writable, and the close
|
|
3554
|
-
* phase's evidence leg turns this commit into terminal dispositions, so a
|
|
3555
|
-
* host-supplied value would let a host author its own `refuted`.
|
|
3556
|
-
*
|
|
3557
|
-
* BOUND TO THE SEED'S OWN DIGEST. The seed recorded a sha256 of the source
|
|
3558
|
-
* report when it was built (`source_digests`); the bytes read here must hash to
|
|
3559
|
-
* it, so a report swapped afterwards — even for another VALID report — answers
|
|
3560
|
-
* `null`. The file is read ONCE and the same bytes are hashed and parsed. A seed
|
|
3561
|
-
* that carries no digest for its source binds nothing, and an unbound commit is
|
|
3562
|
-
* not one this function will vouch for.
|
|
3563
|
-
*
|
|
3564
|
-
* `null` — "no commit is known" — when there is no seed (the run did not start
|
|
3565
|
-
* from a findings report), the source is unbound, changed, unreadable or
|
|
3566
|
-
* invalid, or the report itself states `null`.
|
|
3567
|
-
*/
|
|
3568
|
-
export async function readSeedAuditRead(artifactsDir) {
|
|
3569
|
-
const pathASeed = await readOptionalJsonFile(pathASeedFilePath(artifactsDir));
|
|
3570
|
-
if (!pathASeed)
|
|
3571
|
-
return null;
|
|
3572
|
-
const bound = (pathASeed.source_digests ?? []).find((entry) => entry?.path === pathASeed.audit_findings_path);
|
|
3573
|
-
if (typeof bound?.sha256 !== "string")
|
|
3574
|
-
return null;
|
|
3575
|
-
let source;
|
|
3576
|
-
try {
|
|
3577
|
-
const bytes = await readFile(pathASeed.audit_findings_path);
|
|
3578
|
-
if (hashContent(bytes) !== bound.sha256)
|
|
3579
|
-
return null;
|
|
3580
|
-
source = JSON.parse(bytes.toString("utf8"));
|
|
3581
|
-
}
|
|
3582
|
-
catch {
|
|
3583
|
-
return null;
|
|
3584
|
-
}
|
|
3585
|
-
try {
|
|
3586
|
-
projectApprovedFindings(source);
|
|
3587
|
-
}
|
|
3588
|
-
catch {
|
|
3589
|
-
return null;
|
|
3590
|
-
}
|
|
3591
|
-
return auditReadOf(source);
|
|
3592
|
-
}
|
|
3593
|
-
/**
|
|
3594
|
-
* Convert a completed ImplementationDAG into the extracted-plan.json format
|
|
3595
|
-
* that the existing handlePendingExtractedPlan/applyPlanPipeline path consumes.
|
|
3596
|
-
*
|
|
3597
|
-
* `root` defaults to the repository that owns `artifactsDir`, so the existing
|
|
3598
|
-
* one-argument callers keep working while the pipeline passes the run's real
|
|
3599
|
-
* root for write-scope normalization.
|
|
3600
|
-
*/
|
|
3601
|
-
export async function promoteImplementationDagToExtractedPlan(artifactsDir, root = climbOutOfAuditTools(artifactsDir)) {
|
|
3602
|
-
const paths = intakePaths(artifactsDir);
|
|
3603
|
-
const dagEnvelope = await readContractArtifact(artifactsDir, "implementation_dag");
|
|
3604
|
-
if (!dagEnvelope)
|
|
3605
|
-
return;
|
|
3606
|
-
const dag = envelopePayload(dagEnvelope);
|
|
3607
|
-
const pathASeed = await readOptionalJsonFile(pathASeedFilePath(artifactsDir));
|
|
3608
|
-
const approvedSource = pathASeed
|
|
3609
|
-
? projectApprovedFindings(await readOptionalJsonFile(pathASeed.audit_findings_path))
|
|
3610
|
-
: undefined;
|
|
3611
|
-
// Load obligation_ledger for lens/severity derivation (graceful: may be absent).
|
|
3612
|
-
const ledgerPayload = envelopePayload(await readContractArtifact(artifactsDir, "obligation_ledger"));
|
|
3613
|
-
const obligationMap = new Map();
|
|
3614
|
-
if (ledgerPayload?.obligations) {
|
|
3615
|
-
for (const obl of ledgerPayload.obligations) {
|
|
3616
|
-
// Classified, never cast: an unrecognized kind used to index the lens map
|
|
3617
|
-
// to `undefined` and promote a lens-less finding (MNT-114e4941-3).
|
|
3618
|
-
obligationMap.set(obl.id, classifyObligationKind(String(obl.kind ?? "")));
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3621
|
-
// Auto-phasing (T3): read the persisted phase cut and re-key its module-phase
|
|
3622
|
-
// map by `moduleSlug(name)` — the fragment the obligation ledger encodes into
|
|
3623
|
-
// `OBL-<slug>-…` ids. The block phase ordinal is then derived MECHANICALLY from
|
|
3624
|
-
// each node's obligations (never trusting a worker-carried field, which a node
|
|
3625
|
-
// merge could drop), so a foundation block always sorts below the consumers that
|
|
3626
|
-
// depend on it. Absent cut (single module / no finalized contracts) → no
|
|
3627
|
-
// ordinals, i.e. one phase, no barrier.
|
|
3628
|
-
const phaseCut = await readPhaseCutArtifact(artifactsDir);
|
|
3629
|
-
const slugToOrdinal = new Map();
|
|
3630
|
-
if (phaseCut) {
|
|
3631
|
-
for (const [name, ordinal] of Object.entries(phaseCut.module_phase)) {
|
|
3632
|
-
slugToOrdinal.set(moduleSlug(name), ordinal);
|
|
3633
|
-
}
|
|
3634
|
-
}
|
|
3635
|
-
const lastOrdinal = Math.max(0, ...slugToOrdinal.values());
|
|
3636
|
-
const hasMultiPhase = phaseCut ? phaseCut.phases.length > 1 : false;
|
|
3637
|
-
// Approved-contract attachment (open-bugs.md:474): resolve each node's
|
|
3638
|
-
// obligation-id slugs against the finalized module contracts, so every
|
|
3639
|
-
// promoted block carries VERBATIM the contract(s) it implements and the
|
|
3640
|
-
// dispatch prompt can bind the worker to the approved interface — the
|
|
3641
|
-
// workflow must never depend on the DAG author restating declared values.
|
|
3642
|
-
const finalizedForBlocks = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
3643
|
-
const contractByModuleName = new Map();
|
|
3644
|
-
const contractSlugToName = new Map();
|
|
3645
|
-
for (const mod of finalizedForBlocks?.module_contracts ?? []) {
|
|
3646
|
-
if (isRecord(mod) && typeof mod.name === "string" && mod.name.length > 0) {
|
|
3647
|
-
contractByModuleName.set(mod.name, mod);
|
|
3648
|
-
contractSlugToName.set(moduleSlug(mod.name), mod.name);
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
const contractSlugs = new Set(contractSlugToName.keys());
|
|
3652
|
-
const moduleContractsForNode = (node) => {
|
|
3653
|
-
const names = new Set();
|
|
3654
|
-
for (const obligationId of [
|
|
3655
|
-
...(node.satisfies_obligations ?? []),
|
|
3656
|
-
...(node.verification_obligation_ids ?? []),
|
|
3657
|
-
]) {
|
|
3658
|
-
const slug = moduleSlugForObligationId(obligationId, contractSlugs);
|
|
3659
|
-
const name = slug === null ? undefined : contractSlugToName.get(slug);
|
|
3660
|
-
if (name !== undefined)
|
|
3661
|
-
names.add(name);
|
|
3662
|
-
}
|
|
3663
|
-
return [...names]
|
|
3664
|
-
.sort((left, right) => compareCodeUnits(left, right))
|
|
3665
|
-
.map((name) => ({ module: name, contract: contractByModuleName.get(name) }));
|
|
3666
|
-
};
|
|
3667
|
-
// Root-cause fix for scope-less nodes: the DAG's write scope
|
|
3668
|
-
// (`output_files`/`files_likely_touched`) is host-authored and a coarse
|
|
3669
|
-
// "Remediate <module>" decomposition can leave it EMPTY, which promotes a
|
|
3670
|
-
// finding with empty affected_files AND a block with empty touched_files — an
|
|
3671
|
-
// undispatchable node (no worktree seed, no write scope, no paths for a
|
|
3672
|
-
// single-shot worker to inline) that silently dooms the whole run and
|
|
3673
|
-
// cascade-blocks its dependents. Derive the write scope DETERMINISTICALLY from
|
|
3674
|
-
// the module decomposition instead of trusting the host to have filled it: each
|
|
3675
|
-
// node's obligations are `OBL-<moduleSlug>-…`, and every module declares its
|
|
3676
|
-
// `file_scope`, so a node that declared no files inherits the file_scope of the
|
|
3677
|
-
// module(s) its obligations belong to. A node that DID declare files still gains
|
|
3678
|
-
// those modules' finalized-contract write targets (P38) — the scope is a UNION,
|
|
3679
|
-
// not a precedence. Single-sourced with the DAG validator, which refuses a node
|
|
3680
|
-
// this resolves to nothing for — see buildNodeWriteScopeResolver.
|
|
3681
|
-
const { resolve: deriveNodeFiles } = await buildNodeWriteScopeResolver(artifactsDir);
|
|
3682
|
-
const nodes = (Array.isArray(dag?.nodes) ? [...dag.nodes] : []).sort((left, right) => compareCodeUnits(String(left.id), String(right.id)));
|
|
3683
|
-
// Path-A promotion is an identity-preserving projection. DAG node ids describe
|
|
3684
|
-
// implementation tasks; they may not replace the canonical auditor finding
|
|
3685
|
-
// ids or split/merge the canonical coherence components.
|
|
3686
|
-
const canonicalItemsByNodeId = new Map();
|
|
3687
|
-
if (approvedSource &&
|
|
3688
|
-
nodes.some((node) => Array.isArray(node.source_finding_ids))) {
|
|
3689
|
-
// The SAME evaluator `collectPathARefusals` runs as the pre-promotion gate.
|
|
3690
|
-
// The projection this loop used to build inline was a second copy of that
|
|
3691
|
-
// gate's rule, and the two could disagree about which declarations are
|
|
3692
|
-
// canonical — the gate certifying a DAG this throws on, or the reverse.
|
|
3693
|
-
// This side renders refusals as the THROW the promoter's contract promises.
|
|
3694
|
-
const evaluation = evaluatePathACanonicalGroups({
|
|
3695
|
-
nodes,
|
|
3696
|
-
approvedSource,
|
|
3697
|
-
seedPresent: true,
|
|
3698
|
-
});
|
|
3699
|
-
if (evaluation.refusals.length > 0) {
|
|
3700
|
-
throw new Error(evaluation.refusals.join("\n"));
|
|
3701
|
-
}
|
|
3702
|
-
for (const [nodeId, canonicalGroup] of evaluation.byNodeId) {
|
|
3703
|
-
canonicalItemsByNodeId.set(nodeId, [...canonicalGroup]);
|
|
3704
|
-
}
|
|
3705
|
-
}
|
|
3706
|
-
let findings = nodes.map((node, index) => {
|
|
3707
|
-
const id = ensureNodeId(node.id, index);
|
|
3708
|
-
const contractObligations = [...new Set(node.satisfies_obligations ?? [])];
|
|
3709
|
-
const verificationObligations = [
|
|
3710
|
-
...new Set(node.verification_obligation_ids ?? []),
|
|
3711
|
-
];
|
|
3712
|
-
const addressedCounterexamples = [
|
|
3713
|
-
...new Set(node.addresses_counterexamples ?? []),
|
|
3714
|
-
];
|
|
3715
|
-
const obligationEvidence = [
|
|
3716
|
-
...contractObligations.map((obligationId) => `Satisfies contract obligation: ${obligationId}`),
|
|
3717
|
-
...verificationObligations.map((obligationId) => `Verifies contract obligation: ${obligationId}`),
|
|
3718
|
-
...addressedCounterexamples.map((counterexampleId) => `Addresses accepted counterexample: ${counterexampleId}`),
|
|
3719
|
-
];
|
|
3720
|
-
// Derive lens and severity from obligation kinds; fall back when ledger absent.
|
|
3721
|
-
const satisfiedKinds = contractObligations
|
|
3722
|
-
.map((id) => obligationMap.get(id))
|
|
3723
|
-
.filter((k) => k !== undefined);
|
|
3724
|
-
const { lens, severity } = deriveObligationLensAndSeverity(satisfiedKinds);
|
|
3725
|
-
return {
|
|
3726
|
-
id,
|
|
3727
|
-
title: node.title ?? node.description ?? `Contract-pipeline task ${index + 1}`,
|
|
3728
|
-
category: "General",
|
|
3729
|
-
severity,
|
|
3730
|
-
confidence: "high",
|
|
3731
|
-
lens,
|
|
3732
|
-
summary: node.description ?? node.title ?? "",
|
|
3733
|
-
// output_files (declared write scope) takes priority over files_likely_touched,
|
|
3734
|
-
// unioned with the owning module contract's declared write targets; when the
|
|
3735
|
-
// node declared neither, it inherits the module file_scope (deriveNodeFiles)
|
|
3736
|
-
// so the finding is never scope-less. Map each path to the { path } shape that
|
|
3737
|
-
// Finding.affected_files expects.
|
|
3738
|
-
affected_files: deriveNodeFiles(node).map((p) => ({ path: p })),
|
|
3739
|
-
evidence: obligationEvidence.length > 0
|
|
3740
|
-
? obligationEvidence
|
|
3741
|
-
: [node.description ?? node.title ?? `Contract-pipeline task ${id}`],
|
|
3742
|
-
// The implementation description already lives in summary.
|
|
3743
|
-
contract_goal_id: dag?.goal_id,
|
|
3744
|
-
contract_obligation_ids: contractObligations,
|
|
3745
|
-
verification_obligation_ids: verificationObligations,
|
|
3746
|
-
targeted_commands: node.targeted_commands ?? [],
|
|
3747
|
-
// Distinct node instructions travel on the block, not the finding.
|
|
3748
|
-
};
|
|
3749
|
-
});
|
|
3750
|
-
if (approvedSource && canonicalItemsByNodeId.size > 0) {
|
|
3751
|
-
const nodeByFindingId = new Map();
|
|
3752
|
-
for (const [index, node] of nodes.entries()) {
|
|
3753
|
-
const nodeId = ensureNodeId(node.id, index);
|
|
3754
|
-
for (const findingId of canonicalItemsByNodeId.get(nodeId) ?? []) {
|
|
3755
|
-
nodeByFindingId.set(findingId, node);
|
|
3756
|
-
}
|
|
283
|
+
const issues = [];
|
|
284
|
+
for (const path of paths) {
|
|
285
|
+
try {
|
|
286
|
+
const relative = repoRelativePath(root, path, label);
|
|
287
|
+
const target = resolve(root, relative);
|
|
288
|
+
if (!existsSync(target) && !(await stat(dirname(target))).isDirectory())
|
|
289
|
+
issues.push(`${label}: parent directory for ${path} does not exist.`);
|
|
3757
290
|
}
|
|
3758
|
-
|
|
3759
|
-
.
|
|
3760
|
-
.map((finding) => {
|
|
3761
|
-
const node = nodeByFindingId.get(finding.id);
|
|
3762
|
-
const contractObligations = [...new Set(node.satisfies_obligations ?? [])];
|
|
3763
|
-
const verificationObligations = [
|
|
3764
|
-
...new Set(node.verification_obligation_ids ?? []),
|
|
3765
|
-
];
|
|
3766
|
-
return {
|
|
3767
|
-
...finding,
|
|
3768
|
-
affected_files: [...finding.affected_files].sort((left, right) => compareCodeUnits(left.path, right.path)),
|
|
3769
|
-
contract_goal_id: dag?.goal_id,
|
|
3770
|
-
contract_obligation_ids: contractObligations,
|
|
3771
|
-
verification_obligation_ids: verificationObligations,
|
|
3772
|
-
targeted_commands: [...(node.targeted_commands ?? [])],
|
|
3773
|
-
// The source finding stays intact; its node instructions travel on the block.
|
|
3774
|
-
};
|
|
3775
|
-
});
|
|
3776
|
-
}
|
|
3777
|
-
// finding_id → { obligation_ids, node_ids } trace. Each promoted finding maps
|
|
3778
|
-
// 1:1 to a DAG node, so its node_ids are itself plus every node it depends on
|
|
3779
|
-
// (the upstream nodes whose output it builds on). obligation_ids unions the
|
|
3780
|
-
// satisfied and verification obligations. This is the auditable backward trace
|
|
3781
|
-
// from a remediation finding to the contract obligations it discharges.
|
|
3782
|
-
const nodeIdSet = new Set(nodes.map((n, i) => ensureNodeId(n.id, i)));
|
|
3783
|
-
const traceability = {};
|
|
3784
|
-
for (const [index, node] of nodes.entries()) {
|
|
3785
|
-
const id = ensureNodeId(node.id, index);
|
|
3786
|
-
const obligationIds = [
|
|
3787
|
-
...new Set([
|
|
3788
|
-
...(node.satisfies_obligations ?? []),
|
|
3789
|
-
...(node.verification_obligation_ids ?? []),
|
|
3790
|
-
]),
|
|
3791
|
-
];
|
|
3792
|
-
const dependsOn = (node.depends_on ?? []).filter((dep) => nodeIdSet.has(dep));
|
|
3793
|
-
const nodeIds = [...new Set([id, ...dependsOn])];
|
|
3794
|
-
const findingIds = canonicalItemsByNodeId.get(id) ?? [id];
|
|
3795
|
-
for (const findingId of findingIds) {
|
|
3796
|
-
traceability[findingId] = {
|
|
3797
|
-
obligation_ids: obligationIds,
|
|
3798
|
-
node_ids: nodeIds,
|
|
3799
|
-
};
|
|
291
|
+
catch {
|
|
292
|
+
issues.push(`${label}: invalid/unavailable write path ${path}.`);
|
|
3800
293
|
}
|
|
3801
294
|
}
|
|
3802
|
-
|
|
3803
|
-
const counterexampleEntries = isRecord(counterexamplePayload) && Array.isArray(counterexamplePayload.counterexamples)
|
|
3804
|
-
? counterexamplePayload.counterexamples.map(entry => CounterexampleSchema.parse(entry)) : [];
|
|
3805
|
-
const counterexamplesById = new Map(counterexampleEntries.map(entry => [entry.id, entry]));
|
|
3806
|
-
const blocks = nodes.map((node, index) => {
|
|
3807
|
-
const nodeId = ensureNodeId(node.id, index);
|
|
3808
|
-
const deps = (node.depends_on ?? []).map((depId) => toBlockId(depId));
|
|
3809
|
-
// Same derivation as the finding's affected_files: declared write scope, else
|
|
3810
|
-
// the module file_scope inherited via the node's obligations — so the block's
|
|
3811
|
-
// file-ownership scheduler never sees an empty (undispatchable) touched set.
|
|
3812
|
-
// Normalized before it leaves this producer: the host-handoff substrate binds
|
|
3813
|
-
// this list as the write scope and can validate its shape but never its
|
|
3814
|
-
// correctness.
|
|
3815
|
-
// Refusals are collected, not thrown: `collectDagWriteScopeRefusals` runs
|
|
3816
|
-
// these same two normalizers at the promotion gate and re-emits, so by the
|
|
3817
|
-
// time promotion runs there is nothing left to refuse. The throw below is a
|
|
3818
|
-
// BACKSTOP for a caller that skipped that gate — never the operator-facing
|
|
3819
|
-
// path.
|
|
3820
|
-
//
|
|
3821
|
-
// "Nothing left to refuse" is now TRUE BY CONSTRUCTION, not by hope: the
|
|
3822
|
-
// command half asks the ONE shared `commandLeavesDeclaredShape` predicate
|
|
3823
|
-
// that the host-handoff consumer asks, so a command this gate admits cannot
|
|
3824
|
-
// be refused downstream (and vice versa). It used to be a claim about two
|
|
3825
|
-
// independent implementations that disagreed in both directions.
|
|
3826
|
-
const scope = normalizeBlockTouchedFiles(root, deriveNodeFiles(node), toBlockId(nodeId));
|
|
3827
|
-
const commands = normalizeBlockTargetedCommands(node.targeted_commands ?? [], toBlockId(nodeId));
|
|
3828
|
-
const refusals = [...scope.refusals, ...commands.refusals];
|
|
3829
|
-
if (refusals.length > 0) {
|
|
3830
|
-
throw new Error(`implementation_dag node "${nodeId}" has an unpromotable write scope, which the ` +
|
|
3831
|
-
`promotion gate should have refused first: ${refusals.join(" | ")}`);
|
|
3832
|
-
}
|
|
3833
|
-
const touchedFiles = scope.touched_files;
|
|
3834
|
-
const targetedCommands = commands.targeted_commands;
|
|
3835
|
-
// Phase ordinal from the union of this node's obligations (max → fail-toward-
|
|
3836
|
-
// later). Only stamped when there is a genuine multi-phase cut, so a single-
|
|
3837
|
-
// phase change carries no ordinal and the scheduler runs no barrier.
|
|
3838
|
-
const phaseOrdinal = hasMultiPhase
|
|
3839
|
-
? phaseOrdinalForObligations([
|
|
3840
|
-
...(node.satisfies_obligations ?? []),
|
|
3841
|
-
...(node.verification_obligation_ids ?? []),
|
|
3842
|
-
], slugToOrdinal, lastOrdinal)
|
|
3843
|
-
: undefined;
|
|
3844
|
-
const blockModuleContracts = moduleContractsForNode(node);
|
|
3845
|
-
const counterexamples = [...new Set(node.addresses_counterexamples ?? [])].map(id => {
|
|
3846
|
-
const entry = counterexamplesById.get(id);
|
|
3847
|
-
if (!entry)
|
|
3848
|
-
throw new Error(`Implementation node ${nodeId} names missing counterexample ${id}; repair the upstream counterexample binding.`);
|
|
3849
|
-
return entry;
|
|
3850
|
-
});
|
|
3851
|
-
const implementationContext = ImplementationContextSchema.parse({
|
|
3852
|
-
...(approvedSource && node.description ? { description: node.description } : {}),
|
|
3853
|
-
...(node.preconditions !== undefined ? { preconditions: node.preconditions } : {}),
|
|
3854
|
-
...(node.expected_changes !== undefined ? { expected_changes: node.expected_changes } : {}),
|
|
3855
|
-
...(counterexamples.length > 0 ? { counterexamples } : {}),
|
|
3856
|
-
});
|
|
3857
|
-
return {
|
|
3858
|
-
block_id: toBlockId(nodeId),
|
|
3859
|
-
items: canonicalItemsByNodeId.get(nodeId) ?? [nodeId],
|
|
3860
|
-
...(Object.keys(implementationContext).length > 0 ? { implementation_context: implementationContext } : {}),
|
|
3861
|
-
// INV-remediate-pipeline-02: a block with prerequisites is never
|
|
3862
|
-
// wave-dispatched as independent — parallel_safe derives from depends_on.
|
|
3863
|
-
parallel_safe: deps.length === 0,
|
|
3864
|
-
dependencies: deps,
|
|
3865
|
-
// touched_files is REQUIRED on the block contract; promote the node's
|
|
3866
|
-
// declared write scope so the file-ownership scheduler can read it.
|
|
3867
|
-
touched_files: touchedFiles,
|
|
3868
|
-
...(phaseOrdinal !== undefined ? { phase_ordinal: phaseOrdinal } : {}),
|
|
3869
|
-
...(targetedCommands.length > 0 ? { targeted_commands: targetedCommands } : {}),
|
|
3870
|
-
...(blockModuleContracts.length > 0
|
|
3871
|
-
? { module_contracts: blockModuleContracts }
|
|
3872
|
-
: {}),
|
|
3873
|
-
};
|
|
3874
|
-
});
|
|
3875
|
-
// Detected at the confirm step and persisted, never chosen: the candidates
|
|
3876
|
-
// the host picks from (owner decision 92b0e2dd7cfdc06d). Planning reads the
|
|
3877
|
-
// artifact and spawns nothing (the backend-independent planning contract).
|
|
3878
|
-
const facts = (await readProjectFacts(artifactsDir)) ?? neutralProjectFacts();
|
|
3879
|
-
const extractedPlan = {
|
|
3880
|
-
plan_id: dag?.goal_id ?? `CP-PLAN-${Date.now()}`,
|
|
3881
|
-
goal_id: dag?.goal_id,
|
|
3882
|
-
findings,
|
|
3883
|
-
blocks,
|
|
3884
|
-
// finding_id → { obligation_ids, node_ids } backward trace.
|
|
3885
|
-
traceability,
|
|
3886
|
-
project_type: facts.project_type,
|
|
3887
|
-
candidate_closing_actions: facts.candidate_closing_actions,
|
|
3888
|
-
source: "contract_pipeline",
|
|
3889
|
-
};
|
|
3890
|
-
await writeJsonFile(paths.extractedPlan, extractedPlan);
|
|
295
|
+
return issues;
|
|
3891
296
|
}
|
|
3892
297
|
//# sourceMappingURL=contractPipeline.js.map
|