audit-tools 0.42.1 → 0.44.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/laneSubmissions.d.ts +2 -2
- package/dist/audit/cli/laneSubmissions.js +2 -2
- 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 -36
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +34 -77
- 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/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/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/phases/triage.d.ts.map +1 -1
- package/dist/remediate/phases/triage.js +14 -2
- package/dist/remediate/phases/triage.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 +296 -15
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +1783 -1001
- package/dist/remediate/steps/contractPipeline.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 +657 -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 -3
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js +10 -2
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/remediate/validation/artifacts.d.ts +20 -2
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +224 -130
- 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 +143 -27
- 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/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +5 -0
- 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/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/tooling/commandShape.d.ts +21 -0
- package/dist/shared/tooling/commandShape.d.ts.map +1 -0
- package/dist/shared/tooling/commandShape.js +120 -0
- package/dist/shared/tooling/commandShape.js.map +1 -0
- 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/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/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,33 @@
|
|
|
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, partitionCommandsByDeclaredShape, 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";
|
|
38
50
|
import { compareCodeUnits } from "../../shared/affinityArtifacts.js";
|
|
39
51
|
import { writeCurrentStep } from "./stepWriter.js";
|
|
40
52
|
import { loaderCommand } from "./prompts.js";
|
|
@@ -386,19 +398,87 @@ export async function writePathASeedFromFindings(artifactsDir, auditFindingsPath
|
|
|
386
398
|
block_ids: [...seam.block_ids].sort(compareCodeUnits),
|
|
387
399
|
}))
|
|
388
400
|
.sort((a, b) => compareCodeUnits(a.id, b.id));
|
|
401
|
+
const affectedFiles = [...affectedFilesSet].sort();
|
|
389
402
|
const seed = {
|
|
390
403
|
schema_version: "remediate-code-contract-pipeline/path-a-seed/v1alpha2",
|
|
391
404
|
audit_findings_path: auditFindingsPath,
|
|
392
405
|
finding_count: findings.length,
|
|
393
406
|
findings_summary: findingsSummary,
|
|
394
|
-
affected_files:
|
|
407
|
+
affected_files: affectedFiles,
|
|
395
408
|
work_blocks: workBlocks,
|
|
396
409
|
work_block_seams: workBlockSeams,
|
|
410
|
+
source_digests: await hashSeedSourcePaths(seedRepoRoot(artifactsDir), auditFindingsPath, affectedFiles),
|
|
397
411
|
created_at: new Date().toISOString(),
|
|
398
412
|
};
|
|
399
413
|
await mkdir(contractPipelineDir(artifactsDir), { recursive: true });
|
|
400
414
|
await writeJsonFile(seedPath, seed);
|
|
401
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
|
+
}
|
|
402
482
|
/**
|
|
403
483
|
* Infer the most appropriate repair target from judge classifications when no
|
|
404
484
|
* explicit repair_directive is provided. Examines only accepted classifications
|
|
@@ -658,40 +738,20 @@ export async function validateImplementationDagTraceability(artifactsDir) {
|
|
|
658
738
|
}
|
|
659
739
|
/**
|
|
660
740
|
* Run the fail-closed contract-obligation gates against the persisted contract
|
|
661
|
-
* artifacts
|
|
662
|
-
*
|
|
663
|
-
* - validateEvidenceThreaded (assessment × judge × implementation_dag)
|
|
664
|
-
* - validateDigestCoverage (goal_spec.source_type × finding-enumeration × ledger)
|
|
665
|
-
* - validateReconciliationDerivation (seam report × finalized contracts)
|
|
741
|
+
* artifacts: paired obligations, evidence threading, source-scoped digest
|
|
742
|
+
* coverage, and INV-CO-12 reconciliation derivation.
|
|
666
743
|
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
const seamReport = envelopePayload(await readContractArtifact(artifactsDir, "seam_reconciliation_report"));
|
|
679
|
-
const finalizedContracts = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
680
|
-
const goalSpec = envelopePayload(await readContractArtifact(artifactsDir, "goal_spec"));
|
|
681
|
-
const sourceType = isRecord(goalSpec) && typeof goalSpec.source_type === "string"
|
|
682
|
-
? goalSpec.source_type
|
|
683
|
-
: undefined;
|
|
684
|
-
const findingEnumeration = await readOptionalJsonFile(intakePaths(artifactsDir).findingEnumeration);
|
|
685
|
-
const issues = [
|
|
686
|
-
...validatePairedObligations(obligationLedger, testValidatorPlan),
|
|
687
|
-
...validateEvidenceThreaded(assessment, judge, dag),
|
|
688
|
-
...validateDigestCoverage(sourceType, findingEnumeration, obligationLedger),
|
|
689
|
-
...validateReconciliationDerivation(seamReport, finalizedContracts),
|
|
690
|
-
].filter((issue) => issue.severity === "error");
|
|
691
|
-
return {
|
|
692
|
-
ok: issues.length === 0,
|
|
693
|
-
violations: issues.map((issue) => `[${issue.path}] ${issue.message}`),
|
|
694
|
-
};
|
|
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);
|
|
695
755
|
}
|
|
696
756
|
/**
|
|
697
757
|
* Pre-adversarial structural floor (S5). The subset of the contract-obligation
|
|
@@ -707,38 +767,30 @@ export async function evaluateContractObligationsPromotionGate(artifactsDir) {
|
|
|
707
767
|
* at promotion as the fail-closed backstop; this gate never replaces it.
|
|
708
768
|
*
|
|
709
769
|
* Returns the first failing gate's responsible phase + rendered error lines, or
|
|
710
|
-
* null when the structural floor is clean.
|
|
711
|
-
*
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
const goalSpec = envelopePayload(await readContractArtifact(artifactsDir, "goal_spec"));
|
|
719
|
-
const sourceType = isRecord(goalSpec) && typeof goalSpec.source_type === "string"
|
|
720
|
-
? goalSpec.source_type
|
|
721
|
-
: undefined;
|
|
722
|
-
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)));
|
|
723
778
|
// Upstream-owned checks first: a derivation/coverage gap is fixed in the
|
|
724
779
|
// finalized contracts (the obligation ledger is derived from them).
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
...validateDigestCoverage(sourceType, findingEnumeration, obligationLedger),
|
|
728
|
-
].filter((issue) => issue.severity === "error");
|
|
729
|
-
if (designErrors.length > 0) {
|
|
780
|
+
const design = consumeGateOutcomes(outcomes, ["reconciliation_derivation", "digest_coverage"], PRE_CRITIC_REQUIRED_GATES);
|
|
781
|
+
if (!design.ok) {
|
|
730
782
|
return {
|
|
731
783
|
phase: "contract_finalization",
|
|
732
|
-
errorLines:
|
|
784
|
+
errorLines: design.violations.map((violation) => `- ${violation}`),
|
|
733
785
|
};
|
|
734
786
|
}
|
|
735
787
|
// A testable obligation without a paired spec is fixed in the test plan
|
|
736
788
|
// (skeleton-scaffolded from the derived ledger).
|
|
737
|
-
const
|
|
738
|
-
if (
|
|
789
|
+
const tests = consumeGateOutcomes(outcomes, ["paired_obligations"], PRE_CRITIC_REQUIRED_GATES);
|
|
790
|
+
if (!tests.ok) {
|
|
739
791
|
return {
|
|
740
792
|
phase: "test_validator_plan",
|
|
741
|
-
errorLines:
|
|
793
|
+
errorLines: tests.violations.map((violation) => `- ${violation}`),
|
|
742
794
|
};
|
|
743
795
|
}
|
|
744
796
|
return null;
|
|
@@ -878,11 +930,106 @@ async function readDecomposedModules(artifactsDir) {
|
|
|
878
930
|
return result;
|
|
879
931
|
}
|
|
880
932
|
/**
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
|
|
933
|
+
* Characters that disqualify a finalized-contract entry from being read as a
|
|
934
|
+
* repo-relative write target: any whitespace (prose), `:` (the `artifact:<name>`
|
|
935
|
+
* ordering token and the Windows drive form), and the glob/redirect set no
|
|
936
|
+
* legal declared path carries.
|
|
937
|
+
*/
|
|
938
|
+
const NON_WRITE_TARGET_CHARS = /[\s:*?"<>|]/u;
|
|
939
|
+
/**
|
|
940
|
+
* A finalized module contract's `outputs` / `side_effects` are FREE PROSE that
|
|
941
|
+
* may name a file ("src/foo.ts") or may describe an effect ("writes the run
|
|
942
|
+
* ledger under .audit-tools") or carry an ordering token
|
|
943
|
+
* ("artifact:validated-roster" — see ARTIFACT_TOKEN_PATTERN in phaseCut.ts).
|
|
944
|
+
* Only the first kind is a write target, so this is a deliberately CONSERVATIVE
|
|
945
|
+
* parse: an entry qualifies only when it reads unambiguously as a repo-relative
|
|
946
|
+
* path, and everything else is silently dropped — prose stays prose. A false
|
|
947
|
+
* positive here would widen a worker's write scope on the strength of a
|
|
948
|
+
* sentence, which is strictly worse than the manual widening this replaces.
|
|
949
|
+
*
|
|
950
|
+
* Returns the forward-slashed path, or null when the entry is not one.
|
|
951
|
+
*/
|
|
952
|
+
function contractDeclaredWriteTarget(entry) {
|
|
953
|
+
if (typeof entry !== "string")
|
|
954
|
+
return null;
|
|
955
|
+
const trimmed = entry.trim();
|
|
956
|
+
if (trimmed.length === 0)
|
|
957
|
+
return null;
|
|
958
|
+
if (NON_WRITE_TARGET_CHARS.test(trimmed))
|
|
959
|
+
return null;
|
|
960
|
+
// Absolute (POSIX or Windows-UNC) forms are not repo-relative.
|
|
961
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("\\"))
|
|
962
|
+
return null;
|
|
963
|
+
const normalized = trimmed.replace(/\\/gu, "/");
|
|
964
|
+
if (normalized.split("/").includes(".."))
|
|
965
|
+
return null;
|
|
966
|
+
// A bare word ("session") is an interface name, not a path. Require either a
|
|
967
|
+
// path separator or a file extension.
|
|
968
|
+
if (!normalized.includes("/") && !/\.[a-z0-9]{1,6}$/iu.test(normalized))
|
|
969
|
+
return null;
|
|
970
|
+
return normalized;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* The path-parseable write targets each finalized module contract declares,
|
|
974
|
+
* keyed by `moduleSlug(name)` — the SAME identity the obligation ids encode, so
|
|
975
|
+
* this map joins to the decomposition's modules without a second name space.
|
|
976
|
+
*
|
|
977
|
+
* Degrades to an empty map when the artifact is absent or malformed: this
|
|
978
|
+
* resolver runs on the VALIDATOR's refusal path, so a bad contracts file must
|
|
979
|
+
* cost the widening, never wedge every subsequent next-step with a throw.
|
|
980
|
+
*/
|
|
981
|
+
async function readModuleContractWriteTargets(artifactsDir) {
|
|
982
|
+
let finalized;
|
|
983
|
+
try {
|
|
984
|
+
finalized = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
return new Map();
|
|
988
|
+
}
|
|
989
|
+
const entries = isRecord(finalized) && Array.isArray(finalized.module_contracts)
|
|
990
|
+
? finalized.module_contracts
|
|
991
|
+
: [];
|
|
992
|
+
const bySlug = new Map();
|
|
993
|
+
for (const entry of entries) {
|
|
994
|
+
if (!isRecord(entry) || typeof entry.name !== "string")
|
|
995
|
+
continue;
|
|
996
|
+
const slug = moduleSlug(entry.name);
|
|
997
|
+
if (slug.length === 0)
|
|
998
|
+
continue;
|
|
999
|
+
const targets = bySlug.get(slug) ?? [];
|
|
1000
|
+
const declared = [
|
|
1001
|
+
...(Array.isArray(entry.outputs) ? entry.outputs : []),
|
|
1002
|
+
...(Array.isArray(entry.side_effects) ? entry.side_effects : []),
|
|
1003
|
+
];
|
|
1004
|
+
for (const raw of declared) {
|
|
1005
|
+
const target = contractDeclaredWriteTarget(raw);
|
|
1006
|
+
if (target !== null && !targets.includes(target))
|
|
1007
|
+
targets.push(target);
|
|
1008
|
+
}
|
|
1009
|
+
bySlug.set(slug, targets);
|
|
1010
|
+
}
|
|
1011
|
+
return bySlug;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Single source for "which files may this DAG node write". The scope is the
|
|
1015
|
+
* UNION of two declarations, never one overriding the other:
|
|
1016
|
+
*
|
|
1017
|
+
* - the node's own declared files (`output_files`, else `files_likely_touched`);
|
|
1018
|
+
* - the path-parseable write targets (`outputs` + `side_effects`) declared by
|
|
1019
|
+
* the finalized contract of the module(s) the node's obligations belong to,
|
|
1020
|
+
* resolved by longest-`OBL-<slug>-` prefix so a short slug never mis-claims a
|
|
1021
|
+
* longer module's targets.
|
|
1022
|
+
*
|
|
1023
|
+
* A node that declared NO files of its own additionally inherits the
|
|
1024
|
+
* `file_scope` of those same modules — that inheritance is the scope-less
|
|
1025
|
+
* FALLBACK only, and is deliberately not unioned into a node that did declare.
|
|
1026
|
+
*
|
|
1027
|
+
* ⚠ The declared-files-win EARLY RETURN this used to perform is deliberately
|
|
1028
|
+
* superseded (owner decision, nightly ledger 2026-08-20). The module contract is
|
|
1029
|
+
* where a module's write targets are declared, and they never reached the node
|
|
1030
|
+
* scope — so an implementer was handed an obligation whose declared target file
|
|
1031
|
+
* was missing from `allowed_files`, and a human widened it by hand (four
|
|
1032
|
+
* recoveries in one wave). Union, not precedence.
|
|
886
1033
|
*
|
|
887
1034
|
* ⚠ Shared by the PROMOTER (which derives the scope) and the VALIDATOR (which
|
|
888
1035
|
* refuses when it resolves to nothing) on purpose. Two copies of this resolution
|
|
@@ -900,25 +1047,43 @@ async function readDecomposedModules(artifactsDir) {
|
|
|
900
1047
|
*/
|
|
901
1048
|
async function buildNodeWriteScopeResolver(artifactsDir) {
|
|
902
1049
|
const decomposedModules = await readDecomposedModules(artifactsDir);
|
|
1050
|
+
const contractTargetsBySlug = await readModuleContractWriteTargets(artifactsDir);
|
|
903
1051
|
const moduleScopesBySlug = decomposedModules
|
|
904
|
-
.map((m) => ({
|
|
1052
|
+
.map((m) => ({
|
|
1053
|
+
slug: moduleSlug(m.name),
|
|
1054
|
+
files: m.file_scope,
|
|
1055
|
+
targets: contractTargetsBySlug.get(moduleSlug(m.name)) ?? [],
|
|
1056
|
+
}))
|
|
905
1057
|
.sort((a, b) => b.slug.length - a.slug.length);
|
|
906
1058
|
const resolve = (node) => {
|
|
907
1059
|
const declared = [...new Set(node.output_files ?? node.files_likely_touched ?? [])];
|
|
908
|
-
if (declared.length > 0)
|
|
909
|
-
return declared;
|
|
910
1060
|
const obligationIds = [
|
|
911
1061
|
...(node.satisfies_obligations ?? []),
|
|
912
1062
|
...(node.verification_obligation_ids ?? []),
|
|
913
1063
|
];
|
|
914
1064
|
const inherited = new Set();
|
|
1065
|
+
const ownedTargets = new Set();
|
|
915
1066
|
for (const id of obligationIds) {
|
|
916
1067
|
const owner = moduleScopesBySlug.find((m) => id.startsWith(`OBL-${m.slug}-`));
|
|
917
|
-
if (owner)
|
|
1068
|
+
if (!owner)
|
|
1069
|
+
continue;
|
|
1070
|
+
// file_scope inheritance is the scope-less fallback ONLY; the contract's
|
|
1071
|
+
// declared targets are unioned in either way.
|
|
1072
|
+
if (declared.length === 0)
|
|
918
1073
|
for (const f of owner.files)
|
|
919
1074
|
inherited.add(f);
|
|
1075
|
+
for (const t of owner.targets)
|
|
1076
|
+
ownedTargets.add(t);
|
|
920
1077
|
}
|
|
921
|
-
|
|
1078
|
+
// Content-derived order: the node's own declarations first, in the order
|
|
1079
|
+
// they were declared, then the added targets path-sorted — so the resolved
|
|
1080
|
+
// scope (which reaches the plan's content hash through affected_files)
|
|
1081
|
+
// never churns on module ordering.
|
|
1082
|
+
const base = declared.length > 0 ? declared : [...inherited];
|
|
1083
|
+
const added = [...ownedTargets]
|
|
1084
|
+
.filter((t) => !base.includes(t))
|
|
1085
|
+
.sort((left, right) => compareCodeUnits(left, right));
|
|
1086
|
+
return [...base, ...added];
|
|
922
1087
|
};
|
|
923
1088
|
return { resolve, availableSlugs: moduleScopesBySlug.map((m) => m.slug) };
|
|
924
1089
|
}
|
|
@@ -1061,98 +1226,91 @@ async function resolveAdversarialDepth(artifactsDir) {
|
|
|
1061
1226
|
adversarialDepth: riskSignal ? adversarialDepthForTier(riskSignal.tier) : undefined,
|
|
1062
1227
|
};
|
|
1063
1228
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
// evidence. Extracted to resolveAdversarialDepth (behavior-preserving). The
|
|
1074
|
-
// (possibly raised) riskSignal is also consumed by the granularity-collapse
|
|
1075
|
-
// gate below, so it is returned alongside the depth.
|
|
1076
|
-
const { riskSignal, adversarialDepth } = await resolveAdversarialDepth(artifactsDir);
|
|
1077
|
-
// Detect the path-A seed file: present only for structured_audit runs.
|
|
1078
|
-
const seedPath = pathASeedFilePath(artifactsDir);
|
|
1079
|
-
const pathASeedPath = existsSync(seedPath) ? seedPath : undefined;
|
|
1080
|
-
// Resolve artifact paths for the prompt renderers. The host's world is the
|
|
1081
|
-
// plain INPUT files (D3): every host-facing path — both where a role WRITES its
|
|
1082
|
-
// output and where it READS its upstreams — is `<name>.input.json`. The tool's
|
|
1083
|
-
// canonical envelopes (`<name>.json`) are derived at ingest and never named to
|
|
1084
|
-
// the host.
|
|
1085
|
-
const artifactPaths = {};
|
|
1086
|
-
for (const name of CP_ARTIFACT_NAMES) {
|
|
1087
|
-
artifactPaths[name] = contractInputFilePath(artifactsDir, name);
|
|
1229
|
+
// ── Writers ───────────────────────────────────────────────────────────────────
|
|
1230
|
+
/** The artifact-path map every emitted step carries (existing artifacts only). */
|
|
1231
|
+
function contractStepArtifactPaths(ctx, outputPath) {
|
|
1232
|
+
const stepArtifactPaths = {};
|
|
1233
|
+
if (outputPath)
|
|
1234
|
+
stepArtifactPaths.output = outputPath;
|
|
1235
|
+
for (const [key, value] of Object.entries(ctx.artifactPaths)) {
|
|
1236
|
+
if (value && existsSync(value))
|
|
1237
|
+
stepArtifactPaths[key] = value;
|
|
1088
1238
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1239
|
+
if (ctx.sourcePaths) {
|
|
1240
|
+
stepArtifactPaths.source_manifest = ctx.paths.sourceManifest;
|
|
1241
|
+
stepArtifactPaths.remediation_brief = ctx.paths.brief;
|
|
1242
|
+
}
|
|
1243
|
+
return stepArtifactPaths;
|
|
1244
|
+
}
|
|
1245
|
+
function writeContractPromptStep(ctx, params) {
|
|
1246
|
+
const nextCommand = loaderCommand("next-step");
|
|
1247
|
+
const prompt = `${params.prompt}
|
|
1092
1248
|
|
|
1093
1249
|
After writing the output file, run:
|
|
1094
1250
|
|
|
1095
1251
|
\`${nextCommand}\`
|
|
1096
1252
|
`;
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1253
|
+
return writeCurrentStep({
|
|
1254
|
+
stepKind: CONTRACT_STEP_KIND,
|
|
1255
|
+
status: "ready",
|
|
1256
|
+
runId: ctx.runId,
|
|
1257
|
+
repoRoot: ctx.root,
|
|
1258
|
+
artifactsDir: ctx.artifactsDir,
|
|
1259
|
+
prompt,
|
|
1260
|
+
allowedCommands: [nextCommand],
|
|
1261
|
+
stopCondition: params.stopCondition,
|
|
1262
|
+
artifactPaths: contractStepArtifactPaths(ctx, params.outputPath),
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
function writeContractPhaseStep(ctx, phase, extraSection) {
|
|
1266
|
+
const rendered = renderContractPipelinePrompt({
|
|
1267
|
+
role: phase,
|
|
1268
|
+
artifactPaths: ctx.artifactPaths,
|
|
1269
|
+
sourcePaths: ctx.sourcePaths,
|
|
1270
|
+
repoRoot: ctx.root,
|
|
1271
|
+
pathASeedPath: ctx.pathASeedPath,
|
|
1272
|
+
adversarialDepth: ctx.adversarialDepth,
|
|
1273
|
+
});
|
|
1274
|
+
return writeContractPromptStep(ctx, {
|
|
1275
|
+
prompt: extraSection ? `${rendered.prompt}\n${extraSection}` : rendered.prompt,
|
|
1276
|
+
outputPath: rendered.outputPath,
|
|
1277
|
+
stopCondition: `Stop after writing the contract-pipeline output for phase "${phase}" and running next-step.`,
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
function writeContractBlockedStep(ctx, params) {
|
|
1281
|
+
return writeCurrentStep({
|
|
1282
|
+
stepKind: CONTRACT_STEP_KIND,
|
|
1283
|
+
status: "blocked",
|
|
1284
|
+
runId: ctx.runId,
|
|
1285
|
+
repoRoot: ctx.root,
|
|
1286
|
+
artifactsDir: ctx.artifactsDir,
|
|
1287
|
+
prompt: params.prompt,
|
|
1288
|
+
allowedCommands: [],
|
|
1289
|
+
stopCondition: params.stopCondition,
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* T1 slice 4b: ONE round-trip whose prompt concatenates the rendered specs of
|
|
1294
|
+
* several consecutive authoring phases. The worker writes every named artifact
|
|
1295
|
+
* top-down (each later phase's inputs are the files it wrote in the earlier
|
|
1296
|
+
* sections of the same round-trip), then runs next-step once. The group header
|
|
1297
|
+
* overrides the per-section "stop after writing" lines so they are not read as
|
|
1298
|
+
* three separate stop points.
|
|
1299
|
+
*/
|
|
1300
|
+
function writeCollapsedFramingStep(ctx, phases) {
|
|
1301
|
+
const sections = phases.map((phase) => ({
|
|
1302
|
+
phase,
|
|
1303
|
+
rendered: renderContractPipelinePrompt({
|
|
1123
1304
|
role: phase,
|
|
1124
|
-
artifactPaths,
|
|
1125
|
-
sourcePaths,
|
|
1126
|
-
repoRoot: root,
|
|
1127
|
-
pathASeedPath,
|
|
1128
|
-
adversarialDepth,
|
|
1129
|
-
})
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
stopCondition: `Stop after writing the contract-pipeline output for phase "${phase}" and running next-step.`,
|
|
1134
|
-
});
|
|
1135
|
-
};
|
|
1136
|
-
// T1 slice 4b: emit ONE round-trip whose prompt concatenates the rendered
|
|
1137
|
-
// specs of several consecutive authoring phases. The worker writes every
|
|
1138
|
-
// named artifact top-down (each later phase's inputs are the files it wrote in
|
|
1139
|
-
// the earlier sections of the same round-trip), then runs next-step once. The
|
|
1140
|
-
// group header overrides the per-section "stop after writing" lines so they are
|
|
1141
|
-
// not read as three separate stop points.
|
|
1142
|
-
const buildCollapsedFramingStep = (phases) => {
|
|
1143
|
-
const sections = phases.map((phase) => {
|
|
1144
|
-
const rendered = renderContractPipelinePrompt({
|
|
1145
|
-
role: phase,
|
|
1146
|
-
artifactPaths,
|
|
1147
|
-
sourcePaths,
|
|
1148
|
-
repoRoot: root,
|
|
1149
|
-
pathASeedPath,
|
|
1150
|
-
adversarialDepth,
|
|
1151
|
-
});
|
|
1152
|
-
return { phase, rendered };
|
|
1153
|
-
});
|
|
1154
|
-
const outputPaths = sections.map((s) => s.rendered.outputPath);
|
|
1155
|
-
const header = `# Collapsed Authoring Round-Trip — ${phases.length} Phases
|
|
1305
|
+
artifactPaths: ctx.artifactPaths,
|
|
1306
|
+
sourcePaths: ctx.sourcePaths,
|
|
1307
|
+
repoRoot: ctx.root,
|
|
1308
|
+
pathASeedPath: ctx.pathASeedPath,
|
|
1309
|
+
adversarialDepth: ctx.adversarialDepth,
|
|
1310
|
+
}),
|
|
1311
|
+
}));
|
|
1312
|
+
const outputPaths = sections.map((s) => s.rendered.outputPath);
|
|
1313
|
+
const header = `# Collapsed Authoring Round-Trip — ${phases.length} Phases
|
|
1156
1314
|
|
|
1157
1315
|
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.
|
|
1158
1316
|
|
|
@@ -1162,38 +1320,38 @@ If you cannot complete a section (an artifact would be malformed), write the one
|
|
|
1162
1320
|
|
|
1163
1321
|
Artifacts to produce (in order):
|
|
1164
1322
|
${outputPaths.map((p, i) => `${i + 1}. \`${p}\` (${phases[i]})`).join("\n")}`;
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1323
|
+
const body = sections.map((s) => `\n---\n\n${s.rendered.prompt}`).join("\n");
|
|
1324
|
+
return writeContractPromptStep(ctx, {
|
|
1325
|
+
prompt: `${header}\n${body}`,
|
|
1326
|
+
outputPath: outputPaths[outputPaths.length - 1],
|
|
1327
|
+
stopCondition: `Stop after writing all ${phases.length} collapsed-framing artifacts (${phases.join(", ")}) and running next-step once.`,
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* DC-3: fan a parallel phase out to one bounded item per module. The host owns
|
|
1332
|
+
* grouping, concurrency, and execution choices; this tool supplies only the
|
|
1333
|
+
* complete coherent workload. Each item writes a per-module shard, and the next
|
|
1334
|
+
* next-step merges every shard into the aggregated artifact before any
|
|
1335
|
+
* downstream derivation. A degenerate decomposition (zero or one module) falls
|
|
1336
|
+
* back to the single aggregated step.
|
|
1337
|
+
*/
|
|
1338
|
+
async function writeParallelModuleWaveStep(ctx, phase) {
|
|
1339
|
+
const modules = await readDecomposedModules(ctx.artifactsDir);
|
|
1340
|
+
if (modules.length <= 1) {
|
|
1341
|
+
return writeContractPhaseStep(ctx, phase);
|
|
1342
|
+
}
|
|
1343
|
+
const inputArtifact = "module_decomposition";
|
|
1344
|
+
const inputPaths = ["goal_spec", "context_bundle", "module_decomposition"].map((key) => `- \`${ctx.artifactPaths[key]}\` (${key})`);
|
|
1345
|
+
const moduleLines = modules
|
|
1346
|
+
.map((mod, i) => {
|
|
1347
|
+
const shardPath = moduleShardPath(ctx.artifactsDir, phase, mod.name);
|
|
1348
|
+
const scope = mod.file_scope.length > 0
|
|
1349
|
+
? mod.file_scope.map((p) => `\`${p}\``).join(", ")
|
|
1350
|
+
: "_(no declared file scope)_";
|
|
1351
|
+
return `${i + 1}. **${mod.name}** — file scope: ${scope}\n - Write this module's contract to exactly: \`${shardPath}\``;
|
|
1352
|
+
})
|
|
1353
|
+
.join("\n");
|
|
1354
|
+
const perModuleSchema = `{
|
|
1197
1355
|
"name": "<module-name — must equal the assigned module>",
|
|
1198
1356
|
"inputs": ["<what this module receives>"],
|
|
1199
1357
|
"outputs": ["<what this module produces>"],
|
|
@@ -1203,10 +1361,10 @@ ${outputPaths.map((p, i) => `${i + 1}. \`${p}\` (${phases[i]})`).join("\n")}`;
|
|
|
1203
1361
|
"failure_modes": ["<ways this module can fail and how callers should handle them>"],
|
|
1204
1362
|
"neighbor_needs": [{ "neighbor": "<module-name>", "needs": "<what this module needs>" }]
|
|
1205
1363
|
}`;
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1364
|
+
const taskVerb = "draft its module contract";
|
|
1365
|
+
const cwdNote = `\n> Set the shell/tool working directory to \`${ctx.root}\` before running any commands.\n`;
|
|
1366
|
+
const nextCommand = loaderCommand("next-step");
|
|
1367
|
+
const prompt = `# Per-Module Contract Drafting (${modules.length} modules)
|
|
1210
1368
|
|
|
1211
1369
|
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.
|
|
1212
1370
|
${cwdNote}
|
|
@@ -1236,95 +1394,264 @@ The orchestrator verifies every module shard is present, merges them into \`${PH
|
|
|
1236
1394
|
|
|
1237
1395
|
**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.
|
|
1238
1396
|
`;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1397
|
+
return writeCurrentStep({
|
|
1398
|
+
stepKind: CONTRACT_STEP_KIND,
|
|
1399
|
+
status: "ready",
|
|
1400
|
+
runId: ctx.runId,
|
|
1401
|
+
repoRoot: ctx.root,
|
|
1402
|
+
artifactsDir: ctx.artifactsDir,
|
|
1403
|
+
prompt,
|
|
1404
|
+
allowedCommands: [nextCommand],
|
|
1405
|
+
stopCondition: `Stop after writing every per-module shard for phase "${phase}" and running next-step.`,
|
|
1406
|
+
artifactPaths: contractStepArtifactPaths(ctx),
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* The ONE writer dispatch behind the scaffold's single emission call site. Each
|
|
1411
|
+
* underlying writer is reached from exactly here.
|
|
1412
|
+
*/
|
|
1413
|
+
async function writeContractStepPlan(ctx, plan) {
|
|
1414
|
+
switch (plan.via) {
|
|
1415
|
+
case "phase":
|
|
1416
|
+
return await writeContractPhaseStep(ctx, plan.phase, plan.extraSection);
|
|
1417
|
+
case "step":
|
|
1418
|
+
return await writeContractPromptStep(ctx, plan);
|
|
1419
|
+
case "blocked":
|
|
1420
|
+
return await writeContractBlockedStep(ctx, plan);
|
|
1421
|
+
case "module_wave":
|
|
1422
|
+
return await writeParallelModuleWaveStep(ctx, plan.phase);
|
|
1423
|
+
case "collapsed_framing":
|
|
1424
|
+
return await writeCollapsedFramingStep(ctx, plan.phases);
|
|
1425
|
+
case "rederive":
|
|
1426
|
+
// A deterministic artifact was just written; the frontier moved, so the
|
|
1427
|
+
// whole walk re-runs against the new state rather than guessing the phase.
|
|
1428
|
+
return await buildNextContractPipelineStep(ctx.options);
|
|
1429
|
+
case "pipeline_complete":
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// ── Branch on `evaluated`: consuming the shared gate-outcome record ─────
|
|
1434
|
+
/**
|
|
1435
|
+
* Read every contract-pipeline payload from disk, plus the intake
|
|
1436
|
+
* finding-enumeration, in the shape the shared cross-gate evaluator consumes.
|
|
1437
|
+
* Always a fresh read — there is no payload cache to go stale.
|
|
1438
|
+
*/
|
|
1439
|
+
async function readCrossGateInputs(artifactsDir, root) {
|
|
1440
|
+
const payloads = new Map();
|
|
1441
|
+
for (const name of CP_ARTIFACT_NAMES) {
|
|
1442
|
+
const envelope = await readContractArtifact(artifactsDir, name);
|
|
1443
|
+
if (envelope)
|
|
1444
|
+
payloads.set(name, envelopePayload(envelope));
|
|
1445
|
+
}
|
|
1446
|
+
const findingEnumeration = await readOptionalJsonFile(intakePaths(artifactsDir).findingEnumeration);
|
|
1447
|
+
return { payloads, findingEnumeration, root };
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Read every contract-pipeline payload FRESH for the shared cross-gates.
|
|
1451
|
+
*
|
|
1452
|
+
* REFUSES before this invocation's ingestion + staleness-archive pass has run.
|
|
1453
|
+
* EVERY in-pipeline cross-gate read goes through here — including the two
|
|
1454
|
+
* exported helpers, which take the payloads this reader produced rather than
|
|
1455
|
+
* reading again — so there is no in-pipeline path to a payload that skipped the
|
|
1456
|
+
* check.
|
|
1457
|
+
* That is the freshness half of The branch-on-evaluated freshness rule, made mechanical: a
|
|
1458
|
+
* gate cannot be handed a payload snapshot taken before its own step archived
|
|
1459
|
+
* the stale copy, because the only way to obtain payloads declines to produce
|
|
1460
|
+
* them until `artifactsSettled` is set.
|
|
1461
|
+
*/
|
|
1462
|
+
async function readCrossGatePayloads(ctx) {
|
|
1463
|
+
if (!ctx.artifactsSettled) {
|
|
1464
|
+
throw new Error("contract pipeline: cross-gate payloads were requested before this invocation's " +
|
|
1465
|
+
"ingestion + staleness-archive pass ran. A gate must read artifact payloads AFTER " +
|
|
1466
|
+
"the archive pass, never from a snapshot taken before it.");
|
|
1467
|
+
}
|
|
1468
|
+
return await readCrossGateInputs(ctx.artifactsDir, ctx.root);
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Consume a subset of the shared cross-gate outcomes, branching on `evaluated`
|
|
1472
|
+
* BEFORE an empty `issues` array is allowed to mean "clean".
|
|
1473
|
+
*
|
|
1474
|
+
* `required` is DECLARED PER CALL SITE, as data, because "did not run" means
|
|
1475
|
+
* different things at different boundaries. At a boundary whose upstream phase
|
|
1476
|
+
* order guarantees the gate's input exists, a skip is a refusal — its empty
|
|
1477
|
+
* issue list is proof of nothing. Earlier in the pipeline the same skip means
|
|
1478
|
+
* "not applicable yet", and the gate is simply not required there.
|
|
1479
|
+
*
|
|
1480
|
+
* THE UNCOVERED HALF, stated rather than implied: `digest_coverage` is the one
|
|
1481
|
+
* gate of the eight whose skip is a DOMAIN non-applicability (a source that is
|
|
1482
|
+
* not finding-enumerable) rather than a missing payload, so no boundary lists
|
|
1483
|
+
* it as required and a genuinely absent finding-enumeration file for an
|
|
1484
|
+
* enumerable source still skips silently. Closing that needs the gate module to
|
|
1485
|
+
* expose its enumerability predicate — an edit outside this work item's write
|
|
1486
|
+
* scope.
|
|
1487
|
+
*/
|
|
1488
|
+
export function consumeGateOutcomes(outcomes, selected, required) {
|
|
1489
|
+
const violations = [];
|
|
1490
|
+
for (const gate of selected) {
|
|
1491
|
+
const outcome = outcomes.find((candidate) => candidate.gate === gate);
|
|
1492
|
+
if (!outcome) {
|
|
1493
|
+
violations.push(`[${gate}] produced no outcome record; the gate set changed without this call site.`);
|
|
1494
|
+
continue;
|
|
1244
1495
|
}
|
|
1245
|
-
if (
|
|
1246
|
-
|
|
1247
|
-
|
|
1496
|
+
if (!outcome.evaluated) {
|
|
1497
|
+
if (required.has(gate)) {
|
|
1498
|
+
violations.push(`[${gate}] did not run (${outcome.reason ?? "no reason recorded"}). Its empty ` +
|
|
1499
|
+
`issue list is not proof of a clean gate at this boundary.`);
|
|
1500
|
+
}
|
|
1501
|
+
continue;
|
|
1248
1502
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
runId,
|
|
1253
|
-
repoRoot: root,
|
|
1254
|
-
artifactsDir,
|
|
1255
|
-
prompt,
|
|
1256
|
-
allowedCommands: [nextCommand],
|
|
1257
|
-
stopCondition: `Stop after writing every per-module shard for phase "${phase}" and running next-step.`,
|
|
1258
|
-
artifactPaths: stepArtifactPaths,
|
|
1259
|
-
});
|
|
1260
|
-
};
|
|
1261
|
-
/**
|
|
1262
|
-
* DC-3 merge intercept: when a parallel phase's aggregated artifact is still
|
|
1263
|
-
* missing, merge the per-module shards into it once they are ALL present. A
|
|
1264
|
-
* missing shard re-emits the wave (never promotes a partial aggregate). After
|
|
1265
|
-
* a complete merge the artifact is written enveloped and the pipeline
|
|
1266
|
-
* re-derives; the existing seam_reconciliation / critique pass downstream
|
|
1267
|
-
* stays the consistency gate over the merged contracts.
|
|
1268
|
-
*/
|
|
1269
|
-
const tryMergeModuleShards = async (phase) => {
|
|
1270
|
-
const modules = await readDecomposedModules(artifactsDir);
|
|
1271
|
-
// Degenerate decompositions never used the shard path — let the normal
|
|
1272
|
-
// single-agent aggregate step handle them.
|
|
1273
|
-
if (modules.length <= 1)
|
|
1274
|
-
return "incomplete";
|
|
1275
|
-
const scan = await scanModuleShards(artifactsDir, phase, modules);
|
|
1276
|
-
if (scan.missing.length > 0) {
|
|
1277
|
-
// Completeness not met → re-emit the wave for the missing modules.
|
|
1278
|
-
return buildParallelModuleWaveStep(phase);
|
|
1503
|
+
for (const issue of outcome.issues) {
|
|
1504
|
+
if (issue.severity === "error")
|
|
1505
|
+
violations.push(`[${issue.path}] ${issue.message}`);
|
|
1279
1506
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1507
|
+
}
|
|
1508
|
+
return { ok: violations.length === 0, violations };
|
|
1509
|
+
}
|
|
1510
|
+
/** Locate one gate's outcome in the canonical-order outcome list. */
|
|
1511
|
+
function gateOutcomeOf(outcomes, gate) {
|
|
1512
|
+
return outcomes.find((candidate) => candidate.gate === gate);
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* The gates the PROMOTION boundary requires to have actually run. Every phase
|
|
1516
|
+
* artifact exists by the time `nextPhase` is null, so a skip here can only mean
|
|
1517
|
+
* a payload went missing or malformed — never "too early".
|
|
1518
|
+
*/
|
|
1519
|
+
const PROMOTION_REQUIRED_GATES = new Set([
|
|
1520
|
+
"paired_obligations",
|
|
1521
|
+
"evidence_threaded",
|
|
1522
|
+
"reconciliation_derivation",
|
|
1523
|
+
]);
|
|
1524
|
+
/** The subset of gates the promotion boundary consumes. */
|
|
1525
|
+
const PROMOTION_GATES = [
|
|
1526
|
+
"paired_obligations",
|
|
1527
|
+
"evidence_threaded",
|
|
1528
|
+
"digest_coverage",
|
|
1529
|
+
"reconciliation_derivation",
|
|
1530
|
+
];
|
|
1531
|
+
/**
|
|
1532
|
+
* The gates the PRE-CRITIC structural floor requires. `contract_finalization`,
|
|
1533
|
+
* `seam_reconciliation` and `test_validator_plan` all precede `critic` in the
|
|
1534
|
+
* phase order, so their artifacts exist by the time this boundary is reached.
|
|
1535
|
+
*/
|
|
1536
|
+
const PRE_CRITIC_REQUIRED_GATES = new Set([
|
|
1537
|
+
"paired_obligations",
|
|
1538
|
+
"reconciliation_derivation",
|
|
1539
|
+
]);
|
|
1540
|
+
// ── Gates, in execution order ─────────────────────────────────────────────────
|
|
1541
|
+
/**
|
|
1542
|
+
* Seed source-digest binding. Re-hash every source path the path_a seed
|
|
1543
|
+
* recorded, against the digest it recorded at seed-build time, and refuse with
|
|
1544
|
+
* a classified blocked step on a mismatch — rather than spending the whole
|
|
1545
|
+
* design pipeline on content that no longer holds the findings the seed
|
|
1546
|
+
* enumerates. Runs first, before anything is ingested or derived.
|
|
1547
|
+
*/
|
|
1548
|
+
const seedSourceDigestGate = async (ctx) => {
|
|
1549
|
+
if (!ctx.pathASeedPath)
|
|
1550
|
+
return null;
|
|
1551
|
+
const seed = await readOptionalJsonFile(ctx.pathASeedPath);
|
|
1552
|
+
const mismatches = await detectSeedSourceDigestMismatches(ctx.root, seed);
|
|
1553
|
+
if (mismatches.length === 0)
|
|
1554
|
+
return null;
|
|
1555
|
+
const lines = mismatches
|
|
1556
|
+
.map((mismatch) => `- \`${mismatch.path}\` — recorded \`${mismatch.expected.slice(0, 12)}…\`, ` +
|
|
1557
|
+
`now ${mismatch.actual ? `\`${mismatch.actual.slice(0, 12)}…\`` : "**unreadable**"}`)
|
|
1558
|
+
.join("\n");
|
|
1559
|
+
return {
|
|
1560
|
+
via: "blocked",
|
|
1561
|
+
prompt: `# Source Content Changed Since the Audit Seed Was Built
|
|
1562
|
+
|
|
1563
|
+
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:
|
|
1564
|
+
|
|
1565
|
+
${lines}
|
|
1566
|
+
|
|
1567
|
+
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:
|
|
1568
|
+
|
|
1569
|
+
1. **Re-run the audit extraction** against the current tree, so the findings describe the code as it now stands; or
|
|
1570
|
+
2. **Restore the drifted sources** to the content the audit read, if the change was unintended.
|
|
1571
|
+
|
|
1572
|
+
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.`,
|
|
1573
|
+
stopCondition: "Stop — the contract pipeline is blocked on a source whose content no longer matches the audit seed.",
|
|
1292
1574
|
};
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1575
|
+
};
|
|
1576
|
+
/**
|
|
1577
|
+
* Ingest raw worker outputs into validated envelopes. An output that fails
|
|
1578
|
+
* validation is archived and its producing phase re-emitted with the validation
|
|
1579
|
+
* errors — LLM output is untrusted until validated.
|
|
1580
|
+
*/
|
|
1581
|
+
const invalidIngestionGate = async (ctx) => {
|
|
1582
|
+
const ingestion = await ingestContractArtifacts(ctx.artifactsDir);
|
|
1583
|
+
if (ingestion.invalid.length === 0)
|
|
1584
|
+
return null;
|
|
1585
|
+
const first = ingestion.invalid[0];
|
|
1586
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, first.name, "invalid", ctx.options.renameFn);
|
|
1587
|
+
return {
|
|
1588
|
+
via: "phase",
|
|
1589
|
+
phase: ARTIFACT_TO_PHASE[first.name] ?? "goal_normalization",
|
|
1590
|
+
extraSection: `## Validation Errors From the Previous Attempt
|
|
1302
1591
|
|
|
1303
1592
|
The previous \`${first.name}\` output failed validation and was archived. Fix every issue below in the rewritten output:
|
|
1304
1593
|
|
|
1305
1594
|
${formatValidationIssues(first.issues)}
|
|
1306
|
-
${rejectionRewriteInstruction(archived)}
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1595
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1596
|
+
};
|
|
1597
|
+
};
|
|
1598
|
+
/**
|
|
1599
|
+
* Archive stale artifacts so the staleness DAG re-derives everything downstream
|
|
1600
|
+
* of a repaired (re-ingested) upstream artifact — and ABORT when an archive
|
|
1601
|
+
* fails.
|
|
1602
|
+
*
|
|
1603
|
+
* COR-114e4941: the returned ArchiveOutcome used to be discarded here, alone
|
|
1604
|
+
* among the four archive call sites. `originalFree: false` means the move
|
|
1605
|
+
* failed and the stale file is STILL at its canonical path, where
|
|
1606
|
+
* `contractArtifactExists` (a bare `existsSync`) reports it as present — so the
|
|
1607
|
+
* producing phase was never re-emitted and every downstream derivation (the
|
|
1608
|
+
* obligation ledger, the phase cut, the DAG) was built on content the staleness
|
|
1609
|
+
* DAG had already declared invalid. Refusing here is the only ordering that
|
|
1610
|
+
* keeps that impossible: the frontier is not resolved until every stale
|
|
1611
|
+
* artifact is genuinely out of the way.
|
|
1612
|
+
*/
|
|
1613
|
+
const staleArchiveGate = async (ctx) => {
|
|
1614
|
+
const staleness = await detectStaleArtifacts(ctx.artifactsDir);
|
|
1311
1615
|
for (const name of staleness.stale) {
|
|
1312
|
-
await archiveContractArtifact(artifactsDir, name, "stale");
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1616
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, name, "stale", ctx.options.renameFn);
|
|
1617
|
+
if (archived.originalFree)
|
|
1618
|
+
continue;
|
|
1619
|
+
const phase = ARTIFACT_TO_PHASE[name];
|
|
1620
|
+
if (phase) {
|
|
1621
|
+
return {
|
|
1622
|
+
via: "phase",
|
|
1623
|
+
phase,
|
|
1624
|
+
extraSection: `## A Stale \`${name}\` Could Not Be Archived
|
|
1625
|
+
|
|
1626
|
+
\`${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.
|
|
1627
|
+
|
|
1628
|
+
Rewrite \`${name}\` from its current upstreams.
|
|
1629
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
return {
|
|
1633
|
+
via: "blocked",
|
|
1634
|
+
prompt: `# A Stale Derived Artifact Could Not Be Archived
|
|
1635
|
+
|
|
1636
|
+
\`${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.
|
|
1637
|
+
|
|
1638
|
+
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.`,
|
|
1639
|
+
stopCondition: "Stop — the contract pipeline is blocked on a stale artifact that could not be archived.",
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
// OBL-m-friction-inv-5 (post_repair_rederive): when a judge needs_repair →
|
|
1643
|
+
// regenerate-target landed, the re-ingested target makes its downstream
|
|
1644
|
+
// artifacts stale and they are archived above — the REAL remediate
|
|
1645
|
+
// post-repair re-derive site. Route this backend-observed step-boundary fact
|
|
1646
|
+
// through the single CE-005 chokepoint. Discriminator = repair target
|
|
1647
|
+
// artifact id + repair iteration count, so re-recording the same re-derive is
|
|
1648
|
+
// a collision-free no-op (CE-006).
|
|
1322
1649
|
if (staleness.stale.length > 0) {
|
|
1323
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1650
|
+
const repairState = await readRepairState(ctx.artifactsDir);
|
|
1324
1651
|
const lastRepair = repairState.repairs[repairState.repairs.length - 1];
|
|
1325
1652
|
if (lastRepair) {
|
|
1326
1653
|
const iteration = repairState.repairs.length;
|
|
1327
|
-
await captureStepBoundaryFriction(artifactsDir, runId, {
|
|
1654
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
1328
1655
|
eventType: "post_repair_rederive",
|
|
1329
1656
|
discriminator: `${lastRepair.target}:${iteration}`,
|
|
1330
1657
|
note: `Post-repair re-derive: repair iteration ${iteration} of "${lastRepair.target}" ` +
|
|
@@ -1334,151 +1661,175 @@ ${rejectionRewriteInstruction(archived)}`);
|
|
|
1334
1661
|
}, "remediate-code");
|
|
1335
1662
|
}
|
|
1336
1663
|
}
|
|
1337
|
-
|
|
1338
|
-
//
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1664
|
+
// Ingestion + archiving are done: payloads read from here on are this
|
|
1665
|
+
// invocation's own view. Nothing downstream may read them before this point.
|
|
1666
|
+
ctx.artifactsSettled = true;
|
|
1667
|
+
return null;
|
|
1668
|
+
};
|
|
1669
|
+
/**
|
|
1670
|
+
* Resolve the phase frontier — the one gate that never emits. It sits HERE, and
|
|
1671
|
+
* not at the top, because archiving a stale artifact re-opens its producing
|
|
1672
|
+
* phase: computing the frontier before the archive pass would read a phase as
|
|
1673
|
+
* satisfied by a file the pipeline has just declared invalid.
|
|
1674
|
+
*/
|
|
1675
|
+
const phaseFrontierGate = (ctx) => {
|
|
1676
|
+
ctx.nextPhase = nextMissingContractPhase(ctx.artifactsDir);
|
|
1677
|
+
return null;
|
|
1678
|
+
};
|
|
1679
|
+
/**
|
|
1680
|
+
* Goal-ID consistency (ARC-86b18f1b): every persisted artifact that carries a
|
|
1681
|
+
* goal_id must agree on the same value. A mismatch means two runs were
|
|
1682
|
+
* interleaved; re-emit the earliest mismatched phase so the worker can correct
|
|
1683
|
+
* it. Deliberately phase-independent.
|
|
1684
|
+
*/
|
|
1685
|
+
const goalIdConsistencyGate = async (ctx) => {
|
|
1686
|
+
const goalIdArtifacts = {};
|
|
1687
|
+
for (const name of CP_ARTIFACT_NAMES) {
|
|
1688
|
+
const envelope = await readContractArtifact(ctx.artifactsDir, name);
|
|
1689
|
+
if (envelope)
|
|
1690
|
+
goalIdArtifacts[name] = envelopePayload(envelope);
|
|
1691
|
+
}
|
|
1692
|
+
const goalIdErrors = validateGoalIdConsistency(goalIdArtifacts).filter((issue) => issue.severity === "error");
|
|
1693
|
+
if (goalIdErrors.length === 0)
|
|
1694
|
+
return null;
|
|
1695
|
+
// issue.path is "<artifact_name>.goal_id"; extract the artifact name.
|
|
1696
|
+
const firstPath = goalIdErrors[0]?.path ?? "";
|
|
1697
|
+
const mismatchedArtifact = firstPath.replace(/\.goal_id$/, "");
|
|
1698
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, mismatchedArtifact, "invalid", ctx.options.renameFn);
|
|
1699
|
+
return {
|
|
1700
|
+
via: "phase",
|
|
1701
|
+
phase: ARTIFACT_TO_PHASE[mismatchedArtifact] ?? "goal_normalization",
|
|
1702
|
+
extraSection: `## Goal-ID Consistency Error
|
|
1359
1703
|
|
|
1360
1704
|
Every contract-pipeline artifact must share the same goal_id. The following mismatch was detected:
|
|
1361
1705
|
|
|
1362
|
-
${goalIdErrors.map((
|
|
1706
|
+
${goalIdErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
1363
1707
|
|
|
1364
1708
|
Rewrite the output so its goal_id matches the goal_id established in goal_spec.json.
|
|
1365
|
-
${rejectionRewriteInstruction(archived)}
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1709
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1710
|
+
};
|
|
1711
|
+
};
|
|
1712
|
+
/**
|
|
1713
|
+
* Finalized-module-SET gate (INV-CO-13). `deriveFinalizedModuleContracts` maps
|
|
1714
|
+
* the drafts 1:1, so the deterministic path can never violate this — but it is
|
|
1715
|
+
* not the only writer: a judge or critique repair re-emits contract_finalization
|
|
1716
|
+
* as an LLM step, ingested under a SHAPE-ONLY validator that structurally cannot
|
|
1717
|
+
* see the drafts. A rewrite that merges modules under an invented name and drops
|
|
1718
|
+
* another would otherwise be accepted, and the phase cut, the derived obligation
|
|
1719
|
+
* ids and the DAG write-scope join would all then be built on a module set that
|
|
1720
|
+
* has already lost a module.
|
|
1721
|
+
*
|
|
1722
|
+
* DELIBERATELY PHASE-INDEPENDENT, like the goal-ID gate: rewriting
|
|
1723
|
+
* finalized_module_contracts stales its declared dependent
|
|
1724
|
+
* conceptual_design_critique, which the staleness gate archives BEFORE the
|
|
1725
|
+
* frontier is resolved — so the phase right after a corrupting rewrite is
|
|
1726
|
+
* `critique`, not `critic`. Gating at the critic boundary would not fire until
|
|
1727
|
+
* critique, obligation_ledger, cyclic_seam_resolution, test_validator_plan and
|
|
1728
|
+
* assessment had all been re-spent on the collapsed set.
|
|
1729
|
+
*
|
|
1730
|
+
* The gate is NOT in `PRE_CRITIC_REQUIRED_GATES` / `PROMOTION_REQUIRED_GATES`
|
|
1731
|
+
* because at this phase-independent position a not-evaluated outcome genuinely
|
|
1732
|
+
* means "the drafted or finalized contracts do not exist yet" — the branch on
|
|
1733
|
+
* `evaluated` is taken, and its declared meaning here is "not yet applicable".
|
|
1734
|
+
*/
|
|
1735
|
+
const finalizedModuleSetGate = async (ctx) => {
|
|
1736
|
+
const outcomes = evaluateContractPipelineCrossGateOutcomes(await readCrossGatePayloads(ctx));
|
|
1737
|
+
const outcome = gateOutcomeOf(outcomes, "finalized_module_set_preserved");
|
|
1738
|
+
if (!outcome?.evaluated)
|
|
1739
|
+
return null;
|
|
1740
|
+
const moduleSetErrors = outcome.issues.filter((issue) => issue.severity === "error");
|
|
1741
|
+
if (moduleSetErrors.length === 0)
|
|
1742
|
+
return null;
|
|
1743
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, "finalized_module_contracts", "invalid", ctx.options.renameFn);
|
|
1744
|
+
return {
|
|
1745
|
+
via: "phase",
|
|
1746
|
+
phase: "contract_finalization",
|
|
1747
|
+
extraSection: `## Finalized Module Set Does Not Match the Drafted Contracts
|
|
1402
1748
|
|
|
1403
1749
|
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:
|
|
1404
1750
|
|
|
1405
1751
|
${moduleSetErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
1406
|
-
${rejectionRewriteInstruction(archived)}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1752
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1753
|
+
};
|
|
1754
|
+
};
|
|
1755
|
+
/**
|
|
1756
|
+
* Path-A overlap topology gate. A required audit seam is not advisory:
|
|
1757
|
+
* decomposition must name exactly one seam-preparation module and keep distinct
|
|
1758
|
+
* implementation modules for the participating work blocks. Checked before
|
|
1759
|
+
* module contracts fan out, so the seam is shaped once and downstream authors
|
|
1760
|
+
* can work in parallel against it.
|
|
1761
|
+
*/
|
|
1762
|
+
const workBlockSeamGate = async (ctx) => {
|
|
1763
|
+
if (!contractArtifactExists(ctx.artifactsDir, "module_decomposition"))
|
|
1764
|
+
return null;
|
|
1765
|
+
const seed = await readOptionalJsonFile(pathASeedFilePath(ctx.artifactsDir));
|
|
1766
|
+
if (!seed)
|
|
1767
|
+
return null;
|
|
1768
|
+
const decomposition = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_decomposition"));
|
|
1769
|
+
const seamIssues = validateWorkBlockSeamPreparation(seed, decomposition).filter((issue) => issue.severity === "error");
|
|
1770
|
+
if (seamIssues.length === 0)
|
|
1771
|
+
return null;
|
|
1772
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, "module_decomposition", "invalid", ctx.options.renameFn);
|
|
1773
|
+
return {
|
|
1774
|
+
via: "phase",
|
|
1775
|
+
phase: "decomposition",
|
|
1776
|
+
extraSection: `## Audit Work-Block Seam Errors
|
|
1423
1777
|
|
|
1424
1778
|
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:
|
|
1425
1779
|
|
|
1426
1780
|
${seamIssues.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
1427
|
-
${rejectionRewriteInstruction(archived)}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
});
|
|
1451
|
-
repairState.critique_repairs = critiqueRepairs;
|
|
1452
|
-
await writeRepairState(artifactsDir, repairState);
|
|
1453
|
-
}
|
|
1454
|
-
const rendered = renderContractRepairPrompt({
|
|
1455
|
-
target: "finalized_module_contracts",
|
|
1456
|
-
instruction: "Revise the design to resolve every BLOCKING concern in the conceptual design critique " +
|
|
1457
|
-
`(${gate.blockingIds.join(", ")}). Read conceptual_design_critique.json for each concern's ` +
|
|
1458
|
-
"description, then rewrite the finalized module contracts so the blocking concerns no longer apply.",
|
|
1459
|
-
artifactPaths,
|
|
1460
|
-
repoRoot: root,
|
|
1461
|
-
});
|
|
1462
|
-
return buildStep({
|
|
1463
|
-
prompt: rendered.prompt,
|
|
1464
|
-
outputPath: rendered.outputPath,
|
|
1465
|
-
stopCondition: "Stop after rewriting finalized_module_contracts to resolve the blocking critique concerns and running next-step.",
|
|
1781
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1782
|
+
};
|
|
1783
|
+
};
|
|
1784
|
+
/**
|
|
1785
|
+
* Conceptual-design-critique gate (A1). Once the critique exists, a blocking
|
|
1786
|
+
* concern routes a design repair BEFORE any downstream artifact is derived. The
|
|
1787
|
+
* signal is mechanical (any blocking item), so the author's verdict label can't
|
|
1788
|
+
* wave a blocking concern through. Convergence-terminated: repairing the
|
|
1789
|
+
* finalized contracts re-stales + re-emits the critique, a clean re-critique
|
|
1790
|
+
* proceeds, a stalled loop escalates to the user.
|
|
1791
|
+
*/
|
|
1792
|
+
const conceptualCritiqueGate = async (ctx) => {
|
|
1793
|
+
if (!contractArtifactExists(ctx.artifactsDir, "conceptual_design_critique"))
|
|
1794
|
+
return null;
|
|
1795
|
+
const gate = await evaluateCritiqueGate(ctx.artifactsDir);
|
|
1796
|
+
if (gate.kind === "repair") {
|
|
1797
|
+
const repairState = await readRepairState(ctx.artifactsDir);
|
|
1798
|
+
const critiqueRepairs = repairState.critique_repairs ?? [];
|
|
1799
|
+
if (!critiqueRepairs.some((repair) => repair.critique_hash === gate.critiqueHash)) {
|
|
1800
|
+
critiqueRepairs.push({
|
|
1801
|
+
critique_hash: gate.critiqueHash,
|
|
1802
|
+
at: new Date().toISOString(),
|
|
1803
|
+
blocking_ids: gate.blockingIds,
|
|
1466
1804
|
});
|
|
1805
|
+
repairState.critique_repairs = critiqueRepairs;
|
|
1806
|
+
await writeRepairState(ctx.artifactsDir, repairState);
|
|
1467
1807
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1808
|
+
const rendered = renderContractRepairPrompt({
|
|
1809
|
+
target: "finalized_module_contracts",
|
|
1810
|
+
instruction: "Revise the design to resolve every BLOCKING concern in the conceptual design critique " +
|
|
1811
|
+
`(${gate.blockingIds.join(", ")}). Read conceptual_design_critique.json for each concern's ` +
|
|
1812
|
+
"description, then rewrite the finalized module contracts so the blocking concerns no longer apply.",
|
|
1813
|
+
artifactPaths: ctx.artifactPaths,
|
|
1814
|
+
repoRoot: ctx.root,
|
|
1815
|
+
});
|
|
1816
|
+
return {
|
|
1817
|
+
via: "step",
|
|
1818
|
+
prompt: rendered.prompt,
|
|
1819
|
+
outputPath: rendered.outputPath,
|
|
1820
|
+
stopCondition: "Stop after rewriting finalized_module_contracts to resolve the blocking critique concerns and running next-step.",
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
if (gate.kind === "escalate") {
|
|
1824
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
1825
|
+
eventType: "repair_round",
|
|
1826
|
+
discriminator: `critique_nonconvergence:${gate.reason}`,
|
|
1827
|
+
note: `Conceptual-design critique↔repair loop escalated (${gate.reason}): ${gate.note}`,
|
|
1828
|
+
category: "trap",
|
|
1829
|
+
}, "remediate-code");
|
|
1830
|
+
return {
|
|
1831
|
+
via: "blocked",
|
|
1832
|
+
prompt: `# Conceptual-Design Critique Did Not Converge
|
|
1482
1833
|
|
|
1483
1834
|
${gate.note}
|
|
1484
1835
|
|
|
@@ -1487,372 +1838,350 @@ ${gate.note}
|
|
|
1487
1838
|
${gate.blocking.map((id) => `- ${id}`).join("\n")}
|
|
1488
1839
|
|
|
1489
1840
|
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.`,
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
// gate.kind === "proceed": fall through.
|
|
1495
|
-
}
|
|
1496
|
-
// 2.8. Deterministic artifact derivation (S1, contract-authoring determinism).
|
|
1497
|
-
// The obligation ledger is a pure function of the finalized module
|
|
1498
|
-
// contracts (every invariant/failure mode/module → an obligation), so it
|
|
1499
|
-
// is generated by the tool rather than authored by an LLM phase: the
|
|
1500
|
-
// structure can never be malformed, no judgment is spent on a mechanical
|
|
1501
|
-
// restructuring, and a weak model is never asked to emit it from scratch.
|
|
1502
|
-
// Mirrors the cyclic_seam no-cycles fast path — write the artifact, then
|
|
1503
|
-
// re-derive the next phase.
|
|
1504
|
-
if (nextPhase === "obligation_ledger") {
|
|
1505
|
-
const finalizedPayload = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
1506
|
-
const ledger = deriveObligationLedger(finalizedPayload);
|
|
1507
|
-
await writeDerivedContractArtifact(artifactsDir, "obligation_ledger", ledger);
|
|
1508
|
-
return buildNextContractPipelineStep(options);
|
|
1509
|
-
}
|
|
1510
|
-
// 2.9. Degenerate seam_reconciliation collapse. A single-module decomposition
|
|
1511
|
-
// has NO inter-module seams, so seam_reconciliation is a structural no-op:
|
|
1512
|
-
// write an empty seam report deterministically (no host round-trip),
|
|
1513
|
-
// mirroring the obligation_ledger / cyclic_seam no-op fast paths. The empty
|
|
1514
|
-
// report makes validateReconciliationDerivation pass vacuously. A
|
|
1515
|
-
// multi-module decomposition falls through to the LLM seam_reconciliation
|
|
1516
|
-
// step (which mismatches exist is a judgment call).
|
|
1517
|
-
if (nextPhase === "seam_reconciliation") {
|
|
1518
|
-
const modules = await readDecomposedModules(artifactsDir);
|
|
1519
|
-
if (modules.length <= 1) {
|
|
1520
|
-
const drafted = envelopePayload(await readContractArtifact(artifactsDir, "module_contracts"));
|
|
1521
|
-
const goalId = isRecord(drafted) && typeof drafted.goal_id === "string" ? drafted.goal_id : "";
|
|
1522
|
-
await writeDerivedContractArtifact(artifactsDir, "seam_reconciliation_report", {
|
|
1523
|
-
contract_version: "remediate-code-contract-pipeline/seam-reconciliation-report/v1alpha1",
|
|
1524
|
-
goal_id: goalId,
|
|
1525
|
-
mismatches: [],
|
|
1526
|
-
created_at: new Date().toISOString(),
|
|
1527
|
-
});
|
|
1528
|
-
return buildNextContractPipelineStep(options);
|
|
1529
|
-
}
|
|
1841
|
+
stopCondition: "Stop — the contract pipeline is blocked on a non-converging conceptual-design critique pending a user decision.",
|
|
1842
|
+
};
|
|
1530
1843
|
}
|
|
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
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
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.`,
|
|
1608
|
-
allowedCommands: [],
|
|
1609
|
-
stopCondition: "Stop — the contract pipeline is blocked on a non-converging judge↔repair loop pending a user decision.",
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
// gate.kind === "proceed": fall through to the normal phase step below.
|
|
1613
|
-
}
|
|
1614
|
-
// 4. All phases exist: enforce traceability + referential integrity, then
|
|
1615
|
-
// convert the implementation_dag to an extracted plan.
|
|
1616
|
-
if (!nextPhase) {
|
|
1617
|
-
// 4a. DAG referential integrity + bidirectional coverage (ARC-86b18f1b-2).
|
|
1618
|
-
// Run before the traceability check so specific referential violations
|
|
1619
|
-
// are reported first (traceability is a superset check).
|
|
1620
|
-
const dagPayload = envelopePayload(await readContractArtifact(artifactsDir, "implementation_dag"));
|
|
1621
|
-
const ledgerPayload = envelopePayload(await readContractArtifact(artifactsDir, "obligation_ledger"));
|
|
1622
|
-
const cePayload = envelopePayload(await readContractArtifact(artifactsDir, "counterexample"));
|
|
1623
|
-
const judgePayload = envelopePayload(await readContractArtifact(artifactsDir, "judge_report"));
|
|
1624
|
-
const integrityIssues = validateImplementationDAGIntegrity(dagPayload, ledgerPayload, cePayload, judgePayload);
|
|
1625
|
-
const integrityErrors = integrityIssues.filter((i) => i.severity === "error");
|
|
1626
|
-
if (integrityErrors.length > 0) {
|
|
1627
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1628
|
-
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1629
|
-
return writeCurrentStep({
|
|
1630
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1631
|
-
status: "blocked",
|
|
1632
|
-
runId,
|
|
1633
|
-
repoRoot: root,
|
|
1634
|
-
artifactsDir,
|
|
1635
|
-
prompt: `# Implementation DAG Failed Referential Integrity ${repairState.dag_regenerations.length + 1} Times
|
|
1636
|
-
|
|
1637
|
-
The implementation_dag repeatedly contains referential integrity or coverage violations:
|
|
1638
|
-
|
|
1639
|
-
${integrityErrors.map((i) => `- [${i.path}] ${i.message}`).join("\n")}
|
|
1640
|
-
|
|
1641
|
-
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.
|
|
1642
|
-
`,
|
|
1643
|
-
allowedCommands: [],
|
|
1644
|
-
stopCondition: "Stop after reporting the integrity failure to the user.",
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
repairState.dag_regenerations.push({
|
|
1648
|
-
violations: integrityErrors.map((i) => i.message),
|
|
1844
|
+
return null;
|
|
1845
|
+
};
|
|
1846
|
+
/**
|
|
1847
|
+
* Deterministic obligation-ledger derivation (S1). The ledger is a pure function
|
|
1848
|
+
* of the finalized module contracts (every invariant/failure mode/module → an
|
|
1849
|
+
* obligation), so the tool generates it rather than an LLM phase: the structure
|
|
1850
|
+
* can never be malformed, no judgment is spent on a mechanical restructuring,
|
|
1851
|
+
* and a weak model is never asked to emit it from scratch.
|
|
1852
|
+
*/
|
|
1853
|
+
const obligationLedgerDerivationGate = async (ctx) => {
|
|
1854
|
+
if (ctx.nextPhase !== "obligation_ledger")
|
|
1855
|
+
return null;
|
|
1856
|
+
const finalizedPayload = envelopePayload(await readContractArtifact(ctx.artifactsDir, "finalized_module_contracts"));
|
|
1857
|
+
await writeDerivedContractArtifact(ctx.artifactsDir, "obligation_ledger", deriveObligationLedger(finalizedPayload));
|
|
1858
|
+
return { via: "rederive" };
|
|
1859
|
+
};
|
|
1860
|
+
/**
|
|
1861
|
+
* Degenerate seam_reconciliation collapse. A single-module decomposition has NO
|
|
1862
|
+
* inter-module seams, so seam_reconciliation is a structural no-op: write an
|
|
1863
|
+
* empty seam report deterministically (no host round-trip). The empty report
|
|
1864
|
+
* makes validateReconciliationDerivation pass vacuously. A multi-module
|
|
1865
|
+
* decomposition falls through to the LLM seam_reconciliation step (which
|
|
1866
|
+
* mismatches exist is a judgment call).
|
|
1867
|
+
*/
|
|
1868
|
+
const degenerateSeamReconciliationGate = async (ctx) => {
|
|
1869
|
+
if (ctx.nextPhase !== "seam_reconciliation")
|
|
1870
|
+
return null;
|
|
1871
|
+
const modules = await readDecomposedModules(ctx.artifactsDir);
|
|
1872
|
+
if (modules.length > 1)
|
|
1873
|
+
return null;
|
|
1874
|
+
const drafted = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_contracts"));
|
|
1875
|
+
const goalId = isRecord(drafted) && typeof drafted.goal_id === "string" ? drafted.goal_id : "";
|
|
1876
|
+
await writeDerivedContractArtifact(ctx.artifactsDir, "seam_reconciliation_report", {
|
|
1877
|
+
contract_version: "remediate-code-contract-pipeline/seam-reconciliation-report/v1alpha1",
|
|
1878
|
+
goal_id: goalId,
|
|
1879
|
+
mismatches: [],
|
|
1880
|
+
created_at: new Date().toISOString(),
|
|
1881
|
+
});
|
|
1882
|
+
return { via: "rederive" };
|
|
1883
|
+
};
|
|
1884
|
+
/**
|
|
1885
|
+
* Deterministic contract_finalization (all module counts). Finalization is a
|
|
1886
|
+
* mechanical merge, not fresh authoring: carry each drafted module contract
|
|
1887
|
+
* verbatim (preserving neighbor_needs for the ordering derivation) and attach
|
|
1888
|
+
* the agreed_interface of every seam that touches the module as a
|
|
1889
|
+
* seam_adjustment. The judgment already happened at seam_reconciliation.
|
|
1890
|
+
* Attaching each agreed interface verbatim guarantees the INV-CO-12
|
|
1891
|
+
* reconciliation-derivation gate passes. A downstream gate that still finds the
|
|
1892
|
+
* merge inadequate re-emits contract_finalization as an LLM step — the only path
|
|
1893
|
+
* that still needs judgment.
|
|
1894
|
+
*/
|
|
1895
|
+
const contractFinalizationDerivationGate = async (ctx) => {
|
|
1896
|
+
if (ctx.nextPhase !== "contract_finalization")
|
|
1897
|
+
return null;
|
|
1898
|
+
const drafted = envelopePayload(await readContractArtifact(ctx.artifactsDir, "module_contracts"));
|
|
1899
|
+
const seamReport = envelopePayload(await readContractArtifact(ctx.artifactsDir, "seam_reconciliation_report"));
|
|
1900
|
+
await writeDerivedContractArtifact(ctx.artifactsDir, "finalized_module_contracts", deriveFinalizedModuleContracts(drafted, seamReport));
|
|
1901
|
+
return { via: "rederive" };
|
|
1902
|
+
};
|
|
1903
|
+
/**
|
|
1904
|
+
* Judge gate: implementation planning is reachable only through an approved
|
|
1905
|
+
* verdict (the fixpoint) or a convergent targeted repair. A stalled /
|
|
1906
|
+
* non-converging repair loop escalates to the user (blocked) instead of silently
|
|
1907
|
+
* proceeding with residual risk.
|
|
1908
|
+
*/
|
|
1909
|
+
const judgeRepairGate = async (ctx) => {
|
|
1910
|
+
if (ctx.nextPhase !== "implementation_planning")
|
|
1911
|
+
return null;
|
|
1912
|
+
const gate = await evaluateJudgeGate(ctx.artifactsDir);
|
|
1913
|
+
if (gate.kind === "repair") {
|
|
1914
|
+
const repairTarget = gate.directive.target;
|
|
1915
|
+
const repairState = await readRepairState(ctx.artifactsDir);
|
|
1916
|
+
if (!repairState.repairs.some((repair) => repair.judge_hash === gate.judgeHash)) {
|
|
1917
|
+
repairState.repairs.push({
|
|
1918
|
+
judge_hash: gate.judgeHash,
|
|
1919
|
+
target: repairTarget,
|
|
1649
1920
|
at: new Date().toISOString(),
|
|
1921
|
+
accepted_ce_ids: gate.acceptedCeIds,
|
|
1922
|
+
addressed_ce_fingerprints: gate.addressedCeFingerprints,
|
|
1650
1923
|
});
|
|
1651
|
-
await writeRepairState(artifactsDir, repairState);
|
|
1652
|
-
|
|
1653
|
-
|
|
1924
|
+
await writeRepairState(ctx.artifactsDir, repairState);
|
|
1925
|
+
}
|
|
1926
|
+
const rendered = renderContractRepairPrompt({
|
|
1927
|
+
target: repairTarget,
|
|
1928
|
+
instruction: gate.directive.instruction,
|
|
1929
|
+
artifactPaths: ctx.artifactPaths,
|
|
1930
|
+
repoRoot: ctx.root,
|
|
1931
|
+
});
|
|
1932
|
+
return {
|
|
1933
|
+
via: "step",
|
|
1934
|
+
prompt: rendered.prompt,
|
|
1935
|
+
outputPath: rendered.outputPath,
|
|
1936
|
+
stopCondition: `Stop after rewriting "${repairTarget}" per the judge repair directive and running next-step.`,
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
if (gate.kind === "escalate") {
|
|
1940
|
+
// Non-convergence (stall or runaway backstop): surface it to the user loudly
|
|
1941
|
+
// rather than promoting a plan over an un-converged contract. The
|
|
1942
|
+
// outstanding accepted counterexamples are named so the user can resolve
|
|
1943
|
+
// them (revise the contract design or accept them as known limitations).
|
|
1944
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
1945
|
+
eventType: "repair_round",
|
|
1946
|
+
discriminator: `judge_nonconvergence:${gate.reason}`,
|
|
1947
|
+
note: `Judge↔repair loop escalated (${gate.reason}): ${gate.note}`,
|
|
1948
|
+
category: "trap",
|
|
1949
|
+
}, "remediate-code");
|
|
1950
|
+
return {
|
|
1951
|
+
via: "blocked",
|
|
1952
|
+
prompt: `# Judge↔Repair Loop Did Not Converge
|
|
1654
1953
|
|
|
1655
|
-
|
|
1954
|
+
${gate.note}
|
|
1656
1955
|
|
|
1657
|
-
|
|
1658
|
-
${rejectionRewriteInstruction(archived)}`);
|
|
1659
|
-
}
|
|
1660
|
-
const traceability = await validateImplementationDagTraceability(artifactsDir);
|
|
1661
|
-
if (!traceability.ok) {
|
|
1662
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1663
|
-
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1664
|
-
return writeCurrentStep({
|
|
1665
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1666
|
-
status: "blocked",
|
|
1667
|
-
runId,
|
|
1668
|
-
repoRoot: root,
|
|
1669
|
-
artifactsDir,
|
|
1670
|
-
prompt: `# Implementation DAG Failed Traceability ${repairState.dag_regenerations.length + 1} Times
|
|
1671
|
-
|
|
1672
|
-
The implementation_dag repeatedly contains nodes that trace to no obligation and no judge-accepted counterexample:
|
|
1673
|
-
|
|
1674
|
-
${traceability.violations.map((v) => `- ${v}`).join("\n")}
|
|
1675
|
-
|
|
1676
|
-
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.
|
|
1677
|
-
`,
|
|
1678
|
-
allowedCommands: [],
|
|
1679
|
-
stopCondition: "Stop after reporting the traceability failure to the user.",
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
repairState.dag_regenerations.push({
|
|
1683
|
-
violations: traceability.violations,
|
|
1684
|
-
at: new Date().toISOString(),
|
|
1685
|
-
});
|
|
1686
|
-
await writeRepairState(artifactsDir, repairState);
|
|
1687
|
-
const archived = await archiveContractArtifact(artifactsDir, "implementation_dag", "invalid");
|
|
1688
|
-
return buildPhaseStep("implementation_planning", `## Traceability Errors From the Previous Attempt
|
|
1956
|
+
## Outstanding accepted counterexamples
|
|
1689
1957
|
|
|
1690
|
-
|
|
1958
|
+
${gate.outstanding.length > 0
|
|
1959
|
+
? gate.outstanding.map((id) => `- ${id}`).join("\n")
|
|
1960
|
+
: "_(none newly accepted this round)_"}
|
|
1691
1961
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1709
|
-
status: "blocked",
|
|
1710
|
-
runId,
|
|
1711
|
-
repoRoot: root,
|
|
1712
|
-
artifactsDir,
|
|
1713
|
-
prompt: `# Contract-Obligation Gates Failed ${repairState.dag_regenerations.length + 1} Times
|
|
1714
|
-
|
|
1715
|
-
The contract-obligation promotion gates repeatedly failed and the plan cannot be promoted:
|
|
1716
|
-
|
|
1717
|
-
${obligationGate.violations.map((v) => `- ${v}`).join("\n")}
|
|
1718
|
-
|
|
1719
|
-
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.
|
|
1720
|
-
`,
|
|
1721
|
-
allowedCommands: [],
|
|
1722
|
-
stopCondition: "Stop after reporting the contract-obligation gate failure to the user.",
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
repairState.dag_regenerations.push({
|
|
1726
|
-
violations: obligationGate.violations,
|
|
1727
|
-
at: new Date().toISOString(),
|
|
1728
|
-
});
|
|
1729
|
-
await writeRepairState(artifactsDir, repairState);
|
|
1730
|
-
const archived = await archiveContractArtifact(artifactsDir, "implementation_dag", "invalid");
|
|
1731
|
-
return buildPhaseStep("implementation_planning", `## Contract-Obligation Gate Errors From the Previous Attempt
|
|
1962
|
+
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.`,
|
|
1963
|
+
stopCondition: "Stop — the contract pipeline is blocked on a non-converging judge↔repair loop pending a user decision.",
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
return null;
|
|
1967
|
+
};
|
|
1968
|
+
/** Bounded re-emit of implementation_planning, then blocked — the shared shape
|
|
1969
|
+
* of the four promotion rejections (integrity, traceability, obligation gates,
|
|
1970
|
+
* citation grounding). Single-sourced so the four cannot drift into four
|
|
1971
|
+
* different recovery contracts. */
|
|
1972
|
+
async function dagRegenerationPlan(ctx, params) {
|
|
1973
|
+
const repairState = await readRepairState(ctx.artifactsDir);
|
|
1974
|
+
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1975
|
+
return {
|
|
1976
|
+
via: "blocked",
|
|
1977
|
+
prompt: `# ${params.heading} ${repairState.dag_regenerations.length + 1} Times
|
|
1732
1978
|
|
|
1733
|
-
|
|
1979
|
+
${params.blockedBody}
|
|
1734
1980
|
|
|
1735
|
-
${
|
|
1736
|
-
${rejectionRewriteInstruction(archived)}`);
|
|
1737
|
-
}
|
|
1738
|
-
await promoteImplementationDagToExtractedPlan(artifactsDir);
|
|
1739
|
-
// 4d. M-B3 source-grounded citation gate (promotion backstop): ground every
|
|
1740
|
-
// promoted extracted-plan finding's citations against the working tree.
|
|
1741
|
-
// A finding citing only a non-existent path and no real symbol is a
|
|
1742
|
-
// hallucinated citation; re-emit implementation_planning (bounded by the
|
|
1743
|
-
// same dag_regenerations cap). Fail-closed only on an unreadable tree.
|
|
1744
|
-
const citationGate = await evaluatePromotedPlanCitationGrounding(artifactsDir, root);
|
|
1745
|
-
if (citationGate) {
|
|
1746
|
-
// Grounding failed: the plan was promoted to extracted-plan.json BEFORE this
|
|
1747
|
-
// gate ran, so the ungrounded marker is now on disk. Remove it before any
|
|
1748
|
-
// return — otherwise a subsequent next-step reads the promoted plan via
|
|
1749
|
-
// readExtractedPlanIfPresent and hands it straight to handlePendingExtractedPlan,
|
|
1750
|
-
// bypassing the re-emit and completing the pipeline on hallucinated citations.
|
|
1751
|
-
// No pipelineComplete unless the promoted plan grounds.
|
|
1752
|
-
await rm(intakePaths(artifactsDir).extractedPlan, { force: true });
|
|
1753
|
-
const repairState = await readRepairState(artifactsDir);
|
|
1754
|
-
if (repairState.dag_regenerations.length >= MAX_DAG_REGENERATION_ATTEMPTS) {
|
|
1755
|
-
return writeCurrentStep({
|
|
1756
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1757
|
-
status: "blocked",
|
|
1758
|
-
runId,
|
|
1759
|
-
repoRoot: root,
|
|
1760
|
-
artifactsDir,
|
|
1761
|
-
prompt: `# Citation Grounding Failed ${repairState.dag_regenerations.length + 1} Times
|
|
1762
|
-
|
|
1763
|
-
The promoted plan repeatedly cites components that do not exist in the working tree:
|
|
1764
|
-
|
|
1765
|
-
${citationGate.violations.map((v) => `- ${v}`).join("\n")}
|
|
1766
|
-
|
|
1767
|
-
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.
|
|
1981
|
+
${params.violations.map((violation) => `- ${violation}`).join("\n")}
|
|
1768
1982
|
`,
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
eventType: "phase_reemit",
|
|
1783
|
-
discriminator: "implementation_planning:citation_grounding:promotion",
|
|
1784
|
-
note: "implementation_planning re-emitted: a promoted plan finding cited a " +
|
|
1785
|
-
"component that does not exist in the working tree (M-B3 citation grounding).",
|
|
1786
|
-
category: "trap",
|
|
1787
|
-
}, "remediate-code");
|
|
1788
|
-
return buildPhaseStep("implementation_planning", `## Source-Grounded Citation Gate Errors From the Previous Attempt
|
|
1789
|
-
|
|
1790
|
-
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:
|
|
1983
|
+
stopCondition: `Stop after reporting the failure to the user.`,
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
repairState.dag_regenerations.push({
|
|
1987
|
+
violations: params.violations,
|
|
1988
|
+
at: new Date().toISOString(),
|
|
1989
|
+
});
|
|
1990
|
+
await writeRepairState(ctx.artifactsDir, repairState);
|
|
1991
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, "implementation_dag", "invalid", ctx.options.renameFn);
|
|
1992
|
+
return {
|
|
1993
|
+
via: "phase",
|
|
1994
|
+
phase: "implementation_planning",
|
|
1995
|
+
extraSection: `${params.reEmitBody}
|
|
1791
1996
|
|
|
1792
|
-
${
|
|
1793
|
-
${rejectionRewriteInstruction(archived)}
|
|
1794
|
-
|
|
1997
|
+
${params.violations.map((violation) => `- ${violation}`).join("\n")}
|
|
1998
|
+
${rejectionRewriteInstruction(archived)}`,
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* All phases exist: enforce referential integrity, traceability and the
|
|
2003
|
+
* fail-closed contract-obligation gates, then convert the implementation_dag
|
|
2004
|
+
* into an extracted plan and ground its citations.
|
|
2005
|
+
*/
|
|
2006
|
+
const implementationPlanPromotionGate = async (ctx) => {
|
|
2007
|
+
if (ctx.nextPhase)
|
|
1795
2008
|
return null;
|
|
2009
|
+
// DAG referential integrity + bidirectional coverage (ARC-86b18f1b-2), run
|
|
2010
|
+
// before the traceability check so specific referential violations are
|
|
2011
|
+
// reported first (traceability is a superset check).
|
|
2012
|
+
const outcomes = evaluateContractPipelineCrossGateOutcomes(await readCrossGatePayloads(ctx));
|
|
2013
|
+
const integrity = gateOutcomeOf(outcomes, "implementation_dag_integrity");
|
|
2014
|
+
if (!integrity?.evaluated) {
|
|
2015
|
+
return await dagRegenerationPlan(ctx, {
|
|
2016
|
+
heading: "Implementation DAG Could Not Be Checked",
|
|
2017
|
+
blockedBody: "The implementation_dag integrity gate could not run, so its empty issue list proves nothing:",
|
|
2018
|
+
reEmitBody: `## The Implementation DAG Could Not Be Checked
|
|
2019
|
+
|
|
2020
|
+
The referential-integrity gate could not run against the previous output, so it was never shown to be sound. Rewrite a complete implementation_dag:`,
|
|
2021
|
+
violations: [integrity?.reason ?? "no outcome record was produced for the gate"],
|
|
2022
|
+
});
|
|
1796
2023
|
}
|
|
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
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2024
|
+
const integrityErrors = integrity.issues.filter((issue) => issue.severity === "error");
|
|
2025
|
+
if (integrityErrors.length > 0) {
|
|
2026
|
+
return await dagRegenerationPlan(ctx, {
|
|
2027
|
+
heading: "Implementation DAG Failed Referential Integrity",
|
|
2028
|
+
blockedBody: "The implementation_dag repeatedly contains referential integrity or coverage violations:",
|
|
2029
|
+
reEmitBody: `## Referential Integrity Errors From the Previous Attempt
|
|
2030
|
+
|
|
2031
|
+
The previous implementation_dag was rejected and archived due to referential integrity violations. Fix every issue below:`,
|
|
2032
|
+
violations: integrityErrors.map((issue) => `[${issue.path}] ${issue.message}`),
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
const traceability = await validateImplementationDagTraceability(ctx.artifactsDir);
|
|
2036
|
+
if (!traceability.ok) {
|
|
2037
|
+
return await dagRegenerationPlan(ctx, {
|
|
2038
|
+
heading: "Implementation DAG Failed Traceability",
|
|
2039
|
+
blockedBody: "The implementation_dag repeatedly contains nodes that trace to no obligation and no judge-accepted counterexample:",
|
|
2040
|
+
reEmitBody: `## Traceability Errors From the Previous Attempt
|
|
2041
|
+
|
|
2042
|
+
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:`,
|
|
2043
|
+
violations: traceability.violations,
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
// Contract-obligations promotion gates: fail-closed cross-artifact checks that
|
|
2047
|
+
// must pass before a plan is promoted. These are the invariants that keep the
|
|
2048
|
+
// workflow correct regardless of host strength, so they are enforced here,
|
|
2049
|
+
// never left to host discretion.
|
|
2050
|
+
const obligationGate = await evaluateContractObligationsPromotionGate(ctx.artifactsDir, ctx.root, await readCrossGatePayloads(ctx));
|
|
2051
|
+
if (!obligationGate.ok) {
|
|
2052
|
+
return await dagRegenerationPlan(ctx, {
|
|
2053
|
+
heading: "Contract-Obligation Gates Failed",
|
|
2054
|
+
blockedBody: "The contract-obligation promotion gates repeatedly failed and the plan cannot be promoted:",
|
|
2055
|
+
reEmitBody: `## Contract-Obligation Gate Errors From the Previous Attempt
|
|
2056
|
+
|
|
2057
|
+
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:`,
|
|
2058
|
+
violations: obligationGate.violations,
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
// Write-scope + command SHAPE, before anything is promoted. These refusals
|
|
2062
|
+
// used to throw out of the promoter — an unclassified stack that wedged every
|
|
2063
|
+
// subsequent next-step, reachable from an LLM form as ordinary as a
|
|
2064
|
+
// leading-slash "repo-relative" path. They take the same bounded re-emit as
|
|
2065
|
+
// every other promotion rejection now.
|
|
2066
|
+
const scopeRefusals = await collectDagWriteScopeRefusals(ctx.artifactsDir, ctx.root);
|
|
2067
|
+
if (scopeRefusals.length > 0) {
|
|
2068
|
+
return await dagRegenerationPlan(ctx, {
|
|
2069
|
+
heading: "Block Write Scope Failed",
|
|
2070
|
+
blockedBody: "The implementation_dag repeatedly declares a write scope or targeted command the plan cannot carry:",
|
|
2071
|
+
reEmitBody: `## Write-Scope and Command Errors From the Previous Attempt
|
|
2072
|
+
|
|
2073
|
+
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:`,
|
|
2074
|
+
violations: scopeRefusals,
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
await promoteImplementationDagToExtractedPlan(ctx.artifactsDir, ctx.root);
|
|
2078
|
+
// M-B3 source-grounded citation gate (promotion backstop): ground every
|
|
2079
|
+
// promoted extracted-plan finding's citations against the working tree.
|
|
2080
|
+
const citationGate = await evaluatePromotedPlanCitationGrounding(ctx.artifactsDir, ctx.root);
|
|
2081
|
+
if (citationGate) {
|
|
2082
|
+
// The plan was promoted to extracted-plan.json BEFORE this gate ran, so the
|
|
2083
|
+
// ungrounded marker is now on disk. Remove it before any return — otherwise
|
|
2084
|
+
// a subsequent next-step reads the promoted plan and hands it straight to
|
|
2085
|
+
// handlePendingExtractedPlan, bypassing the re-emit and completing the
|
|
2086
|
+
// pipeline on hallucinated citations.
|
|
2087
|
+
await rm(ctx.paths.extractedPlan, { force: true });
|
|
2088
|
+
// The grounding-driven re-emit is a backend-observed step-boundary fact:
|
|
2089
|
+
// route it through the single CE-005 chokepoint as phase_reemit.
|
|
2090
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
2091
|
+
eventType: "phase_reemit",
|
|
2092
|
+
discriminator: "implementation_planning:citation_grounding:promotion",
|
|
2093
|
+
note: "implementation_planning re-emitted: a promoted plan finding cited a " +
|
|
2094
|
+
"component that does not exist in the working tree (M-B3 citation grounding).",
|
|
2095
|
+
category: "trap",
|
|
2096
|
+
}, "remediate-code");
|
|
2097
|
+
return await dagRegenerationPlan(ctx, {
|
|
2098
|
+
heading: "Citation Grounding Failed",
|
|
2099
|
+
blockedBody: "The promoted plan repeatedly cites components that do not exist in the working tree:",
|
|
2100
|
+
reEmitBody: `## Source-Grounded Citation Gate Errors From the Previous Attempt
|
|
2101
|
+
|
|
2102
|
+
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:`,
|
|
2103
|
+
violations: citationGate.violations,
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
// Normalized block write scope, tracked-tree half. Runs AFTER the
|
|
2107
|
+
// citation gate because the two overlap but neither contains the other: a
|
|
2108
|
+
// finding grounds on any real path OR symbol, so a node with plausible prose
|
|
2109
|
+
// can ground while the write scope a host would bind a worker to is still
|
|
2110
|
+
// fabricated. Same bounded recovery, same plan removal.
|
|
2111
|
+
const writeScopeGate = await evaluatePromotedPlanWriteScope(ctx.artifactsDir, ctx.root);
|
|
2112
|
+
if (writeScopeGate) {
|
|
2113
|
+
await rm(ctx.paths.extractedPlan, { force: true });
|
|
2114
|
+
return await dagRegenerationPlan(ctx, {
|
|
2115
|
+
heading: "Block Write Scope Failed",
|
|
2116
|
+
blockedBody: "The promoted plan repeatedly declares a block write scope that does not exist in the working tree:",
|
|
2117
|
+
reEmitBody: `## Block Write-Scope Errors From the Previous Attempt
|
|
2118
|
+
|
|
2119
|
+
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:`,
|
|
2120
|
+
violations: writeScopeGate.violations,
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
return { via: "pipeline_complete" };
|
|
2124
|
+
};
|
|
2125
|
+
/** Render the detected cycles for a prompt. */
|
|
2126
|
+
function renderCycleDescriptions(cycles) {
|
|
2127
|
+
return cycles
|
|
2128
|
+
.map((cycle, index) => `Cycle ${index + 1}: [${cycle.members.join(", ")}]`)
|
|
2129
|
+
.join("\n");
|
|
2130
|
+
}
|
|
2131
|
+
/** Build the seam-obligation graph from the obligation ledger AS IT STANDS NOW. */
|
|
2132
|
+
async function readSeamObligationGraph(artifactsDir) {
|
|
2133
|
+
const envelope = await readContractArtifact(artifactsDir, "obligation_ledger");
|
|
2134
|
+
const ledger = envelopePayload(envelope);
|
|
2135
|
+
const obligationIds = new Set((ledger?.obligations ?? []).map((o) => o.id));
|
|
2136
|
+
return {
|
|
2137
|
+
nodes: (ledger?.obligations ?? []).map((obligation) => ({
|
|
2138
|
+
id: obligation.id,
|
|
2139
|
+
needs: (obligation.depends_on ?? []).filter((dep) => obligationIds.has(dep)),
|
|
2140
|
+
})),
|
|
2141
|
+
goalId: ledger?.goal_id ?? "",
|
|
2142
|
+
ledgerHash: envelope?.content_hash ?? "unknown",
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Cyclic-seam resolution gate: runs after obligation_ledger is present and
|
|
2147
|
+
* before assessment. Detects circular interface-definition obligations, then
|
|
2148
|
+
* routes to an LLM resolution step when cycles are found. Cap:
|
|
2149
|
+
* MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS; on exhaustion, route to a user-decision
|
|
2150
|
+
* step (then blocked if still unresolved).
|
|
2151
|
+
*/
|
|
2152
|
+
const cyclicSeamResolutionGate = async (ctx) => {
|
|
2153
|
+
if (ctx.nextPhase !== "cyclic_seam_resolution")
|
|
2154
|
+
return null;
|
|
2155
|
+
const graph = await readSeamObligationGraph(ctx.artifactsDir);
|
|
2156
|
+
const detectedCycles = detectCyclicSeamObligations(graph.nodes);
|
|
2157
|
+
if (detectedCycles.length === 0) {
|
|
2158
|
+
await writeDerivedContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution", {
|
|
2159
|
+
contract_version: "remediate-code-contract-pipeline/cyclic-seam-resolution/v1alpha1",
|
|
2160
|
+
goal_id: graph.goalId,
|
|
2161
|
+
cycles: [],
|
|
2162
|
+
status: "no_cycles",
|
|
2163
|
+
created_at: new Date().toISOString(),
|
|
2164
|
+
});
|
|
2165
|
+
return { via: "rederive" };
|
|
2166
|
+
}
|
|
2167
|
+
const repairState = await readCyclicSeamRepairState(ctx.artifactsDir);
|
|
2168
|
+
const attemptsForLedger = repairState.attempts.filter((attempt) => attempt.ledger_hash === graph.ledgerHash);
|
|
2169
|
+
// Guard: the artifact exists and is already marked resolved/no_cycles. This
|
|
2170
|
+
// branch should not normally be reached (the artifact exists, so the frontier
|
|
2171
|
+
// skips it), but re-deriving is the safe answer.
|
|
2172
|
+
const existingResolution = envelopePayload(await readContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution"));
|
|
2173
|
+
if (existingResolution &&
|
|
2174
|
+
(existingResolution.status === "resolved" ||
|
|
2175
|
+
existingResolution.status === "no_cycles")) {
|
|
2176
|
+
return { via: "rederive" };
|
|
2177
|
+
}
|
|
2178
|
+
const cycleDescriptions = renderCycleDescriptions(detectedCycles);
|
|
2179
|
+
if (attemptsForLedger.length >= MAX_CYCLIC_SEAM_RESOLUTION_ATTEMPTS) {
|
|
2180
|
+
if (!repairState.user_decision_emitted) {
|
|
1845
2181
|
repairState.user_decision_emitted = true;
|
|
1846
|
-
await writeCyclicSeamRepairState(artifactsDir, repairState);
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
.join("\n");
|
|
1850
|
-
return writeCurrentStep({
|
|
1851
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1852
|
-
status: "blocked",
|
|
1853
|
-
runId,
|
|
1854
|
-
repoRoot: root,
|
|
1855
|
-
artifactsDir,
|
|
2182
|
+
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2183
|
+
return {
|
|
2184
|
+
via: "blocked",
|
|
1856
2185
|
prompt: `# Cyclic Seam Resolution — User Decision Required
|
|
1857
2186
|
|
|
1858
2187
|
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:
|
|
@@ -1864,27 +2193,17 @@ ${cycleDescriptions}
|
|
|
1864
2193
|
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.
|
|
1865
2194
|
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.
|
|
1866
2195
|
|
|
1867
|
-
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.
|
|
2196
|
+
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.
|
|
1868
2197
|
|
|
1869
2198
|
If you choose to stop instead, this run will remain blocked.
|
|
1870
2199
|
`,
|
|
1871
|
-
allowedCommands: [],
|
|
1872
2200
|
stopCondition: "Stop after presenting the user-decision prompt. Do not attempt further resolution.",
|
|
1873
|
-
}
|
|
2201
|
+
};
|
|
1874
2202
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
.map((c, i) => `Cycle ${i + 1}: [${c.members.join(", ")}]`)
|
|
1880
|
-
.join("\n");
|
|
1881
|
-
return writeCurrentStep({
|
|
1882
|
-
stepKind: CONTRACT_STEP_KIND,
|
|
1883
|
-
status: "blocked",
|
|
1884
|
-
runId,
|
|
1885
|
-
repoRoot: root,
|
|
1886
|
-
artifactsDir,
|
|
1887
|
-
prompt: `# Cyclic Seam Resolution — Blocked
|
|
2203
|
+
// The user decision was emitted and cycles are still present — blocked.
|
|
2204
|
+
return {
|
|
2205
|
+
via: "blocked",
|
|
2206
|
+
prompt: `# Cyclic Seam Resolution — Blocked
|
|
1888
2207
|
|
|
1889
2208
|
Cycles in the obligation graph remain unresolved after the automatic cap and a user-decision step. The run cannot proceed without manual intervention.
|
|
1890
2209
|
|
|
@@ -1892,47 +2211,51 @@ ${cycleDescriptions}
|
|
|
1892
2211
|
|
|
1893
2212
|
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.
|
|
1894
2213
|
`,
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2214
|
+
stopCondition: "Stop — the run is blocked on cyclic seam resolution.",
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
// Emit the LLM cyclic-seam-resolution step.
|
|
2218
|
+
const outputPath = contractInputFilePath(ctx.artifactsDir, "cyclic_seam_resolution");
|
|
2219
|
+
const ledgerInputPath = contractInputFilePath(ctx.artifactsDir, "obligation_ledger");
|
|
2220
|
+
const priorRejection = [...repairState.attempts]
|
|
2221
|
+
.reverse()
|
|
2222
|
+
.find((attempt) => attempt.ledger_hash === graph.ledgerHash && attempt.recheck_reason)?.recheck_reason;
|
|
2223
|
+
const rejectionSection = priorRejection
|
|
2224
|
+
? `\n## Why the Previous Attempt Was Rejected\n\n${priorRejection}\n`
|
|
2225
|
+
: "";
|
|
2226
|
+
repairState.attempts.push({
|
|
2227
|
+
ledger_hash: graph.ledgerHash,
|
|
2228
|
+
at: new Date().toISOString(),
|
|
2229
|
+
recheck_passed: false,
|
|
2230
|
+
});
|
|
2231
|
+
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2232
|
+
return {
|
|
2233
|
+
via: "step",
|
|
2234
|
+
prompt: `# Cyclic Seam Resolution
|
|
1913
2235
|
|
|
1914
|
-
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.
|
|
2236
|
+
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.
|
|
1915
2237
|
|
|
1916
2238
|
## Detected Cycles
|
|
1917
2239
|
|
|
1918
2240
|
${cycleDescriptions}
|
|
1919
|
-
|
|
2241
|
+
${rejectionSection}
|
|
1920
2242
|
## Sanctioned Break Strategies
|
|
1921
2243
|
|
|
1922
2244
|
For each cycle, choose one:
|
|
1923
2245
|
|
|
1924
|
-
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.
|
|
1925
|
-
2. **Single authority** — Designate one
|
|
2246
|
+
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.
|
|
2247
|
+
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.
|
|
1926
2248
|
|
|
1927
2249
|
## Required Inputs
|
|
1928
2250
|
|
|
1929
|
-
- \`${
|
|
2251
|
+
- \`${ledgerInputPath}\` (obligation_ledger)
|
|
1930
2252
|
|
|
1931
2253
|
## Your Task
|
|
1932
2254
|
|
|
1933
|
-
|
|
2255
|
+
Two files, both required — the record alone is not a break:
|
|
1934
2256
|
|
|
1935
|
-
\`${
|
|
2257
|
+
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.
|
|
2258
|
+
2. **Write the resolution record** to exactly \`${outputPath}\`, naming for each cycle the obligation id you designated:
|
|
1936
2259
|
|
|
1937
2260
|
\`\`\`json
|
|
1938
2261
|
{
|
|
@@ -1942,6 +2265,7 @@ Read the obligation_ledger. For each detected cycle, decide which break strategy
|
|
|
1942
2265
|
{
|
|
1943
2266
|
"members": ["<obligation-id>", "..."],
|
|
1944
2267
|
"break_strategy": "mediator | single_authority",
|
|
2268
|
+
"designated_obligation_id": "<the mediating obligation, or the single authority — must exist in the rewritten ledger>",
|
|
1945
2269
|
"resolution_description": "<what was changed and why>",
|
|
1946
2270
|
"exception_registration": "<if single_authority: the named scoped exception; otherwise null>"
|
|
1947
2271
|
}
|
|
@@ -1952,225 +2276,431 @@ Read the obligation_ledger. For each detected cycle, decide which break strategy
|
|
|
1952
2276
|
|
|
1953
2277
|
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 [].
|
|
1954
2278
|
|
|
1955
|
-
**Stop after writing the
|
|
1956
|
-
|
|
1957
|
-
After writing the output file, run:
|
|
1958
|
-
|
|
1959
|
-
\`${nextCommand}\`
|
|
2279
|
+
**Stop after writing the two files.** Do not edit source files. Do not advance to the next pipeline step.
|
|
1960
2280
|
`,
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2281
|
+
outputPath,
|
|
2282
|
+
stopCondition: "Stop after rewriting the obligation_ledger, writing the cyclic_seam_resolution output file, and running next-step.",
|
|
2283
|
+
};
|
|
2284
|
+
};
|
|
2285
|
+
/**
|
|
2286
|
+
* Cyclic-seam RE-CHECK. The worker has written a `resolved` record; verify the
|
|
2287
|
+
* break it actually authored against the obligation graph as it actually
|
|
2288
|
+
* stands, and archive + loop back when it does not hold.
|
|
2289
|
+
*
|
|
2290
|
+
* TST-61cff370 / TST-114e4941: this check used to be vacuous. It fabricated a
|
|
2291
|
+
* synthetic node per cycle — `{ id: "_mediator_A_B", needs: [] }` or
|
|
2292
|
+
* `{ id: "_authority_A_B", needs: [] }` — and asked whether redirecting the
|
|
2293
|
+
* cycle's edges at that edge-free sink would be acyclic, against the SAME
|
|
2294
|
+
* unmodified ledger. For any single detected cycle the answer is yes by
|
|
2295
|
+
* construction, so the re-check could never reject: a worker could claim
|
|
2296
|
+
* `status: "resolved"` while changing nothing, and the pipeline advanced. It
|
|
2297
|
+
* now reads the designated obligation off the record and validates it against
|
|
2298
|
+
* the live graph — see `validateAuthoredCycleBreak`.
|
|
2299
|
+
*/
|
|
2300
|
+
const cyclicSeamRecheckGate = async (ctx) => {
|
|
2301
|
+
const resolutionEnvelope = await readContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution");
|
|
2302
|
+
if (!resolutionEnvelope)
|
|
2303
|
+
return null;
|
|
2304
|
+
const resolution = envelopePayload(resolutionEnvelope);
|
|
2305
|
+
if (!resolution ||
|
|
2306
|
+
resolution.status !== "resolved" ||
|
|
2307
|
+
!Array.isArray(resolution.cycles) ||
|
|
2308
|
+
resolution.cycles.length === 0) {
|
|
2309
|
+
return null;
|
|
1964
2310
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
if (resolutionEnvelope) {
|
|
1978
|
-
const resolution = envelopePayload(resolutionEnvelope);
|
|
1979
|
-
if (resolution &&
|
|
1980
|
-
resolution.status === "resolved" &&
|
|
1981
|
-
Array.isArray(resolution.cycles) &&
|
|
1982
|
-
resolution.cycles.length > 0) {
|
|
1983
|
-
// Re-check: build the patched graph and verify no cycles remain.
|
|
1984
|
-
const obligationLedgerPayload = envelopePayload(await readContractArtifact(artifactsDir, "obligation_ledger"));
|
|
1985
|
-
const obligationIds = new Set((obligationLedgerPayload?.obligations ?? []).map((o) => o.id));
|
|
1986
|
-
const seamNodes = (obligationLedgerPayload?.obligations ?? []).map((obl) => ({
|
|
1987
|
-
id: obl.id,
|
|
1988
|
-
needs: (obl.depends_on ?? []).filter((dep) => obligationIds.has(dep)),
|
|
1989
|
-
}));
|
|
1990
|
-
// For each cycle in the resolution, apply the stated break and re-check.
|
|
1991
|
-
let recheckFailed = false;
|
|
1992
|
-
for (const cycleRecord of resolution.cycles) {
|
|
1993
|
-
if (!Array.isArray(cycleRecord.members))
|
|
1994
|
-
continue;
|
|
1995
|
-
const members = cycleRecord.members;
|
|
1996
|
-
const mediatorId = cycleRecord.break_strategy === "mediator"
|
|
1997
|
-
? `_mediator_${members.join("_")}`
|
|
1998
|
-
: null;
|
|
1999
|
-
const validationResult = validateCycleBreak({ members }, seamNodes, mediatorId
|
|
2000
|
-
? { id: mediatorId, needs: [] }
|
|
2001
|
-
: // single_authority: the designated owner keeps all edges;
|
|
2002
|
-
// non-owner loses edges to cycle members — model as mediator=no-op.
|
|
2003
|
-
{ id: `_authority_${members.join("_")}`, needs: [] });
|
|
2004
|
-
if (!validationResult.accepted) {
|
|
2005
|
-
recheckFailed = true;
|
|
2006
|
-
break;
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
if (recheckFailed) {
|
|
2010
|
-
const ledgerEnvelope = await readContractArtifact(artifactsDir, "obligation_ledger");
|
|
2011
|
-
const ledgerHash = ledgerEnvelope?.content_hash ?? "unknown";
|
|
2012
|
-
const repairState = await readCyclicSeamRepairState(artifactsDir);
|
|
2013
|
-
// Mark the last attempt as recheck_failed.
|
|
2014
|
-
const last = repairState.attempts.at(-1);
|
|
2015
|
-
if (last && last.ledger_hash === ledgerHash) {
|
|
2016
|
-
last.recheck_passed = false;
|
|
2017
|
-
}
|
|
2018
|
-
await writeCyclicSeamRepairState(artifactsDir, repairState);
|
|
2019
|
-
await archiveContractArtifact(artifactsDir, "cyclic_seam_resolution", "invalid");
|
|
2020
|
-
// Re-enter to emit the next attempt or cap.
|
|
2021
|
-
return buildNextContractPipelineStep(options);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2311
|
+
const graph = await readSeamObligationGraph(ctx.artifactsDir);
|
|
2312
|
+
let rejection;
|
|
2313
|
+
for (const cycleRecord of resolution.cycles) {
|
|
2314
|
+
if (!Array.isArray(cycleRecord.members))
|
|
2315
|
+
continue;
|
|
2316
|
+
const members = cycleRecord.members.filter((member) => typeof member === "string");
|
|
2317
|
+
const strategy = cycleRecord.break_strategy;
|
|
2318
|
+
if (strategy !== "mediator" && strategy !== "single_authority") {
|
|
2319
|
+
rejection =
|
|
2320
|
+
`Cycle [${members.join(", ")}] declares break_strategy ` +
|
|
2321
|
+
`${JSON.stringify(strategy ?? null)}, which is neither "mediator" nor "single_authority".`;
|
|
2322
|
+
break;
|
|
2024
2323
|
}
|
|
2324
|
+
const authored = {
|
|
2325
|
+
strategy,
|
|
2326
|
+
designatedId: typeof cycleRecord.designated_obligation_id === "string"
|
|
2327
|
+
? cycleRecord.designated_obligation_id
|
|
2328
|
+
: undefined,
|
|
2329
|
+
};
|
|
2330
|
+
const validation = validateAuthoredCycleBreak({ members }, graph.nodes, authored);
|
|
2331
|
+
if (!validation.accepted) {
|
|
2332
|
+
rejection = validation.reason ?? `Cycle [${members.join(", ")}] was not resolved.`;
|
|
2333
|
+
break;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
if (!rejection)
|
|
2337
|
+
return null;
|
|
2338
|
+
const repairState = await readCyclicSeamRepairState(ctx.artifactsDir);
|
|
2339
|
+
const last = repairState.attempts.at(-1);
|
|
2340
|
+
// Carry the reason forward so the NEXT resolution prompt says what failed,
|
|
2341
|
+
// instead of re-asking for the same claim and burning the attempt cap on an
|
|
2342
|
+
// unexplained retry. A record that appeared without a matching emitted
|
|
2343
|
+
// attempt (a resumed run, a hand-written artifact) still gets its rejection
|
|
2344
|
+
// recorded — the outcome is the attempt.
|
|
2345
|
+
if (last && last.ledger_hash === graph.ledgerHash) {
|
|
2346
|
+
last.recheck_passed = false;
|
|
2347
|
+
last.recheck_reason = rejection;
|
|
2348
|
+
}
|
|
2349
|
+
else {
|
|
2350
|
+
repairState.attempts.push({
|
|
2351
|
+
ledger_hash: graph.ledgerHash,
|
|
2352
|
+
at: new Date().toISOString(),
|
|
2353
|
+
recheck_passed: false,
|
|
2354
|
+
recheck_reason: rejection,
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
await writeCyclicSeamRepairState(ctx.artifactsDir, repairState);
|
|
2358
|
+
const archived = await archiveContractArtifact(ctx.artifactsDir, "cyclic_seam_resolution", "invalid", ctx.options.renameFn);
|
|
2359
|
+
if (!archived.originalFree) {
|
|
2360
|
+
// The rejected record is STILL at its canonical path, and re-deriving would
|
|
2361
|
+
// read the same record, reject it again, fail to archive it again — an
|
|
2362
|
+
// unbounded loop with no cap to stop it: the attempt ledger updates the
|
|
2363
|
+
// same entry in place (one ledger hash), and `rederive` carries no depth
|
|
2364
|
+
// bound. This branch is what makes the re-check's new ability to REJECT
|
|
2365
|
+
// safe; before the re-check could reject, the failure was unreachable.
|
|
2366
|
+
return {
|
|
2367
|
+
via: "blocked",
|
|
2368
|
+
prompt: `# A Rejected Cyclic-Seam Resolution Could Not Be Archived
|
|
2369
|
+
|
|
2370
|
+
The cycle-break re-check rejected the resolution record:
|
|
2371
|
+
|
|
2372
|
+
${rejection}
|
|
2373
|
+
|
|
2374
|
+
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.
|
|
2375
|
+
|
|
2376
|
+
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.`,
|
|
2377
|
+
stopCondition: "Stop — a rejected cyclic-seam resolution could not be archived and would otherwise loop.",
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
// Re-enter to emit the next attempt or the cap.
|
|
2381
|
+
return { via: "rederive" };
|
|
2382
|
+
};
|
|
2383
|
+
/**
|
|
2384
|
+
* Design-spec structural gates before the adversarial critic phase, in the order
|
|
2385
|
+
* they run: the design artifact's own structure, then the cheap cross-artifact
|
|
2386
|
+
* floor, then citation grounding. Error-severity gate failures re-emit the
|
|
2387
|
+
* responsible phase; warning-only results (e.g. circular obligation
|
|
2388
|
+
* dependencies) ride the critic prompt as advisory.
|
|
2389
|
+
*/
|
|
2390
|
+
const preCriticStructuralGate = async (ctx) => {
|
|
2391
|
+
if (ctx.nextPhase !== "critic")
|
|
2392
|
+
return null;
|
|
2393
|
+
const outcomes = evaluateContractPipelineCrossGateOutcomes(await readCrossGatePayloads(ctx));
|
|
2394
|
+
// (a) The design artifact itself. `contract_finalization` precedes `critic`,
|
|
2395
|
+
// so a not-evaluated outcome here means the payload is malformed, not
|
|
2396
|
+
// absent — its empty issue list is not proof of a clean design.
|
|
2397
|
+
const designSpec = gateOutcomeOf(outcomes, "design_spec");
|
|
2398
|
+
if (!designSpec?.evaluated) {
|
|
2399
|
+
return {
|
|
2400
|
+
via: "phase",
|
|
2401
|
+
phase: "contract_finalization",
|
|
2402
|
+
extraSection: `## Design Structural Gates Could Not Run
|
|
2403
|
+
|
|
2404
|
+
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.
|
|
2405
|
+
`,
|
|
2406
|
+
};
|
|
2025
2407
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
// advisory section to the critic prompt so the critic can take them into account.
|
|
2033
|
-
if (nextPhase === "critic") {
|
|
2034
|
-
// 5a. Design-spec structural gates on the finalized_module_contracts (the
|
|
2035
|
-
// "design" artifact) + obligation_ledger run first: a malformed design
|
|
2036
|
-
// artifact (error) re-emits the design phase, and a circular-obligation
|
|
2037
|
-
// dependency (warning) is appended to the critic prompt as advisory.
|
|
2038
|
-
const finalizedModuleContractsPayload = envelopePayload(await readContractArtifact(artifactsDir, "finalized_module_contracts"));
|
|
2039
|
-
const obligationLedgerPayload = envelopePayload(await readContractArtifact(artifactsDir, "obligation_ledger"));
|
|
2040
|
-
const gateIssues = validateDesignSpecGates(finalizedModuleContractsPayload, obligationLedgerPayload);
|
|
2041
|
-
const gateErrors = gateIssues.filter((issue) => issue.severity === "error");
|
|
2042
|
-
const gateWarnings = gateIssues.filter((issue) => issue.severity === "warning");
|
|
2043
|
-
if (gateErrors.length > 0) {
|
|
2044
|
-
// Re-emit the contract_finalization (design) phase with gate errors appended.
|
|
2045
|
-
const errorLines = gateErrors
|
|
2046
|
-
.map((issue) => `- [${issue.path}] ${issue.message}`)
|
|
2047
|
-
.join("\n");
|
|
2048
|
-
return buildPhaseStep("contract_finalization", `## Design Structural Gate Errors
|
|
2408
|
+
const gateErrors = designSpec.issues.filter((issue) => issue.severity === "error");
|
|
2409
|
+
if (gateErrors.length > 0) {
|
|
2410
|
+
return {
|
|
2411
|
+
via: "phase",
|
|
2412
|
+
phase: "contract_finalization",
|
|
2413
|
+
extraSection: `## Design Structural Gate Errors
|
|
2049
2414
|
|
|
2050
2415
|
The contract_finalization output failed deterministic structural gates. Fix every issue below before adversarial review can begin:
|
|
2051
2416
|
|
|
2052
|
-
${
|
|
2053
|
-
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2417
|
+
${gateErrors.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2418
|
+
`,
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
const gateWarnings = designSpec.issues.filter((issue) => issue.severity === "warning");
|
|
2422
|
+
if (gateWarnings.length > 0) {
|
|
2423
|
+
return {
|
|
2424
|
+
via: "phase",
|
|
2425
|
+
phase: "critic",
|
|
2426
|
+
extraSection: `## Advisory: Design Structural Warnings
|
|
2060
2427
|
|
|
2061
2428
|
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:
|
|
2062
2429
|
|
|
2063
|
-
${
|
|
2064
|
-
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2430
|
+
${gateWarnings.map((issue) => `- [${issue.path}] ${issue.message}`).join("\n")}
|
|
2431
|
+
`,
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
// (b) Pre-adversarial structural floor (S5): the cheap cross-artifact checks
|
|
2435
|
+
// whose inputs all exist by the critic phase, so the adversarial loop only
|
|
2436
|
+
// ever sees structurally-sound obligations/tests/contracts and a gap is
|
|
2437
|
+
// re-emitted to the precise responsible phase instead of being discovered
|
|
2438
|
+
// at promotion after the adversarial budget is spent.
|
|
2439
|
+
const preCriticGate = await evaluatePreCriticStructuralGate(ctx.artifactsDir, ctx.root, await readCrossGatePayloads(ctx));
|
|
2440
|
+
if (preCriticGate) {
|
|
2441
|
+
return {
|
|
2442
|
+
via: "phase",
|
|
2443
|
+
phase: preCriticGate.phase,
|
|
2444
|
+
extraSection: `## Pre-Adversarial Structural Gate Errors
|
|
2077
2445
|
|
|
2078
2446
|
The ${preCriticGate.phase} output failed deterministic structural gates. Fix every issue below before adversarial review begins:
|
|
2079
2447
|
|
|
2080
2448
|
${preCriticGate.errorLines.join("\n")}
|
|
2081
|
-
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2449
|
+
`,
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
// (c) M-B3 source-grounded citation gate at the pre-critic boundary: ground
|
|
2453
|
+
// the module_decomposition's file_scope citations against the working tree
|
|
2454
|
+
// before the adversarial loop. A module citing only a non-existent path and
|
|
2455
|
+
// no real symbol is re-emitted to the `decomposition` phase — the phase that
|
|
2456
|
+
// OWNS file_scope (the finalized contracts carry interface fields, not
|
|
2457
|
+
// paths, so re-emitting contract_finalization could never change file_scope
|
|
2458
|
+
// and an ungrounded scope would loop forever).
|
|
2459
|
+
const preCriticCitationGate = await evaluatePreCriticCitationGrounding(ctx.artifactsDir, ctx.root);
|
|
2460
|
+
if (preCriticCitationGate) {
|
|
2461
|
+
await captureStepBoundaryFriction(ctx.artifactsDir, ctx.runId, {
|
|
2462
|
+
eventType: "phase_reemit",
|
|
2463
|
+
discriminator: "decomposition:citation_grounding:pre_critic",
|
|
2464
|
+
note: "decomposition re-emitted: a module's file_scope cited a component " +
|
|
2465
|
+
"that does not exist in the working tree (M-B3 citation grounding).",
|
|
2466
|
+
category: "trap",
|
|
2467
|
+
}, "remediate-code");
|
|
2468
|
+
return {
|
|
2469
|
+
via: "phase",
|
|
2470
|
+
phase: "decomposition",
|
|
2471
|
+
extraSection: `## Source-Grounded Citation Gate Errors
|
|
2102
2472
|
|
|
2103
2473
|
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:
|
|
2104
2474
|
|
|
2105
2475
|
${preCriticCitationGate.errorLines.join("\n")}
|
|
2106
|
-
|
|
2107
|
-
}
|
|
2476
|
+
`,
|
|
2477
|
+
};
|
|
2108
2478
|
}
|
|
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
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2479
|
+
return null;
|
|
2480
|
+
};
|
|
2481
|
+
/**
|
|
2482
|
+
* DC-3 merge intercept: when a parallel phase's aggregated artifact is still
|
|
2483
|
+
* missing, merge the per-module shards into it once they are ALL present.
|
|
2484
|
+
* Returns true when the aggregate was written. A missing shard (or a degenerate
|
|
2485
|
+
* ≤1-module decomposition, which never used the shard path) returns false, and
|
|
2486
|
+
* the caller re-emits the wave — never a partial aggregate. After a complete
|
|
2487
|
+
* merge the artifact is written enveloped and the pipeline re-derives; the
|
|
2488
|
+
* seam_reconciliation / critique pass downstream stays the consistency gate over
|
|
2489
|
+
* the merged contracts.
|
|
2490
|
+
*/
|
|
2491
|
+
async function tryMergeModuleShards(artifactsDir, phase) {
|
|
2492
|
+
const modules = await readDecomposedModules(artifactsDir);
|
|
2493
|
+
if (modules.length <= 1)
|
|
2494
|
+
return false;
|
|
2495
|
+
const scan = await scanModuleShards(artifactsDir, phase, modules);
|
|
2496
|
+
if (scan.missing.length > 0)
|
|
2497
|
+
return false;
|
|
2498
|
+
// goal_id: the upstream module_decomposition is authoritative (every artifact
|
|
2499
|
+
// shares one goal_id; the goal-ID consistency gate enforces it). Fall back to
|
|
2500
|
+
// a shard's goal_id only if the decomposition somehow lacks one.
|
|
2501
|
+
const decompositionGoalId = await readDecompositionGoalId(artifactsDir);
|
|
2502
|
+
const goalId = decompositionGoalId ||
|
|
2503
|
+
[...scan.present.values()]
|
|
2504
|
+
.map((contract) => (typeof contract.goal_id === "string" ? contract.goal_id : undefined))
|
|
2505
|
+
.find((candidate) => Boolean(candidate)) ||
|
|
2506
|
+
"";
|
|
2507
|
+
await writeDerivedContractArtifact(artifactsDir, PARALLEL_MODULE_PHASES[phase], mergeModuleShards(modules, scan.present, goalId));
|
|
2508
|
+
return true;
|
|
2509
|
+
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Parallel-capable phase (DC-3): `module_contract_drafting` fans out to one
|
|
2512
|
+
* agent per module. The aggregated `module_contracts` artifact is missing here,
|
|
2513
|
+
* so first try to merge per-module shards (the worker may have just written
|
|
2514
|
+
* them) — a COMPLETE shard set merges into the aggregated artifact and the
|
|
2515
|
+
* pipeline re-derives; anything else re-emits the wave (which itself falls back
|
|
2516
|
+
* to the single aggregated step for a degenerate ≤1-module decomposition).
|
|
2517
|
+
*/
|
|
2518
|
+
const parallelModuleWaveGate = async (ctx) => {
|
|
2519
|
+
const phase = ctx.nextPhase;
|
|
2520
|
+
if (phase === null || !isParallelModulePhase(phase))
|
|
2521
|
+
return null;
|
|
2522
|
+
const merged = await tryMergeModuleShards(ctx.artifactsDir, phase);
|
|
2523
|
+
return merged ? { via: "rederive" } : { via: "module_wave", phase };
|
|
2524
|
+
};
|
|
2525
|
+
/**
|
|
2526
|
+
* Auto-phasing (T3): at the conceptual-design critique, hand the critic the
|
|
2527
|
+
* tool-DERIVED phase cut so it assesses design quality WITHIN a mechanically
|
|
2528
|
+
* dependency-ordered foundations→consumers phasing, instead of rejecting an
|
|
2529
|
+
* arbitrary N-goal change as "over-scoped" and forcing the host to re-scope by
|
|
2530
|
+
* hand at intake. The cut is derived from the finalized module contracts'
|
|
2531
|
+
* directional neighbor_needs edges and PERSISTED as `phase_cut.json`, so the cut
|
|
2532
|
+
* the critic sees and the cut the implementation-DAG promotion enforces are one
|
|
2533
|
+
* source. Only injected when there is a genuine multi-phase cut to communicate.
|
|
2534
|
+
*/
|
|
2535
|
+
const phaseCutCritiqueGate = async (ctx) => {
|
|
2536
|
+
if (ctx.nextPhase !== "critique")
|
|
2537
|
+
return null;
|
|
2538
|
+
const cut = await ensurePhaseCutArtifact(ctx.artifactsDir);
|
|
2539
|
+
if (!cut || cut.phases.length <= 1)
|
|
2540
|
+
return null;
|
|
2541
|
+
const reReview = await buildReReviewSection("critique", ctx.artifactsDir);
|
|
2542
|
+
const phaseCutSection = renderPhaseCutSection(cut);
|
|
2543
|
+
return {
|
|
2544
|
+
via: "phase",
|
|
2545
|
+
phase: "critique",
|
|
2546
|
+
extraSection: reReview ? `${phaseCutSection}\n${reReview}` : phaseCutSection,
|
|
2547
|
+
};
|
|
2548
|
+
};
|
|
2549
|
+
/**
|
|
2550
|
+
* Granularity collapse (T1 slice 4b): for low-complexity work, fold the framing
|
|
2551
|
+
* suffix [nextPhase..decomposition] into ONE round-trip producing several
|
|
2552
|
+
* artifacts, instead of one gated step per phase. Reads the POST-escalation
|
|
2553
|
+
* riskSignal (the escalate-on-evidence intercept may have already raised the
|
|
2554
|
+
* tier), so the dial is never frozen at run start — `fine` for medium/high keeps
|
|
2555
|
+
* full per-phase isolation. Only collapses a genuine multi-phase suffix.
|
|
2556
|
+
*/
|
|
2557
|
+
const collapsedFramingGate = (ctx) => {
|
|
2558
|
+
const phase = ctx.nextPhase;
|
|
2559
|
+
if (phase === null ||
|
|
2560
|
+
roundTripGranularityForTier(ctx.riskSignal?.tier) !== "collapsed" ||
|
|
2561
|
+
!FRAMING_COLLAPSE_GROUP.includes(phase)) {
|
|
2562
|
+
return null;
|
|
2144
2563
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2564
|
+
const startIdx = FRAMING_COLLAPSE_GROUP.indexOf(phase);
|
|
2565
|
+
const suffix = FRAMING_COLLAPSE_GROUP.slice(startIdx);
|
|
2566
|
+
if (suffix.length <= 1)
|
|
2567
|
+
return null;
|
|
2568
|
+
return { via: "collapsed_framing", phases: [...suffix] };
|
|
2569
|
+
};
|
|
2570
|
+
/**
|
|
2571
|
+
* Skeleton-scaffolded phases (S3): the tool pre-fills structure/ids from the
|
|
2572
|
+
* derived obligation ledger so the worker fills only the judgment slots.
|
|
2573
|
+
*/
|
|
2574
|
+
const scaffoldedPhaseGate = async (ctx) => {
|
|
2575
|
+
const phase = ctx.nextPhase;
|
|
2576
|
+
if (phase !== "test_validator_plan" && phase !== "implementation_planning") {
|
|
2577
|
+
return null;
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
via: "phase",
|
|
2581
|
+
phase,
|
|
2582
|
+
extraSection: await buildScaffoldSection(phase, ctx.artifactsDir),
|
|
2583
|
+
};
|
|
2584
|
+
};
|
|
2585
|
+
/**
|
|
2586
|
+
* The fallback: the ordinary per-phase step. Diff-based re-review (B2) rides it
|
|
2587
|
+
* — when a verdict-bearing review phase is re-emitted because an upstream
|
|
2588
|
+
* changed, the worker gets its prior verdict plus the precise
|
|
2589
|
+
* changed-since-last-review delta, so it re-affirms cheaply or revises only the
|
|
2590
|
+
* affected items rather than running blind.
|
|
2591
|
+
*
|
|
2592
|
+
* Reached only when every gate declined, which by construction means
|
|
2593
|
+
* `nextPhase` is a real phase: the promotion gate above never declines when the
|
|
2594
|
+
* frontier is null.
|
|
2595
|
+
*/
|
|
2596
|
+
const ordinaryPhaseStep = async (ctx) => {
|
|
2597
|
+
const phase = ctx.nextPhase;
|
|
2598
|
+
if (phase === null) {
|
|
2599
|
+
throw new Error("contract pipeline: the gate walk reached the fallback with no next phase — " +
|
|
2600
|
+
"the promotion gate must handle a null frontier.");
|
|
2160
2601
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2602
|
+
return {
|
|
2603
|
+
via: "phase",
|
|
2604
|
+
phase,
|
|
2605
|
+
extraSection: await buildReReviewSection(phase, ctx.artifactsDir),
|
|
2606
|
+
};
|
|
2607
|
+
};
|
|
2608
|
+
/**
|
|
2609
|
+
* THE ORDERED GATE TABLE. Insertion order IS execution order (the walk consumes
|
|
2610
|
+
* the scaffold's derived `handledKeys`), names are unique by construction (a
|
|
2611
|
+
* duplicate object key is a compile error), and no gate can emit a step of its
|
|
2612
|
+
* own — the scaffold owns the single emission site.
|
|
2613
|
+
*/
|
|
2614
|
+
const CONTRACT_PIPELINE_GATES = {
|
|
2615
|
+
seed_source_digest_bound: seedSourceDigestGate,
|
|
2616
|
+
ingested_artifact_invalid: invalidIngestionGate,
|
|
2617
|
+
stale_artifact_archived: staleArchiveGate,
|
|
2618
|
+
phase_frontier_resolved: phaseFrontierGate,
|
|
2619
|
+
goal_id_consistent: goalIdConsistencyGate,
|
|
2620
|
+
finalized_module_set_preserved: finalizedModuleSetGate,
|
|
2621
|
+
work_block_seam_prepared: workBlockSeamGate,
|
|
2622
|
+
conceptual_critique_converged: conceptualCritiqueGate,
|
|
2623
|
+
obligation_ledger_derived: obligationLedgerDerivationGate,
|
|
2624
|
+
degenerate_seam_reconciliation_collapsed: degenerateSeamReconciliationGate,
|
|
2625
|
+
contract_finalization_derived: contractFinalizationDerivationGate,
|
|
2626
|
+
judge_repair_converged: judgeRepairGate,
|
|
2627
|
+
implementation_plan_promoted: implementationPlanPromotionGate,
|
|
2628
|
+
cyclic_seam_resolved: cyclicSeamResolutionGate,
|
|
2629
|
+
cyclic_seam_rechecked: cyclicSeamRecheckGate,
|
|
2630
|
+
pre_critic_structural: preCriticStructuralGate,
|
|
2631
|
+
parallel_module_wave: parallelModuleWaveGate,
|
|
2632
|
+
phase_cut_critique: phaseCutCritiqueGate,
|
|
2633
|
+
collapsed_framing_round_trip: collapsedFramingGate,
|
|
2634
|
+
scaffolded_phase: scaffoldedPhaseGate,
|
|
2635
|
+
};
|
|
2636
|
+
/**
|
|
2637
|
+
* Bind the ONE shared step-emission scaffold to an invocation's context.
|
|
2638
|
+
*
|
|
2639
|
+
* Scaffold ADOPTER, never a second scaffold: this consumes `createStepEmissionScaffold` from
|
|
2640
|
+
* `audit-tools/shared` — the same scaffold the audit orchestrator entry point
|
|
2641
|
+
* drives — rather than a second one of this module's own. The pipeline's
|
|
2642
|
+
* numbered early-return-and-re-emit shape is `emitFirstApplicable`, a row shape
|
|
2643
|
+
* in that table, not a fork of it.
|
|
2644
|
+
*/
|
|
2645
|
+
function createContractPipelineEmission(ctx) {
|
|
2646
|
+
return createStepEmissionScaffold({
|
|
2647
|
+
table: CONTRACT_PIPELINE_GATES,
|
|
2648
|
+
fallback: ordinaryPhaseStep,
|
|
2649
|
+
write: (plan) => writeContractStepPlan(ctx, plan),
|
|
2650
|
+
// The pipeline's externally-observable emission is the PERSISTED step
|
|
2651
|
+
// contract, which `write` has just produced; the CLI renders it to the host.
|
|
2652
|
+
// There is deliberately no second stdout announcement here.
|
|
2653
|
+
log: () => { },
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* The gate walk order, exported so a drift guard reads the real set instead of
|
|
2658
|
+
* reconstructing one by reflecting over a chain of `if` statements.
|
|
2659
|
+
*
|
|
2660
|
+
* This and the scaffold's `handledKeys` are BOTH `Object.keys` of the SAME
|
|
2661
|
+
* object literal, and the walk consumes `handledKeys` directly — so the two
|
|
2662
|
+
* agree by construction, not by a test that compares them. No such test exists,
|
|
2663
|
+
* and none is needed: there is no second list to drift from.
|
|
2664
|
+
*/
|
|
2665
|
+
export const CONTRACT_PIPELINE_GATE_ORDER = Object.freeze(Object.keys(CONTRACT_PIPELINE_GATES));
|
|
2666
|
+
/**
|
|
2667
|
+
* Build and write the next contract-pipeline step.
|
|
2668
|
+
* Returns null when the pipeline is complete and the extracted plan is ready.
|
|
2669
|
+
*/
|
|
2670
|
+
export async function buildNextContractPipelineStep(options) {
|
|
2671
|
+
const { root, artifactsDir, runId, sourcePaths } = options;
|
|
2672
|
+
// Adversarial-depth dial (T1 slices 3/4): derive the depth for the critique /
|
|
2673
|
+
// critic phases from the intake risk signal, escalating on decomposition
|
|
2674
|
+
// evidence. The (possibly raised) riskSignal is also consumed by the
|
|
2675
|
+
// granularity-collapse gate, so it is carried on the context alongside it.
|
|
2676
|
+
const { riskSignal, adversarialDepth } = await resolveAdversarialDepth(artifactsDir);
|
|
2677
|
+
// Resolve artifact paths for the prompt renderers. The host's world is the
|
|
2678
|
+
// plain INPUT files (D3): every host-facing path — both where a role WRITES
|
|
2679
|
+
// its output and where it READS its upstreams — is `<name>.input.json`. The
|
|
2680
|
+
// tool's canonical envelopes (`<name>.json`) are derived at ingest and never
|
|
2681
|
+
// named to the host.
|
|
2682
|
+
const artifactPaths = {};
|
|
2683
|
+
for (const name of CP_ARTIFACT_NAMES) {
|
|
2684
|
+
artifactPaths[name] = contractInputFilePath(artifactsDir, name);
|
|
2166
2685
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2686
|
+
const seedPath = pathASeedFilePath(artifactsDir);
|
|
2687
|
+
const ctx = {
|
|
2688
|
+
options,
|
|
2689
|
+
root,
|
|
2690
|
+
artifactsDir,
|
|
2691
|
+
runId,
|
|
2692
|
+
sourcePaths,
|
|
2693
|
+
paths: intakePaths(artifactsDir),
|
|
2694
|
+
artifactPaths,
|
|
2695
|
+
// Present only for structured_audit runs.
|
|
2696
|
+
pathASeedPath: existsSync(seedPath) ? seedPath : undefined,
|
|
2697
|
+
riskSignal,
|
|
2698
|
+
adversarialDepth,
|
|
2699
|
+
artifactsSettled: false,
|
|
2700
|
+
nextPhase: null,
|
|
2701
|
+
};
|
|
2702
|
+
const emission = createContractPipelineEmission(ctx);
|
|
2703
|
+
return await emission.emitFirstApplicable([...emission.handledKeys], ctx);
|
|
2174
2704
|
}
|
|
2175
2705
|
/**
|
|
2176
2706
|
* Build the diff-based re-review section for a review phase being re-emitted after
|
|
@@ -2189,13 +2719,60 @@ async function buildReReviewSection(phase, artifactsDir) {
|
|
|
2189
2719
|
const delta = await computeReReviewDelta(artifactsDir, artifact, snapshot);
|
|
2190
2720
|
return renderReReviewSection(artifact, snapshot, delta);
|
|
2191
2721
|
}
|
|
2192
|
-
|
|
2193
|
-
|
|
2722
|
+
// ── DAG → extracted plan conversion ──────────────────────────────────────────
|
|
2723
|
+
// ── Obligation-kind → lens/severity mappings ──────────────────────────────────
|
|
2724
|
+
/**
|
|
2725
|
+
* The obligation-kind vocabulary, in priority order (higher index = higher
|
|
2726
|
+
* priority; `invariant` is highest).
|
|
2727
|
+
*
|
|
2728
|
+
* MNT-114e4941-3: this used to be a THIRD independent copy of the vocabulary —
|
|
2729
|
+
* a local `type ObligationKind` union beside derive.ts's `TESTABLE_KINDS` and
|
|
2730
|
+
* contractPipelineGates.ts's `TESTABLE_OBLIGATION_KINDS`, with nothing forcing
|
|
2731
|
+
* the three to agree, while the ledger's own `obligation.kind` is typed as a
|
|
2732
|
+
* bare `string`. The consequence was not theoretical: an unrecognized kind was
|
|
2733
|
+
* CAST to this union, scored -1 by `indexOf`, and then indexed the lens map to
|
|
2734
|
+
* `undefined` — so a ledger kind outside these four promoted a finding with
|
|
2735
|
+
* `lens: undefined`.
|
|
2736
|
+
*
|
|
2737
|
+
* It is now single-sourced two ways at once:
|
|
2738
|
+
* • MEMBERSHIP — {@link obligationKindVocabularyDivergence} reconciles this
|
|
2739
|
+
* list against the gate module's exported `TESTABLE_OBLIGATION_KINDS`, so a
|
|
2740
|
+
* kind added there and not here is a red contract test rather than a silent
|
|
2741
|
+
* misclassification;
|
|
2742
|
+
* • SEMANTICS — an unrecognized kind is not dropped or cast. It is routed
|
|
2743
|
+
* through the gate module's own `isTestablePhaseObligation` predicate, so
|
|
2744
|
+
* the two modules answer "is this kind testable?" with ONE implementation.
|
|
2745
|
+
*/
|
|
2746
|
+
export const OBLIGATION_KIND_PRIORITY = [
|
|
2194
2747
|
"test",
|
|
2195
2748
|
"structural",
|
|
2196
2749
|
"behavioral",
|
|
2197
2750
|
"invariant",
|
|
2198
2751
|
];
|
|
2752
|
+
const OBLIGATION_KIND_SET = new Set(OBLIGATION_KIND_PRIORITY);
|
|
2753
|
+
/**
|
|
2754
|
+
* Classify a raw ledger `kind` string (which the ledger types as a bare
|
|
2755
|
+
* `string`) into this module's vocabulary. A recognized kind maps to itself; an
|
|
2756
|
+
* unrecognized one is classified by the SHARED testability predicate rather
|
|
2757
|
+
* than guessed here — testable ⇒ `behavioral` (the testable default, so it
|
|
2758
|
+
* carries a real lens and a mid severity), otherwise ⇒ `structural`.
|
|
2759
|
+
*/
|
|
2760
|
+
export function classifyObligationKind(kind) {
|
|
2761
|
+
if (OBLIGATION_KIND_SET.has(kind))
|
|
2762
|
+
return kind;
|
|
2763
|
+
return isTestablePhaseObligation(kind) ? "behavioral" : "structural";
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* Kinds the gate module declares TESTABLE that this module's vocabulary does
|
|
2767
|
+
* not carry — the drift MNT-114e4941-3 names, reported as data so a contract
|
|
2768
|
+
* test can go red on it instead of a reviewer having to notice.
|
|
2769
|
+
* Empty when the two agree.
|
|
2770
|
+
*/
|
|
2771
|
+
export function obligationKindVocabularyDivergence() {
|
|
2772
|
+
return [...TESTABLE_OBLIGATION_KINDS]
|
|
2773
|
+
.filter((kind) => !OBLIGATION_KIND_SET.has(kind))
|
|
2774
|
+
.sort();
|
|
2775
|
+
}
|
|
2199
2776
|
function deriveObligationLensAndSeverity(kinds) {
|
|
2200
2777
|
if (kinds.length === 0) {
|
|
2201
2778
|
return { lens: "correctness", severity: "medium" };
|
|
@@ -2222,11 +2799,144 @@ function deriveObligationLensAndSeverity(kinds) {
|
|
|
2222
2799
|
};
|
|
2223
2800
|
return { lens: lensMap[topKind], severity: severityMap[topKind] };
|
|
2224
2801
|
}
|
|
2802
|
+
// ── Normalized block write scope + declared command shape ─────────────────────────────────────
|
|
2803
|
+
//
|
|
2804
|
+
// `touched_files` is the PROMPT-BOUND WRITE SCOPE the host-handoff substrate
|
|
2805
|
+
// enforces against the landed diff, and `targeted_commands` are executed
|
|
2806
|
+
// verbatim through a shell in the repository root. That consumer can check the
|
|
2807
|
+
// SHAPE of what it is handed; it can never check whether the shape is CORRECT
|
|
2808
|
+
// for this repository. So the producer normalizes here: an absolute or
|
|
2809
|
+
// separator-inconsistent path becomes one canonical repo-relative form, a path
|
|
2810
|
+
// that escapes the repository is refused outright, and a command carrying shell
|
|
2811
|
+
// chaining or substitution is refused rather than handed to a shell.
|
|
2812
|
+
/**
|
|
2813
|
+
* The tracked-path corpus for write-scope checking, or null when the tree
|
|
2814
|
+
* cannot be read. Null degrades to "shape-only normalization" exactly as the
|
|
2815
|
+
* M-B3 citation gate degrades on an unreadable tree — a fixture directory or a
|
|
2816
|
+
* fresh checkout must not be bricked, only an unsound path in a REAL tree is
|
|
2817
|
+
* refused.
|
|
2818
|
+
*/
|
|
2819
|
+
function readTrackedWriteScopeCorpus(root) {
|
|
2820
|
+
if (!isInsideGitWorkTree(root))
|
|
2821
|
+
return null;
|
|
2822
|
+
const files = enumerateRepoTreePaths(root);
|
|
2823
|
+
if (files.size === 0)
|
|
2824
|
+
return null;
|
|
2825
|
+
const directories = new Set();
|
|
2826
|
+
for (const path of files) {
|
|
2827
|
+
const segments = path.split("/");
|
|
2828
|
+
for (let i = 1; i < segments.length; i += 1) {
|
|
2829
|
+
directories.add(segments.slice(0, i).join("/"));
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
return { files, directories };
|
|
2833
|
+
}
|
|
2834
|
+
export function normalizeBlockTouchedFiles(root, files, blockId) {
|
|
2835
|
+
const normalized = new Set();
|
|
2836
|
+
const refusals = [];
|
|
2837
|
+
for (const raw of files) {
|
|
2838
|
+
const candidate = typeof raw === "string" ? raw.trim() : "";
|
|
2839
|
+
if (candidate.length === 0) {
|
|
2840
|
+
refusals.push(`Block "${blockId}" declares an empty touched_files entry.`);
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2843
|
+
const absolute = isAbsolute(candidate) ? candidate : resolve(root, candidate);
|
|
2844
|
+
try {
|
|
2845
|
+
normalized.add(repoRelativePath(root, absolute, `block "${blockId}" touched_files entry`));
|
|
2846
|
+
}
|
|
2847
|
+
catch {
|
|
2848
|
+
refusals.push(`Block "${blockId}" declares the touched_files entry ${JSON.stringify(raw)}, which ` +
|
|
2849
|
+
`does not resolve to a path beneath the repository root. A POSIX-absolute form ` +
|
|
2850
|
+
`("/src/x.ts") is read as absolute, not repo-relative — drop the leading slash. The ` +
|
|
2851
|
+
`write scope is re-checked against the landed diff, so it may only name paths ` +
|
|
2852
|
+
`beneath ${root}.`);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
// Content-derived order: an incidentally-ordered write scope would churn the
|
|
2856
|
+
// plan's content hash on every re-promotion.
|
|
2857
|
+
return {
|
|
2858
|
+
touched_files: [...normalized].sort((left, right) => compareCodeUnits(left, right)),
|
|
2859
|
+
refusals,
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* The tracked-tree half of The normalized-write-scope invariant, run against
|
|
2864
|
+
* the PROMOTED plan so a violation takes the same bounded re-emit path the M-B3
|
|
2865
|
+
* citation gate takes, rather than throwing out of the promotion.
|
|
2866
|
+
*
|
|
2867
|
+
* It exists because the citation gate is NOT a superset: a finding grounds if
|
|
2868
|
+
* ANY cited path OR SYMBOL is real, so a node whose prose names a real symbol
|
|
2869
|
+
* can ground while its declared write scope is still fabricated — and the write
|
|
2870
|
+
* scope is what a host binds a worker to.
|
|
2871
|
+
*
|
|
2872
|
+
* A path that is not tracked but whose parent directory IS stays legal: a
|
|
2873
|
+
* remediation block legitimately creates new files, and dropping a declared
|
|
2874
|
+
* write target is the failure mode that strands an implementer with an
|
|
2875
|
+
* obligation it has no scope to discharge. Fail-open on an unreadable tree, as
|
|
2876
|
+
* the citation gate does.
|
|
2877
|
+
*/
|
|
2878
|
+
export async function evaluatePromotedPlanWriteScope(artifactsDir, root) {
|
|
2879
|
+
const corpus = readTrackedWriteScopeCorpus(root);
|
|
2880
|
+
if (!corpus)
|
|
2881
|
+
return null;
|
|
2882
|
+
const plan = await readOptionalJsonFile(intakePaths(artifactsDir).extractedPlan);
|
|
2883
|
+
const violations = [];
|
|
2884
|
+
for (const block of Array.isArray(plan?.blocks) ? plan.blocks : []) {
|
|
2885
|
+
const blockId = typeof block.block_id === "string" ? block.block_id : "(unnamed block)";
|
|
2886
|
+
const touched = Array.isArray(block.touched_files) ? block.touched_files : [];
|
|
2887
|
+
for (const path of touched) {
|
|
2888
|
+
if (typeof path !== "string")
|
|
2889
|
+
continue;
|
|
2890
|
+
const key = normalizeRepoPath(path);
|
|
2891
|
+
const parent = key.includes("/") ? key.slice(0, key.lastIndexOf("/")) : "";
|
|
2892
|
+
if (corpus.files.has(key) || parent === "" || corpus.directories.has(parent)) {
|
|
2893
|
+
continue;
|
|
2894
|
+
}
|
|
2895
|
+
violations.push(`Block "${blockId}" declares the write-scope path "${path}", whose directory does ` +
|
|
2896
|
+
`not exist in the tracked tree.`);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
return violations.length > 0 ? { violations } : null;
|
|
2900
|
+
}
|
|
2901
|
+
export function normalizeBlockTargetedCommands(commands, blockId) {
|
|
2902
|
+
const partitioned = partitionCommandsByDeclaredShape(commands, (kind, raw) => kind === "empty"
|
|
2903
|
+
? `Block "${blockId}" declares an empty targeted_commands entry.`
|
|
2904
|
+
: `Block "${blockId}" declares the targeted_commands entry ${JSON.stringify(raw)}, ` +
|
|
2905
|
+
`which carries shell chaining, substitution or redirection. A targeted command is ` +
|
|
2906
|
+
`executed verbatim through a shell, so it must be one invocation — split it into ` +
|
|
2907
|
+
`separate entries.`);
|
|
2908
|
+
return { targeted_commands: partitioned.commands, refusals: partitioned.refusals };
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Collect every write-scope and command refusal the promotion WOULD hit, before
|
|
2912
|
+
* a plan is written. Runs the same two normalizers over the same derived node
|
|
2913
|
+
* scope the promoter uses, so this pre-check and the promotion cannot disagree
|
|
2914
|
+
* about what is refusable — and the refusal reaches the host as the bounded
|
|
2915
|
+
* `implementation_planning` re-emit every other promotion rejection takes,
|
|
2916
|
+
* rather than as a thrown stack that wedges every subsequent next-step.
|
|
2917
|
+
*/
|
|
2918
|
+
export async function collectDagWriteScopeRefusals(artifactsDir, root) {
|
|
2919
|
+
const dag = envelopePayload(await readContractArtifact(artifactsDir, "implementation_dag"));
|
|
2920
|
+
const nodes = Array.isArray(dag?.nodes) ? dag.nodes : [];
|
|
2921
|
+
if (nodes.length === 0)
|
|
2922
|
+
return [];
|
|
2923
|
+
const { resolve: deriveNodeFiles } = await buildNodeWriteScopeResolver(artifactsDir);
|
|
2924
|
+
const refusals = [];
|
|
2925
|
+
for (const [index, node] of nodes.entries()) {
|
|
2926
|
+
const blockId = toBlockId(ensureNodeId(node.id, index));
|
|
2927
|
+
refusals.push(...normalizeBlockTouchedFiles(root, deriveNodeFiles(node), blockId).refusals, ...normalizeBlockTargetedCommands(node.targeted_commands ?? [], blockId).refusals);
|
|
2928
|
+
}
|
|
2929
|
+
return refusals;
|
|
2930
|
+
}
|
|
2225
2931
|
/**
|
|
2226
2932
|
* Convert a completed ImplementationDAG into the extracted-plan.json format
|
|
2227
2933
|
* that the existing handlePendingExtractedPlan/applyPlanPipeline path consumes.
|
|
2934
|
+
*
|
|
2935
|
+
* `root` defaults to the repository that owns `artifactsDir`, so the existing
|
|
2936
|
+
* one-argument callers keep working while the pipeline passes the run's real
|
|
2937
|
+
* root for write-scope normalization.
|
|
2228
2938
|
*/
|
|
2229
|
-
export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
2939
|
+
export async function promoteImplementationDagToExtractedPlan(artifactsDir, root = climbOutOfAuditTools(artifactsDir)) {
|
|
2230
2940
|
const paths = intakePaths(artifactsDir);
|
|
2231
2941
|
const dagEnvelope = await readContractArtifact(artifactsDir, "implementation_dag");
|
|
2232
2942
|
if (!dagEnvelope)
|
|
@@ -2241,7 +2951,9 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2241
2951
|
const obligationMap = new Map();
|
|
2242
2952
|
if (ledgerPayload?.obligations) {
|
|
2243
2953
|
for (const obl of ledgerPayload.obligations) {
|
|
2244
|
-
|
|
2954
|
+
// Classified, never cast: an unrecognized kind used to index the lens map
|
|
2955
|
+
// to `undefined` and promote a lens-less finding (MNT-114e4941-3).
|
|
2956
|
+
obligationMap.set(obl.id, classifyObligationKind(String(obl.kind ?? "")));
|
|
2245
2957
|
}
|
|
2246
2958
|
}
|
|
2247
2959
|
// Auto-phasing (T3): read the persisted phase cut and re-key its module-phase
|
|
@@ -2270,9 +2982,10 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2270
2982
|
// the module decomposition instead of trusting the host to have filled it: each
|
|
2271
2983
|
// node's obligations are `OBL-<moduleSlug>-…`, and every module declares its
|
|
2272
2984
|
// `file_scope`, so a node that declared no files inherits the file_scope of the
|
|
2273
|
-
// module(s) its obligations belong to.
|
|
2274
|
-
//
|
|
2275
|
-
//
|
|
2985
|
+
// module(s) its obligations belong to. A node that DID declare files still gains
|
|
2986
|
+
// those modules' finalized-contract write targets (P38) — the scope is a UNION,
|
|
2987
|
+
// not a precedence. Single-sourced with the DAG validator, which refuses a node
|
|
2988
|
+
// this resolves to nothing for — see buildNodeWriteScopeResolver.
|
|
2276
2989
|
const { resolve: deriveNodeFiles } = await buildNodeWriteScopeResolver(artifactsDir);
|
|
2277
2990
|
const nodes = (Array.isArray(dag?.nodes) ? [...dag.nodes] : []).sort((left, right) => String(left.id).localeCompare(String(right.id)));
|
|
2278
2991
|
// Path-A promotion is an identity-preserving projection. DAG node ids describe
|
|
@@ -2339,8 +3052,9 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2339
3052
|
confidence: "high",
|
|
2340
3053
|
lens,
|
|
2341
3054
|
summary: node.description ?? node.title ?? "",
|
|
2342
|
-
// output_files (declared write scope) takes priority over files_likely_touched
|
|
2343
|
-
//
|
|
3055
|
+
// output_files (declared write scope) takes priority over files_likely_touched,
|
|
3056
|
+
// unioned with the owning module contract's declared write targets; when the
|
|
3057
|
+
// node declared neither, it inherits the module file_scope (deriveNodeFiles)
|
|
2344
3058
|
// so the finding is never scope-less. Map each path to the { path } shape that
|
|
2345
3059
|
// Finding.affected_files expects.
|
|
2346
3060
|
affected_files: deriveNodeFiles(node).map((p) => ({ path: p })),
|
|
@@ -2419,7 +3133,29 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2419
3133
|
// Same derivation as the finding's affected_files: declared write scope, else
|
|
2420
3134
|
// the module file_scope inherited via the node's obligations — so the block's
|
|
2421
3135
|
// file-ownership scheduler never sees an empty (undispatchable) touched set.
|
|
2422
|
-
|
|
3136
|
+
// Normalized before it leaves this producer: the host-handoff substrate binds
|
|
3137
|
+
// this list as the write scope and can validate its shape but never its
|
|
3138
|
+
// correctness.
|
|
3139
|
+
// Refusals are collected, not thrown: `collectDagWriteScopeRefusals` runs
|
|
3140
|
+
// these same two normalizers at the promotion gate and re-emits, so by the
|
|
3141
|
+
// time promotion runs there is nothing left to refuse. The throw below is a
|
|
3142
|
+
// BACKSTOP for a caller that skipped that gate — never the operator-facing
|
|
3143
|
+
// path.
|
|
3144
|
+
//
|
|
3145
|
+
// "Nothing left to refuse" is now TRUE BY CONSTRUCTION, not by hope: the
|
|
3146
|
+
// command half asks the ONE shared `commandLeavesDeclaredShape` predicate
|
|
3147
|
+
// that the host-handoff consumer asks, so a command this gate admits cannot
|
|
3148
|
+
// be refused downstream (and vice versa). It used to be a claim about two
|
|
3149
|
+
// independent implementations that disagreed in both directions.
|
|
3150
|
+
const scope = normalizeBlockTouchedFiles(root, deriveNodeFiles(node), toBlockId(nodeId));
|
|
3151
|
+
const commands = normalizeBlockTargetedCommands(node.targeted_commands ?? [], toBlockId(nodeId));
|
|
3152
|
+
const refusals = [...scope.refusals, ...commands.refusals];
|
|
3153
|
+
if (refusals.length > 0) {
|
|
3154
|
+
throw new Error(`implementation_dag node "${nodeId}" has an unpromotable write scope, which the ` +
|
|
3155
|
+
`promotion gate should have refused first: ${refusals.join(" | ")}`);
|
|
3156
|
+
}
|
|
3157
|
+
const touchedFiles = scope.touched_files;
|
|
3158
|
+
const targetedCommands = commands.targeted_commands;
|
|
2423
3159
|
// Phase ordinal from the union of this node's obligations (max → fail-toward-
|
|
2424
3160
|
// later). Only stamped when there is a genuine multi-phase cut, so a single-
|
|
2425
3161
|
// phase change carries no ordinal and the scheduler runs no barrier.
|
|
@@ -2440,9 +3176,7 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2440
3176
|
// declared write scope so the file-ownership scheduler can read it.
|
|
2441
3177
|
touched_files: touchedFiles,
|
|
2442
3178
|
...(phaseOrdinal !== undefined ? { phase_ordinal: phaseOrdinal } : {}),
|
|
2443
|
-
...(
|
|
2444
|
-
? { targeted_commands: [...node.targeted_commands] }
|
|
2445
|
-
: {}),
|
|
3179
|
+
...(targetedCommands.length > 0 ? { targeted_commands: targetedCommands } : {}),
|
|
2446
3180
|
};
|
|
2447
3181
|
});
|
|
2448
3182
|
const extractedPlan = {
|
|
@@ -2458,6 +3192,54 @@ export async function promoteImplementationDagToExtractedPlan(artifactsDir) {
|
|
|
2458
3192
|
};
|
|
2459
3193
|
await writeJsonFile(paths.extractedPlan, extractedPlan);
|
|
2460
3194
|
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Read the pinned planning outputs for a run, or null when no plan has been
|
|
3197
|
+
* promoted yet.
|
|
3198
|
+
*/
|
|
3199
|
+
export async function readContractPipelinePlanningOutputs(artifactsDir) {
|
|
3200
|
+
const plan = await readOptionalJsonFile(intakePaths(artifactsDir).extractedPlan);
|
|
3201
|
+
if (!plan)
|
|
3202
|
+
return null;
|
|
3203
|
+
const stringsOf = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
3204
|
+
const blocks = (Array.isArray(plan.blocks) ? plan.blocks : [])
|
|
3205
|
+
.map((block) => ({
|
|
3206
|
+
block_id: typeof block.block_id === "string" ? block.block_id : "",
|
|
3207
|
+
items: stringsOf(block.items),
|
|
3208
|
+
touched_files: stringsOf(block.touched_files),
|
|
3209
|
+
targeted_commands: stringsOf(block.targeted_commands),
|
|
3210
|
+
}))
|
|
3211
|
+
.sort((left, right) => compareCodeUnits(left.block_id, right.block_id));
|
|
3212
|
+
const findingBytes = new Map();
|
|
3213
|
+
for (const finding of Array.isArray(plan.findings) ? plan.findings : []) {
|
|
3214
|
+
if (typeof finding.id !== "string")
|
|
3215
|
+
continue;
|
|
3216
|
+
const text = `${String(finding.title ?? "")}\n${String(finding.summary ?? "")}`;
|
|
3217
|
+
findingBytes.set(finding.id, Buffer.byteLength(text, "utf8"));
|
|
3218
|
+
}
|
|
3219
|
+
const claimCounts = new Map();
|
|
3220
|
+
for (const block of blocks) {
|
|
3221
|
+
for (const item of block.items) {
|
|
3222
|
+
claimCounts.set(item, (claimCounts.get(item) ?? 0) + 1);
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
const findingIds = [...findingBytes.keys()].sort((left, right) => compareCodeUnits(left, right));
|
|
3226
|
+
return {
|
|
3227
|
+
block_membership: blocks,
|
|
3228
|
+
coverage: {
|
|
3229
|
+
finding_ids: findingIds,
|
|
3230
|
+
exhaustive_once: findingIds.length > 0 &&
|
|
3231
|
+
findingIds.every((id) => claimCounts.get(id) === 1) &&
|
|
3232
|
+
[...claimCounts.keys()].every((id) => findingBytes.has(id)),
|
|
3233
|
+
},
|
|
3234
|
+
token_estimates: blocks.map((block) => ({
|
|
3235
|
+
block_id: block.block_id,
|
|
3236
|
+
estimated_tokens: estimateTokensFromBytes(block.items.reduce((total, item) => total + (findingBytes.get(item) ?? 0), 0)),
|
|
3237
|
+
})),
|
|
3238
|
+
seed_source_digests: [
|
|
3239
|
+
...((await readOptionalJsonFile(pathASeedFilePath(artifactsDir)))?.source_digests ?? []),
|
|
3240
|
+
].sort((left, right) => compareCodeUnits(left.path, right.path)),
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
2461
3243
|
// ── Lean-path extracted plan (the `low` risk tier's plan emission) ────────────
|
|
2462
3244
|
//
|
|
2463
3245
|
// The heavy pipeline above derives its `extracted-plan.json` from the adversarial
|