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