fix(solver): enforce PDD purpose gate

This commit is contained in:
Mikael Hugo 2026-05-15 18:32:59 +02:00
parent bb0c87fdac
commit af1401e4ea
2 changed files with 68 additions and 0 deletions

View file

@ -165,6 +165,32 @@ function renderPdd(pdd = {}) {
].join("\n");
}
const REQUIRED_PDD_FIELDS = [
["purpose", "Purpose"],
["consumer", "Consumer"],
["contract", "Contract"],
["failureBoundary", "Failure boundary"],
["evidence", "Evidence"],
["nonGoals", "Non-goals"],
["invariants", "Invariants"],
["assumptions", "Assumptions"],
];
/**
* Return missing PDD field labels for a checkpoint payload.
*
* Purpose: make ADR-0000's purpose gate executable instead of leaving the
* eight-field contract as prompt-only guidance.
*
* Consumer: assessAutonomousSolverTurn before accepting continue/complete
* checkpoint outcomes.
*/
export function missingPddFieldLabels(pdd = {}) {
return REQUIRED_PDD_FIELDS.filter(
([key]) => !String(pdd?.[key] ?? "").trim(),
).map(([, label]) => label);
}
function renderProjection(state) {
const checkpoint = state.latestCheckpoint ?? {};
return [
@ -1080,6 +1106,17 @@ export function assessAutonomousSolverTurn(
maxCheckpointCount: MAX_CHECKPOINTS_PER_ITERATION,
};
}
const missingPdd = missingPddFieldLabels(checkpoint.pdd);
if (checkpoint.outcome !== "blocked" && missingPdd.length > 0) {
return {
action: "pause",
reason: "solver-purpose-gate",
state,
checkpoint,
missingPddFields: missingPdd,
message: `BLOCKED: purpose unclear - ${missingPdd.join(", ")}`,
};
}
if (
(checkpoint.outcome === "continue" || checkpoint.outcome === "decide") &&
(checkpoint.remainingItems?.length ?? 0) === 0

View file

@ -19,6 +19,7 @@ import {
getSolverPhase,
isNoOpExecutorTranscript,
MAX_EXECUTOR_REFUSAL_ESCALATIONS,
missingPddFieldLabels,
readAutonomousSolverState,
readLatestAutonomousSolverCheckpoint,
recordAutonomousSolverMissingCheckpointRetry,
@ -1124,6 +1125,36 @@ describe("appendAutonomousSolverCheckpoint sticky identity", () => {
expect(assessment.action).toBe("complete");
});
test("assessAutonomousSolverTurn_missing_pdd_fields_pauses_purpose_gate", () => {
const project = makeProject();
beginAutonomousSolverIteration(project, "execute-task", "M001/S04/T03");
appendAutonomousSolverCheckpoint(project, {
unitType: "execute-task",
unitId: "M001/S04/T03",
outcome: "complete",
summary: "Done.",
completedItems: ["work"],
remainingItems: [],
verificationEvidence: ["npm test"],
pdd: pdd({ purpose: "", evidence: "" }),
});
const assessment = assessAutonomousSolverTurn(
project,
"execute-task",
"M001/S04/T03",
);
expect(assessment.action).toBe("pause");
expect(assessment.reason).toBe("solver-purpose-gate");
expect(assessment.missingPddFields).toEqual(["Purpose", "Evidence"]);
expect(assessment.message).toContain("BLOCKED: purpose unclear");
});
test("missingPddFieldLabels_when_all_fields_present_returns_empty", () => {
expect(missingPddFieldLabels(pdd())).toEqual([]);
});
test("matching unitId does not flag mismatch", () => {
const project = makeProject();
beginAutonomousSolverIteration(project, "execute-task", "M001/S04/T02");