From 31c98dba13255f3e145853633108139c295761ee Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:47:14 -0700 Subject: [PATCH] fix(review): predict path-independent pre-merge checks and disclaim diff-dependent gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The predict_gate oracle (buildPredictedGateVerdict) ran the advisory + gate but ignored the repo's review.pre_merge_checks, focus-manifest path policy, and slop — all of which the LIVE gate enforces. So a PR the gate would auto-close on an enforced title/description rule predicted 'success', misleading contributors. Evaluate the PATH-INDEPENDENT pre-merge checks (empty whenPaths — title / description / label assertions) from the SAME public .gittensory.yml the predictor already reads; their inputs are exactly the real PR's, so the result matches the live gate. Path-gated checks, manifest-policy, and slop need the PR's changed files/diff (unavailable pre-submission) — they are now explicitly disclaimed in the verdict note so a 'success' is not misread as complete. Full parity for those ships when the predictor accepts changed paths (follow-up). No output-schema change (PredictedGateVerdict shape unchanged), so no OpenAPI regen. --- src/rules/predicted-gate.ts | 18 +++++++++++-- test/unit/predicted-gate.test.ts | 46 ++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/rules/predicted-gate.ts b/src/rules/predicted-gate.ts index 40e9284e9e..3062037b2c 100644 --- a/src/rules/predicted-gate.ts +++ b/src/rules/predicted-gate.ts @@ -18,6 +18,7 @@ const OSS_ANTI_SLOP_FUNNEL = { registerUrl: GITTENSOR_HOME_URL, } as const; import { buildPullRequestAdvisory, evaluateGateCheck, type GateCheckConclusion } from "./advisory"; +import { evaluatePreMergeChecks } from "../review/pre-merge-checks"; /** * Pre-submission "will my PR pass the gate?" prediction for a MINER, computed BEFORE a PR exists. @@ -55,8 +56,11 @@ export type PredictedGateVerdict = { const PREDICTED_GATE_NOTE = "Predicted from the repo's public .gittensory.yml gate config + safe defaults. The maintainer may have " + "private dashboard overrides not reflected here, and the dual-model AI-consensus blocker is only " + - "evaluated on a real PR. Every author is gated the same: a configured hard blocker fails the gate " + - "regardless of confirmed-contributor status (which affects only on-chain scoring)."; + "evaluated on a real PR. Diff-dependent checks are NOT evaluated pre-submission and may still fail the real " + + "gate: the slop score, the focus-manifest path policy, and any pre-merge check scoped to changed paths " + + "(path-independent title/description/label pre-merge checks ARE predicted). Every author is gated the same: " + + "a configured hard blocker fails the gate regardless of confirmed-contributor status (which affects only " + + "on-chain scoring)."; export type PredictedGateInput = { repoFullName: string; @@ -149,6 +153,16 @@ export function buildPredictedGateVerdict(args: { const linkedIssueAuthorLogins = syntheticPr.linkedIssues.map((issueNumber) => issueAuthorByNumber.get(issueNumber) ?? null); const advisory = buildPullRequestAdvisory(repo, syntheticPr, { otherOpenPullRequests: pullRequests, requireLinkedIssue, linkedIssueAuthorLogins }); + // Deterministic pre-merge checks parity (#11/#18), partial: the LIVE gate enforces the repo's + // `review.pre_merge_checks` (from the SAME public .gittensory.yml the predictor already reads), but the + // predictor ignored them — so a PR the gate would auto-close on an enforced title/description rule predicted + // "success". Evaluate the PATH-INDEPENDENT checks (empty `whenPaths` — title/description/label assertions) here: + // their inputs (title/body/labels) are exactly the real PR's, so the result is identical to live. Path-gated + // checks, manifest-policy, and slop need the PR's changed files/diff (not available pre-submission) and are + // disclaimed in the note below; they reach parity once the predictor accepts changed paths. + const alwaysApplyPreMergeChecks = manifest.review.preMergeChecks.filter((check) => check.whenPaths.length === 0); + advisory.findings.push(...evaluatePreMergeChecks(alwaysApplyPreMergeChecks, { title: syntheticPr.title, body: syntheticPr.body, labels: syntheticPr.labels, changedPaths: [] })); + // Pack-aware (#693): under `oss-anti-slop` the gate blocks ANY author, so drop the confirmed-contributor // gate entirely (mirrors gateCheckPolicy). `gittensor` keeps it. Pack comes from the PUBLIC .gittensory.yml. const pack: GatePolicyPack = gate.pack ?? "gittensor"; diff --git a/test/unit/predicted-gate.test.ts b/test/unit/predicted-gate.test.ts index 828bf5f0d4..93af2a46d5 100644 --- a/test/unit/predicted-gate.test.ts +++ b/test/unit/predicted-gate.test.ts @@ -21,10 +21,10 @@ const BASE_INPUT: PredictedGateInput = { linkedIssues: [7], }; -function verdict(args: { gate: Record; input?: Partial; issues?: IssueRecord[]; pullRequests?: PullRequestRecord[] }) { +function verdict(args: { gate: Record; review?: Record; input?: Partial; issues?: IssueRecord[]; pullRequests?: PullRequestRecord[] }) { return buildPredictedGateVerdict({ input: { ...BASE_INPUT, ...args.input }, - manifest: parseFocusManifest({ gate: args.gate }), + manifest: parseFocusManifest({ gate: args.gate, ...(args.review ? { review: args.review } : {}) }), repo: REPO, issues: args.issues ?? [openIssue(7, "Uploads should retry on 5xx")], pullRequests: args.pullRequests ?? [], @@ -174,6 +174,48 @@ describe("buildPredictedGateVerdict", () => { expect(result.conclusion).toBe("failure"); expect(result.blockers.some((b) => b.code === "missing_linked_issue")).toBe(true); }); + + it("predicts a BLOCK for an enforced path-INDEPENDENT pre-merge check the title fails (#11/#18)", () => { + // The repo's public .gittensory.yml enforces a conventional-style title; the PR title lacks "[FEAT]". + const result = verdict({ + gate: {}, + review: { pre_merge_checks: [{ name: "Conventional title", title_contains: "[FEAT]", enforce: true }] }, + }); + expect(result.conclusion).toBe("failure"); + expect(result.blockers.some((b) => b.code === "pre_merge_check_required")).toBe(true); + }); + + it("predicts a PASS once the path-independent pre-merge check is satisfied", () => { + const result = verdict({ + gate: {}, + input: { title: "[FEAT] Add retry to the upload client" }, + review: { pre_merge_checks: [{ name: "Conventional title", title_contains: "[FEAT]", enforce: true }] }, + }); + expect(result.conclusion).not.toBe("failure"); + expect(result.blockers.some((b) => b.code === "pre_merge_check_required")).toBe(false); + }); + + it("surfaces a non-enforced path-independent pre-merge check as a WARNING, not a blocker", () => { + const result = verdict({ + gate: {}, + review: { pre_merge_checks: [{ name: "Mention testing", description_contains: "tested", enforce: false }] }, + }); + expect(result.conclusion).not.toBe("failure"); + expect(result.warnings.some((w) => w.code === "pre_merge_check_failed")).toBe(true); + }); + + it("does NOT predict a path-GATED pre-merge check pre-submission (no diff) and discloses the gap in the note (#11/#18)", () => { + // A path-gated check whose title assertion the PR fails — but it is scoped to changed paths, which are + // unknown pre-submission, so it must be skipped (not falsely block) and called out in the note. + const result = verdict({ + gate: {}, + review: { pre_merge_checks: [{ name: "Tests for src", title_contains: "ZZZ-never", when_paths: ["src/**"], enforce: true }] }, + }); + expect(result.blockers.some((b) => b.code === "pre_merge_check_required")).toBe(false); + expect(result.warnings.some((w) => w.code === "pre_merge_check_unresolved")).toBe(false); + expect(result.note).toContain("scoped to changed paths"); + expect(result.note.toLowerCase()).toContain("slop"); + }); }); describe("pack-aware prediction (#693)", () => {