Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ export {
type IterateLoopOutcome,
type IterateLoopResult,
} from "./miner/iterate-loop.js";
export {
isSlopBandWithinThreshold,
shouldSubmit,
SUBMISSION_GATE_PASSING_CONCLUSION,
type SubmissionGateCandidate,
type SubmissionGateDecision,
type SubmissionGateMode,
} from "./miner/submission-gate.js";
export {
codingAgentModeExecutes,
isGlobalMinerCodingAgentPause,
Expand Down
108 changes: 108 additions & 0 deletions packages/gittensory-engine/src/miner/submission-gate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Gated-submission trigger (#2336): THE safety-critical chokepoint of Phase 4 -- the actual decision point
// that decides "call gittensory_open_pr NOW" for an autonomous run. Conservatively requires BOTH a predicted-
// gate PASS AND a slop score under a configurable threshold before a local-write open_pr action spec is ever
// built. Any ambiguity (a missing/errored signal) resolves to NOT submitting -- this function never defaults
// to allow.
//
// SEQUENCING: runs BEFORE `buildOpenPrSpec` (src/mcp/local-write-tools.ts) is ever called -- i.e. before the
// Governor chokepoint (#2340) ever sees an open_pr action spec to evaluate. The two are complementary, not
// redundant: this gates on CONTENT-QUALITY signals (predicted-gate conclusion, slop risk) specific to the
// candidate diff; the chokepoint gates on RESOURCE/GOVERNANCE signals (rate-limit, budget, reputation, self-
// plagiarism, dry-run mode) that apply to every write action class, not just open_pr. "The actual call site
// invoking buildOpenPrSpec / gittensory_open_pr is gated exclusively through this function" (this issue's own
// deliverable) is a POLICY this and every future call site must honor -- wiring a real call site is a later,
// separate issue (mirrors #2333/#2335's own split between loop mechanics and policy).
//
// INPUT SHAPE: `predictedGateVerdict`/`slopAssessment` are typed exactly as the fields `SelfReviewVerdict`
// (self-review-adapter.ts, #2334) already carries, so a caller can pass the SAME verdict the iterate-loop's
// own self-review (#2333) already computed at handoff time -- this is a defense-in-depth RE-CHECK of that
// verdict immediately before submission, not a redundant re-computation from scratch.
//
// DRY-RUN: mirrors `src/settings/autonomy.ts`'s deny-by-default dial (`AUTONOMY_LEVELS`, `"observe"` as the
// floor) for safe rollout of THIS function's own thresholds -- distinct from, and evaluated separately from,
// the Governor chokepoint's own dry-run/live action-mode dial (#2342), which gates autonomous WRITING at all
// for a repo. `"observe"` here is specifically for safely calibrating the predicted-gate/slop thresholds
// against live traffic before ever trusting them to gate a real submission.

import type { PredictedGateVerdict } from "../predicted-gate.js";
import type { SelfReviewSlopAssessment, SelfReviewSlopBand } from "./self-review-adapter.js";

/** The one literal conclusion value that counts as a clear predicted-gate pass -- same literal self-review-
* adapter.ts's `SELF_REVIEW_PASSING_CONCLUSION` uses, kept as an independent constant here so this module has
* no runtime dependency beyond types on self-review-adapter.ts. */
export const SUBMISSION_GATE_PASSING_CONCLUSION = "success" as const;

const SLOP_BAND_SEVERITY: Readonly<Record<SelfReviewSlopBand, number>> = Object.freeze({
clean: 0,
low: 1,
elevated: 2,
high: 3,
});

/** True when `band` is at or under `threshold`'s severity (inclusive) -- e.g. a `"low"` band is within a
* `"elevated"` threshold, and a band exactly equal to the threshold still passes. */
export function isSlopBandWithinThreshold(band: SelfReviewSlopBand, threshold: SelfReviewSlopBand): boolean {
return SLOP_BAND_SEVERITY[band] <= SLOP_BAND_SEVERITY[threshold];
}

/** `"observe"` mirrors `AUTONOMY_LEVELS`' deny-by-default floor: {@link shouldSubmit} still computes and
* returns the real signal-based decision (for logging), but structurally forces `allow: false` regardless --
* not left to an external caller to remember to also check the mode before acting on `allow: true`. */
export type SubmissionGateMode = "observe" | "enforce";

export type SubmissionGateCandidate = {
/** `null` means the predictor was unreachable or errored -- fails closed, exactly like a genuine non-passing
* verdict, never treated as "no opinion, so allow". */
predictedGateVerdict: PredictedGateVerdict | null;
/** `null` means the slop check errored -- fails closed, exactly like a genuine over-threshold assessment. */
slopAssessment: SelfReviewSlopAssessment | null;
/** The maximum slop band that still permits submission (inclusive of this exact band). */
slopThreshold: SelfReviewSlopBand;
mode: SubmissionGateMode;
};

export type SubmissionGateDecision = {
allow: boolean;
/** Always populated when `allow` is `false` (including in `"observe"` mode, prefixed to distinguish a
* would-have-allowed dry-run from a real block) -- every decision is auditable, not just denials. */
reasons: string[];
};

/** The pure signal check, independent of `mode` -- {@link shouldSubmit} layers the observe/enforce dial on
* top of this. Returns an empty array only when BOTH signals genuinely pass. */
function evaluateSubmissionSignals(candidate: SubmissionGateCandidate): string[] {
const reasons: string[] = [];

if (candidate.predictedGateVerdict === null) {
reasons.push("predicted_gate_unavailable");
} else if (candidate.predictedGateVerdict.conclusion !== SUBMISSION_GATE_PASSING_CONCLUSION) {
const blockerCodes = candidate.predictedGateVerdict.blockers.map((blocker) => blocker.code).join(",");
reasons.push(`predicted_gate_not_passing:${candidate.predictedGateVerdict.conclusion}${blockerCodes ? `:${blockerCodes}` : ""}`);
}

if (candidate.slopAssessment === null) {
reasons.push("slop_assessment_unavailable");
} else if (!isSlopBandWithinThreshold(candidate.slopAssessment.band, candidate.slopThreshold)) {
reasons.push(`slop_band_exceeds_threshold:${candidate.slopAssessment.band}>${candidate.slopThreshold}`);
}

return reasons;
}

/**
* THE gate: build (or invoke) `gittensory_open_pr`'s action spec ONLY when this returns `allow: true`. Requires
* BOTH a clean predicted-gate pass AND a slop band at or under the configured threshold; any missing signal, or
* `mode: "observe"`, forces `allow: false`. Pure; identical inputs always yield the identical decision.
*/
export function shouldSubmit(candidate: SubmissionGateCandidate): SubmissionGateDecision {
const reasons = evaluateSubmissionSignals(candidate);
const signalsPass = reasons.length === 0;

if (candidate.mode === "observe") {
return {
allow: false,
reasons: signalsPass ? ["observe_mode_active:would_have_allowed"] : ["observe_mode_active:would_have_blocked", ...reasons],
};
}
return { allow: signalsPass, reasons };
}
150 changes: 150 additions & 0 deletions packages/gittensory-engine/test/submission-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import assert from "node:assert/strict";
import { test } from "node:test";

import {
isSlopBandWithinThreshold,
shouldSubmit,
SUBMISSION_GATE_PASSING_CONCLUSION,
type PredictedGateVerdict,
type SelfReviewSlopAssessment,
type SelfReviewSlopBand,
type SubmissionGateCandidate,
} from "../dist/index.js";

function passingVerdict(): PredictedGateVerdict {
return {
predicted: true,
basis: "public_config",
pack: "oss-anti-slop",
conclusion: "success",
title: "Predicted gate: pass",
summary: "Every check is expected to pass.",
readinessScore: 92,
confirmedContributor: undefined,
blockers: [],
warnings: [],
funnel: null,
note: "",
};
}

function failingVerdict(blockers: PredictedGateVerdict["blockers"] = [{ code: "duplicate_pr_risk", title: "Likely duplicate", detail: "Matches an existing open PR." }]): PredictedGateVerdict {
return {
predicted: true,
basis: "public_config",
pack: "oss-anti-slop",
conclusion: "failure",
title: "Predicted gate: fail",
summary: "At least one check is expected to fail.",
readinessScore: 15,
confirmedContributor: undefined,
blockers,
warnings: [],
funnel: null,
note: "",
};
}

function slop(band: SelfReviewSlopBand, slopRisk = 0): SelfReviewSlopAssessment {
return { slopRisk, band, findings: [] };
}

function baseCandidate(overrides: Partial<SubmissionGateCandidate> = {}): SubmissionGateCandidate {
return {
predictedGateVerdict: passingVerdict(),
slopAssessment: slop("clean"),
slopThreshold: "low",
mode: "enforce",
...overrides,
};
}

test("barrel: the public entrypoint re-exports the submission gate (#2336)", () => {
assert.equal(typeof shouldSubmit, "function");
assert.equal(typeof isSlopBandWithinThreshold, "function");
assert.equal(SUBMISSION_GATE_PASSING_CONCLUSION, "success");
});

test("pass/pass: a clean predicted-gate pass with slop under threshold allows, with no reasons", () => {
const decision = shouldSubmit(baseCandidate());
assert.deepEqual(decision, { allow: true, reasons: [] });
});

test("fail/pass: a non-passing predicted-gate verdict blocks even with slop cleanly under threshold", () => {
const decision = shouldSubmit(baseCandidate({ predictedGateVerdict: failingVerdict() }));
assert.equal(decision.allow, false);
assert.equal(decision.reasons.length, 1);
assert.match(decision.reasons[0] ?? "", /^predicted_gate_not_passing:failure:duplicate_pr_risk$/);
});

test("fail/pass: a non-passing verdict with NO blockers listed still formats a reason, without a dangling separator", () => {
const decision = shouldSubmit(baseCandidate({ predictedGateVerdict: failingVerdict([]) }));
assert.equal(decision.reasons[0], "predicted_gate_not_passing:failure");
});

test("pass/fail: a clean predicted-gate pass blocks when slop exceeds the configured threshold", () => {
const decision = shouldSubmit(baseCandidate({ slopAssessment: slop("high"), slopThreshold: "low" }));
assert.equal(decision.allow, false);
assert.deepEqual(decision.reasons, ["slop_band_exceeds_threshold:high>low"]);
});

test("both-fail: a non-passing verdict AND over-threshold slop blocks with both reasons listed", () => {
const decision = shouldSubmit(baseCandidate({ predictedGateVerdict: failingVerdict(), slopAssessment: slop("high"), slopThreshold: "low" }));
assert.equal(decision.allow, false);
assert.equal(decision.reasons.length, 2);
assert.ok(decision.reasons.some((r) => r.startsWith("predicted_gate_not_passing")));
assert.ok(decision.reasons.some((r) => r.startsWith("slop_band_exceeds_threshold")));
});

test("fail-closed: a null predictedGateVerdict (predictor unreachable) blocks, never treated as no-opinion-so-allow", () => {
const decision = shouldSubmit(baseCandidate({ predictedGateVerdict: null }));
assert.equal(decision.allow, false);
assert.deepEqual(decision.reasons, ["predicted_gate_unavailable"]);
});

test("fail-closed: a null slopAssessment (slop check errored) blocks, never treated as no-opinion-so-allow", () => {
const decision = shouldSubmit(baseCandidate({ slopAssessment: null }));
assert.equal(decision.allow, false);
assert.deepEqual(decision.reasons, ["slop_assessment_unavailable"]);
});

test("fail-closed: both signals missing blocks with both unavailable reasons listed", () => {
const decision = shouldSubmit(baseCandidate({ predictedGateVerdict: null, slopAssessment: null }));
assert.equal(decision.allow, false);
assert.deepEqual(decision.reasons, ["predicted_gate_unavailable", "slop_assessment_unavailable"]);
});

test("observe mode: forces allow: false even for signals that would otherwise cleanly pass", () => {
const decision = shouldSubmit(baseCandidate({ mode: "observe" }));
assert.equal(decision.allow, false);
assert.deepEqual(decision.reasons, ["observe_mode_active:would_have_allowed"]);
});

test("observe mode: a would-have-blocked decision is distinguishable from a would-have-allowed one, with the real reasons preserved", () => {
const decision = shouldSubmit(baseCandidate({ mode: "observe", predictedGateVerdict: null }));
assert.equal(decision.allow, false);
assert.deepEqual(decision.reasons, ["observe_mode_active:would_have_blocked", "predicted_gate_unavailable"]);
});

test("isSlopBandWithinThreshold: a band exactly equal to the threshold passes (inclusive boundary)", () => {
assert.equal(isSlopBandWithinThreshold("elevated", "elevated"), true);
});

test("isSlopBandWithinThreshold: a band one severity level under the threshold passes", () => {
assert.equal(isSlopBandWithinThreshold("low", "elevated"), true);
});

test("isSlopBandWithinThreshold: a band one severity level over the threshold fails", () => {
assert.equal(isSlopBandWithinThreshold("high", "elevated"), false);
});

test("isSlopBandWithinThreshold: the full clean..high ordering is respected end to end", () => {
const order: SelfReviewSlopBand[] = ["clean", "low", "elevated", "high"];
for (let i = 0; i < order.length; i += 1) {
for (let j = 0; j < order.length; j += 1) {
const band = order[i] as SelfReviewSlopBand;
const threshold = order[j] as SelfReviewSlopBand;
assert.equal(isSlopBandWithinThreshold(band, threshold), i <= j, `${band} within ${threshold}`);
}
}
});