diff --git a/scripts/pre-open-gate.mjs b/scripts/pre-open-gate.mjs index 1f7244ec..a4a09f04 100644 --- a/scripts/pre-open-gate.mjs +++ b/scripts/pre-open-gate.mjs @@ -101,6 +101,9 @@ export function evaluate(plan, evidence = null) { } } + const incompleteReasons = (plan?.uncertainty || []) + .filter((item) => item?.blocksCompletion !== false) + .map((item) => `review_scope:${String(item?.code || "unknown")}`); const complete = implementationDiffPresent && plan.complete && bugScope.complete && securityScope.complete; const finalBlockers = implementationDiffPresent ? blockers @@ -119,6 +122,7 @@ export function evaluate(plan, evidence = null) { probeEvidenceErrors: probes.errors, blockers: finalBlockers, clearedByEvidence, + incompleteReasons, decision, complete, implementationDiffPresent, @@ -126,7 +130,7 @@ export function evaluate(plan, evidence = null) { }; } -function report({ repo, baseRef, headRef, baseRefOid, headRefOid, diffIdentity, fileCount, bugScope, securityScope, requiredProbes, probeEvidenceErrors, blockers, clearedByEvidence, decision, complete, implementationDiffPresent, evidenceApplied }) { +function report({ repo, baseRef, headRef, baseRefOid, headRefOid, diffIdentity, fileCount, bugScope, securityScope, requiredProbes, probeEvidenceErrors, blockers, clearedByEvidence, incompleteReasons, decision, complete, implementationDiffPresent, evidenceApplied }) { return { schemaVersion: 1, kind: "github-delivery/pre-open-gate", @@ -148,6 +152,7 @@ function report({ repo, baseRef, headRef, baseRefOid, headRefOid, diffIdentity, probeEvidenceErrors, blockers, clearedByEvidence, + incompleteReasons: Array.isArray(incompleteReasons) ? incompleteReasons : [], instructions: [ "workflow:implementation_missing: this pre-open gate requires a non-empty candidate implementation diff; implement first, then rerun the gate before publication.", "decision=blocked: complete every remaining required bug lens, security surface, and deterministic probe on this branch diff (with --evidence-file), fix Confirmed High/Critical findings, then rerun before opening the PR.", @@ -225,12 +230,17 @@ export function compactPreOpenGateReport(result) { }; } + const incompleteReasons = sortedUnique(result?.incompleteReasons || []); const nextAction = result?.decision === "ready" ? "proceed_to_publication" : result?.decision === "blocked" ? "complete_evidence" - : "restore_branch_evidence"; + : incompleteReasons.includes("review_scope:patch_missing") + ? "inspect_missing_patch_evidence" + : incompleteReasons.some((reason) => reason.startsWith("review_scope:probe_registry_invalid")) + ? "repair_probe_registry" + : "resolve_incomplete_review_scope"; return { schemaVersion: 1, @@ -248,6 +258,7 @@ export function compactPreOpenGateReport(result) { implementationDiffPresent: result?.implementationDiffPresent, evidenceApplied: result?.evidenceApplied, blockerCount: blockers.length, + incompleteReasons, remaining, evidenceRequirements: { schemaVersion: PRE_OPEN_EVIDENCE_SCHEMA_VERSION, diff --git a/tests/unit/pre-open-gate-compact.test.mjs b/tests/unit/pre-open-gate-compact.test.mjs index 2b00e5eb..cfceba66 100644 --- a/tests/unit/pre-open-gate-compact.test.mjs +++ b/tests/unit/pre-open-gate-compact.test.mjs @@ -95,6 +95,19 @@ test("pre-open output carries generator version and implementation digests", () assert.deepEqual(compactPreOpenGateReport(report).generator, generator); }); +test("unknown compact report names the incomplete review-scope cause", () => { + const report = blockedReport(); + report.decision = "unknown"; + report.complete = false; + report.blockers = []; + report.incompleteReasons = ["review_scope:patch_missing"]; + + const compact = compactPreOpenGateReport(report); + assert.equal(compact.decision, "unknown"); + assert.deepEqual(compact.incompleteReasons, ["review_scope:patch_missing"]); + assert.equal(compact.nextAction, "inspect_missing_patch_evidence"); +}); + test("ready compact report has one unambiguous publication disposition", () => { const report = blockedReport(); report.decision = "ready";