fix(solver): enforce PDD purpose gate
This commit is contained in:
parent
bb0c87fdac
commit
af1401e4ea
2 changed files with 68 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue