audit-tools 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dispatch/merge-results.mjs +229 -140
- package/dispatch/validate-result.mjs +98 -40
- package/dist/audit/cli/conceptualDispatch.d.ts.map +1 -1
- package/dist/audit/cli/conceptualDispatch.js +35 -3
- package/dist/audit/cli/conceptualDispatch.js.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.d.ts +9 -1
- package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.js +94 -38
- package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
- package/dist/audit/cli/nextStepCommand.d.ts +64 -0
- package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
- package/dist/audit/cli/nextStepCommand.js +877 -783
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts +12 -0
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +34 -1
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/steps.d.ts +2 -2
- package/dist/audit/extractors/graph.d.ts +28 -2
- package/dist/audit/extractors/graph.d.ts.map +1 -1
- package/dist/audit/extractors/graph.js +122 -11
- package/dist/audit/extractors/graph.js.map +1 -1
- package/dist/audit/extractors/graphRoutes.d.ts +8 -0
- package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
- package/dist/audit/extractors/graphRoutes.js +71 -4
- package/dist/audit/extractors/graphRoutes.js.map +1 -1
- package/dist/audit/io/artifacts.d.ts +55 -0
- package/dist/audit/io/artifacts.d.ts.map +1 -1
- package/dist/audit/io/artifacts.js +189 -12
- package/dist/audit/io/artifacts.js.map +1 -1
- package/dist/audit/io/toolingManifest.d.ts +11 -0
- package/dist/audit/io/toolingManifest.d.ts.map +1 -1
- package/dist/audit/io/toolingManifest.js +13 -3
- package/dist/audit/io/toolingManifest.js.map +1 -1
- package/dist/audit/orchestrator/advance.d.ts +64 -0
- package/dist/audit/orchestrator/advance.d.ts.map +1 -1
- package/dist/audit/orchestrator/advance.js +191 -57
- package/dist/audit/orchestrator/advance.js.map +1 -1
- package/dist/audit/orchestrator/charterClarificationExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/charterClarificationExecutor.js +55 -21
- package/dist/audit/orchestrator/charterClarificationExecutor.js.map +1 -1
- package/dist/audit/orchestrator/flowCoverage.d.ts.map +1 -1
- package/dist/audit/orchestrator/flowCoverage.js +7 -10
- package/dist/audit/orchestrator/flowCoverage.js.map +1 -1
- package/dist/audit/orchestrator/flowPlanning.d.ts +54 -1
- package/dist/audit/orchestrator/flowPlanning.d.ts.map +1 -1
- package/dist/audit/orchestrator/flowPlanning.js +77 -18
- package/dist/audit/orchestrator/flowPlanning.js.map +1 -1
- package/dist/audit/orchestrator/flowRequeue.d.ts.map +1 -1
- package/dist/audit/orchestrator/flowRequeue.js +6 -10
- package/dist/audit/orchestrator/flowRequeue.js.map +1 -1
- package/dist/audit/orchestrator/partitionTaskGraph.d.ts.map +1 -1
- package/dist/audit/orchestrator/partitionTaskGraph.js +2 -2
- package/dist/audit/orchestrator/partitionTaskGraph.js.map +1 -1
- package/dist/audit/orchestrator/requeueFold.d.ts.map +1 -1
- package/dist/audit/orchestrator/requeueFold.js +18 -5
- package/dist/audit/orchestrator/requeueFold.js.map +1 -1
- package/dist/audit/orchestrator/staleness.d.ts +24 -1
- package/dist/audit/orchestrator/staleness.d.ts.map +1 -1
- package/dist/audit/orchestrator/staleness.js +147 -16
- package/dist/audit/orchestrator/staleness.js.map +1 -1
- package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts +26 -0
- package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/syntaxResolutionExecutor.js +97 -12
- package/dist/audit/orchestrator/syntaxResolutionExecutor.js.map +1 -1
- package/dist/audit/orchestrator/taskBuilder.d.ts.map +1 -1
- package/dist/audit/orchestrator/taskBuilder.js +111 -21
- package/dist/audit/orchestrator/taskBuilder.js.map +1 -1
- package/dist/audit/orchestrator/trivialAudit.d.ts +17 -1
- package/dist/audit/orchestrator/trivialAudit.d.ts.map +1 -1
- package/dist/audit/orchestrator/trivialAudit.js +31 -4
- package/dist/audit/orchestrator/trivialAudit.js.map +1 -1
- package/dist/audit/reporting/synthesis.d.ts.map +1 -1
- package/dist/audit/reporting/synthesis.js +2 -3
- package/dist/audit/reporting/synthesis.js.map +1 -1
- package/dist/audit/reporting/workBlocks.d.ts.map +1 -1
- package/dist/audit/reporting/workBlocks.js +4 -38
- package/dist/audit/reporting/workBlocks.js.map +1 -1
- package/dist/audit/types/analyzerCapability.d.ts +6 -6
- package/dist/audit/types/charterRegister.d.ts +30 -12
- package/dist/audit/types/charterRegister.d.ts.map +1 -1
- package/dist/audit/types/charterRegister.js +22 -9
- package/dist/audit/types/charterRegister.js.map +1 -1
- package/dist/audit/validation/anchorGrounding.d.ts.map +1 -1
- package/dist/audit/validation/anchorGrounding.js +17 -0
- package/dist/audit/validation/anchorGrounding.js.map +1 -1
- package/dist/audit/validation/auditResults.d.ts +17 -0
- package/dist/audit/validation/auditResults.d.ts.map +1 -1
- package/dist/audit/validation/auditResults.js +50 -17
- package/dist/audit/validation/auditResults.js.map +1 -1
- package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts +48 -0
- package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/cyclicSeamResolution.js +128 -0
- package/dist/remediate/contractPipeline/cyclicSeamResolution.js.map +1 -1
- package/dist/remediate/contractPipeline/derive.d.ts +8 -5
- package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/derive.js +8 -16
- package/dist/remediate/contractPipeline/derive.js.map +1 -1
- package/dist/remediate/index.d.ts.map +1 -1
- package/dist/remediate/index.js +43 -2
- package/dist/remediate/index.js.map +1 -1
- package/dist/remediate/intent/intentOrdering.d.ts +13 -5
- package/dist/remediate/intent/intentOrdering.d.ts.map +1 -1
- package/dist/remediate/intent/intentOrdering.js +56 -25
- package/dist/remediate/intent/intentOrdering.js.map +1 -1
- package/dist/remediate/phases/close.d.ts +57 -2
- package/dist/remediate/phases/close.d.ts.map +1 -1
- package/dist/remediate/phases/close.js +260 -36
- package/dist/remediate/phases/close.js.map +1 -1
- package/dist/remediate/state/disposition.d.ts +26 -4
- package/dist/remediate/state/disposition.d.ts.map +1 -1
- package/dist/remediate/state/itemStatus.d.ts +33 -6
- package/dist/remediate/state/itemStatus.d.ts.map +1 -1
- package/dist/remediate/state/itemStatus.js +154 -39
- package/dist/remediate/state/itemStatus.js.map +1 -1
- package/dist/remediate/state/types.d.ts +41 -2
- package/dist/remediate/state/types.d.ts.map +1 -1
- package/dist/remediate/state/types.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts +287 -15
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +1664 -987
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.js +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts +137 -2
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.js +713 -59
- package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
- package/dist/remediate/steps/finalGate.d.ts +136 -37
- package/dist/remediate/steps/finalGate.d.ts.map +1 -1
- package/dist/remediate/steps/finalGate.js +120 -86
- package/dist/remediate/steps/finalGate.js.map +1 -1
- package/dist/remediate/steps/gateCommands.d.ts +8 -0
- package/dist/remediate/steps/gateCommands.d.ts.map +1 -1
- package/dist/remediate/steps/gateCommands.js +40 -11
- package/dist/remediate/steps/gateCommands.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +96 -5
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +668 -147
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/types.d.ts +4 -1
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js +10 -0
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/remediate/validation/artifacts.d.ts +20 -0
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +239 -13
- package/dist/remediate/validation/artifacts.js.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.d.ts +58 -0
- package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.js +167 -43
- package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
- package/dist/shared/analyzers/acquisitionEngine.d.ts +72 -25
- package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
- package/dist/shared/analyzers/acquisitionEngine.js +149 -15
- package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
- package/dist/shared/analyzers/binaryAcquisition.d.ts +27 -2
- package/dist/shared/analyzers/binaryAcquisition.d.ts.map +1 -1
- package/dist/shared/analyzers/binaryAcquisition.js +149 -43
- package/dist/shared/analyzers/binaryAcquisition.js.map +1 -1
- package/dist/shared/analyzers/candidates.d.ts +9 -8
- package/dist/shared/analyzers/candidates.d.ts.map +1 -1
- package/dist/shared/analyzers/candidates.js +96 -64
- package/dist/shared/analyzers/candidates.js.map +1 -1
- package/dist/shared/analyzers/normalizeExternal.d.ts +40 -12
- package/dist/shared/analyzers/normalizeExternal.d.ts.map +1 -1
- package/dist/shared/analyzers/normalizeExternal.js +74 -18
- package/dist/shared/analyzers/normalizeExternal.js.map +1 -1
- package/dist/shared/analyzers/rubocop.d.ts +14 -13
- package/dist/shared/analyzers/rubocop.d.ts.map +1 -1
- package/dist/shared/analyzers/rubocop.js +29 -6
- package/dist/shared/analyzers/rubocop.js.map +1 -1
- package/dist/shared/analyzers/types.d.ts +196 -13
- package/dist/shared/analyzers/types.d.ts.map +1 -1
- package/dist/shared/analyzers/types.js +122 -9
- package/dist/shared/analyzers/types.js.map +1 -1
- package/dist/shared/decompose/charterClarification.d.ts +12 -1
- package/dist/shared/decompose/charterClarification.d.ts.map +1 -1
- package/dist/shared/decompose/charterClarification.js +11 -4
- package/dist/shared/decompose/charterClarification.js.map +1 -1
- package/dist/shared/decompose/charterExtraction.d.ts +20 -10
- package/dist/shared/decompose/charterExtraction.d.ts.map +1 -1
- package/dist/shared/decompose/charterExtraction.js +52 -23
- package/dist/shared/decompose/charterExtraction.js.map +1 -1
- package/dist/shared/decompose/contentCoherence.d.ts +43 -2
- package/dist/shared/decompose/contentCoherence.d.ts.map +1 -1
- package/dist/shared/decompose/contentCoherence.js +109 -5
- package/dist/shared/decompose/contentCoherence.js.map +1 -1
- package/dist/shared/decompose/modularity.d.ts +17 -0
- package/dist/shared/decompose/modularity.d.ts.map +1 -1
- package/dist/shared/decompose/modularity.js +78 -3
- package/dist/shared/decompose/modularity.js.map +1 -1
- package/dist/shared/decompose/workBlockSeams.d.ts +45 -0
- package/dist/shared/decompose/workBlockSeams.d.ts.map +1 -0
- package/dist/shared/decompose/workBlockSeams.js +82 -0
- package/dist/shared/decompose/workBlockSeams.js.map +1 -0
- package/dist/shared/index.d.ts +4 -3
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +4 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/intent/freeFormIntentInterpreter.d.ts +12 -2
- package/dist/shared/intent/freeFormIntentInterpreter.d.ts.map +1 -1
- package/dist/shared/intent/freeFormIntentInterpreter.js +27 -22
- package/dist/shared/intent/freeFormIntentInterpreter.js.map +1 -1
- package/dist/shared/intent/pathScope.d.ts +12 -1
- package/dist/shared/intent/pathScope.d.ts.map +1 -1
- package/dist/shared/intent/pathScope.js +56 -19
- package/dist/shared/intent/pathScope.js.map +1 -1
- package/dist/shared/intent/sharedIntentData.d.ts +19 -1
- package/dist/shared/intent/sharedIntentData.d.ts.map +1 -1
- package/dist/shared/intent/sharedIntentData.js +33 -8
- package/dist/shared/intent/sharedIntentData.js.map +1 -1
- package/dist/shared/io/fileLock.d.ts +12 -0
- package/dist/shared/io/fileLock.d.ts.map +1 -1
- package/dist/shared/io/fileLock.js +48 -5
- package/dist/shared/io/fileLock.js.map +1 -1
- package/dist/shared/io/json.d.ts +13 -0
- package/dist/shared/io/json.d.ts.map +1 -1
- package/dist/shared/io/json.js +19 -1
- package/dist/shared/io/json.js.map +1 -1
- package/dist/shared/reporting/auditDeliverable.d.ts.map +1 -1
- package/dist/shared/reporting/auditDeliverable.js +8 -3
- package/dist/shared/reporting/auditDeliverable.js.map +1 -1
- package/dist/shared/steps/stepEmissionScaffold.d.ts +69 -0
- package/dist/shared/steps/stepEmissionScaffold.d.ts.map +1 -0
- package/dist/shared/steps/stepEmissionScaffold.js +83 -0
- package/dist/shared/steps/stepEmissionScaffold.js.map +1 -0
- package/dist/shared/submission/handRecovery.d.ts.map +1 -1
- package/dist/shared/submission/handRecovery.js +69 -4
- package/dist/shared/submission/handRecovery.js.map +1 -1
- package/dist/shared/submission/submissionIdentity.d.ts +10 -1
- package/dist/shared/submission/submissionIdentity.d.ts.map +1 -1
- package/dist/shared/submission/submissionIdentity.js +12 -3
- package/dist/shared/submission/submissionIdentity.js.map +1 -1
- package/dist/shared/submission/submissionLedger.d.ts +43 -2
- package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
- package/dist/shared/submission/submissionLedger.js +50 -7
- package/dist/shared/submission/submissionLedger.js.map +1 -1
- package/dist/shared/tooling/allowlistedExec.d.ts +9 -0
- package/dist/shared/tooling/allowlistedExec.d.ts.map +1 -1
- package/dist/shared/tooling/allowlistedExec.js +19 -2
- package/dist/shared/tooling/allowlistedExec.js.map +1 -1
- package/dist/shared/types/charter.d.ts +24 -0
- package/dist/shared/types/charter.d.ts.map +1 -1
- package/dist/shared/types/charter.js +9 -0
- package/dist/shared/types/charter.js.map +1 -1
- package/dist/shared/types/finding.d.ts +105 -53
- package/dist/shared/types/finding.d.ts.map +1 -1
- package/dist/shared/types/finding.js +38 -11
- package/dist/shared/types/finding.js.map +1 -1
- package/dist/shared/types/remediationOutcome.d.ts +282 -9
- package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
- package/dist/shared/types/remediationOutcome.js +174 -1
- package/dist/shared/types/remediationOutcome.js.map +1 -1
- package/dist/shared/validation/findingsReport.d.ts.map +1 -1
- package/dist/shared/validation/findingsReport.js +19 -1
- package/dist/shared/validation/findingsReport.js.map +1 -1
- package/package.json +2 -1
- package/remediate-code.mjs +15 -0
- package/wrapper/remediate-code-wrapper-install-hosts.mjs +45 -4
- package/wrapper/remediate-code-wrapper-install-renderers.mjs +12 -2
|
@@ -20,21 +20,34 @@
|
|
|
20
20
|
* non-converging judge can never oscillate forever.
|
|
21
21
|
*/
|
|
22
22
|
import { existsSync } from "node:fs";
|
|
23
|
-
import { mkdir, rename, rm } from "node:fs/promises";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
import { writeJsonFile, readOptionalJsonFile, formatValidationIssues, hashContent, isRecord, withFsRetry, projectApprovedFindings, captureStepBoundaryFriction, } from "audit-tools/shared";
|
|
23
|
+
import { mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
24
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
25
|
+
import { writeJsonFile, readOptionalJsonFile, formatValidationIssues, hashContent, isRecord, withFsRetry, projectApprovedFindings, captureStepBoundaryFriction, climbOutOfAuditTools, estimateTokensFromBytes, normalizeRepoPath, repoRelativePath, } from "audit-tools/shared";
|
|
26
|
+
import { createStepEmissionScaffold, } from "../../shared/steps/stepEmissionScaffold.js";
|
|
26
27
|
import { counterexampleFingerprint } from "../contractPipeline/counterexampleFingerprint.js";
|
|
27
28
|
import { CP_ARTIFACT_NAMES, contractArtifactExists, contractArtifactFilePath, contractInputFilePath, contractPipelineDir, detectStaleArtifacts, envelopePayload, envelopeSemanticHash, isEnvelope, pathASeedFilePath, payloadSemanticHash, readContractArtifact, stampToolCreatedAt, writeContractArtifact, writeDerivedContractArtifact, } from "../contractPipeline/artifactStore.js";
|
|
28
29
|
import { readIntakeRiskSignal, writeIntakeRiskSignal, escalateRiskSignal, decompositionRiskEvidence, adversarialDepthForTier, roundTripGranularityForTier, } from "../riskSignal.js";
|
|
29
30
|
import { phaseOrdinalForObligations, moduleSlug, renderPhaseCutSection, } from "../contractPipeline/phaseCut.js";
|
|
30
31
|
import { ensurePhaseCutArtifact, readPhaseCutArtifact } from "../contractPipeline/phaseCutArtifact.js";
|
|
31
|
-
import { detectCyclicSeamObligations,
|
|
32
|
+
import { detectCyclicSeamObligations, validateAuthoredCycleBreak, } from "../contractPipeline/cyclicSeamResolution.js";
|
|
32
33
|
import { deriveObligationLedger, deriveFinalizedModuleContracts, buildTestValidatorPlanScaffold, buildImplementationDagScaffold, acceptedCounterexampleIds, advisoryCritiqueItems, } from "../contractPipeline/derive.js";
|
|
33
34
|
import { ensureNodeId, toBlockId } from "../contractPipeline/idRegistry.js";
|
|
34
35
|
import { captureReviewSnapshot, computeReReviewDelta, isReviewArtifact, readReviewSnapshot, renderReReviewSection, reviewSnapshotExists, } from "../contractPipeline/reviewSnapshot.js";
|
|
35
36
|
import { captureTestPlanCarry, readTestPlanCarry, } from "../contractPipeline/testPlanCarry.js";
|
|
36
37
|
import { renderContractPipelinePrompt, renderContractRepairPrompt, CONTRACT_PIPELINE_PHASE_ORDER, PHASE_TO_ARTIFACT, } from "./contractPipelinePrompts.js";
|
|
37
|
-
|
|
38
|
+
// The seven cross-artifact validators this module used to call one by one are
|
|
39
|
+
// gone from this list on purpose: every one of them is now reached through
|
|
40
|
+
// `evaluateContractPipelineCrossGateOutcomes`, so a call site cannot read a
|
|
41
|
+
// gate's issue array without also seeing whether the gate RAN
|
|
42
|
+
// (the branch-on-evaluated rule). What remains here are the checks that are not
|
|
43
|
+
// part of that eight-gate set.
|
|
44
|
+
import { CONTRACT_PIPELINE_VALIDATORS, CP_MODULE_CONTRACTS_VERSION, validateGoalIdConsistency, validateWorkBlockSeamPreparation, validateContractCitationGrounding, } from "../validation/contractPipeline.js";
|
|
45
|
+
// Imported from the gate module DIRECTLY (as derive.ts does), not through the
|
|
46
|
+
// validation barrel: the barrel re-exports the older flattened cross-gate
|
|
47
|
+
// runner, and re-exporting the outcome vocabulary through it would mean editing
|
|
48
|
+
// a file outside this work item's write scope.
|
|
49
|
+
import { evaluateContractPipelineCrossGateOutcomes, enumerateRepoTreePaths, isInsideGitWorkTree, isTestablePhaseObligation, TESTABLE_OBLIGATION_KINDS, } from "../validation/contractPipelineGates.js";
|
|
50
|
+
import { compareCodeUnits } from "../../shared/affinityArtifacts.js";
|
|
38
51
|
import { writeCurrentStep } from "./stepWriter.js";
|
|
39
52
|
import { loaderCommand } from "./prompts.js";
|
|
40
53
|
import { intakePaths } from "../intake.js";
|
|
@@ -354,7 +367,7 @@ export async function writePathASeedFromFindings(artifactsDir, auditFindingsPath
|
|
|
354
367
|
const seedPath = pathASeedFilePath(artifactsDir);
|
|
355
368
|
if (existsSync(seedPath))
|
|
356
369
|
return; // idempotent
|
|
357
|
-
const findings = [...approved.findings].sort((left, right) => left.id
|
|
370
|
+
const findings = [...approved.findings].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
358
371
|
const affectedFilesSet = new Set();
|
|
359
372
|
const findingsSummary = findings.map((finding) => ({
|
|
360
373
|
id: finding.id,
|
|
@@ -374,28 +387,98 @@ export async function writePathASeedFromFindings(artifactsDir, auditFindingsPath
|
|
|
374
387
|
owned_files: [...block.owned_files].sort(),
|
|
375
388
|
depends_on: [...block.depends_on].sort(),
|
|
376
389
|
}))
|
|
377
|
-
.sort((a, b) => a.id
|
|
390
|
+
.sort((a, b) => compareCodeUnits(a.id, b.id));
|
|
391
|
+
// Code-unit order, not `localeCompare`, on EVERY persisted seed array: the
|
|
392
|
+
// seed order must not depend on the host's ICU collation, and seam ids are
|
|
393
|
+
// hex now — a locale that orders digits against letters differently would
|
|
394
|
+
// reshuffle the file.
|
|
378
395
|
const workBlockSeams = approved.workBlockSeams
|
|
379
396
|
.map((seam) => ({
|
|
380
397
|
...seam,
|
|
381
|
-
block_ids: [...seam.block_ids],
|
|
382
|
-
shared_files: [...seam.shared_files].sort(),
|
|
383
|
-
shared_unit_ids: [...seam.shared_unit_ids].sort(),
|
|
398
|
+
block_ids: [...seam.block_ids].sort(compareCodeUnits),
|
|
384
399
|
}))
|
|
385
|
-
.sort((a, b) => a.id
|
|
400
|
+
.sort((a, b) => compareCodeUnits(a.id, b.id));
|
|
401
|
+
const affectedFiles = [...affectedFilesSet].sort();
|
|
386
402
|
const seed = {
|
|
387
403
|
schema_version: "remediate-code-contract-pipeline/path-a-seed/v1alpha2",
|
|
388
404
|
audit_findings_path: auditFindingsPath,
|
|
389
405
|
finding_count: findings.length,
|
|
390
406
|
findings_summary: findingsSummary,
|
|
391
|
-
affected_files:
|
|
407
|
+
affected_files: affectedFiles,
|
|
392
408
|
work_blocks: workBlocks,
|
|
393
409
|
work_block_seams: workBlockSeams,
|
|
410
|
+
source_digests: await hashSeedSourcePaths(seedRepoRoot(artifactsDir), auditFindingsPath, affectedFiles),
|
|
394
411
|
created_at: new Date().toISOString(),
|
|
395
412
|
};
|
|
396
413
|
await mkdir(contractPipelineDir(artifactsDir), { recursive: true });
|
|
397
414
|
await writeJsonFile(seedPath, seed);
|
|
398
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* The repository root that owns `artifactsDir`, for resolving the seed's
|
|
418
|
+
* repo-relative `affected_files`. Derived through the shared
|
|
419
|
+
* `climbOutOfAuditTools` rather than a hand-rolled `../..`, so the one
|
|
420
|
+
* `.audit-tools` layout rule stays single-sourced (and a caller that hands us a
|
|
421
|
+
* dir outside the tree simply gets that dir back, which resolves relative paths
|
|
422
|
+
* against it — the same thing every other artifact path in this module does).
|
|
423
|
+
*/
|
|
424
|
+
function seedRepoRoot(artifactsDir) {
|
|
425
|
+
return climbOutOfAuditTools(artifactsDir);
|
|
426
|
+
}
|
|
427
|
+
/** Absolute form of a seed-recorded path (absolute entries pass through). */
|
|
428
|
+
function resolveSeedSourcePath(root, path) {
|
|
429
|
+
return isAbsolute(path) ? path : resolve(root, path);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* sha256 every seed source path that EXISTS at seed-build time. A path that is
|
|
433
|
+
* absent is not recorded at all — the seed binds what it actually read, and a
|
|
434
|
+
* finding citing a file that does not exist yet (a new-file remediation) must
|
|
435
|
+
* not mint a digest that can never match.
|
|
436
|
+
*/
|
|
437
|
+
async function hashSeedSourcePaths(root, auditFindingsPath, affectedFiles) {
|
|
438
|
+
const digests = [];
|
|
439
|
+
// Content-derived order (path-sorted, deduped): an incidentally-ordered array
|
|
440
|
+
// would churn the seed's content hash on every re-derivation.
|
|
441
|
+
const candidates = [...new Set([auditFindingsPath, ...affectedFiles])].sort((left, right) => compareCodeUnits(left, right));
|
|
442
|
+
for (const path of candidates) {
|
|
443
|
+
const absolute = resolveSeedSourcePath(root, path);
|
|
444
|
+
let content;
|
|
445
|
+
try {
|
|
446
|
+
content = await readFile(absolute);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
continue; // Not readable at seed time — nothing to bind.
|
|
450
|
+
}
|
|
451
|
+
digests.push({ path, sha256: hashContent(content) });
|
|
452
|
+
}
|
|
453
|
+
return digests;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Seed source-digest binding — re-hash every path the path_a seed recorded and
|
|
457
|
+
* report the ones that moved. Pure over (root, seed): the caller decides what a
|
|
458
|
+
* mismatch means, so this is directly red-green testable without a pipeline.
|
|
459
|
+
*
|
|
460
|
+
* A seed with no `source_digests` (written before the field existed) binds
|
|
461
|
+
* nothing and yields no mismatches.
|
|
462
|
+
*/
|
|
463
|
+
export async function detectSeedSourceDigestMismatches(root, seed) {
|
|
464
|
+
const mismatches = [];
|
|
465
|
+
for (const entry of seed?.source_digests ?? []) {
|
|
466
|
+
if (typeof entry?.path !== "string" || typeof entry?.sha256 !== "string")
|
|
467
|
+
continue;
|
|
468
|
+
const absolute = resolveSeedSourcePath(root, entry.path);
|
|
469
|
+
let actual = null;
|
|
470
|
+
try {
|
|
471
|
+
actual = hashContent(await readFile(absolute));
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
actual = null;
|
|
475
|
+
}
|
|
476
|
+
if (actual !== entry.sha256) {
|
|
477
|
+
mismatches.push({ path: entry.path, expected: entry.sha256, actual });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return mismatches;
|
|
481
|
+
}
|
|
399
482
|
/**
|
|
400
483
|
* Infer the most appropriate repair target from judge classifications when no
|
|
401
484
|
* explicit repair_directive is provided. Examines only accepted classifications
|
|
@@ -655,40 +738,20 @@ export async function validateImplementationDagTraceability(artifactsDir) {
|
|
|
655
738
|
}
|
|
656
739
|
/**
|
|
657
740
|
* Run the fail-closed contract-obligation gates against the persisted contract
|
|
658
|
-
* artifacts
|
|
659
|
-
*
|
|
660
|
-
* - validateEvidenceThreaded (assessment × judge × implementation_dag)
|
|
661
|
-
* - validateDigestCoverage (goal_spec.source_type × finding-enumeration × ledger)
|
|
662
|
-
* - validateReconciliationDerivation (seam report × finalized contracts)
|
|
741
|
+
* artifacts: paired obligations, evidence threading, source-scoped digest
|
|
742
|
+
* coverage, and INV-CO-12 reconciliation derivation.
|
|
663
743
|
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
const seamReport = envelopePayload(await readContractArtifact(artifactsDir, "seam_reconciliation_report"));
|
|
676
|
-
const finalizedContracts = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
677
|
-
const goalSpec = envelopePayload(await readContractArtifact(artifactsDir, "goal_spec"));
|
|
678
|
-
const sourceType = isRecord(goalSpec) && typeof goalSpec.source_type === "string"
|
|
679
|
-
? goalSpec.source_type
|
|
680
|
-
: undefined;
|
|
681
|
-
const findingEnumeration = await readOptionalJsonFile(intakePaths(artifactsDir).findingEnumeration);
|
|
682
|
-
const issues = [
|
|
683
|
-
...validatePairedObligations(obligationLedger, testValidatorPlan),
|
|
684
|
-
...validateEvidenceThreaded(assessment, judge, dag),
|
|
685
|
-
...validateDigestCoverage(sourceType, findingEnumeration, obligationLedger),
|
|
686
|
-
...validateReconciliationDerivation(seamReport, finalizedContracts),
|
|
687
|
-
].filter((issue) => issue.severity === "error");
|
|
688
|
-
return {
|
|
689
|
-
ok: issues.length === 0,
|
|
690
|
-
violations: issues.map((issue) => `[${issue.path}] ${issue.message}`),
|
|
691
|
-
};
|
|
744
|
+
* Branch on `evaluated` before trusting emptiness. This no longer flattens four `ValidationIssue[]`
|
|
745
|
+
* into one array, where a gate that never RAN and a gate that ran CLEAN both
|
|
746
|
+
* contributed nothing and were indistinguishable. It consumes the shared
|
|
747
|
+
* gate-outcome record and branches on `evaluated` first: at this boundary every
|
|
748
|
+
* phase artifact exists, so a skipped gate is a violation, not a pass. See
|
|
749
|
+
* {@link consumeGateOutcomes} for the per-boundary `required` policy and the one
|
|
750
|
+
* declared exception (`digest_coverage`).
|
|
751
|
+
*/
|
|
752
|
+
export async function evaluateContractObligationsPromotionGate(artifactsDir, root = climbOutOfAuditTools(artifactsDir), inputs) {
|
|
753
|
+
const outcomes = evaluateContractPipelineCrossGateOutcomes(inputs ?? (await readCrossGateInputs(artifactsDir, root)));
|
|
754
|
+
return consumeGateOutcomes(outcomes, PROMOTION_GATES, PROMOTION_REQUIRED_GATES);
|
|
692
755
|
}
|
|
693
756
|
/**
|
|
694
757
|
* Pre-adversarial structural floor (S5). The subset of the contract-obligation
|
|
@@ -704,38 +767,30 @@ export async function evaluateContractObligationsPromotionGate(artifactsDir) {
|
|
|
704
767
|
* at promotion as the fail-closed backstop; this gate never replaces it.
|
|
705
768
|
*
|
|
706
769
|
* Returns the first failing gate's responsible phase + rendered error lines, or
|
|
707
|
-
* null when the structural floor is clean.
|
|
708
|
-
*
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
const goalSpec = envelopePayload(await readContractArtifact(artifactsDir, "goal_spec"));
|
|
716
|
-
const sourceType = isRecord(goalSpec) && typeof goalSpec.source_type === "string"
|
|
717
|
-
? goalSpec.source_type
|
|
718
|
-
: undefined;
|
|
719
|
-
const findingEnumeration = await readOptionalJsonFile(intakePaths(artifactsDir).findingEnumeration);
|
|
770
|
+
* null when the structural floor is clean. Branches on each outcome's
|
|
771
|
+
* `evaluated` before its empty issue list is allowed to mean clean
|
|
772
|
+
* (the branch-on-evaluated rule); `contract_finalization`, `seam_reconciliation`
|
|
773
|
+
* and `test_validator_plan` all precede `critic` in the phase order, so a
|
|
774
|
+
* skipped gate here is a malformed payload rather than an absent one.
|
|
775
|
+
*/
|
|
776
|
+
export async function evaluatePreCriticStructuralGate(artifactsDir, root = climbOutOfAuditTools(artifactsDir), inputs) {
|
|
777
|
+
const outcomes = evaluateContractPipelineCrossGateOutcomes(inputs ?? (await readCrossGateInputs(artifactsDir, root)));
|
|
720
778
|
// Upstream-owned checks first: a derivation/coverage gap is fixed in the
|
|
721
779
|
// finalized contracts (the obligation ledger is derived from them).
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
...validateDigestCoverage(sourceType, findingEnumeration, obligationLedger),
|
|
725
|
-
].filter((issue) => issue.severity === "error");
|
|
726
|
-
if (designErrors.length > 0) {
|
|
780
|
+
const design = consumeGateOutcomes(outcomes, ["reconciliation_derivation", "digest_coverage"], PRE_CRITIC_REQUIRED_GATES);
|
|
781
|
+
if (!design.ok) {
|
|
727
782
|
return {
|
|
728
783
|
phase: "contract_finalization",
|
|
729
|
-
errorLines:
|
|
784
|
+
errorLines: design.violations.map((violation) => `- ${violation}`),
|
|
730
785
|
};
|
|
731
786
|
}
|
|
732
787
|
// A testable obligation without a paired spec is fixed in the test plan
|
|
733
788
|
// (skeleton-scaffolded from the derived ledger).
|
|
734
|
-
const
|
|
735
|
-
if (
|
|
789
|
+
const tests = consumeGateOutcomes(outcomes, ["paired_obligations"], PRE_CRITIC_REQUIRED_GATES);
|
|
790
|
+
if (!tests.ok) {
|
|
736
791
|
return {
|
|
737
792
|
phase: "test_validator_plan",
|
|
738
|
-
errorLines:
|
|
793
|
+
errorLines: tests.violations.map((violation) => `- ${violation}`),
|
|
739
794
|
};
|
|
740
795
|
}
|
|
741
796
|
return null;
|
|
@@ -1058,98 +1113,91 @@ async function resolveAdversarialDepth(artifactsDir) {
|
|
|
1058
1113
|
adversarialDepth: riskSignal ? adversarialDepthForTier(riskSignal.tier) : undefined,
|
|
1059
1114
|
};
|
|
1060
1115
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
// evidence. Extracted to resolveAdversarialDepth (behavior-preserving). The
|
|
1071
|
-
// (possibly raised) riskSignal is also consumed by the granularity-collapse
|
|
1072
|
-
// gate below, so it is returned alongside the depth.
|
|
1073
|
-
const { riskSignal, adversarialDepth } = await resolveAdversarialDepth(artifactsDir);
|
|
1074
|
-
// Detect the path-A seed file: present only for structured_audit runs.
|
|
1075
|
-
const seedPath = pathASeedFilePath(artifactsDir);
|
|
1076
|
-
const pathASeedPath = existsSync(seedPath) ? seedPath : undefined;
|
|
1077
|
-
// Resolve artifact paths for the prompt renderers. The host's world is the
|
|
1078
|
-
// plain INPUT files (D3): every host-facing path — both where a role WRITES its
|
|
1079
|
-
// output and where it READS its upstreams — is `<name>.input.json`. The tool's
|
|
1080
|
-
// canonical envelopes (`<name>.json`) are derived at ingest and never named to
|
|
1081
|
-
// the host.
|
|
1082
|
-
const artifactPaths = {};
|
|
1083
|
-
for (const name of CP_ARTIFACT_NAMES) {
|
|
1084
|
-
artifactPaths[name] = contractInputFilePath(artifactsDir, name);
|
|
1116
|
+
// ── Writers ───────────────────────────────────────────────────────────────────
|
|
1117
|
+
/** The artifact-path map every emitted step carries (existing artifacts only). */
|
|
1118
|
+
function contractStepArtifactPaths(ctx, outputPath) {
|
|
1119
|
+
const stepArtifactPaths = {};
|
|
1120
|
+
if (outputPath)
|
|
1121
|
+
stepArtifactPaths.output = outputPath;
|
|
1122
|
+
for (const [key, value] of Object.entries(ctx.artifactPaths)) {
|
|
1123
|
+
if (value && existsSync(value))
|
|
1124
|
+
stepArtifactPaths[key] = value;
|
|
1085
1125
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1126
|
+
if (ctx.sourcePaths) {
|
|
1127
|
+
stepArtifactPaths.source_manifest = ctx.paths.sourceManifest;
|
|
1128
|
+
stepArtifactPaths.remediation_brief = ctx.paths.brief;
|
|
1129
|
+
}
|
|
1130
|
+
return stepArtifactPaths;
|
|
1131
|
+
}
|
|
1132
|
+
function writeContractPromptStep(ctx, params) {
|
|
1133
|
+
const nextCommand = loaderCommand("next-step");
|
|
1134
|
+
const prompt = `${params.prompt}
|
|
1089
1135
|
|
|
1090
1136
|
After writing the output file, run:
|
|
1091
1137
|
|
|
1092
1138
|
\`${nextCommand}\`
|
|
1093
1139
|
`;
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1140
|
+
return writeCurrentStep({
|
|
1141
|
+
stepKind: CONTRACT_STEP_KIND,
|
|
1142
|
+
status: "ready",
|
|
1143
|
+
runId: ctx.runId,
|
|
1144
|
+
repoRoot: ctx.root,
|
|
1145
|
+
artifactsDir: ctx.artifactsDir,
|
|
1146
|
+
prompt,
|
|
1147
|
+
allowedCommands: [nextCommand],
|
|
1148
|
+
stopCondition: params.stopCondition,
|
|
1149
|
+
artifactPaths: contractStepArtifactPaths(ctx, params.outputPath),
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
function writeContractPhaseStep(ctx, phase, extraSection) {
|
|
1153
|
+
const rendered = renderContractPipelinePrompt({
|
|
1154
|
+
role: phase,
|
|
1155
|
+
artifactPaths: ctx.artifactPaths,
|
|
1156
|
+
sourcePaths: ctx.sourcePaths,
|
|
1157
|
+
repoRoot: ctx.root,
|
|
1158
|
+
pathASeedPath: ctx.pathASeedPath,
|
|
1159
|
+
adversarialDepth: ctx.adversarialDepth,
|
|
1160
|
+
});
|
|
1161
|
+
return writeContractPromptStep(ctx, {
|
|
1162
|
+
prompt: extraSection ? `${rendered.prompt}\n${extraSection}` : rendered.prompt,
|
|
1163
|
+
outputPath: rendered.outputPath,
|
|
1164
|
+
stopCondition: `Stop after writing the contract-pipeline output for phase "${phase}" and running next-step.`,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
function writeContractBlockedStep(ctx, params) {
|
|
1168
|
+
return writeCurrentStep({
|
|
1169
|
+
stepKind: CONTRACT_STEP_KIND,
|
|
1170
|
+
status: "blocked",
|
|
1171
|
+
runId: ctx.runId,
|
|
1172
|
+
repoRoot: ctx.root,
|
|
1173
|
+
artifactsDir: ctx.artifactsDir,
|
|
1174
|
+
prompt: params.prompt,
|
|
1175
|
+
allowedCommands: [],
|
|
1176
|
+
stopCondition: params.stopCondition,
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* T1 slice 4b: ONE round-trip whose prompt concatenates the rendered specs of
|
|
1181
|
+
* several consecutive authoring phases. The worker writes every named artifact
|
|
1182
|
+
* top-down (each later phase's inputs are the files it wrote in the earlier
|
|
1183
|
+
* sections of the same round-trip), then runs next-step once. The group header
|
|
1184
|
+
* overrides the per-section "stop after writing" lines so they are not read as
|
|
1185
|
+
* three separate stop points.
|
|
1186
|
+
*/
|
|
1187
|
+
function writeCollapsedFramingStep(ctx, phases) {
|
|
1188
|
+
const sections = phases.map((phase) => ({
|
|
1189
|
+
phase,
|
|
1190
|
+
rendered: renderContractPipelinePrompt({
|
|
1120
1191
|
role: phase,
|
|
1121
|
-
artifactPaths,
|
|
1122
|
-
sourcePaths,
|
|
1123
|
-
repoRoot: root,
|
|
1124
|
-
pathASeedPath,
|
|
1125
|
-
adversarialDepth,
|
|
1126
|
-
})
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
stopCondition: `Stop after writing the contract-pipeline output for phase "${phase}" and running next-step.`,
|
|
1131
|
-
});
|
|
1132
|
-
};
|
|
1133
|
-
// T1 slice 4b: emit ONE round-trip whose prompt concatenates the rendered
|
|
1134
|
-
// specs of several consecutive authoring phases. The worker writes every
|
|
1135
|
-
// named artifact top-down (each later phase's inputs are the files it wrote in
|
|
1136
|
-
// the earlier sections of the same round-trip), then runs next-step once. The
|
|
1137
|
-
// group header overrides the per-section "stop after writing" lines so they are
|
|
1138
|
-
// not read as three separate stop points.
|
|
1139
|
-
const buildCollapsedFramingStep = (phases) => {
|
|
1140
|
-
const sections = phases.map((phase) => {
|
|
1141
|
-
const rendered = renderContractPipelinePrompt({
|
|
1142
|
-
role: phase,
|
|
1143
|
-
artifactPaths,
|
|
1144
|
-
sourcePaths,
|
|
1145
|
-
repoRoot: root,
|
|
1146
|
-
pathASeedPath,
|
|
1147
|
-
adversarialDepth,
|
|
1148
|
-
});
|
|
1149
|
-
return { phase, rendered };
|
|
1150
|
-
});
|
|
1151
|
-
const outputPaths = sections.map((s) => s.rendered.outputPath);
|
|
1152
|
-
const header = `# Collapsed Authoring Round-Trip — ${phases.length} Phases
|
|
1192
|
+
artifactPaths: ctx.artifactPaths,
|
|
1193
|
+
sourcePaths: ctx.sourcePaths,
|
|
1194
|
+
repoRoot: ctx.root,
|
|
1195
|
+
pathASeedPath: ctx.pathASeedPath,
|
|
1196
|
+
adversarialDepth: ctx.adversarialDepth,
|
|
1197
|
+
}),
|
|
1198
|
+
}));
|
|
1199
|
+
const outputPaths = sections.map((s) => s.rendered.outputPath);
|
|
1200
|
+
const header = `# Collapsed Authoring Round-Trip — ${phases.length} Phases
|
|
1153
1201
|
|
|
1154
1202
|
This is a low-complexity change, so these ${phases.length} coherent authoring phases are combined into a SINGLE round-trip. Complete EVERY section below — author them top-down, writing each artifact to its named path (each later section's inputs are the files you write in the earlier sections of this same round-trip). Then run next-step ONCE.
|
|
1155
1203
|
|
|
@@ -1159,38 +1207,38 @@ If you cannot complete a section (an artifact would be malformed), write the one
|
|
|
1159
1207
|
|
|
1160
1208
|
Artifacts to produce (in order):
|
|
1161
1209
|
${outputPaths.map((p, i) => `${i + 1}. \`${p}\` (${phases[i]})`).join("\n")}`;
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1210
|
+
const body = sections.map((s) => `\n---\n\n${s.rendered.prompt}`).join("\n");
|
|
1211
|
+
return writeContractPromptStep(ctx, {
|
|
1212
|
+
prompt: `${header}\n${body}`,
|
|
1213
|
+
outputPath: outputPaths[outputPaths.length - 1],
|
|
1214
|
+
stopCondition: `Stop after writing all ${phases.length} collapsed-framing artifacts (${phases.join(", ")}) and running next-step once.`,
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* DC-3: fan a parallel phase out to one bounded item per module. The host owns
|
|
1219
|
+
* grouping, concurrency, and execution choices; this tool supplies only the
|
|
1220
|
+
* complete coherent workload. Each item writes a per-module shard, and the next
|
|
1221
|
+
* next-step merges every shard into the aggregated artifact before any
|
|
1222
|
+
* downstream derivation. A degenerate decomposition (zero or one module) falls
|
|
1223
|
+
* back to the single aggregated step.
|
|
1224
|
+
*/
|
|
1225
|
+
async function writeParallelModuleWaveStep(ctx, phase) {
|
|
1226
|
+
const modules = await readDecomposedModules(ctx.artifactsDir);
|
|
1227
|
+
if (modules.length <= 1) {
|
|
1228
|
+
return writeContractPhaseStep(ctx, phase);
|
|
1229
|
+
}
|
|
1230
|
+
const inputArtifact = "module_decomposition";
|
|
1231
|
+
const inputPaths = ["goal_spec", "context_bundle", "module_decomposition"].map((key) => `- \`${ctx.artifactPaths[key]}\` (${key})`);
|
|
1232
|
+
const moduleLines = modules
|
|
1233
|
+
.map((mod, i) => {
|
|
1234
|
+
const shardPath = moduleShardPath(ctx.artifactsDir, phase, mod.name);
|
|
1235
|
+
const scope = mod.file_scope.length > 0
|
|
1236
|
+
? mod.file_scope.map((p) => `\`${p}\``).join(", ")
|
|
1237
|
+
: "_(no declared file scope)_";
|
|
1238
|
+
return `${i + 1}. **${mod.name}** — file scope: ${scope}\n - Write this module's contract to exactly: \`${shardPath}\``;
|
|
1239
|
+
})
|
|
1240
|
+
.join("\n");
|
|
1241
|
+
const perModuleSchema = `{
|
|
1194
1242
|
"name": "<module-name — must equal the assigned module>",
|
|
1195
1243
|
"inputs": ["<what this module receives>"],
|
|
1196
1244
|
"outputs": ["<what this module produces>"],
|
|
@@ -1200,10 +1248,10 @@ ${outputPaths.map((p, i) => `${i + 1}. \`${p}\` (${phases[i]})`).join("\n")}`;
|
|
|
1200
1248
|
"failure_modes": ["<ways this module can fail and how callers should handle them>"],
|
|
1201
1249
|
"neighbor_needs": [{ "neighbor": "<module-name>", "needs": "<what this module needs>" }]
|
|
1202
1250
|
}`;
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1251
|
+
const taskVerb = "draft its module contract";
|
|
1252
|
+
const cwdNote = `\n> Set the shell/tool working directory to \`${ctx.root}\` before running any commands.\n`;
|
|
1253
|
+
const nextCommand = loaderCommand("next-step");
|
|
1254
|
+
const prompt = `# Per-Module Contract Drafting (${modules.length} modules)
|
|
1207
1255
|
|
|
1208
1256
|
This phase publishes one bounded item per module. Complete all ${modules.length} items below; the host owns how they are grouped or executed. Each item reads only its module's file scope, then writes ONLY that module's contract shard — no item owns both sides of a seam, and no item writes the aggregated artifact.
|
|
1209
1257
|
${cwdNote}
|
|
@@ -1233,95 +1281,264 @@ The orchestrator verifies every module shard is present, merges them into \`${PH
|
|
|
1233
1281
|
|
|
1234
1282
|
**Stop after the per-module shards are written and you run next-step.** Do not edit source files. Do not write the aggregated artifact. Do not advance further.
|
|
1235
1283
|
`;
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1284
|
+
return writeCurrentStep({
|
|
1285
|
+
stepKind: CONTRACT_STEP_KIND,
|
|
1286
|
+
status: "ready",
|
|
1287
|
+
runId: ctx.runId,
|
|
1288
|
+
repoRoot: ctx.root,
|
|
1289
|
+
artifactsDir: ctx.artifactsDir,
|
|
1290
|
+
prompt,
|
|
1291
|
+
allowedCommands: [nextCommand],
|
|
1292
|
+
stopCondition: `Stop after writing every per-module shard for phase "${phase}" and running next-step.`,
|
|
1293
|
+
artifactPaths: contractStepArtifactPaths(ctx),
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* The ONE writer dispatch behind the scaffold's single emission call site. Each
|
|
1298
|
+
* underlying writer is reached from exactly here.
|
|
1299
|
+
*/
|
|
1300
|
+
async function writeContractStepPlan(ctx, plan) {
|
|
1301
|
+
switch (plan.via) {
|
|
1302
|
+
case "phase":
|
|
1303
|
+
return await writeContractPhaseStep(ctx, plan.phase, plan.extraSection);
|
|
1304
|
+
case "step":
|
|
1305
|
+
return await writeContractPromptStep(ctx, plan);
|
|
1306
|
+
case "blocked":
|
|
1307
|
+
return await writeContractBlockedStep(ctx, plan);
|
|
1308
|
+
case "module_wave":
|
|
1309
|
+
return await writeParallelModuleWaveStep(ctx, plan.phase);
|
|
1310
|
+
case "collapsed_framing":
|
|
1311
|
+
return await writeCollapsedFramingStep(ctx, plan.phases);
|
|
1312
|
+
case "rederive":
|
|
1313
|
+
// A deterministic artifact was just written; the frontier moved, so the
|
|
1314
|
+
// whole walk re-runs against the new state rather than guessing the phase.
|
|
1315
|
+
return await buildNextContractPipelineStep(ctx.options);
|
|
1316
|
+
case "pipeline_complete":
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
// ── Branch on `evaluated`: consuming the shared gate-outcome record ─────
|
|
1321
|
+
/**
|
|
1322
|
+
* Read every contract-pipeline payload from disk, plus the intake
|
|
1323
|
+
* finding-enumeration, in the shape the shared cross-gate evaluator consumes.
|
|
1324
|
+
* Always a fresh read — there is no payload cache to go stale.
|
|
1325
|
+
*/
|
|
1326
|
+
async function readCrossGateInputs(artifactsDir, root) {
|
|
1327
|
+
const payloads = new Map();
|
|
1328
|
+
for (const name of CP_ARTIFACT_NAMES) {
|
|
1329
|
+
const envelope = await readContractArtifact(artifactsDir, name);
|
|
1330
|
+
if (envelope)
|
|
1331
|
+
payloads.set(name, envelopePayload(envelope));
|
|
1332
|
+
}
|
|
1333
|
+
const findingEnumeration = await readOptionalJsonFile(intakePaths(artifactsDir).findingEnumeration);
|
|
1334
|
+
return { payloads, findingEnumeration, root };
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Read every contract-pipeline payload FRESH for the shared cross-gates.
|
|
1338
|
+
*
|
|
1339
|
+
* REFUSES before this invocation's ingestion + staleness-archive pass has run.
|
|
1340
|
+
* EVERY in-pipeline cross-gate read goes through here — including the two
|
|
1341
|
+
* exported helpers, which take the payloads this reader produced rather than
|
|
1342
|
+
* reading again — so there is no in-pipeline path to a payload that skipped the
|
|
1343
|
+
* check.
|
|
1344
|
+
* That is the freshness half of The branch-on-evaluated freshness rule, made mechanical: a
|
|
1345
|
+
* gate cannot be handed a payload snapshot taken before its own step archived
|
|
1346
|
+
* the stale copy, because the only way to obtain payloads declines to produce
|
|
1347
|
+
* them until `artifactsSettled` is set.
|
|
1348
|
+
*/
|
|
1349
|
+
async function readCrossGatePayloads(ctx) {
|
|
1350
|
+
if (!ctx.artifactsSettled) {
|
|
1351
|
+
throw new Error("contract pipeline: cross-gate payloads were requested before this invocation's " +
|
|
1352
|
+
"ingestion + staleness-archive pass ran. A gate must read artifact payloads AFTER " +
|
|
1353
|
+
"the archive pass, never from a snapshot taken before it.");
|
|
1354
|
+
}
|
|
1355
|
+
return await readCrossGateInputs(ctx.artifactsDir, ctx.root);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Consume a subset of the shared cross-gate outcomes, branching on `evaluated`
|
|
1359
|
+
* BEFORE an empty `issues` array is allowed to mean "clean".
|
|
1360
|
+
*
|
|
1361
|
+
* `required` is DECLARED PER CALL SITE, as data, because "did not run" means
|
|
1362
|
+
* different things at different boundaries. At a boundary whose upstream phase
|
|
1363
|
+
* order guarantees the gate's input exists, a skip is a refusal — its empty
|
|
1364
|
+
* issue list is proof of nothing. Earlier in the pipeline the same skip means
|
|
1365
|
+
* "not applicable yet", and the gate is simply not required there.
|
|
1366
|
+
*
|
|
1367
|
+
* THE UNCOVERED HALF, stated rather than implied: `digest_coverage` is the one
|
|
1368
|
+
* gate of the eight whose skip is a DOMAIN non-applicability (a source that is
|
|
1369
|
+
* not finding-enumerable) rather than a missing payload, so no boundary lists
|
|
1370
|
+
* it as required and a genuinely absent finding-enumeration file for an
|
|
1371
|
+
* enumerable source still skips silently. Closing that needs the gate module to
|
|
1372
|
+
* expose its enumerability predicate — an edit outside this work item's write
|
|
1373
|
+
* scope.
|
|
1374
|
+
*/
|
|
1375
|
+
export function consumeGateOutcomes(outcomes, selected, required) {
|
|
1376
|
+
const violations = [];
|
|
1377
|
+
for (const gate of selected) {
|
|
1378
|
+
const outcome = outcomes.find((candidate) => candidate.gate === gate);
|
|
1379
|
+
if (!outcome) {
|
|
1380
|
+
violations.push(`[${gate}] produced no outcome record; the gate set changed without this call site.`);
|
|
1381
|
+
continue;
|
|
1241
1382
|
}
|
|
1242
|
-
if (
|
|
1243
|
-
|
|
1244
|
-
|
|
1383
|
+
if (!outcome.evaluated) {
|
|
1384
|
+
if (required.has(gate)) {
|
|
1385
|
+
violations.push(`[${gate}] did not run (${outcome.reason ?? "no reason recorded"}). Its empty ` +
|
|
1386
|
+
`issue list is not proof of a clean gate at this boundary.`);
|
|
1387
|
+
}
|
|
1388
|
+
continue;
|
|
1245
1389
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
runId,
|
|
1250
|
-
repoRoot: root,
|
|
1251
|
-
artifactsDir,
|
|
1252
|
-
prompt,
|
|
1253
|
-
allowedCommands: [nextCommand],
|
|
1254
|
-
stopCondition: `Stop after writing every per-module shard for phase "${phase}" and running next-step.`,
|
|
1255
|
-
artifactPaths: stepArtifactPaths,
|
|
1256
|
-
});
|
|
1257
|
-
};
|
|
1258
|
-
/**
|
|
1259
|
-
* DC-3 merge intercept: when a parallel phase's aggregated artifact is still
|
|
1260
|
-
* missing, merge the per-module shards into it once they are ALL present. A
|
|
1261
|
-
* missing shard re-emits the wave (never promotes a partial aggregate). After
|
|
1262
|
-
* a complete merge the artifact is written enveloped and the pipeline
|
|
1263
|
-
* re-derives; the existing seam_reconciliation / critique pass downstream
|
|
1264
|
-
* stays the consistency gate over the merged contracts.
|
|
1265
|
-
*/
|
|
1266
|
-
const tryMergeModuleShards = async (phase) => {
|
|
1267
|
-
const modules = await readDecomposedModules(artifactsDir);
|
|
1268
|
-
// Degenerate decompositions never used the shard path — let the normal
|
|
1269
|
-
// single-agent aggregate step handle them.
|
|
1270
|
-
if (modules.length <= 1)
|
|
1271
|
-
return "incomplete";
|
|
1272
|
-
const scan = await scanModuleShards(artifactsDir, phase, modules);
|
|
1273
|
-
if (scan.missing.length > 0) {
|
|
1274
|
-
// Completeness not met → re-emit the wave for the missing modules.
|
|
1275
|
-
return buildParallelModuleWaveStep(phase);
|
|
1390
|
+
for (const issue of outcome.issues) {
|
|
1391
|
+
if (issue.severity === "error")
|
|
1392
|
+
violations.push(`[${issue.path}] ${issue.message}`);
|
|
1276
1393
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1394
|
+
}
|
|
1395
|
+
return { ok: violations.length === 0, violations };
|
|
1396
|
+
}
|
|
1397
|
+
/** Locate one gate's outcome in the canonical-order outcome list. */
|
|
1398
|
+
function gateOutcomeOf(outcomes, gate) {
|
|
1399
|
+
return outcomes.find((candidate) => candidate.gate === gate);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* The gates the PROMOTION boundary requires to have actually run. Every phase
|
|
1403
|
+
* artifact exists by the time `nextPhase` is null, so a skip here can only mean
|
|
1404
|
+
* a payload went missing or malformed — never "too early".
|
|
1405
|
+
*/
|
|
1406
|
+
const PROMOTION_REQUIRED_GATES = new Set([
|
|
1407
|
+
"paired_obligations",
|
|
1408
|
+
"evidence_threaded",
|
|
1409
|
+
"reconciliation_derivation",
|
|
1410
|
+
]);
|
|
1411
|
+
/** The subset of gates the promotion boundary consumes. */
|
|
1412
|
+
const PROMOTION_GATES = [
|
|
1413
|
+
"paired_obligations",
|
|
1414
|
+
"evidence_threaded",
|
|
1415
|
+
"digest_coverage",
|
|
1416
|
+
"reconciliation_derivation",
|
|
1417
|
+
];
|
|
1418
|
+
/**
|
|
1419
|
+
* The gates the PRE-CRITIC structural floor requires. `contract_finalization`,
|
|
1420
|
+
* `seam_reconciliation` and `test_validator_plan` all precede `critic` in the
|
|
1421
|
+
* phase order, so their artifacts exist by the time this boundary is reached.
|
|
1422
|
+
*/
|
|
1423
|
+
const PRE_CRITIC_REQUIRED_GATES = new Set([
|
|
1424
|
+
"paired_obligations",
|
|
1425
|
+
"reconciliation_derivation",
|
|
1426
|
+
]);
|
|
1427
|
+
// ── Gates, in execution order ─────────────────────────────────────────────────
|
|
1428
|
+
/**
|
|
1429
|
+
* Seed source-digest binding. Re-hash every source path the path_a seed
|
|
1430
|
+
* recorded, against the digest it recorded at seed-build time, and refuse with
|
|
1431
|
+
* a classified blocked step on a mismatch — rather than spending the whole
|
|
1432
|
+
* design pipeline on content that no longer holds the findings the seed
|
|
1433
|
+
* enumerates. Runs first, before anything is ingested or derived.
|
|
1434
|
+
*/
|
|
1435
|
+
const seedSourceDigestGate = async (ctx) => {
|
|
1436
|
+
if (!ctx.pathASeedPath)
|
|
1437
|
+
return null;
|
|
1438
|
+
const seed = await readOptionalJsonFile(ctx.pathASeedPath);
|
|
1439
|
+
const mismatches = await detectSeedSourceDigestMismatches(ctx.root, seed);
|
|
1440
|
+
if (mismatches.length === 0)
|
|
1441
|
+
return null;
|
|
1442
|
+
const lines = mismatches
|
|
1443
|
+
.map((mismatch) => `- \`${mismatch.path}\` — recorded \`${mismatch.expected.slice(0, 12)}…\`, ` +
|
|
1444
|
+
`now ${mismatch.actual ? `\`${mismatch.actual.slice(0, 12)}…\`` : "**unreadable**"}`)
|
|
1445
|
+
.join("\n");
|
|
1446
|
+
return {
|
|
1447
|
+
via: "blocked",
|
|
1448
|
+
prompt: `# Source Content Changed Since the Audit Seed Was Built
|
|
1449
|
+
|
|
1450
|
+
The path-A seed records a sha256 for every source it was built from. The following no longer match, so the findings this pipeline is designing against may no longer describe the code:
|
|
1451
|
+
|
|
1452
|
+
${lines}
|
|
1453
|
+
|
|
1454
|
+
The findings this pipeline is designing against were derived from the recorded content, so re-deriving them is the only thing that makes the design sound again. Decide with the user:
|
|
1455
|
+
|
|
1456
|
+
1. **Re-run the audit extraction** against the current tree, so the findings describe the code as it now stands; or
|
|
1457
|
+
2. **Restore the drifted sources** to the content the audit read, if the change was unintended.
|
|
1458
|
+
|
|
1459
|
+
Only as an explicit LAST resort — an accepted, recorded decision to design against findings that no longer match the code — delete \`${ctx.pathASeedPath}\` and re-run next-step. That rebuilds the seed from the CURRENT sources while keeping the OLD findings, which clears this alarm without re-deriving anything.`,
|
|
1460
|
+
stopCondition: "Stop — the contract pipeline is blocked on a source whose content no longer matches the audit seed.",
|
|
1289
1461
|
};
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1462
|
+
};
|
|
1463
|
+
/**
|
|
1464
|
+
* Ingest raw worker outputs into validated envelopes. An output that fails
|
|
1465
|
+
* validation is archived and its producing phase re-emitted with the validation
|
|
1466
|
+
* errors — LLM output is untrusted until validated.
|
|
1467
|
+
*/
|
|
1468
|
+
const invalidIngestionGate = async (ctx) => {
|
|
1469
|
+
const ingestion = await ingestContractArtifacts(ctx.artifactsDir);
|
|
1470
|
+
if (ingestion.invalid.length === 0)
|
|
1471
|
+
return null;
|
|
1472
|
+
const first = ingestion.invalid[0];
|
|
1473
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, first.name, "invalid", ctx.options.renameFn);
|
|
1474
|
+
return {
|
|
1475
|
+
via: "phase",
|
|
1476
|
+
phase: ARTIFACT_TO_PHASE[first.name] ?? "goal_normalization",
|
|
1477
|
+
extraSection: `## Validation Errors From the Previous Attempt
|
|
1299
1478
|
|
|
1300
1479
|
The previous \`${first.name}\` output failed validation and was archived. Fix every issue below in the rewritten output:
|
|
1301
1480
|
|
|
1302
1481
|
${formatValidationIssues(first.issues)}
|
|
1303
|
-
${rejectionRewriteInstruction(archived)}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1482
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1483
|
+
};
|
|
1484
|
+
};
|
|
1485
|
+
/**
|
|
1486
|
+
* Archive stale artifacts so the staleness DAG re-derives everything downstream
|
|
1487
|
+
* of a repaired (re-ingested) upstream artifact — and ABORT when an archive
|
|
1488
|
+
* fails.
|
|
1489
|
+
*
|
|
1490
|
+
* COR-114e4941: the returned ArchiveOutcome used to be discarded here, alone
|
|
1491
|
+
* among the four archive call sites. `originalFree: false` means the move
|
|
1492
|
+
* failed and the stale file is STILL at its canonical path, where
|
|
1493
|
+
* `contractArtifactExists` (a bare `existsSync`) reports it as present — so the
|
|
1494
|
+
* producing phase was never re-emitted and every downstream derivation (the
|
|
1495
|
+
* obligation ledger, the phase cut, the DAG) was built on content the staleness
|
|
1496
|
+
* DAG had already declared invalid. Refusing here is the only ordering that
|
|
1497
|
+
* keeps that impossible: the frontier is not resolved until every stale
|
|
1498
|
+
* artifact is genuinely out of the way.
|
|
1499
|
+
*/
|
|
1500
|
+
const staleArchiveGate = async (ctx) => {
|
|
1501
|
+
const staleness = await detectStaleArtifacts(ctx.artifactsDir);
|
|
1308
1502
|
for (const name of staleness.stale) {
|
|
1309
|
-
await archiveContractArtifact(artifactsDir, name, "stale");
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1503
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, name, "stale", ctx.options.renameFn);
|
|
1504
|
+
if (archived.originalFree)
|
|
1505
|
+
continue;
|
|
1506
|
+
const phase = ARTIFACT_TO_PHASE[name];
|
|
1507
|
+
if (phase) {
|
|
1508
|
+
return {
|
|
1509
|
+
via: "phase",
|
|
1510
|
+
phase,
|
|
1511
|
+
extraSection: `## A Stale \`${name}\` Could Not Be Archived
|
|
1512
|
+
|
|
1513
|
+
\`${name}\` is stale (an upstream it depends on changed) but the tool could not move it into the contract history directory, so the stale content is still at its canonical path. The pipeline will not derive anything downstream of it.
|
|
1514
|
+
|
|
1515
|
+
Rewrite \`${name}\` from its current upstreams.
|
|
1516
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
return {
|
|
1520
|
+
via: "blocked",
|
|
1521
|
+
prompt: `# A Stale Derived Artifact Could Not Be Archived
|
|
1522
|
+
|
|
1523
|
+
\`${name}\` is stale but could not be moved into the contract history directory, and it is tool-derived — no authoring phase owns it, so it cannot simply be re-emitted.
|
|
1524
|
+
|
|
1525
|
+
Remove or unlock \`${contractArtifactFilePath(ctx.artifactsDir, name)}\` (and its \`.input.json\` sibling if present), then re-run next-step so the pipeline re-derives it from the current upstreams. Proceeding on the stale copy would build the obligation ledger, phase cut and implementation DAG on content the staleness DAG has already declared invalid.`,
|
|
1526
|
+
stopCondition: "Stop — the contract pipeline is blocked on a stale artifact that could not be archived.",
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
// OBL-m-friction-inv-5 (post_repair_rederive): when a judge needs_repair →
|
|
1530
|
+
// regenerate-target landed, the re-ingested target makes its downstream
|
|
1531
|
+
// artifacts stale and they are archived above — the REAL remediate
|
|
1532
|
+
// post-repair re-derive site. Route this backend-observed step-boundary fact
|
|
1533
|
+
// through the single CE-005 chokepoint. Discriminator = repair target
|
|
1534
|
+
// artifact id + repair iteration count, so re-recording the same re-derive is
|
|
1535
|
+
// a collision-free no-op (CE-006).
|
|
1319
1536
|
if (staleness.stale.length > 0) {
|
|
1320
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1537
|
+
const repairState = await readRepairState(ctx.artifactsDir);
|
|
1321
1538
|
const lastRepair = repairState.repairs[repairState.repairs.length - 1];
|
|
1322
1539
|
if (lastRepair) {
|
|
1323
1540
|
const iteration = repairState.repairs.length;
|
|
1324
|
-
await captureStepBoundaryFriction(artifactsDir, runId, {
|
|
1541
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
1325
1542
|
eventType: "post_repair_rederive",
|
|
1326
1543
|
discriminator: `${lastRepair.target}:${iteration}`,
|
|
1327
1544
|
note: `Post-repair re-derive: repair iteration ${iteration} of "${lastRepair.target}" ` +
|
|
@@ -1331,151 +1548,175 @@ ${rejectionRewriteInstruction(archived)}`);
|
|
|
1331
1548
|
}, "remediate-code");
|
|
1332
1549
|
}
|
|
1333
1550
|
}
|
|
1334
|
-
|
|
1335
|
-
//
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1551
|
+
// Ingestion + archiving are done: payloads read from here on are this
|
|
1552
|
+
// invocation's own view. Nothing downstream may read them before this point.
|
|
1553
|
+
ctx.artifactsSettled = true;
|
|
1554
|
+
return null;
|
|
1555
|
+
};
|
|
1556
|
+
/**
|
|
1557
|
+
* Resolve the phase frontier — the one gate that never emits. It sits HERE, and
|
|
1558
|
+
* not at the top, because archiving a stale artifact re-opens its producing
|
|
1559
|
+
* phase: computing the frontier before the archive pass would read a phase as
|
|
1560
|
+
* satisfied by a file the pipeline has just declared invalid.
|
|
1561
|
+
*/
|
|
1562
|
+
const phaseFrontierGate = (ctx) => {
|
|
1563
|
+
ctx.nextPhase = nextMissingContractPhase(ctx.artifactsDir);
|
|
1564
|
+
return null;
|
|
1565
|
+
};
|
|
1566
|
+
/**
|
|
1567
|
+
* Goal-ID consistency (ARC-86b18f1b): every persisted artifact that carries a
|
|
1568
|
+
* goal_id must agree on the same value. A mismatch means two runs were
|
|
1569
|
+
* interleaved; re-emit the earliest mismatched phase so the worker can correct
|
|
1570
|
+
* it. Deliberately phase-independent.
|
|
1571
|
+
*/
|
|
1572
|
+
const goalIdConsistencyGate = async (ctx) => {
|
|
1573
|
+
const goalIdArtifacts = {};
|
|
1574
|
+
for (const name of CP_ARTIFACT_NAMES) {
|
|
1575
|
+
const envelope = await readContractArtifact(ctx.artifactsDir, name);
|
|
1576
|
+
if (envelope)
|
|
1577
|
+
goalIdArtifacts[name] = envelopePayload(envelope);
|
|
1578
|
+
}
|
|
1579
|
+
const goalIdErrors = validateGoalIdConsistency(goalIdArtifacts).filter((issue) => issue.severity === "error");
|
|
1580
|
+
if (goalIdErrors.length === 0)
|
|
1581
|
+
return null;
|
|
1582
|
+
// issue.path is "<artifact_name>.goal_id"; extract the artifact name.
|
|
1583
|
+
const firstPath = goalIdErrors[0]?.path ?? "";
|
|
1584
|
+
const mismatchedArtifact = firstPath.replace(/\.goal_id$/, "");
|
|
1585
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, mismatchedArtifact, "invalid", ctx.options.renameFn);
|
|
1586
|
+
return {
|
|
1587
|
+
via: "phase",
|
|
1588
|
+
phase: ARTIFACT_TO_PHASE[mismatchedArtifact] ?? "goal_normalization",
|
|
1589
|
+
extraSection: `## Goal-ID Consistency Error
|
|
1356
1590
|
|
|
1357
1591
|
Every contract-pipeline artifact must share the same goal_id. The following mismatch was detected:
|
|
1358
1592
|
|
|
1359
|
-
${goalIdErrors.map((
|
|
1593
|
+
${goalIdErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
1360
1594
|
|
|
1361
1595
|
Rewrite the output so its goal_id matches the goal_id established in goal_spec.json.
|
|
1362
|
-
${rejectionRewriteInstruction(archived)}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1596
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1597
|
+
};
|
|
1598
|
+
};
|
|
1599
|
+
/**
|
|
1600
|
+
* Finalized-module-SET gate (INV-CO-13). `deriveFinalizedModuleContracts` maps
|
|
1601
|
+
* the drafts 1:1, so the deterministic path can never violate this — but it is
|
|
1602
|
+
* not the only writer: a judge or critique repair re-emits contract_finalization
|
|
1603
|
+
* as an LLM step, ingested under a SHAPE-ONLY validator that structurally cannot
|
|
1604
|
+
* see the drafts. A rewrite that merges modules under an invented name and drops
|
|
1605
|
+
* another would otherwise be accepted, and the phase cut, the derived obligation
|
|
1606
|
+
* ids and the DAG write-scope join would all then be built on a module set that
|
|
1607
|
+
* has already lost a module.
|
|
1608
|
+
*
|
|
1609
|
+
* DELIBERATELY PHASE-INDEPENDENT, like the goal-ID gate: rewriting
|
|
1610
|
+
* finalized_module_contracts stales its declared dependent
|
|
1611
|
+
* conceptual_design_critique, which the staleness gate archives BEFORE the
|
|
1612
|
+
* frontier is resolved — so the phase right after a corrupting rewrite is
|
|
1613
|
+
* `critique`, not `critic`. Gating at the critic boundary would not fire until
|
|
1614
|
+
* critique, obligation_ledger, cyclic_seam_resolution, test_validator_plan and
|
|
1615
|
+
* assessment had all been re-spent on the collapsed set.
|
|
1616
|
+
*
|
|
1617
|
+
* The gate is NOT in `PRE_CRITIC_REQUIRED_GATES` / `PROMOTION_REQUIRED_GATES`
|
|
1618
|
+
* because at this phase-independent position a not-evaluated outcome genuinely
|
|
1619
|
+
* means "the drafted or finalized contracts do not exist yet" — the branch on
|
|
1620
|
+
* `evaluated` is taken, and its declared meaning here is "not yet applicable".
|
|
1621
|
+
*/
|
|
1622
|
+
const finalizedModuleSetGate = async (ctx) => {
|
|
1623
|
+
const outcomes = evaluateContractPipelineCrossGateOutcomes(await readCrossGatePayloads(ctx));
|
|
1624
|
+
const outcome = gateOutcomeOf(outcomes, "finalized_module_set_preserved");
|
|
1625
|
+
if (!outcome?.evaluated)
|
|
1626
|
+
return null;
|
|
1627
|
+
const moduleSetErrors = outcome.issues.filter((issue) => issue.severity === "error");
|
|
1628
|
+
if (moduleSetErrors.length === 0)
|
|
1629
|
+
return null;
|
|
1630
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, "finalized_module_contracts", "invalid", ctx.options.renameFn);
|
|
1631
|
+
return {
|
|
1632
|
+
via: "phase",
|
|
1633
|
+
phase: "contract_finalization",
|
|
1634
|
+
extraSection: `## Finalized Module Set Does Not Match the Drafted Contracts
|
|
1399
1635
|
|
|
1400
1636
|
Finalization carries every drafted module contract through — it may incorporate seam-reconciliation decisions into a module's interface, but it may never drop, merge, rename, or invent a module. The following mismatches were detected:
|
|
1401
1637
|
|
|
1402
1638
|
${moduleSetErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
1403
|
-
${rejectionRewriteInstruction(archived)}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1639
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1640
|
+
};
|
|
1641
|
+
};
|
|
1642
|
+
/**
|
|
1643
|
+
* Path-A overlap topology gate. A required audit seam is not advisory:
|
|
1644
|
+
* decomposition must name exactly one seam-preparation module and keep distinct
|
|
1645
|
+
* implementation modules for the participating work blocks. Checked before
|
|
1646
|
+
* module contracts fan out, so the seam is shaped once and downstream authors
|
|
1647
|
+
* can work in parallel against it.
|
|
1648
|
+
*/
|
|
1649
|
+
const workBlockSeamGate = async (ctx) => {
|
|
1650
|
+
if (!contractArtifactExists(ctx.artifactsDir, "module_decomposition"))
|
|
1651
|
+
return null;
|
|
1652
|
+
const seed = await readOptionalJsonFile(pathASeedFilePath(ctx.artifactsDir));
|
|
1653
|
+
if (!seed)
|
|
1654
|
+
return null;
|
|
1655
|
+
const decomposition = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_decomposition"));
|
|
1656
|
+
const seamIssues = validateWorkBlockSeamPreparation(seed, decomposition).filter((issue) => issue.severity === "error");
|
|
1657
|
+
if (seamIssues.length === 0)
|
|
1658
|
+
return null;
|
|
1659
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, "module_decomposition", "invalid", ctx.options.renameFn);
|
|
1660
|
+
return {
|
|
1661
|
+
via: "phase",
|
|
1662
|
+
phase: "decomposition",
|
|
1663
|
+
extraSection: `## Audit Work-Block Seam Errors
|
|
1420
1664
|
|
|
1421
1665
|
The module decomposition dropped or blurred required audit work-block seams. Fix every issue below. Keep implementation work blocks distinct, add exactly one seam-preparation module per required seam (one module may prepare several seams), and list the corresponding source_work_block_ids / prepares_seam_ids:
|
|
1422
1666
|
|
|
1423
1667
|
${seamIssues.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
1424
|
-
${rejectionRewriteInstruction(archived)}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
});
|
|
1448
|
-
repairState.critique_repairs = critiqueRepairs;
|
|
1449
|
-
await writeRepairState(artifactsDir, repairState);
|
|
1450
|
-
}
|
|
1451
|
-
const rendered = renderContractRepairPrompt({
|
|
1452
|
-
target: "finalized_module_contracts",
|
|
1453
|
-
instruction: "Revise the design to resolve every BLOCKING concern in the conceptual design critique " +
|
|
1454
|
-
`(${gate.blockingIds.join(", ")}). Read conceptual_design_critique.json for each concern's ` +
|
|
1455
|
-
"description, then rewrite the finalized module contracts so the blocking concerns no longer apply.",
|
|
1456
|
-
artifactPaths,
|
|
1457
|
-
repoRoot: root,
|
|
1458
|
-
});
|
|
1459
|
-
return buildStep({
|
|
1460
|
-
prompt: rendered.prompt,
|
|
1461
|
-
outputPath: rendered.outputPath,
|
|
1462
|
-
stopCondition: "Stop after rewriting finalized_module_contracts to resolve the blocking critique concerns and running next-step.",
|
|
1668
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1669
|
+
};
|
|
1670
|
+
};
|
|
1671
|
+
/**
|
|
1672
|
+
* Conceptual-design-critique gate (A1). Once the critique exists, a blocking
|
|
1673
|
+
* concern routes a design repair BEFORE any downstream artifact is derived. The
|
|
1674
|
+
* signal is mechanical (any blocking item), so the author's verdict label can't
|
|
1675
|
+
* wave a blocking concern through. Convergence-terminated: repairing the
|
|
1676
|
+
* finalized contracts re-stales + re-emits the critique, a clean re-critique
|
|
1677
|
+
* proceeds, a stalled loop escalates to the user.
|
|
1678
|
+
*/
|
|
1679
|
+
const conceptualCritiqueGate = async (ctx) => {
|
|
1680
|
+
if (!contractArtifactExists(ctx.artifactsDir, "conceptual_design_critique"))
|
|
1681
|
+
return null;
|
|
1682
|
+
const gate = await evaluateCritiqueGate(ctx.artifactsDir);
|
|
1683
|
+
if (gate.kind === "repair") {
|
|
1684
|
+
const repairState = await readRepairState(ctx.artifactsDir);
|
|
1685
|
+
const critiqueRepairs = repairState.critique_repairs ?? [];
|
|
1686
|
+
if (!critiqueRepairs.some((repair) => repair.critique_hash === gate.critiqueHash)) {
|
|
1687
|
+
critiqueRepairs.push({
|
|
1688
|
+
critique_hash: gate.critiqueHash,
|
|
1689
|
+
at: new Date().toISOString(),
|
|
1690
|
+
blocking_ids: gate.blockingIds,
|
|
1463
1691
|
});
|
|
1692
|
+
repairState.critique_repairs = critiqueRepairs;
|
|
1693
|
+
await writeRepairState(ctx.artifactsDir, repairState);
|
|
1464
1694
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1695
|
+
const rendered = renderContractRepairPrompt({
|
|
1696
|
+
target: "finalized_module_contracts",
|
|
1697
|
+
instruction: "Revise the design to resolve every BLOCKING concern in the conceptual design critique " +
|
|
1698
|
+
`(${gate.blockingIds.join(", ")}). Read conceptual_design_critique.json for each concern's ` +
|
|
1699
|
+
"description, then rewrite the finalized module contracts so the blocking concerns no longer apply.",
|
|
1700
|
+
artifactPaths: ctx.artifactPaths,
|
|
1701
|
+
repoRoot: ctx.root,
|
|
1702
|
+
});
|
|
1703
|
+
return {
|
|
1704
|
+
via: "step",
|
|
1705
|
+
prompt: rendered.prompt,
|
|
1706
|
+
outputPath: rendered.outputPath,
|
|
1707
|
+
stopCondition: "Stop after rewriting finalized_module_contracts to resolve the blocking critique concerns and running next-step.",
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
if (gate.kind === "escalate") {
|
|
1711
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
1712
|
+
eventType: "repair_round",
|
|
1713
|
+
discriminator: `critique_nonconvergence:${gate.reason}`,
|
|
1714
|
+
note: `Conceptual-design critique↔repair loop escalated (${gate.reason}): ${gate.note}`,
|
|
1715
|
+
category: "trap",
|
|
1716
|
+
}, "remediate-code");
|
|
1717
|
+
return {
|
|
1718
|
+
via: "blocked",
|
|
1719
|
+
prompt: `# Conceptual-Design Critique Did Not Converge
|
|
1479
1720
|
|
|
1480
1721
|
${gate.note}
|
|
1481
1722
|
|
|
@@ -1484,372 +1725,350 @@ ${gate.note}
|
|
|
1484
1725
|
${gate.blocking.map((id) => `- ${id}`).join("\n")}
|
|
1485
1726
|
|
|
1486
1727
|
Read conceptual_design_critique.json, decide with the user how to resolve each blocking concern (revise the contract design and re-run, or downgrade it to advisory), then re-run next-step.`,
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
});
|
|
1490
|
-
}
|
|
1491
|
-
// gate.kind === "proceed": fall through.
|
|
1492
|
-
}
|
|
1493
|
-
// 2.8. Deterministic artifact derivation (S1, contract-authoring determinism).
|
|
1494
|
-
// The obligation ledger is a pure function of the finalized module
|
|
1495
|
-
// contracts (every invariant/failure mode/module → an obligation), so it
|
|
1496
|
-
// is generated by the tool rather than authored by an LLM phase: the
|
|
1497
|
-
// structure can never be malformed, no judgment is spent on a mechanical
|
|
1498
|
-
// restructuring, and a weak model is never asked to emit it from scratch.
|
|
1499
|
-
// Mirrors the cyclic_seam no-cycles fast path — write the artifact, then
|
|
1500
|
-
// re-derive the next phase.
|
|
1501
|
-
if (nextPhase === "obligation_ledger") {
|
|
1502
|
-
const finalizedPayload = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
1503
|
-
const ledger = deriveObligationLedger(finalizedPayload);
|
|
1504
|
-
await writeDerivedContractArtifact(artifactsDir, "obligation_ledger", ledger);
|
|
1505
|
-
return buildNextContractPipelineStep(options);
|
|
1506
|
-
}
|
|
1507
|
-
// 2.9. Degenerate seam_reconciliation collapse. A single-module decomposition
|
|
1508
|
-
// has NO inter-module seams, so seam_reconciliation is a structural no-op:
|
|
1509
|
-
// write an empty seam report deterministically (no host round-trip),
|
|
1510
|
-
// mirroring the obligation_ledger / cyclic_seam no-op fast paths. The empty
|
|
1511
|
-
// report makes validateReconciliationDerivation pass vacuously. A
|
|
1512
|
-
// multi-module decomposition falls through to the LLM seam_reconciliation
|
|
1513
|
-
// step (which mismatches exist is a judgment call).
|
|
1514
|
-
if (nextPhase === "seam_reconciliation") {
|
|
1515
|
-
const modules = await readDecomposedModules(artifactsDir);
|
|
1516
|
-
if (modules.length <= 1) {
|
|
1517
|
-
const drafted = envelopePayload(await readContractArtifact(artifactsDir, "module_contracts"));
|
|
1518
|
-
const goalId = isRecord(drafted) && typeof drafted.goal_id === "string" ? drafted.goal_id : "";
|
|
1519
|
-
await writeDerivedContractArtifact(artifactsDir, "seam_reconciliation_report", {
|
|
1520
|
-
contract_version: "remediate-code-contract-pipeline/seam-reconciliation-report/v1alpha1",
|
|
1521
|
-
goal_id: goalId,
|
|
1522
|
-
mismatches: [],
|
|
1523
|
-
created_at: new Date().toISOString(),
|
|
1524
|
-
});
|
|
1525
|
-
return buildNextContractPipelineStep(options);
|
|
1526
|
-
}
|
|
1728
|
+
stopCondition: "Stop — the contract pipeline is blocked on a non-converging conceptual-design critique pending a user decision.",
|
|
1729
|
+
};
|
|
1527
1730
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1731
|
+
return null;
|
|
1732
|
+
};
|
|
1733
|
+
/**
|
|
1734
|
+
* Deterministic obligation-ledger derivation (S1). The ledger is a pure function
|
|
1735
|
+
* of the finalized module contracts (every invariant/failure mode/module → an
|
|
1736
|
+
* obligation), so the tool generates it rather than an LLM phase: the structure
|
|
1737
|
+
* can never be malformed, no judgment is spent on a mechanical restructuring,
|
|
1738
|
+
* and a weak model is never asked to emit it from scratch.
|
|
1739
|
+
*/
|
|
1740
|
+
const obligationLedgerDerivationGate = async (ctx) => {
|
|
1741
|
+
if (ctx.nextPhase !== "obligation_ledger")
|
|
1742
|
+
return null;
|
|
1743
|
+
const finalizedPayload = envelopePayload(await readContractArtifact(ctx.artifactsDir, "finalized_module_contracts"));
|
|
1744
|
+
await writeDerivedContractArtifact(ctx.artifactsDir, "obligation_ledger", deriveObligationLedger(finalizedPayload));
|
|
1745
|
+
return { via: "rederive" };
|
|
1746
|
+
};
|
|
1747
|
+
/**
|
|
1748
|
+
* Degenerate seam_reconciliation collapse. A single-module decomposition has NO
|
|
1749
|
+
* inter-module seams, so seam_reconciliation is a structural no-op: write an
|
|
1750
|
+
* empty seam report deterministically (no host round-trip). The empty report
|
|
1751
|
+
* makes validateReconciliationDerivation pass vacuously. A multi-module
|
|
1752
|
+
* decomposition falls through to the LLM seam_reconciliation step (which
|
|
1753
|
+
* mismatches exist is a judgment call).
|
|
1754
|
+
*/
|
|
1755
|
+
const degenerateSeamReconciliationGate = async (ctx) => {
|
|
1756
|
+
if (ctx.nextPhase !== "seam_reconciliation")
|
|
1757
|
+
return null;
|
|
1758
|
+
const modules = await readDecomposedModules(ctx.artifactsDir);
|
|
1759
|
+
if (modules.length > 1)
|
|
1760
|
+
return null;
|
|
1761
|
+
const drafted = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_contracts"));
|
|
1762
|
+
const goalId = isRecord(drafted) && typeof drafted.goal_id === "string" ? drafted.goal_id : "";
|
|
1763
|
+
await writeDerivedContractArtifact(ctx.artifactsDir, "seam_reconciliation_report", {
|
|
1764
|
+
contract_version: "remediate-code-contract-pipeline/seam-reconciliation-report/v1alpha1",
|
|
1765
|
+
goal_id: goalId,
|
|
1766
|
+
mismatches: [],
|
|
1767
|
+
created_at: new Date().toISOString(),
|
|
1768
|
+
});
|
|
1769
|
+
return { via: "rederive" };
|
|
1770
|
+
};
|
|
1771
|
+
/**
|
|
1772
|
+
* Deterministic contract_finalization (all module counts). Finalization is a
|
|
1773
|
+
* mechanical merge, not fresh authoring: carry each drafted module contract
|
|
1774
|
+
* verbatim (preserving neighbor_needs for the ordering derivation) and attach
|
|
1775
|
+
* the agreed_interface of every seam that touches the module as a
|
|
1776
|
+
* seam_adjustment. The judgment already happened at seam_reconciliation.
|
|
1777
|
+
* Attaching each agreed interface verbatim guarantees the INV-CO-12
|
|
1778
|
+
* reconciliation-derivation gate passes. A downstream gate that still finds the
|
|
1779
|
+
* merge inadequate re-emits contract_finalization as an LLM step — the only path
|
|
1780
|
+
* that still needs judgment.
|
|
1781
|
+
*/
|
|
1782
|
+
const contractFinalizationDerivationGate = async (ctx) => {
|
|
1783
|
+
if (ctx.nextPhase !== "contract_finalization")
|
|
1784
|
+
return null;
|
|
1785
|
+
const drafted = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_contracts"));
|
|
1786
|
+
const seamReport = envelopePayload(await readContractArtifact(ctx.artifactsDir, "seam_reconciliation_report"));
|
|
1787
|
+
await writeDerivedContractArtifact(ctx.artifactsDir, "finalized_module_contracts", deriveFinalizedModuleContracts(drafted, seamReport));
|
|
1788
|
+
return { via: "rederive" };
|
|
1789
|
+
};
|
|
1790
|
+
/**
|
|
1791
|
+
* Judge gate: implementation planning is reachable only through an approved
|
|
1792
|
+
* verdict (the fixpoint) or a convergent targeted repair. A stalled /
|
|
1793
|
+
* non-converging repair loop escalates to the user (blocked) instead of silently
|
|
1794
|
+
* proceeding with residual risk.
|
|
1795
|
+
*/
|
|
1796
|
+
const judgeRepairGate = async (ctx) => {
|
|
1797
|
+
if (ctx.nextPhase !== "implementation_planning")
|
|
1798
|
+
return null;
|
|
1799
|
+
const gate = await evaluateJudgeGate(ctx.artifactsDir);
|
|
1800
|
+
if (gate.kind === "repair") {
|
|
1801
|
+
const repairTarget = gate.directive.target;
|
|
1802
|
+
const repairState = await readRepairState(ctx.artifactsDir);
|
|
1803
|
+
if (!repairState.repairs.some((repair) => repair.judge_hash === gate.judgeHash)) {
|
|
1804
|
+
repairState.repairs.push({
|
|
1805
|
+
judge_hash: gate.judgeHash,
|
|
1566
1806
|
target: repairTarget,
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
});
|
|
1571
|
-
return buildStep({
|
|
1572
|
-
prompt: rendered.prompt,
|
|
1573
|
-
outputPath: rendered.outputPath,
|
|
1574
|
-
stopCondition: `Stop after rewriting "${repairTarget}" per the judge repair directive and running next-step.`,
|
|
1807
|
+
at: new Date().toISOString(),
|
|
1808
|
+
accepted_ce_ids: gate.acceptedCeIds,
|
|
1809
|
+
addressed_ce_fingerprints: gate.addressedCeFingerprints,
|
|
1575
1810
|
});
|
|
1811
|
+
await writeRepairState(ctx.artifactsDir, repairState);
|
|
1576
1812
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1813
|
+
const rendered = renderContractRepairPrompt({
|
|
1814
|
+
target: repairTarget,
|
|
1815
|
+
instruction: gate.directive.instruction,
|
|
1816
|
+
artifactPaths: ctx.artifactPaths,
|
|
1817
|
+
repoRoot: ctx.root,
|
|
1818
|
+
});
|
|
1819
|
+
return {
|
|
1820
|
+
via: "step",
|
|
1821
|
+
prompt: rendered.prompt,
|
|
1822
|
+
outputPath: rendered.outputPath,
|
|
1823
|
+
stopCondition: `Stop after rewriting "${repairTarget}" per the judge repair directive and running next-step.`,
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
if (gate.kind === "escalate") {
|
|
1827
|
+
// Non-convergence (stall or runaway backstop): surface it to the user loudly
|
|
1828
|
+
// rather than promoting a plan over an un-converged contract. The
|
|
1829
|
+
// outstanding accepted counterexamples are named so the user can resolve
|
|
1830
|
+
// them (revise the contract design or accept them as known limitations).
|
|
1831
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
1832
|
+
eventType: "repair_round",
|
|
1833
|
+
discriminator: `judge_nonconvergence:${gate.reason}`,
|
|
1834
|
+
note: `Judge↔repair loop escalated (${gate.reason}): ${gate.note}`,
|
|
1835
|
+
category: "trap",
|
|
1836
|
+
}, "remediate-code");
|
|
1837
|
+
return {
|
|
1838
|
+
via: "blocked",
|
|
1839
|
+
prompt: `# Judge↔Repair Loop Did Not Converge
|
|
1595
1840
|
|
|
1596
1841
|
${gate.note}
|
|
1597
1842
|
|
|
1598
1843
|
## Outstanding accepted counterexamples
|
|
1599
1844
|
|
|
1600
1845
|
${gate.outstanding.length > 0
|
|
1601
|
-
|
|
1602
|
-
|
|
1846
|
+
? gate.outstanding.map((id) => `- ${id}`).join("\n")
|
|
1847
|
+
: "_(none newly accepted this round)_"}
|
|
1603
1848
|
|
|
1604
1849
|
Read the judge_report and counterexample artifacts, decide with the user how to resolve each outstanding counterexample (revise the contract design and re-run, or accept it as a known limitation), then re-run next-step.`,
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const judgePayload = envelopePayload(await readContractArtifact(artifactsDir, "judge_report"));
|
|
1621
|
-
const integrityIssues = validateImplementationDAGIntegrity(dagPayload, ledgerPayload, cePayload, judgePayload);
|
|
1622
|
-
const integrityErrors = integrityIssues.filter((i) => i.severity === "error");
|
|
1623
|
-
if (integrityErrors.length > 0) {
|
|
1624
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1625
|
-
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1626
|
-
return writeCurrentStep({
|
|
1627
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1628
|
-
status: "blocked",
|
|
1629
|
-
runId,
|
|
1630
|
-
repoRoot: root,
|
|
1631
|
-
artifactsDir,
|
|
1632
|
-
prompt: `# Implementation DAG Failed Referential Integrity ${repairState.dag_regenerations.length + 1} Times
|
|
1633
|
-
|
|
1634
|
-
The implementation_dag repeatedly contains referential integrity or coverage violations:
|
|
1635
|
-
|
|
1636
|
-
${integrityErrors.map((i) => `- [${i.path}] ${i.message}`).join("\n")}
|
|
1637
|
-
|
|
1638
|
-
Report this to the user and stop. The contract pipeline cannot promote a plan with integrity violations; the run needs a corrected implementation_dag or obligation_ledger.
|
|
1639
|
-
`,
|
|
1640
|
-
allowedCommands: [],
|
|
1641
|
-
stopCondition: "Stop after reporting the integrity failure to the user.",
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
repairState.dag_regenerations.push({
|
|
1645
|
-
violations: integrityErrors.map((i) => i.message),
|
|
1646
|
-
at: new Date().toISOString(),
|
|
1647
|
-
});
|
|
1648
|
-
await writeRepairState(artifactsDir, repairState);
|
|
1649
|
-
const archived = await archiveContractArtifact(artifactsDir, "implementation_dag", "invalid");
|
|
1650
|
-
return buildPhaseStep("implementation_planning", `## Referential Integrity Errors From the Previous Attempt
|
|
1850
|
+
stopCondition: "Stop — the contract pipeline is blocked on a non-converging judge↔repair loop pending a user decision.",
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
return null;
|
|
1854
|
+
};
|
|
1855
|
+
/** Bounded re-emit of implementation_planning, then blocked — the shared shape
|
|
1856
|
+
* of the four promotion rejections (integrity, traceability, obligation gates,
|
|
1857
|
+
* citation grounding). Single-sourced so the four cannot drift into four
|
|
1858
|
+
* different recovery contracts. */
|
|
1859
|
+
async function dagRegenerationPlan(ctx, params) {
|
|
1860
|
+
const repairState = await readRepairState(ctx.artifactsDir);
|
|
1861
|
+
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1862
|
+
return {
|
|
1863
|
+
via: "blocked",
|
|
1864
|
+
prompt: `# ${params.heading} ${repairState.dag_regenerations.length + 1} Times
|
|
1651
1865
|
|
|
1652
|
-
|
|
1866
|
+
${params.blockedBody}
|
|
1653
1867
|
|
|
1654
|
-
${
|
|
1655
|
-
${rejectionRewriteInstruction(archived)}`);
|
|
1656
|
-
}
|
|
1657
|
-
const traceability = await validateImplementationDagTraceability(artifactsDir);
|
|
1658
|
-
if (!traceability.ok) {
|
|
1659
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1660
|
-
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1661
|
-
return writeCurrentStep({
|
|
1662
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1663
|
-
status: "blocked",
|
|
1664
|
-
runId,
|
|
1665
|
-
repoRoot: root,
|
|
1666
|
-
artifactsDir,
|
|
1667
|
-
prompt: `# Implementation DAG Failed Traceability ${repairState.dag_regenerations.length + 1} Times
|
|
1668
|
-
|
|
1669
|
-
The implementation_dag repeatedly contains nodes that trace to no obligation and no judge-accepted counterexample:
|
|
1670
|
-
|
|
1671
|
-
${traceability.violations.map((v) => `- ${v}`).join("\n")}
|
|
1672
|
-
|
|
1673
|
-
Report this to the user and stop. The contract pipeline cannot promote an untraceable plan; the run needs a corrected goal/design or manual intervention.
|
|
1868
|
+
${params.violations.map((violation) => `- ${violation}`).join("\n")}
|
|
1674
1869
|
`,
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
${traceability.violations.map((v) => `- ${v}`).join("\n")}
|
|
1690
|
-
${rejectionRewriteInstruction(archived)}`);
|
|
1691
|
-
}
|
|
1692
|
-
// 4c. Contract-obligations promotion gates (CP-BLOCK-N-contract-obligations):
|
|
1693
|
-
// fail-closed cross-artifact checks that must pass before a plan is
|
|
1694
|
-
// promoted — paired obligations, evidence threading, source-scoped
|
|
1695
|
-
// digest coverage, and INV-CO-12 reconciliation derivation. Reuses the
|
|
1696
|
-
// dag_regenerations cap: bounded re-emit of implementation_planning, then
|
|
1697
|
-
// blocked. These are the invariants that keep the workflow correct
|
|
1698
|
-
// regardless of host strength, so they are enforced here, never left to
|
|
1699
|
-
// host discretion.
|
|
1700
|
-
const obligationGate = await evaluateContractObligationsPromotionGate(artifactsDir);
|
|
1701
|
-
if (!obligationGate.ok) {
|
|
1702
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1703
|
-
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1704
|
-
return writeCurrentStep({
|
|
1705
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1706
|
-
status: "blocked",
|
|
1707
|
-
runId,
|
|
1708
|
-
repoRoot: root,
|
|
1709
|
-
artifactsDir,
|
|
1710
|
-
prompt: `# Contract-Obligation Gates Failed ${repairState.dag_regenerations.length + 1} Times
|
|
1711
|
-
|
|
1712
|
-
The contract-obligation promotion gates repeatedly failed and the plan cannot be promoted:
|
|
1713
|
-
|
|
1714
|
-
${obligationGate.violations.map((v) => `- ${v}`).join("\n")}
|
|
1715
|
-
|
|
1716
|
-
Report this to the user and stop. The contract pipeline cannot promote a plan that drops obligation coverage, evidence, or a reconciled seam; the run needs a corrected obligation_ledger, test_validator_plan, finalized_module_contracts, or implementation_dag.
|
|
1717
|
-
`,
|
|
1718
|
-
allowedCommands: [],
|
|
1719
|
-
stopCondition: "Stop after reporting the contract-obligation gate failure to the user.",
|
|
1720
|
-
});
|
|
1721
|
-
}
|
|
1722
|
-
repairState.dag_regenerations.push({
|
|
1723
|
-
violations: obligationGate.violations,
|
|
1724
|
-
at: new Date().toISOString(),
|
|
1725
|
-
});
|
|
1726
|
-
await writeRepairState(artifactsDir, repairState);
|
|
1727
|
-
const archived = await archiveContractArtifact(artifactsDir, "implementation_dag", "invalid");
|
|
1728
|
-
return buildPhaseStep("implementation_planning", `## Contract-Obligation Gate Errors From the Previous Attempt
|
|
1729
|
-
|
|
1730
|
-
The previous implementation_dag (and/or upstream contract artifacts) failed the fail-closed contract-obligation gates. Fix every issue below before the plan can be promoted:
|
|
1731
|
-
|
|
1732
|
-
${obligationGate.violations.map((v) => `- ${v}`).join("\n")}
|
|
1733
|
-
${rejectionRewriteInstruction(archived)}`);
|
|
1734
|
-
}
|
|
1735
|
-
await promoteImplementationDagToExtractedPlan(artifactsDir);
|
|
1736
|
-
// 4d. M-B3 source-grounded citation gate (promotion backstop): ground every
|
|
1737
|
-
// promoted extracted-plan finding's citations against the working tree.
|
|
1738
|
-
// A finding citing only a non-existent path and no real symbol is a
|
|
1739
|
-
// hallucinated citation; re-emit implementation_planning (bounded by the
|
|
1740
|
-
// same dag_regenerations cap). Fail-closed only on an unreadable tree.
|
|
1741
|
-
const citationGate = await evaluatePromotedPlanCitationGrounding(artifactsDir, root);
|
|
1742
|
-
if (citationGate) {
|
|
1743
|
-
// Grounding failed: the plan was promoted to extracted-plan.json BEFORE this
|
|
1744
|
-
// gate ran, so the ungrounded marker is now on disk. Remove it before any
|
|
1745
|
-
// return — otherwise a subsequent next-step reads the promoted plan via
|
|
1746
|
-
// readExtractedPlanIfPresent and hands it straight to handlePendingExtractedPlan,
|
|
1747
|
-
// bypassing the re-emit and completing the pipeline on hallucinated citations.
|
|
1748
|
-
// No pipelineComplete unless the promoted plan grounds.
|
|
1749
|
-
await rm(intakePaths(artifactsDir).extractedPlan, { force: true });
|
|
1750
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1751
|
-
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1752
|
-
return writeCurrentStep({
|
|
1753
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1754
|
-
status: "blocked",
|
|
1755
|
-
runId,
|
|
1756
|
-
repoRoot: root,
|
|
1757
|
-
artifactsDir,
|
|
1758
|
-
prompt: `# Citation Grounding Failed ${repairState.dag_regenerations.length + 1} Times
|
|
1759
|
-
|
|
1760
|
-
The promoted plan repeatedly cites components that do not exist in the working tree:
|
|
1761
|
-
|
|
1762
|
-
${citationGate.violations.map((v) => `- ${v}`).join("\n")}
|
|
1763
|
-
|
|
1764
|
-
Report this to the user and stop. The contract pipeline cannot promote a plan whose findings cite non-existent files or symbols; the run needs a corrected implementation_dag.
|
|
1765
|
-
`,
|
|
1766
|
-
allowedCommands: [],
|
|
1767
|
-
stopCondition: "Stop after reporting the citation-grounding failure to the user.",
|
|
1768
|
-
});
|
|
1769
|
-
}
|
|
1770
|
-
repairState.dag_regenerations.push({
|
|
1771
|
-
violations: citationGate.violations,
|
|
1772
|
-
at: new Date().toISOString(),
|
|
1773
|
-
});
|
|
1774
|
-
await writeRepairState(artifactsDir, repairState);
|
|
1775
|
-
const archived = await archiveContractArtifact(artifactsDir, "implementation_dag", "invalid");
|
|
1776
|
-
// The grounding-driven re-emit is a backend-observed step-boundary fact:
|
|
1777
|
-
// route it through the single CE-005 chokepoint as phase_reemit.
|
|
1778
|
-
await captureStepBoundaryFriction(artifactsDir, runId, {
|
|
1779
|
-
eventType: "phase_reemit",
|
|
1780
|
-
discriminator: "implementation_planning:citation_grounding:promotion",
|
|
1781
|
-
note: "implementation_planning re-emitted: a promoted plan finding cited a " +
|
|
1782
|
-
"component that does not exist in the working tree (M-B3 citation grounding).",
|
|
1783
|
-
category: "trap",
|
|
1784
|
-
}, "remediate-code");
|
|
1785
|
-
return buildPhaseStep("implementation_planning", `## Source-Grounded Citation Gate Errors From the Previous Attempt
|
|
1786
|
-
|
|
1787
|
-
The previous implementation_dag produced findings that cite components not present in the working tree. Every cited path or symbol must point at something real:
|
|
1870
|
+
stopCondition: `Stop after reporting the failure to the user.`,
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
repairState.dag_regenerations.push({
|
|
1874
|
+
violations: params.violations,
|
|
1875
|
+
at: new Date().toISOString(),
|
|
1876
|
+
});
|
|
1877
|
+
await writeRepairState(ctx.artifactsDir, repairState);
|
|
1878
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, "implementation_dag", "invalid", ctx.options.renameFn);
|
|
1879
|
+
return {
|
|
1880
|
+
via: "phase",
|
|
1881
|
+
phase: "implementation_planning",
|
|
1882
|
+
extraSection: `${params.reEmitBody}
|
|
1788
1883
|
|
|
1789
|
-
${
|
|
1790
|
-
${rejectionRewriteInstruction(archived)}
|
|
1791
|
-
|
|
1884
|
+
${params.violations.map((violation) => `- ${violation}`).join("\n")}
|
|
1885
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* All phases exist: enforce referential integrity, traceability and the
|
|
1890
|
+
* fail-closed contract-obligation gates, then convert the implementation_dag
|
|
1891
|
+
* into an extracted plan and ground its citations.
|
|
1892
|
+
*/
|
|
1893
|
+
const implementationPlanPromotionGate = async (ctx) => {
|
|
1894
|
+
if (ctx.nextPhase)
|
|
1792
1895
|
return null;
|
|
1896
|
+
// DAG referential integrity + bidirectional coverage (ARC-86b18f1b-2), run
|
|
1897
|
+
// before the traceability check so specific referential violations are
|
|
1898
|
+
// reported first (traceability is a superset check).
|
|
1899
|
+
const outcomes = evaluateContractPipelineCrossGateOutcomes(await readCrossGatePayloads(ctx));
|
|
1900
|
+
const integrity = gateOutcomeOf(outcomes, "implementation_dag_integrity");
|
|
1901
|
+
if (!integrity?.evaluated) {
|
|
1902
|
+
return await dagRegenerationPlan(ctx, {
|
|
1903
|
+
heading: "Implementation DAG Could Not Be Checked",
|
|
1904
|
+
blockedBody: "The implementation_dag integrity gate could not run, so its empty issue list proves nothing:",
|
|
1905
|
+
reEmitBody: `## The Implementation DAG Could Not Be Checked
|
|
1906
|
+
|
|
1907
|
+
The referential-integrity gate could not run against the previous output, so it was never shown to be sound. Rewrite a complete implementation_dag:`,
|
|
1908
|
+
violations: [integrity?.reason ?? "no outcome record was produced for the gate"],
|
|
1909
|
+
});
|
|
1793
1910
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1911
|
+
const integrityErrors = integrity.issues.filter((issue) => issue.severity === "error");
|
|
1912
|
+
if (integrityErrors.length > 0) {
|
|
1913
|
+
return await dagRegenerationPlan(ctx, {
|
|
1914
|
+
heading: "Implementation DAG Failed Referential Integrity",
|
|
1915
|
+
blockedBody: "The implementation_dag repeatedly contains referential integrity or coverage violations:",
|
|
1916
|
+
reEmitBody: `## Referential Integrity Errors From the Previous Attempt
|
|
1917
|
+
|
|
1918
|
+
The previous implementation_dag was rejected and archived due to referential integrity violations. Fix every issue below:`,
|
|
1919
|
+
violations: integrityErrors.map((issue) => `[${issue.path}] ${issue.message}`),
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
const traceability = await validateImplementationDagTraceability(ctx.artifactsDir);
|
|
1923
|
+
if (!traceability.ok) {
|
|
1924
|
+
return await dagRegenerationPlan(ctx, {
|
|
1925
|
+
heading: "Implementation DAG Failed Traceability",
|
|
1926
|
+
blockedBody: "The implementation_dag repeatedly contains nodes that trace to no obligation and no judge-accepted counterexample:",
|
|
1927
|
+
reEmitBody: `## Traceability Errors From the Previous Attempt
|
|
1928
|
+
|
|
1929
|
+
The previous implementation_dag was rejected and archived. Every node must trace to at least one obligation from the obligation ledger or one judge-accepted counterexample:`,
|
|
1930
|
+
violations: traceability.violations,
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
// Contract-obligations promotion gates: fail-closed cross-artifact checks that
|
|
1934
|
+
// must pass before a plan is promoted. These are the invariants that keep the
|
|
1935
|
+
// workflow correct regardless of host strength, so they are enforced here,
|
|
1936
|
+
// never left to host discretion.
|
|
1937
|
+
const obligationGate = await evaluateContractObligationsPromotionGate(ctx.artifactsDir, ctx.root, await readCrossGatePayloads(ctx));
|
|
1938
|
+
if (!obligationGate.ok) {
|
|
1939
|
+
return await dagRegenerationPlan(ctx, {
|
|
1940
|
+
heading: "Contract-Obligation Gates Failed",
|
|
1941
|
+
blockedBody: "The contract-obligation promotion gates repeatedly failed and the plan cannot be promoted:",
|
|
1942
|
+
reEmitBody: `## Contract-Obligation Gate Errors From the Previous Attempt
|
|
1943
|
+
|
|
1944
|
+
The previous implementation_dag (and/or upstream contract artifacts) failed the fail-closed contract-obligation gates. Fix every issue below before the plan can be promoted:`,
|
|
1945
|
+
violations: obligationGate.violations,
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
// Write-scope + command SHAPE, before anything is promoted. These refusals
|
|
1949
|
+
// used to throw out of the promoter — an unclassified stack that wedged every
|
|
1950
|
+
// subsequent next-step, reachable from an LLM form as ordinary as a
|
|
1951
|
+
// leading-slash "repo-relative" path. They take the same bounded re-emit as
|
|
1952
|
+
// every other promotion rejection now.
|
|
1953
|
+
const scopeRefusals = await collectDagWriteScopeRefusals(ctx.artifactsDir, ctx.root);
|
|
1954
|
+
if (scopeRefusals.length > 0) {
|
|
1955
|
+
return await dagRegenerationPlan(ctx, {
|
|
1956
|
+
heading: "Block Write Scope Failed",
|
|
1957
|
+
blockedBody: "The implementation_dag repeatedly declares a write scope or targeted command the plan cannot carry:",
|
|
1958
|
+
reEmitBody: `## Write-Scope and Command Errors From the Previous Attempt
|
|
1959
|
+
|
|
1960
|
+
Each node's declared write scope becomes the block \`touched_files\` a host binds an implementer to and re-checks against the landed diff, and each targeted command is executed verbatim through a shell. Fix every entry below:`,
|
|
1961
|
+
violations: scopeRefusals,
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
await promoteImplementationDagToExtractedPlan(ctx.artifactsDir, ctx.root);
|
|
1965
|
+
// M-B3 source-grounded citation gate (promotion backstop): ground every
|
|
1966
|
+
// promoted extracted-plan finding's citations against the working tree.
|
|
1967
|
+
const citationGate = await evaluatePromotedPlanCitationGrounding(ctx.artifactsDir, ctx.root);
|
|
1968
|
+
if (citationGate) {
|
|
1969
|
+
// The plan was promoted to extracted-plan.json BEFORE this gate ran, so the
|
|
1970
|
+
// ungrounded marker is now on disk. Remove it before any return — otherwise
|
|
1971
|
+
// a subsequent next-step reads the promoted plan and hands it straight to
|
|
1972
|
+
// handlePendingExtractedPlan, bypassing the re-emit and completing the
|
|
1973
|
+
// pipeline on hallucinated citations.
|
|
1974
|
+
await rm(ctx.paths.extractedPlan, { force: true });
|
|
1975
|
+
// The grounding-driven re-emit is a backend-observed step-boundary fact:
|
|
1976
|
+
// route it through the single CE-005 chokepoint as phase_reemit.
|
|
1977
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
1978
|
+
eventType: "phase_reemit",
|
|
1979
|
+
discriminator: "implementation_planning:citation_grounding:promotion",
|
|
1980
|
+
note: "implementation_planning re-emitted: a promoted plan finding cited a " +
|
|
1981
|
+
"component that does not exist in the working tree (M-B3 citation grounding).",
|
|
1982
|
+
category: "trap",
|
|
1983
|
+
}, "remediate-code");
|
|
1984
|
+
return await dagRegenerationPlan(ctx, {
|
|
1985
|
+
heading: "Citation Grounding Failed",
|
|
1986
|
+
blockedBody: "The promoted plan repeatedly cites components that do not exist in the working tree:",
|
|
1987
|
+
reEmitBody: `## Source-Grounded Citation Gate Errors From the Previous Attempt
|
|
1988
|
+
|
|
1989
|
+
The previous implementation_dag produced findings that cite components not present in the working tree. Every cited path or symbol must point at something real:`,
|
|
1990
|
+
violations: citationGate.violations,
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
// Normalized block write scope, tracked-tree half. Runs AFTER the
|
|
1994
|
+
// citation gate because the two overlap but neither contains the other: a
|
|
1995
|
+
// finding grounds on any real path OR symbol, so a node with plausible prose
|
|
1996
|
+
// can ground while the write scope a host would bind a worker to is still
|
|
1997
|
+
// fabricated. Same bounded recovery, same plan removal.
|
|
1998
|
+
const writeScopeGate = await evaluatePromotedPlanWriteScope(ctx.artifactsDir, ctx.root);
|
|
1999
|
+
if (writeScopeGate) {
|
|
2000
|
+
await rm(ctx.paths.extractedPlan, { force: true });
|
|
2001
|
+
return await dagRegenerationPlan(ctx, {
|
|
2002
|
+
heading: "Block Write Scope Failed",
|
|
2003
|
+
blockedBody: "The promoted plan repeatedly declares a block write scope that does not exist in the working tree:",
|
|
2004
|
+
reEmitBody: `## Block Write-Scope Errors From the Previous Attempt
|
|
2005
|
+
|
|
2006
|
+
Each node's declared write scope becomes the block \`touched_files\` a host binds an implementer to and re-checks against the landed diff. The following entries name a directory that does not exist:`,
|
|
2007
|
+
violations: writeScopeGate.violations,
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
return { via: "pipeline_complete" };
|
|
2011
|
+
};
|
|
2012
|
+
/** Render the detected cycles for a prompt. */
|
|
2013
|
+
function renderCycleDescriptions(cycles) {
|
|
2014
|
+
return cycles
|
|
2015
|
+
.map((cycle, index) => `Cycle ${index + 1}: [${cycle.members.join(", ")}]`)
|
|
2016
|
+
.join("\n");
|
|
2017
|
+
}
|
|
2018
|
+
/** Build the seam-obligation graph from the obligation ledger AS IT STANDS NOW. */
|
|
2019
|
+
async function readSeamObligationGraph(artifactsDir) {
|
|
2020
|
+
const envelope = await readContractArtifact(artifactsDir, "obligation_ledger");
|
|
2021
|
+
const ledger = envelopePayload(envelope);
|
|
2022
|
+
const obligationIds = new Set((ledger?.obligations ?? []).map((o) => o.id));
|
|
2023
|
+
return {
|
|
2024
|
+
nodes: (ledger?.obligations ?? []).map((obligation) => ({
|
|
2025
|
+
id: obligation.id,
|
|
2026
|
+
needs: (obligation.depends_on ?? []).filter((dep) => obligationIds.has(dep)),
|
|
2027
|
+
})),
|
|
2028
|
+
goalId: ledger?.goal_id ?? "",
|
|
2029
|
+
ledgerHash: envelope?.content_hash ?? "unknown",
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Cyclic-seam resolution gate: runs after obligation_ledger is present and
|
|
2034
|
+
* before assessment. Detects circular interface-definition obligations, then
|
|
2035
|
+
* routes to an LLM resolution step when cycles are found. Cap:
|
|
2036
|
+
* MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS; on exhaustion, route to a user-decision
|
|
2037
|
+
* step (then blocked if still unresolved).
|
|
2038
|
+
*/
|
|
2039
|
+
const cyclicSeamResolutionGate = async (ctx) => {
|
|
2040
|
+
if (ctx.nextPhase !== "cyclic_seam_resolution")
|
|
2041
|
+
return null;
|
|
2042
|
+
const graph = await readSeamObligationGraph(ctx.artifactsDir);
|
|
2043
|
+
const detectedCycles = detectCyclicSeamObligations(graph.nodes);
|
|
2044
|
+
if (detectedCycles.length === 0) {
|
|
2045
|
+
await writeDerivedContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution", {
|
|
2046
|
+
contract_version: "remediate-code-contract-pipeline/cyclic-seam-resolution/v1alpha1",
|
|
2047
|
+
goal_id: graph.goalId,
|
|
2048
|
+
cycles: [],
|
|
2049
|
+
status: "no_cycles",
|
|
2050
|
+
created_at: new Date().toISOString(),
|
|
2051
|
+
});
|
|
2052
|
+
return { via: "rederive" };
|
|
2053
|
+
}
|
|
2054
|
+
const repairState = await readCyclicSeamRepairState(ctx.artifactsDir);
|
|
2055
|
+
const attemptsForLedger = repairState.attempts.filter((attempt) => attempt.ledger_hash === graph.ledgerHash);
|
|
2056
|
+
// Guard: the artifact exists and is already marked resolved/no_cycles. This
|
|
2057
|
+
// branch should not normally be reached (the artifact exists, so the frontier
|
|
2058
|
+
// skips it), but re-deriving is the safe answer.
|
|
2059
|
+
const existingResolution = envelopePayload(await readContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution"));
|
|
2060
|
+
if (existingResolution &&
|
|
2061
|
+
(existingResolution.status === "resolved" ||
|
|
2062
|
+
existingResolution.status === "no_cycles")) {
|
|
2063
|
+
return { via: "rederive" };
|
|
2064
|
+
}
|
|
2065
|
+
const cycleDescriptions = renderCycleDescriptions(detectedCycles);
|
|
2066
|
+
if (attemptsForLedger.length >= MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS) {
|
|
2067
|
+
if (!repairState.user_decision_emitted) {
|
|
1842
2068
|
repairState.user_decision_emitted = true;
|
|
1843
|
-
await writeCyclicSeamRepairState(artifactsDir, repairState);
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
.join("\n");
|
|
1847
|
-
return writeCurrentStep({
|
|
1848
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1849
|
-
status: "blocked",
|
|
1850
|
-
runId,
|
|
1851
|
-
repoRoot: root,
|
|
1852
|
-
artifactsDir,
|
|
2069
|
+
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2070
|
+
return {
|
|
2071
|
+
via: "blocked",
|
|
1853
2072
|
prompt: `# Cyclic Seam Resolution — User Decision Required
|
|
1854
2073
|
|
|
1855
2074
|
The automatic cycle-break resolution reached its cap (${MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS} attempt(s)) without producing a valid cycle-free obligation graph. The following obligation cycles remain unresolved:
|
|
@@ -1861,27 +2080,17 @@ ${cycleDescriptions}
|
|
|
1861
2080
|
1. **Mediator module** — Introduce a third obligation/module that both sides depend on. The mediator owns the shared primitive; neither original module defines an interface for the other.
|
|
1862
2081
|
2. **Single authority** — Designate one obligation/module as the definitive owner of the co-defined interface. The other becomes a consumer only. This is recorded as a named, scoped exception.
|
|
1863
2082
|
|
|
1864
|
-
To proceed, manually rewrite \`${contractInputFilePath(artifactsDir, "obligation_ledger")}\` so that no circular \`depends_on\` references exist, then delete \`${contractInputFilePath(artifactsDir, "cyclic_seam_resolution")}\` and \`${cyclicSeamRepairStatePath(artifactsDir)}\` and re-run next-step.
|
|
2083
|
+
To proceed, manually rewrite \`${contractInputFilePath(ctx.artifactsDir, "obligation_ledger")}\` so that no circular \`depends_on\` references exist, then delete \`${contractInputFilePath(ctx.artifactsDir, "cyclic_seam_resolution")}\` and \`${cyclicSeamRepairStatePath(ctx.artifactsDir)}\` and re-run next-step.
|
|
1865
2084
|
|
|
1866
2085
|
If you choose to stop instead, this run will remain blocked.
|
|
1867
2086
|
`,
|
|
1868
|
-
allowedCommands: [],
|
|
1869
2087
|
stopCondition: "Stop after presenting the user-decision prompt. Do not attempt further resolution.",
|
|
1870
|
-
}
|
|
2088
|
+
};
|
|
1871
2089
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
.map((c, i) => `Cycle ${i + 1}: [${c.members.join(", ")}]`)
|
|
1877
|
-
.join("\n");
|
|
1878
|
-
return writeCurrentStep({
|
|
1879
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1880
|
-
status: "blocked",
|
|
1881
|
-
runId,
|
|
1882
|
-
repoRoot: root,
|
|
1883
|
-
artifactsDir,
|
|
1884
|
-
prompt: `# Cyclic Seam Resolution — Blocked
|
|
2090
|
+
// The user decision was emitted and cycles are still present — blocked.
|
|
2091
|
+
return {
|
|
2092
|
+
via: "blocked",
|
|
2093
|
+
prompt: `# Cyclic Seam Resolution — Blocked
|
|
1885
2094
|
|
|
1886
2095
|
Cycles in the obligation graph remain unresolved after the automatic cap and a user-decision step. The run cannot proceed without manual intervention.
|
|
1887
2096
|
|
|
@@ -1889,47 +2098,51 @@ ${cycleDescriptions}
|
|
|
1889
2098
|
|
|
1890
2099
|
Manually rewrite the obligation_ledger to remove circular depends_on references, delete the cyclic_seam_resolution artifact and cyclic-seam-repair-state.json, and re-run next-step.
|
|
1891
2100
|
`,
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2101
|
+
stopCondition: "Stop — the run is blocked on cyclic seam resolution.",
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
// Emit the LLM cyclic-seam-resolution step.
|
|
2105
|
+
const outputPath = contractInputFilePath(ctx.artifactsDir, "cyclic_seam_resolution");
|
|
2106
|
+
const ledgerInputPath = contractInputFilePath(ctx.artifactsDir, "obligation_ledger");
|
|
2107
|
+
const priorRejection = [...repairState.attempts]
|
|
2108
|
+
.reverse()
|
|
2109
|
+
.find((attempt) => attempt.ledger_hash === graph.ledgerHash && attempt.recheck_reason)?.recheck_reason;
|
|
2110
|
+
const rejectionSection = priorRejection
|
|
2111
|
+
? `\n## Why the Previous Attempt Was Rejected\n\n${priorRejection}\n`
|
|
2112
|
+
: "";
|
|
2113
|
+
repairState.attempts.push({
|
|
2114
|
+
ledger_hash: graph.ledgerHash,
|
|
2115
|
+
at: new Date().toISOString(),
|
|
2116
|
+
recheck_passed: false,
|
|
2117
|
+
});
|
|
2118
|
+
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2119
|
+
return {
|
|
2120
|
+
via: "step",
|
|
2121
|
+
prompt: `# Cyclic Seam Resolution
|
|
1910
2122
|
|
|
1911
|
-
Circular interface-definition obligations were detected in the obligation ledger. You must resolve every cycle using one of the two sanctioned strategies below, then write the resolution record.
|
|
2123
|
+
Circular interface-definition obligations were detected in the obligation ledger. You must resolve every cycle using one of the two sanctioned strategies below, then REWRITE THE LEDGER and write the resolution record.
|
|
1912
2124
|
|
|
1913
2125
|
## Detected Cycles
|
|
1914
2126
|
|
|
1915
2127
|
${cycleDescriptions}
|
|
1916
|
-
|
|
2128
|
+
${rejectionSection}
|
|
1917
2129
|
## Sanctioned Break Strategies
|
|
1918
2130
|
|
|
1919
2131
|
For each cycle, choose one:
|
|
1920
2132
|
|
|
1921
|
-
1. **Mediator module** — Introduce a third obligation/module that both sides depend on. The mediator owns the shared primitive; neither original module defines an interface for the other.
|
|
1922
|
-
2. **Single authority** — Designate one
|
|
2133
|
+
1. **Mediator module** — Introduce a third obligation/module that both sides depend on. The mediator owns the shared primitive; neither original module defines an interface for the other. The mediator must be an obligation that EXISTS in the ledger and is NOT a member of the cycle.
|
|
2134
|
+
2. **Single authority** — Designate one of the cycle's own obligations as the definitive owner of the interface. The others become consumers only. Record this as an explicit, scoped exception.
|
|
1923
2135
|
|
|
1924
2136
|
## Required Inputs
|
|
1925
2137
|
|
|
1926
|
-
- \`${
|
|
2138
|
+
- \`${ledgerInputPath}\` (obligation_ledger)
|
|
1927
2139
|
|
|
1928
2140
|
## Your Task
|
|
1929
2141
|
|
|
1930
|
-
|
|
2142
|
+
Two files, both required — the record alone is not a break:
|
|
1931
2143
|
|
|
1932
|
-
\`${
|
|
2144
|
+
1. **Rewrite \`${ledgerInputPath}\`** so the cycle's \`depends_on\` edges actually route through the obligation you designate. The re-check re-runs cycle detection over the ledger you leave behind; a resolution record whose ledger still carries the cycle is rejected, not accepted.
|
|
2145
|
+
2. **Write the resolution record** to exactly \`${outputPath}\`, naming for each cycle the obligation id you designated:
|
|
1933
2146
|
|
|
1934
2147
|
\`\`\`json
|
|
1935
2148
|
{
|
|
@@ -1939,6 +2152,7 @@ Read the obligation_ledger. For each detected cycle, decide which break strategy
|
|
|
1939
2152
|
{
|
|
1940
2153
|
"members": ["<obligation-id>", "..."],
|
|
1941
2154
|
"break_strategy": "mediator | single_authority",
|
|
2155
|
+
"designated_obligation_id": "<the mediating obligation, or the single authority — must exist in the rewritten ledger>",
|
|
1942
2156
|
"resolution_description": "<what was changed and why>",
|
|
1943
2157
|
"exception_registration": "<if single_authority: the named scoped exception; otherwise null>"
|
|
1944
2158
|
}
|
|
@@ -1949,225 +2163,431 @@ Read the obligation_ledger. For each detected cycle, decide which break strategy
|
|
|
1949
2163
|
|
|
1950
2164
|
If after analysis you find the cycles are already broken (e.g. upon re-reading the ledger the depends_on edges do not actually form a cycle), set status to "no_cycles" and cycles to [].
|
|
1951
2165
|
|
|
1952
|
-
**Stop after writing the
|
|
1953
|
-
|
|
1954
|
-
After writing the output file, run:
|
|
1955
|
-
|
|
1956
|
-
\`${nextCommand}\`
|
|
2166
|
+
**Stop after writing the two files.** Do not edit source files. Do not advance to the next pipeline step.
|
|
1957
2167
|
`,
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2168
|
+
outputPath,
|
|
2169
|
+
stopCondition: "Stop after rewriting the obligation_ledger, writing the cyclic_seam_resolution output file, and running next-step.",
|
|
2170
|
+
};
|
|
2171
|
+
};
|
|
2172
|
+
/**
|
|
2173
|
+
* Cyclic-seam RE-CHECK. The worker has written a `resolved` record; verify the
|
|
2174
|
+
* break it actually authored against the obligation graph as it actually
|
|
2175
|
+
* stands, and archive + loop back when it does not hold.
|
|
2176
|
+
*
|
|
2177
|
+
* TST-61cff370 / TST-114e4941: this check used to be vacuous. It fabricated a
|
|
2178
|
+
* synthetic node per cycle — `{ id: "_mediator_A_B", needs: [] }` or
|
|
2179
|
+
* `{ id: "_authority_A_B", needs: [] }` — and asked whether redirecting the
|
|
2180
|
+
* cycle's edges at that edge-free sink would be acyclic, against the SAME
|
|
2181
|
+
* unmodified ledger. For any single detected cycle the answer is yes by
|
|
2182
|
+
* construction, so the re-check could never reject: a worker could claim
|
|
2183
|
+
* `status: "resolved"` while changing nothing, and the pipeline advanced. It
|
|
2184
|
+
* now reads the designated obligation off the record and validates it against
|
|
2185
|
+
* the live graph — see `validateAuthoredCycleBreak`.
|
|
2186
|
+
*/
|
|
2187
|
+
const cyclicSeamRecheckGate = async (ctx) => {
|
|
2188
|
+
const resolutionEnvelope = await readContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution");
|
|
2189
|
+
if (!resolutionEnvelope)
|
|
2190
|
+
return null;
|
|
2191
|
+
const resolution = envelopePayload(resolutionEnvelope);
|
|
2192
|
+
if (!resolution ||
|
|
2193
|
+
resolution.status !== "resolved" ||
|
|
2194
|
+
!Array.isArray(resolution.cycles) ||
|
|
2195
|
+
resolution.cycles.length === 0) {
|
|
2196
|
+
return null;
|
|
1961
2197
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
if (resolutionEnvelope) {
|
|
1975
|
-
const resolution = envelopePayload(resolutionEnvelope);
|
|
1976
|
-
if (resolution &&
|
|
1977
|
-
resolution.status === "resolved" &&
|
|
1978
|
-
Array.isArray(resolution.cycles) &&
|
|
1979
|
-
resolution.cycles.length > 0) {
|
|
1980
|
-
// Re-check: build the patched graph and verify no cycles remain.
|
|
1981
|
-
const obligationLedgerPayload = envelopePayload(await readContractArtifact(artifactsDir, "obligation_ledger"));
|
|
1982
|
-
const obligationIds = new Set((obligationLedgerPayload?.obligations ?? []).map((o) => o.id));
|
|
1983
|
-
const seamNodes = (obligationLedgerPayload?.obligations ?? []).map((obl) => ({
|
|
1984
|
-
id: obl.id,
|
|
1985
|
-
needs: (obl.depends_on ?? []).filter((dep) => obligationIds.has(dep)),
|
|
1986
|
-
}));
|
|
1987
|
-
// For each cycle in the resolution, apply the stated break and re-check.
|
|
1988
|
-
let recheckFailed = false;
|
|
1989
|
-
for (const cycleRecord of resolution.cycles) {
|
|
1990
|
-
if (!Array.isArray(cycleRecord.members))
|
|
1991
|
-
continue;
|
|
1992
|
-
const members = cycleRecord.members;
|
|
1993
|
-
const mediatorId = cycleRecord.break_strategy === "mediator"
|
|
1994
|
-
? `_mediator_${members.join("_")}`
|
|
1995
|
-
: null;
|
|
1996
|
-
const validationResult = validateCycleBreak({ members }, seamNodes, mediatorId
|
|
1997
|
-
? { id: mediatorId, needs: [] }
|
|
1998
|
-
: // single_authority: the designated owner keeps all edges;
|
|
1999
|
-
// non-owner loses edges to cycle members — model as mediator=no-op.
|
|
2000
|
-
{ id: `_authority_${members.join("_")}`, needs: [] });
|
|
2001
|
-
if (!validationResult.accepted) {
|
|
2002
|
-
recheckFailed = true;
|
|
2003
|
-
break;
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
if (recheckFailed) {
|
|
2007
|
-
const ledgerEnvelope = await readContractArtifact(artifactsDir, "obligation_ledger");
|
|
2008
|
-
const ledgerHash = ledgerEnvelope?.content_hash ?? "unknown";
|
|
2009
|
-
const repairState = await readCyclicSeamRepairState(artifactsDir);
|
|
2010
|
-
// Mark the last attempt as recheck_failed.
|
|
2011
|
-
const last = repairState.attempts.at(-1);
|
|
2012
|
-
if (last && last.ledger_hash === ledgerHash) {
|
|
2013
|
-
last.recheck_passed = false;
|
|
2014
|
-
}
|
|
2015
|
-
await writeCyclicSeamRepairState(artifactsDir, repairState);
|
|
2016
|
-
await archiveContractArtifact(artifactsDir, "cyclic_seam_resolution", "invalid");
|
|
2017
|
-
// Re-enter to emit the next attempt or cap.
|
|
2018
|
-
return buildNextContractPipelineStep(options);
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2198
|
+
const graph = await readSeamObligationGraph(ctx.artifactsDir);
|
|
2199
|
+
let rejection;
|
|
2200
|
+
for (const cycleRecord of resolution.cycles) {
|
|
2201
|
+
if (!Array.isArray(cycleRecord.members))
|
|
2202
|
+
continue;
|
|
2203
|
+
const members = cycleRecord.members.filter((member) => typeof member === "string");
|
|
2204
|
+
const strategy = cycleRecord.break_strategy;
|
|
2205
|
+
if (strategy !== "mediator" && strategy !== "single_authority") {
|
|
2206
|
+
rejection =
|
|
2207
|
+
`Cycle [${members.join(", ")}] declares break_strategy ` +
|
|
2208
|
+
`${JSON.stringify(strategy ?? null)}, which is neither "mediator" nor "single_authority".`;
|
|
2209
|
+
break;
|
|
2021
2210
|
}
|
|
2211
|
+
const authored = {
|
|
2212
|
+
strategy,
|
|
2213
|
+
designatedId: typeof cycleRecord.designated_obligation_id === "string"
|
|
2214
|
+
? cycleRecord.designated_obligation_id
|
|
2215
|
+
: undefined,
|
|
2216
|
+
};
|
|
2217
|
+
const validation = validateAuthoredCycleBreak({ members }, graph.nodes, authored);
|
|
2218
|
+
if (!validation.accepted) {
|
|
2219
|
+
rejection = validation.reason ?? `Cycle [${members.join(", ")}] was not resolved.`;
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
if (!rejection)
|
|
2224
|
+
return null;
|
|
2225
|
+
const repairState = await readCyclicSeamRepairState(ctx.artifactsDir);
|
|
2226
|
+
const last = repairState.attempts.at(-1);
|
|
2227
|
+
// Carry the reason forward so the NEXT resolution prompt says what failed,
|
|
2228
|
+
// instead of re-asking for the same claim and burning the attempt cap on an
|
|
2229
|
+
// unexplained retry. A record that appeared without a matching emitted
|
|
2230
|
+
// attempt (a resumed run, a hand-written artifact) still gets its rejection
|
|
2231
|
+
// recorded — the outcome is the attempt.
|
|
2232
|
+
if (last && last.ledger_hash === graph.ledgerHash) {
|
|
2233
|
+
last.recheck_passed = false;
|
|
2234
|
+
last.recheck_reason = rejection;
|
|
2235
|
+
}
|
|
2236
|
+
else {
|
|
2237
|
+
repairState.attempts.push({
|
|
2238
|
+
ledger_hash: graph.ledgerHash,
|
|
2239
|
+
at: new Date().toISOString(),
|
|
2240
|
+
recheck_passed: false,
|
|
2241
|
+
recheck_reason: rejection,
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2245
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution", "invalid", ctx.options.renameFn);
|
|
2246
|
+
if (!archived.originalFree) {
|
|
2247
|
+
// The rejected record is STILL at its canonical path, and re-deriving would
|
|
2248
|
+
// read the same record, reject it again, fail to archive it again — an
|
|
2249
|
+
// unbounded loop with no cap to stop it: the attempt ledger updates the
|
|
2250
|
+
// same entry in place (one ledger hash), and `rederive` carries no depth
|
|
2251
|
+
// bound. This branch is what makes the re-check's new ability to REJECT
|
|
2252
|
+
// safe; before the re-check could reject, the failure was unreachable.
|
|
2253
|
+
return {
|
|
2254
|
+
via: "blocked",
|
|
2255
|
+
prompt: `# A Rejected Cyclic-Seam Resolution Could Not Be Archived
|
|
2256
|
+
|
|
2257
|
+
The cycle-break re-check rejected the resolution record:
|
|
2258
|
+
|
|
2259
|
+
${rejection}
|
|
2260
|
+
|
|
2261
|
+
The record could not be moved into the contract history directory, so it is still at its canonical path. Re-running would read the same rejected record and loop without bound, so the run stops here instead.
|
|
2262
|
+
|
|
2263
|
+
Remove or unlock \`${contractArtifactFilePath(ctx.artifactsDir, "cyclic_seam_resolution")}\` (and its \`.input.json\` sibling if present), then re-run next-step so the resolution phase is re-emitted with the rejection above.`,
|
|
2264
|
+
stopCondition: "Stop — a rejected cyclic-seam resolution could not be archived and would otherwise loop.",
|
|
2265
|
+
};
|
|
2022
2266
|
}
|
|
2023
|
-
//
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2267
|
+
// Re-enter to emit the next attempt or the cap.
|
|
2268
|
+
return { via: "rederive" };
|
|
2269
|
+
};
|
|
2270
|
+
/**
|
|
2271
|
+
* Design-spec structural gates before the adversarial critic phase, in the order
|
|
2272
|
+
* they run: the design artifact's own structure, then the cheap cross-artifact
|
|
2273
|
+
* floor, then citation grounding. Error-severity gate failures re-emit the
|
|
2274
|
+
* responsible phase; warning-only results (e.g. circular obligation
|
|
2275
|
+
* dependencies) ride the critic prompt as advisory.
|
|
2276
|
+
*/
|
|
2277
|
+
const preCriticStructuralGate = async (ctx) => {
|
|
2278
|
+
if (ctx.nextPhase !== "critic")
|
|
2279
|
+
return null;
|
|
2280
|
+
const outcomes = evaluateContractPipelineCrossGateOutcomes(await readCrossGatePayloads(ctx));
|
|
2281
|
+
// (a) The design artifact itself. `contract_finalization` precedes `critic`,
|
|
2282
|
+
// so a not-evaluated outcome here means the payload is malformed, not
|
|
2283
|
+
// absent — its empty issue list is not proof of a clean design.
|
|
2284
|
+
const designSpec = gateOutcomeOf(outcomes, "design_spec");
|
|
2285
|
+
if (!designSpec?.evaluated) {
|
|
2286
|
+
return {
|
|
2287
|
+
via: "phase",
|
|
2288
|
+
phase: "contract_finalization",
|
|
2289
|
+
extraSection: `## Design Structural Gates Could Not Run
|
|
2290
|
+
|
|
2291
|
+
The finalized module contracts could not be checked before adversarial review: ${designSpec?.reason ?? "no outcome record was produced for the design gate"}. Rewrite a complete, well-formed finalized_module_contracts artifact.
|
|
2292
|
+
`,
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
const gateErrors = designSpec.issues.filter((issue) => issue.severity === "error");
|
|
2296
|
+
if (gateErrors.length > 0) {
|
|
2297
|
+
return {
|
|
2298
|
+
via: "phase",
|
|
2299
|
+
phase: "contract_finalization",
|
|
2300
|
+
extraSection: `## Design Structural Gate Errors
|
|
2046
2301
|
|
|
2047
2302
|
The contract_finalization output failed deterministic structural gates. Fix every issue below before adversarial review can begin:
|
|
2048
2303
|
|
|
2049
|
-
${
|
|
2050
|
-
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2304
|
+
${gateErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2305
|
+
`,
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
const gateWarnings = designSpec.issues.filter((issue) => issue.severity === "warning");
|
|
2309
|
+
if (gateWarnings.length > 0) {
|
|
2310
|
+
return {
|
|
2311
|
+
via: "phase",
|
|
2312
|
+
phase: "critic",
|
|
2313
|
+
extraSection: `## Advisory: Design Structural Warnings
|
|
2057
2314
|
|
|
2058
2315
|
The following structural issues were detected and should inform your adversarial review. They do not block the pipeline but may indicate areas of design fragility:
|
|
2059
2316
|
|
|
2060
|
-
${
|
|
2061
|
-
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2317
|
+
${gateWarnings.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2318
|
+
`,
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
// (b) Pre-adversarial structural floor (S5): the cheap cross-artifact checks
|
|
2322
|
+
// whose inputs all exist by the critic phase, so the adversarial loop only
|
|
2323
|
+
// ever sees structurally-sound obligations/tests/contracts and a gap is
|
|
2324
|
+
// re-emitted to the precise responsible phase instead of being discovered
|
|
2325
|
+
// at promotion after the adversarial budget is spent.
|
|
2326
|
+
const preCriticGate = await evaluatePreCriticStructuralGate(ctx.artifactsDir, ctx.root, await readCrossGatePayloads(ctx));
|
|
2327
|
+
if (preCriticGate) {
|
|
2328
|
+
return {
|
|
2329
|
+
via: "phase",
|
|
2330
|
+
phase: preCriticGate.phase,
|
|
2331
|
+
extraSection: `## Pre-Adversarial Structural Gate Errors
|
|
2074
2332
|
|
|
2075
2333
|
The ${preCriticGate.phase} output failed deterministic structural gates. Fix every issue below before adversarial review begins:
|
|
2076
2334
|
|
|
2077
2335
|
${preCriticGate.errorLines.join("\n")}
|
|
2078
|
-
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2336
|
+
`,
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
// (c) M-B3 source-grounded citation gate at the pre-critic boundary: ground
|
|
2340
|
+
// the module_decomposition's file_scope citations against the working tree
|
|
2341
|
+
// before the adversarial loop. A module citing only a non-existent path and
|
|
2342
|
+
// no real symbol is re-emitted to the `decomposition` phase — the phase that
|
|
2343
|
+
// OWNS file_scope (the finalized contracts carry interface fields, not
|
|
2344
|
+
// paths, so re-emitting contract_finalization could never change file_scope
|
|
2345
|
+
// and an ungrounded scope would loop forever).
|
|
2346
|
+
const preCriticCitationGate = await evaluatePreCriticCitationGrounding(ctx.artifactsDir, ctx.root);
|
|
2347
|
+
if (preCriticCitationGate) {
|
|
2348
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
2349
|
+
eventType: "phase_reemit",
|
|
2350
|
+
discriminator: "decomposition:citation_grounding:pre_critic",
|
|
2351
|
+
note: "decomposition re-emitted: a module's file_scope cited a component " +
|
|
2352
|
+
"that does not exist in the working tree (M-B3 citation grounding).",
|
|
2353
|
+
category: "trap",
|
|
2354
|
+
}, "remediate-code");
|
|
2355
|
+
return {
|
|
2356
|
+
via: "phase",
|
|
2357
|
+
phase: "decomposition",
|
|
2358
|
+
extraSection: `## Source-Grounded Citation Gate Errors
|
|
2099
2359
|
|
|
2100
2360
|
A module's file_scope cites a component that does not exist in the working tree. file_scope lives in the module decomposition (the finalized contracts carry interface fields, not paths), so fix the offending path(s) in the decomposition — every cited path or symbol must point at something real before adversarial review begins:
|
|
2101
2361
|
|
|
2102
2362
|
${preCriticCitationGate.errorLines.join("\n")}
|
|
2103
|
-
|
|
2104
|
-
}
|
|
2363
|
+
`,
|
|
2364
|
+
};
|
|
2105
2365
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
//
|
|
2126
|
-
//
|
|
2127
|
-
//
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2366
|
+
return null;
|
|
2367
|
+
};
|
|
2368
|
+
/**
|
|
2369
|
+
* DC-3 merge intercept: when a parallel phase's aggregated artifact is still
|
|
2370
|
+
* missing, merge the per-module shards into it once they are ALL present.
|
|
2371
|
+
* Returns true when the aggregate was written. A missing shard (or a degenerate
|
|
2372
|
+
* ≤1-module decomposition, which never used the shard path) returns false, and
|
|
2373
|
+
* the caller re-emits the wave — never a partial aggregate. After a complete
|
|
2374
|
+
* merge the artifact is written enveloped and the pipeline re-derives; the
|
|
2375
|
+
* seam_reconciliation / critique pass downstream stays the consistency gate over
|
|
2376
|
+
* the merged contracts.
|
|
2377
|
+
*/
|
|
2378
|
+
async function tryMergeModuleShards(artifactsDir, phase) {
|
|
2379
|
+
const modules = await readDecomposedModules(artifactsDir);
|
|
2380
|
+
if (modules.length <= 1)
|
|
2381
|
+
return false;
|
|
2382
|
+
const scan = await scanModuleShards(artifactsDir, phase, modules);
|
|
2383
|
+
if (scan.missing.length > 0)
|
|
2384
|
+
return false;
|
|
2385
|
+
// goal_id: the upstream module_decomposition is authoritative (every artifact
|
|
2386
|
+
// shares one goal_id; the goal-ID consistency gate enforces it). Fall back to
|
|
2387
|
+
// a shard's goal_id only if the decomposition somehow lacks one.
|
|
2388
|
+
const decompositionGoalId = await readDecompositionGoalId(artifactsDir);
|
|
2389
|
+
const goalId = decompositionGoalId ||
|
|
2390
|
+
[...scan.present.values()]
|
|
2391
|
+
.map((contract) => (typeof contract.goal_id === "string" ? contract.goal_id : undefined))
|
|
2392
|
+
.find((candidate) => Boolean(candidate)) ||
|
|
2393
|
+
"";
|
|
2394
|
+
await writeDerivedContractArtifact(artifactsDir, PARALLEL_MODULE_PHASES[phase], mergeModuleShards(modules, scan.present, goalId));
|
|
2395
|
+
return true;
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Parallel-capable phase (DC-3): `module_contract_drafting` fans out to one
|
|
2399
|
+
* agent per module. The aggregated `module_contracts` artifact is missing here,
|
|
2400
|
+
* so first try to merge per-module shards (the worker may have just written
|
|
2401
|
+
* them) — a COMPLETE shard set merges into the aggregated artifact and the
|
|
2402
|
+
* pipeline re-derives; anything else re-emits the wave (which itself falls back
|
|
2403
|
+
* to the single aggregated step for a degenerate ≤1-module decomposition).
|
|
2404
|
+
*/
|
|
2405
|
+
const parallelModuleWaveGate = async (ctx) => {
|
|
2406
|
+
const phase = ctx.nextPhase;
|
|
2407
|
+
if (phase === null || !isParallelModulePhase(phase))
|
|
2408
|
+
return null;
|
|
2409
|
+
const merged = await tryMergeModuleShards(ctx.artifactsDir, phase);
|
|
2410
|
+
return merged ? { via: "rederive" } : { via: "module_wave", phase };
|
|
2411
|
+
};
|
|
2412
|
+
/**
|
|
2413
|
+
* Auto-phasing (T3): at the conceptual-design critique, hand the critic the
|
|
2414
|
+
* tool-DERIVED phase cut so it assesses design quality WITHIN a mechanically
|
|
2415
|
+
* dependency-ordered foundations→consumers phasing, instead of rejecting an
|
|
2416
|
+
* arbitrary N-goal change as "over-scoped" and forcing the host to re-scope by
|
|
2417
|
+
* hand at intake. The cut is derived from the finalized module contracts'
|
|
2418
|
+
* directional neighbor_needs edges and PERSISTED as `phase_cut.json`, so the cut
|
|
2419
|
+
* the critic sees and the cut the implementation-DAG promotion enforces are one
|
|
2420
|
+
* source. Only injected when there is a genuine multi-phase cut to communicate.
|
|
2421
|
+
*/
|
|
2422
|
+
const phaseCutCritiqueGate = async (ctx) => {
|
|
2423
|
+
if (ctx.nextPhase !== "critique")
|
|
2424
|
+
return null;
|
|
2425
|
+
const cut = await ensurePhaseCutArtifact(ctx.artifactsDir);
|
|
2426
|
+
if (!cut || cut.phases.length <= 1)
|
|
2427
|
+
return null;
|
|
2428
|
+
const reReview = await buildReReviewSection("critique", ctx.artifactsDir);
|
|
2429
|
+
const phaseCutSection = renderPhaseCutSection(cut);
|
|
2430
|
+
return {
|
|
2431
|
+
via: "phase",
|
|
2432
|
+
phase: "critique",
|
|
2433
|
+
extraSection: reReview ? `${phaseCutSection}\n${reReview}` : phaseCutSection,
|
|
2434
|
+
};
|
|
2435
|
+
};
|
|
2436
|
+
/**
|
|
2437
|
+
* Granularity collapse (T1 slice 4b): for low-complexity work, fold the framing
|
|
2438
|
+
* suffix [nextPhase..decomposition] into ONE round-trip producing several
|
|
2439
|
+
* artifacts, instead of one gated step per phase. Reads the POST-escalation
|
|
2440
|
+
* riskSignal (the escalate-on-evidence intercept may have already raised the
|
|
2441
|
+
* tier), so the dial is never frozen at run start — `fine` for medium/high keeps
|
|
2442
|
+
* full per-phase isolation. Only collapses a genuine multi-phase suffix.
|
|
2443
|
+
*/
|
|
2444
|
+
const collapsedFramingGate = (ctx) => {
|
|
2445
|
+
const phase = ctx.nextPhase;
|
|
2446
|
+
if (phase === null ||
|
|
2447
|
+
roundTripGranularityForTier(ctx.riskSignal?.tier) !== "collapsed" ||
|
|
2448
|
+
!FRAMING_COLLAPSE_GROUP.includes(phase)) {
|
|
2449
|
+
return null;
|
|
2141
2450
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2451
|
+
const startIdx = FRAMING_COLLAPSE_GROUP.indexOf(phase);
|
|
2452
|
+
const suffix = FRAMING_COLLAPSE_GROUP.slice(startIdx);
|
|
2453
|
+
if (suffix.length <= 1)
|
|
2454
|
+
return null;
|
|
2455
|
+
return { via: "collapsed_framing", phases: [...suffix] };
|
|
2456
|
+
};
|
|
2457
|
+
/**
|
|
2458
|
+
* Skeleton-scaffolded phases (S3): the tool pre-fills structure/ids from the
|
|
2459
|
+
* derived obligation ledger so the worker fills only the judgment slots.
|
|
2460
|
+
*/
|
|
2461
|
+
const scaffoldedPhaseGate = async (ctx) => {
|
|
2462
|
+
const phase = ctx.nextPhase;
|
|
2463
|
+
if (phase !== "test_validator_plan" && phase !== "implementation_planning") {
|
|
2464
|
+
return null;
|
|
2465
|
+
}
|
|
2466
|
+
return {
|
|
2467
|
+
via: "phase",
|
|
2468
|
+
phase,
|
|
2469
|
+
extraSection: await buildScaffoldSection(phase, ctx.artifactsDir),
|
|
2470
|
+
};
|
|
2471
|
+
};
|
|
2472
|
+
/**
|
|
2473
|
+
* The fallback: the ordinary per-phase step. Diff-based re-review (B2) rides it
|
|
2474
|
+
* — when a verdict-bearing review phase is re-emitted because an upstream
|
|
2475
|
+
* changed, the worker gets its prior verdict plus the precise
|
|
2476
|
+
* changed-since-last-review delta, so it re-affirms cheaply or revises only the
|
|
2477
|
+
* affected items rather than running blind.
|
|
2478
|
+
*
|
|
2479
|
+
* Reached only when every gate declined, which by construction means
|
|
2480
|
+
* `nextPhase` is a real phase: the promotion gate above never declines when the
|
|
2481
|
+
* frontier is null.
|
|
2482
|
+
*/
|
|
2483
|
+
const ordinaryPhaseStep = async (ctx) => {
|
|
2484
|
+
const phase = ctx.nextPhase;
|
|
2485
|
+
if (phase === null) {
|
|
2486
|
+
throw new Error("contract pipeline: the gate walk reached the fallback with no next phase — " +
|
|
2487
|
+
"the promotion gate must handle a null frontier.");
|
|
2157
2488
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2489
|
+
return {
|
|
2490
|
+
via: "phase",
|
|
2491
|
+
phase,
|
|
2492
|
+
extraSection: await buildReReviewSection(phase, ctx.artifactsDir),
|
|
2493
|
+
};
|
|
2494
|
+
};
|
|
2495
|
+
/**
|
|
2496
|
+
* THE ORDERED GATE TABLE. Insertion order IS execution order (the walk consumes
|
|
2497
|
+
* the scaffold's derived `handledKeys`), names are unique by construction (a
|
|
2498
|
+
* duplicate object key is a compile error), and no gate can emit a step of its
|
|
2499
|
+
* own — the scaffold owns the single emission site.
|
|
2500
|
+
*/
|
|
2501
|
+
const CONTRACT_PIPELINE_GATES = {
|
|
2502
|
+
seed_source_digest_bound: seedSourceDigestGate,
|
|
2503
|
+
ingested_artifact_invalid: invalidIngestionGate,
|
|
2504
|
+
stale_artifact_archived: staleArchiveGate,
|
|
2505
|
+
phase_frontier_resolved: phaseFrontierGate,
|
|
2506
|
+
goal_id_consistent: goalIdConsistencyGate,
|
|
2507
|
+
finalized_module_set_preserved: finalizedModuleSetGate,
|
|
2508
|
+
work_block_seam_prepared: workBlockSeamGate,
|
|
2509
|
+
conceptual_critique_converged: conceptualCritiqueGate,
|
|
2510
|
+
obligation_ledger_derived: obligationLedgerDerivationGate,
|
|
2511
|
+
degenerate_seam_reconciliation_collapsed: degenerateSeamReconciliationGate,
|
|
2512
|
+
contract_finalization_derived: contractFinalizationDerivationGate,
|
|
2513
|
+
judge_repair_converged: judgeRepairGate,
|
|
2514
|
+
implementation_plan_promoted: implementationPlanPromotionGate,
|
|
2515
|
+
cyclic_seam_resolved: cyclicSeamResolutionGate,
|
|
2516
|
+
cyclic_seam_rechecked: cyclicSeamRecheckGate,
|
|
2517
|
+
pre_critic_structural: preCriticStructuralGate,
|
|
2518
|
+
parallel_module_wave: parallelModuleWaveGate,
|
|
2519
|
+
phase_cut_critique: phaseCutCritiqueGate,
|
|
2520
|
+
collapsed_framing_round_trip: collapsedFramingGate,
|
|
2521
|
+
scaffolded_phase: scaffoldedPhaseGate,
|
|
2522
|
+
};
|
|
2523
|
+
/**
|
|
2524
|
+
* Bind the ONE shared step-emission scaffold to an invocation's context.
|
|
2525
|
+
*
|
|
2526
|
+
* Scaffold ADOPTER, never a second scaffold: this consumes `createStepEmissionScaffold` from
|
|
2527
|
+
* `audit-tools/shared` — the same scaffold the audit orchestrator entry point
|
|
2528
|
+
* drives — rather than a second one of this module's own. The pipeline's
|
|
2529
|
+
* numbered early-return-and-re-emit shape is `emitFirstApplicable`, a row shape
|
|
2530
|
+
* in that table, not a fork of it.
|
|
2531
|
+
*/
|
|
2532
|
+
function createContractPipelineEmission(ctx) {
|
|
2533
|
+
return createStepEmissionScaffold({
|
|
2534
|
+
table: CONTRACT_PIPELINE_GATES,
|
|
2535
|
+
fallback: ordinaryPhaseStep,
|
|
2536
|
+
write: (plan) => writeContractStepPlan(ctx, plan),
|
|
2537
|
+
// The pipeline's externally-observable emission is the PERSISTED step
|
|
2538
|
+
// contract, which `write` has just produced; the CLI renders it to the host.
|
|
2539
|
+
// There is deliberately no second stdout announcement here.
|
|
2540
|
+
log: () => { },
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* The gate walk order, exported so a drift guard reads the real set instead of
|
|
2545
|
+
* reconstructing one by reflecting over a chain of `if` statements.
|
|
2546
|
+
*
|
|
2547
|
+
* This and the scaffold's `handledKeys` are BOTH `Object.keys` of the SAME
|
|
2548
|
+
* object literal, and the walk consumes `handledKeys` directly — so the two
|
|
2549
|
+
* agree by construction, not by a test that compares them. No such test exists,
|
|
2550
|
+
* and none is needed: there is no second list to drift from.
|
|
2551
|
+
*/
|
|
2552
|
+
export const CONTRACT_PIPELINE_GATE_ORDER = Object.freeze(Object.keys(CONTRACT_PIPELINE_GATES));
|
|
2553
|
+
/**
|
|
2554
|
+
* Build and write the next contract-pipeline step.
|
|
2555
|
+
* Returns null when the pipeline is complete and the extracted plan is ready.
|
|
2556
|
+
*/
|
|
2557
|
+
export async function buildNextContractPipelineStep(options) {
|
|
2558
|
+
const { root, artifactsDir, runId, sourcePaths } = options;
|
|
2559
|
+
// Adversarial-depth dial (T1 slices 3/4): derive the depth for the critique /
|
|
2560
|
+
// critic phases from the intake risk signal, escalating on decomposition
|
|
2561
|
+
// evidence. The (possibly raised) riskSignal is also consumed by the
|
|
2562
|
+
// granularity-collapse gate, so it is carried on the context alongside it.
|
|
2563
|
+
const { riskSignal, adversarialDepth } = await resolveAdversarialDepth(artifactsDir);
|
|
2564
|
+
// Resolve artifact paths for the prompt renderers. The host's world is the
|
|
2565
|
+
// plain INPUT files (D3): every host-facing path — both where a role WRITES
|
|
2566
|
+
// its output and where it READS its upstreams — is `<name>.input.json`. The
|
|
2567
|
+
// tool's canonical envelopes (`<name>.json`) are derived at ingest and never
|
|
2568
|
+
// named to the host.
|
|
2569
|
+
const artifactPaths = {};
|
|
2570
|
+
for (const name of CP_ARTIFACT_NAMES) {
|
|
2571
|
+
artifactPaths[name] = contractInputFilePath(artifactsDir, name);
|
|
2163
2572
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2573
|
+
const seedPath = pathASeedFilePath(artifactsDir);
|
|
2574
|
+
const ctx = {
|
|
2575
|
+
options,
|
|
2576
|
+
root,
|
|
2577
|
+
artifactsDir,
|
|
2578
|
+
runId,
|
|
2579
|
+
sourcePaths,
|
|
2580
|
+
paths: intakePaths(artifactsDir),
|
|
2581
|
+
artifactPaths,
|
|
2582
|
+
// Present only for structured_audit runs.
|
|
2583
|
+
pathASeedPath: existsSync(seedPath) ? seedPath : undefined,
|
|
2584
|
+
riskSignal,
|
|
2585
|
+
adversarialDepth,
|
|
2586
|
+
artifactsSettled: false,
|
|
2587
|
+
nextPhase: null,
|
|
2588
|
+
};
|
|
2589
|
+
const emission = createContractPipelineEmission(ctx);
|
|
2590
|
+
return await emission.emitFirstApplicable([...emission.handledKeys], ctx);
|
|
2171
2591
|
}
|
|
2172
2592
|
/**
|
|
2173
2593
|
* Build the diff-based re-review section for a review phase being re-emitted after
|
|
@@ -2186,13 +2606,60 @@ async function buildReReviewSection(phase, artifactsDir) {
|
|
|
2186
2606
|
const delta = await computeReReviewDelta(artifactsDir, artifact, snapshot);
|
|
2187
2607
|
return renderReReviewSection(artifact, snapshot, delta);
|
|
2188
2608
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2609
|
+
// ── DAG → extracted plan conversion ──────────────────────────────────────────
|
|
2610
|
+
// ── Obligation-kind → lens/severity mappings ──────────────────────────────────
|
|
2611
|
+
/**
|
|
2612
|
+
* The obligation-kind vocabulary, in priority order (higher index = higher
|
|
2613
|
+
* priority; `invariant` is highest).
|
|
2614
|
+
*
|
|
2615
|
+
* MNT-114e4941-3: this used to be a THIRD independent copy of the vocabulary —
|
|
2616
|
+
* a local `type ObligationKind` union beside derive.ts's `TESTABLE_KINDS` and
|
|
2617
|
+
* contractPipelineGates.ts's `TESTABLE_OBLIGATION_KINDS`, with nothing forcing
|
|
2618
|
+
* the three to agree, while the ledger's own `obligation.kind` is typed as a
|
|
2619
|
+
* bare `string`. The consequence was not theoretical: an unrecognized kind was
|
|
2620
|
+
* CAST to this union, scored -1 by `indexOf`, and then indexed the lens map to
|
|
2621
|
+
* `undefined` — so a ledger kind outside these four promoted a finding with
|
|
2622
|
+
* `lens: undefined`.
|
|
2623
|
+
*
|
|
2624
|
+
* It is now single-sourced two ways at once:
|
|
2625
|
+
* • MEMBERSHIP — {@link obligationKindVocabularyDivergence} reconciles this
|
|
2626
|
+
* list against the gate module's exported `TESTABLE_OBLIGATION_KINDS`, so a
|
|
2627
|
+
* kind added there and not here is a red contract test rather than a silent
|
|
2628
|
+
* misclassification;
|
|
2629
|
+
* • SEMANTICS — an unrecognized kind is not dropped or cast. It is routed
|
|
2630
|
+
* through the gate module's own `isTestablePhaseObligation` predicate, so
|
|
2631
|
+
* the two modules answer "is this kind testable?" with ONE implementation.
|
|
2632
|
+
*/
|
|
2633
|
+
export const OBLIGATION_KIND_PRIORITY = [
|
|
2191
2634
|
"test",
|
|
2192
2635
|
"structural",
|
|
2193
2636
|
"behavioral",
|
|
2194
2637
|
"invariant",
|
|
2195
2638
|
];
|
|
2639
|
+
const OBLIGATION_KIND_SET = new Set(OBLIGATION_KIND_PRIORITY);
|
|
2640
|
+
/**
|
|
2641
|
+
* Classify a raw ledger `kind` string (which the ledger types as a bare
|
|
2642
|
+
* `string`) into this module's vocabulary. A recognized kind maps to itself; an
|
|
2643
|
+
* unrecognized one is classified by the SHARED testability predicate rather
|
|
2644
|
+
* than guessed here — testable ⇒ `behavioral` (the testable default, so it
|
|
2645
|
+
* carries a real lens and a mid severity), otherwise ⇒ `structural`.
|
|
2646
|
+
*/
|
|
2647
|
+
export function classifyObligationKind(kind) {
|
|
2648
|
+
if (OBLIGATION_KIND_SET.has(kind))
|
|
2649
|
+
return kind;
|
|
2650
|
+
return isTestablePhaseObligation(kind) ? "behavioral" : "structural";
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Kinds the gate module declares TESTABLE that this module's vocabulary does
|
|
2654
|
+
* not carry — the drift MNT-114e4941-3 names, reported as data so a contract
|
|
2655
|
+
* test can go red on it instead of a reviewer having to notice.
|
|
2656
|
+
* Empty when the two agree.
|
|
2657
|
+
*/
|
|
2658
|
+
export function obligationKindVocabularyDivergence() {
|
|
2659
|
+
return [...TESTABLE_OBLIGATION_KINDS]
|
|
2660
|
+
.filter((kind) => !OBLIGATION_KIND_SET.has(kind))
|
|
2661
|
+
.sort();
|
|
2662
|
+
}
|
|
2196
2663
|
function deriveObligationLensAndSeverity(kinds) {
|
|
2197
2664
|
if (kinds.length === 0) {
|
|
2198
2665
|
return { lens: "correctness", severity: "medium" };
|
|
@@ -2219,11 +2686,157 @@ function deriveObligationLensAndSeverity(kinds) {
|
|
|
2219
2686
|
};
|
|
2220
2687
|
return { lens: lensMap[topKind], severity: severityMap[topKind] };
|
|
2221
2688
|
}
|
|
2689
|
+
// ── Normalized block write scope + declared command shape ─────────────────────────────────────
|
|
2690
|
+
//
|
|
2691
|
+
// `touched_files` is the PROMPT-BOUND WRITE SCOPE the host-handoff substrate
|
|
2692
|
+
// enforces against the landed diff, and `targeted_commands` are executed
|
|
2693
|
+
// verbatim through a shell in the repository root. That consumer can check the
|
|
2694
|
+
// SHAPE of what it is handed; it can never check whether the shape is CORRECT
|
|
2695
|
+
// for this repository. So the producer normalizes here: an absolute or
|
|
2696
|
+
// separator-inconsistent path becomes one canonical repo-relative form, a path
|
|
2697
|
+
// that escapes the repository is refused outright, and a command carrying shell
|
|
2698
|
+
// chaining or substitution is refused rather than handed to a shell.
|
|
2699
|
+
/** Shell metacharacters a declared package-script / test invocation never needs. */
|
|
2700
|
+
const SHELL_METACHARACTERS = /[&|;<>`$\n\r\0]/;
|
|
2701
|
+
/**
|
|
2702
|
+
* The tracked-path corpus for write-scope checking, or null when the tree
|
|
2703
|
+
* cannot be read. Null degrades to "shape-only normalization" exactly as the
|
|
2704
|
+
* M-B3 citation gate degrades on an unreadable tree — a fixture directory or a
|
|
2705
|
+
* fresh checkout must not be bricked, only an unsound path in a REAL tree is
|
|
2706
|
+
* refused.
|
|
2707
|
+
*/
|
|
2708
|
+
function readTrackedWriteScopeCorpus(root) {
|
|
2709
|
+
if (!isInsideGitWorkTree(root))
|
|
2710
|
+
return null;
|
|
2711
|
+
const files = enumerateRepoTreePaths(root);
|
|
2712
|
+
if (files.size === 0)
|
|
2713
|
+
return null;
|
|
2714
|
+
const directories = new Set();
|
|
2715
|
+
for (const path of files) {
|
|
2716
|
+
const segments = path.split("/");
|
|
2717
|
+
for (let i = 1; i < segments.length; i += 1) {
|
|
2718
|
+
directories.add(segments.slice(0, i).join("/"));
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
return { files, directories };
|
|
2722
|
+
}
|
|
2723
|
+
export function normalizeBlockTouchedFiles(root, files, blockId) {
|
|
2724
|
+
const normalized = new Set();
|
|
2725
|
+
const refusals = [];
|
|
2726
|
+
for (const raw of files) {
|
|
2727
|
+
const candidate = typeof raw === "string" ? raw.trim() : "";
|
|
2728
|
+
if (candidate.length === 0) {
|
|
2729
|
+
refusals.push(`Block "${blockId}" declares an empty touched_files entry.`);
|
|
2730
|
+
continue;
|
|
2731
|
+
}
|
|
2732
|
+
const absolute = isAbsolute(candidate) ? candidate : resolve(root, candidate);
|
|
2733
|
+
try {
|
|
2734
|
+
normalized.add(repoRelativePath(root, absolute, `block "${blockId}" touched_files entry`));
|
|
2735
|
+
}
|
|
2736
|
+
catch {
|
|
2737
|
+
refusals.push(`Block "${blockId}" declares the touched_files entry ${JSON.stringify(raw)}, which ` +
|
|
2738
|
+
`does not resolve to a path beneath the repository root. A POSIX-absolute form ` +
|
|
2739
|
+
`("/src/x.ts") is read as absolute, not repo-relative — drop the leading slash. The ` +
|
|
2740
|
+
`write scope is re-checked against the landed diff, so it may only name paths ` +
|
|
2741
|
+
`beneath ${root}.`);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
// Content-derived order: an incidentally-ordered write scope would churn the
|
|
2745
|
+
// plan's content hash on every re-promotion.
|
|
2746
|
+
return {
|
|
2747
|
+
touched_files: [...normalized].sort((left, right) => compareCodeUnits(left, right)),
|
|
2748
|
+
refusals,
|
|
2749
|
+
};
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* The tracked-tree half of The normalized-write-scope invariant, run against
|
|
2753
|
+
* the PROMOTED plan so a violation takes the same bounded re-emit path the M-B3
|
|
2754
|
+
* citation gate takes, rather than throwing out of the promotion.
|
|
2755
|
+
*
|
|
2756
|
+
* It exists because the citation gate is NOT a superset: a finding grounds if
|
|
2757
|
+
* ANY cited path OR SYMBOL is real, so a node whose prose names a real symbol
|
|
2758
|
+
* can ground while its declared write scope is still fabricated — and the write
|
|
2759
|
+
* scope is what a host binds a worker to.
|
|
2760
|
+
*
|
|
2761
|
+
* A path that is not tracked but whose parent directory IS stays legal: a
|
|
2762
|
+
* remediation block legitimately creates new files, and dropping a declared
|
|
2763
|
+
* write target is the failure mode that strands an implementer with an
|
|
2764
|
+
* obligation it has no scope to discharge. Fail-open on an unreadable tree, as
|
|
2765
|
+
* the citation gate does.
|
|
2766
|
+
*/
|
|
2767
|
+
export async function evaluatePromotedPlanWriteScope(artifactsDir, root) {
|
|
2768
|
+
const corpus = readTrackedWriteScopeCorpus(root);
|
|
2769
|
+
if (!corpus)
|
|
2770
|
+
return null;
|
|
2771
|
+
const plan = await readOptionalJsonFile(intakePaths(artifactsDir).extractedPlan);
|
|
2772
|
+
const violations = [];
|
|
2773
|
+
for (const block of Array.isArray(plan?.blocks) ? plan.blocks : []) {
|
|
2774
|
+
const blockId = typeof block.block_id === "string" ? block.block_id : "(unnamed block)";
|
|
2775
|
+
const touched = Array.isArray(block.touched_files) ? block.touched_files : [];
|
|
2776
|
+
for (const path of touched) {
|
|
2777
|
+
if (typeof path !== "string")
|
|
2778
|
+
continue;
|
|
2779
|
+
const key = normalizeRepoPath(path);
|
|
2780
|
+
const parent = key.includes("/") ? key.slice(0, key.lastIndexOf("/")) : "";
|
|
2781
|
+
if (corpus.files.has(key) || parent === "" || corpus.directories.has(parent)) {
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
violations.push(`Block "${blockId}" declares the write-scope path "${path}", whose directory does ` +
|
|
2785
|
+
`not exist in the tracked tree.`);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
return violations.length > 0 ? { violations } : null;
|
|
2789
|
+
}
|
|
2790
|
+
export function normalizeBlockTargetedCommands(commands, blockId) {
|
|
2791
|
+
const normalized = [];
|
|
2792
|
+
const refusals = [];
|
|
2793
|
+
for (const raw of commands) {
|
|
2794
|
+
const command = typeof raw === "string" ? raw.trim() : "";
|
|
2795
|
+
if (command.length === 0) {
|
|
2796
|
+
refusals.push(`Block "${blockId}" declares an empty targeted_commands entry.`);
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
if (SHELL_METACHARACTERS.test(command)) {
|
|
2800
|
+
refusals.push(`Block "${blockId}" declares the targeted_commands entry ${JSON.stringify(raw)}, ` +
|
|
2801
|
+
`which carries shell chaining, substitution or redirection. A targeted command is ` +
|
|
2802
|
+
`executed verbatim through a shell, so it must be one invocation — split it into ` +
|
|
2803
|
+
`separate entries.`);
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
normalized.push(command);
|
|
2807
|
+
}
|
|
2808
|
+
return { targeted_commands: normalized, refusals };
|
|
2809
|
+
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Collect every write-scope and command refusal the promotion WOULD hit, before
|
|
2812
|
+
* a plan is written. Runs the same two normalizers over the same derived node
|
|
2813
|
+
* scope the promoter uses, so this pre-check and the promotion cannot disagree
|
|
2814
|
+
* about what is refusable — and the refusal reaches the host as the bounded
|
|
2815
|
+
* `implementation_planning` re-emit every other promotion rejection takes,
|
|
2816
|
+
* rather than as a thrown stack that wedges every subsequent next-step.
|
|
2817
|
+
*/
|
|
2818
|
+
export async function collectDagWriteScopeRefusals(artifactsDir, root) {
|
|
2819
|
+
const dag = envelopePayload(await readContractArtifact(artifactsDir, "implementation_dag"));
|
|
2820
|
+
const nodes = Array.isArray(dag?.nodes) ? dag.nodes : [];
|
|
2821
|
+
if (nodes.length === 0)
|
|
2822
|
+
return [];
|
|
2823
|
+
const { resolve: deriveNodeFiles } = await buildNodeWriteScopeResolver(artifactsDir);
|
|
2824
|
+
const refusals = [];
|
|
2825
|
+
for (const [index, node] of nodes.entries()) {
|
|
2826
|
+
const blockId = toBlockId(ensureNodeId(node.id, index));
|
|
2827
|
+
refusals.push(...normalizeBlockTouchedFiles(root, deriveNodeFiles(node), blockId).refusals, ...normalizeBlockTargetedCommands(node.targeted_commands ?? [], blockId).refusals);
|
|
2828
|
+
}
|
|
2829
|
+
return refusals;
|
|
2830
|
+
}
|
|
2222
2831
|
/**
|
|
2223
2832
|
* Convert a completed ImplementationDAG into the extracted-plan.json format
|
|
2224
2833
|
* that the existing handlePendingExtractedPlan/applyPlanPipeline path consumes.
|
|
2834
|
+
*
|
|
2835
|
+
* `root` defaults to the repository that owns `artifactsDir`, so the existing
|
|
2836
|
+
* one-argument callers keep working while the pipeline passes the run's real
|
|
2837
|
+
* root for write-scope normalization.
|
|
2225
2838
|
*/
|
|
2226
|
-
export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
2839
|
+
export async function promoteImplementationDagToExtractedPlan(artifactsDir, root = climbOutOfAuditTools(artifactsDir)) {
|
|
2227
2840
|
const paths = intakePaths(artifactsDir);
|
|
2228
2841
|
const dagEnvelope = await readContractArtifact(artifactsDir, "implementation_dag");
|
|
2229
2842
|
if (!dagEnvelope)
|
|
@@ -2238,7 +2851,9 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2238
2851
|
const obligationMap = new Map();
|
|
2239
2852
|
if (ledgerPayload?.obligations) {
|
|
2240
2853
|
for (const obl of ledgerPayload.obligations) {
|
|
2241
|
-
|
|
2854
|
+
// Classified, never cast: an unrecognized kind used to index the lens map
|
|
2855
|
+
// to `undefined` and promote a lens-less finding (MNT-114e4941-3).
|
|
2856
|
+
obligationMap.set(obl.id, classifyObligationKind(String(obl.kind ?? "")));
|
|
2242
2857
|
}
|
|
2243
2858
|
}
|
|
2244
2859
|
// Auto-phasing (T3): read the persisted phase cut and re-key its module-phase
|
|
@@ -2416,7 +3031,23 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2416
3031
|
// Same derivation as the finding's affected_files: declared write scope, else
|
|
2417
3032
|
// the module file_scope inherited via the node's obligations — so the block's
|
|
2418
3033
|
// file-ownership scheduler never sees an empty (undispatchable) touched set.
|
|
2419
|
-
|
|
3034
|
+
// Normalized before it leaves this producer: the host-handoff substrate binds
|
|
3035
|
+
// this list as the write scope and can validate its shape but never its
|
|
3036
|
+
// correctness.
|
|
3037
|
+
// Refusals are collected, not thrown: `collectDagWriteScopeRefusals` runs
|
|
3038
|
+
// these same two normalizers at the promotion gate and re-emits, so by the
|
|
3039
|
+
// time promotion runs there is nothing left to refuse. The throw below is a
|
|
3040
|
+
// BACKSTOP for a caller that skipped that gate — never the operator-facing
|
|
3041
|
+
// path.
|
|
3042
|
+
const scope = normalizeBlockTouchedFiles(root, deriveNodeFiles(node), toBlockId(nodeId));
|
|
3043
|
+
const commands = normalizeBlockTargetedCommands(node.targeted_commands ?? [], toBlockId(nodeId));
|
|
3044
|
+
const refusals = [...scope.refusals, ...commands.refusals];
|
|
3045
|
+
if (refusals.length > 0) {
|
|
3046
|
+
throw new Error(`implementation_dag node "${nodeId}" has an unpromotable write scope, which the ` +
|
|
3047
|
+
`promotion gate should have refused first: ${refusals.join(" | ")}`);
|
|
3048
|
+
}
|
|
3049
|
+
const touchedFiles = scope.touched_files;
|
|
3050
|
+
const targetedCommands = commands.targeted_commands;
|
|
2420
3051
|
// Phase ordinal from the union of this node's obligations (max → fail-toward-
|
|
2421
3052
|
// later). Only stamped when there is a genuine multi-phase cut, so a single-
|
|
2422
3053
|
// phase change carries no ordinal and the scheduler runs no barrier.
|
|
@@ -2437,9 +3068,7 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2437
3068
|
// declared write scope so the file-ownership scheduler can read it.
|
|
2438
3069
|
touched_files: touchedFiles,
|
|
2439
3070
|
...(phaseOrdinal !== undefined ? { phase_ordinal: phaseOrdinal } : {}),
|
|
2440
|
-
...(
|
|
2441
|
-
? { targeted_commands: [...node.targeted_commands] }
|
|
2442
|
-
: {}),
|
|
3071
|
+
...(targetedCommands.length > 0 ? { targeted_commands: targetedCommands } : {}),
|
|
2443
3072
|
};
|
|
2444
3073
|
});
|
|
2445
3074
|
const extractedPlan = {
|
|
@@ -2455,6 +3084,54 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2455
3084
|
};
|
|
2456
3085
|
await writeJsonFile(paths.extractedPlan, extractedPlan);
|
|
2457
3086
|
}
|
|
3087
|
+
/**
|
|
3088
|
+
* Read the pinned planning outputs for a run, or null when no plan has been
|
|
3089
|
+
* promoted yet.
|
|
3090
|
+
*/
|
|
3091
|
+
export async function readContractPipelinePlanningOutputs(artifactsDir) {
|
|
3092
|
+
const plan = await readOptionalJsonFile(intakePaths(artifactsDir).extractedPlan);
|
|
3093
|
+
if (!plan)
|
|
3094
|
+
return null;
|
|
3095
|
+
const stringsOf = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
3096
|
+
const blocks = (Array.isArray(plan.blocks) ? plan.blocks : [])
|
|
3097
|
+
.map((block) => ({
|
|
3098
|
+
block_id: typeof block.block_id === "string" ? block.block_id : "",
|
|
3099
|
+
items: stringsOf(block.items),
|
|
3100
|
+
touched_files: stringsOf(block.touched_files),
|
|
3101
|
+
targeted_commands: stringsOf(block.targeted_commands),
|
|
3102
|
+
}))
|
|
3103
|
+
.sort((left, right) => compareCodeUnits(left.block_id, right.block_id));
|
|
3104
|
+
const findingBytes = new Map();
|
|
3105
|
+
for (const finding of Array.isArray(plan.findings) ? plan.findings : []) {
|
|
3106
|
+
if (typeof finding.id !== "string")
|
|
3107
|
+
continue;
|
|
3108
|
+
const text = `${String(finding.title ?? "")}\n${String(finding.summary ?? "")}`;
|
|
3109
|
+
findingBytes.set(finding.id, Buffer.byteLength(text, "utf8"));
|
|
3110
|
+
}
|
|
3111
|
+
const claimCounts = new Map();
|
|
3112
|
+
for (const block of blocks) {
|
|
3113
|
+
for (const item of block.items) {
|
|
3114
|
+
claimCounts.set(item, (claimCounts.get(item) ?? 0) + 1);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
const findingIds = [...findingBytes.keys()].sort((left, right) => compareCodeUnits(left, right));
|
|
3118
|
+
return {
|
|
3119
|
+
block_membership: blocks,
|
|
3120
|
+
coverage: {
|
|
3121
|
+
finding_ids: findingIds,
|
|
3122
|
+
exhaustive_once: findingIds.length > 0 &&
|
|
3123
|
+
findingIds.every((id) => claimCounts.get(id) === 1) &&
|
|
3124
|
+
[...claimCounts.keys()].every((id) => findingBytes.has(id)),
|
|
3125
|
+
},
|
|
3126
|
+
token_estimates: blocks.map((block) => ({
|
|
3127
|
+
block_id: block.block_id,
|
|
3128
|
+
estimated_tokens: estimateTokensFromBytes(block.items.reduce((total, item) => total + (findingBytes.get(item) ?? 0), 0)),
|
|
3129
|
+
})),
|
|
3130
|
+
seed_source_digests: [
|
|
3131
|
+
...((await readOptionalJsonFile(pathASeedFilePath(artifactsDir)))?.source_digests ?? []),
|
|
3132
|
+
].sort((left, right) => compareCodeUnits(left.path, right.path)),
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
2458
3135
|
// ── Lean-path extracted plan (the `low` risk tier's plan emission) ────────────
|
|
2459
3136
|
//
|
|
2460
3137
|
// The heavy pipeline above derives its `extracted-plan.json` from the adversarial
|