diff --git a/.gittensory.yml.example b/.gittensory.yml.example index aaed6cc35d..91e87c4b38 100644 --- a/.gittensory.yml.example +++ b/.gittensory.yml.example @@ -142,10 +142,12 @@ gate: # off | advisory | block. Default: off. manifestPolicy: off - # First-time-contributor grace. When true, softens a would-be block to - # advisory for a genuine newcomer (0 merged PRs, < 3 closed-unmerged PRs). - # Repeat offenders and authors with merge history are gated normally. - # Bool. Default: false. + # First-time-contributor grace. RESERVED / currently INERT: this value is + # parsed and stored, but the gate does not read it — a first-time + # contributor with a real blocker is one-shot closed the same as a + # repeat contributor (blocker findings must remain closure outcomes). + # Setting this to true has no effect today; kept for potential future + # use. Bool. Default: false. firstTimeContributorGrace: false # AI maintainer review. Opt-in; the AI capabilities are switched on at the diff --git a/src/rules/advisory.ts b/src/rules/advisory.ts index 3c2ecfac55..8c1c48b609 100644 --- a/src/rules/advisory.ts +++ b/src/rules/advisory.ts @@ -50,13 +50,15 @@ export type GateCheckPolicy = { * the PR author also filed the linked issue — becomes a hard blocker. Defaults to `advisory` — the * finding is surfaced but never blocks unless the maintainer opts in. */ selfAuthoredLinkedIssueGateMode?: GateRuleMode | undefined; - /** First-time-contributor grace (#552). When true AND the author is a genuine newcomer (0 merged PRs in - * this repo) who is NOT a repeat offender (< 3 closed-unmerged PRs), a would-be BLOCK is softened to a - * neutral/advisory gate. `undefined`/false = the grace rule does not apply and blockers gate normally. */ + /** First-time-contributor grace (#552). RESERVED / currently INERT (#2266): threaded through from config, + * but evaluateGateCheckCore never reads it (see the removal note below) — a would-be blocker gates a + * genuine newcomer exactly like a repeat contributor. Kept for potential future use. */ firstTimeContributorGrace?: boolean | undefined; - /** The PR author's merged PR count in THIS repo (newcomer = 0). Used only by the grace rule. */ + /** The PR author's merged PR count in THIS repo. RESERVED / currently INERT (#2266) alongside + * firstTimeContributorGrace above — populated but never read by the gate evaluator today. */ authorMergedPrCount?: number | undefined; - /** The PR author's closed-unmerged PR count in THIS repo (repeat offender = >= 3). Used only by grace. */ + /** The PR author's closed-unmerged PR count in THIS repo. RESERVED / currently INERT (#2266) alongside + * firstTimeContributorGrace above — populated but never read by the gate evaluator today. */ authorClosedUnmergedPrCount?: number | undefined; /** The PR author's confirmed-Gittensor status. Carried for context/telemetry only — it no longer * changes the gate verdict (every author is gated identically; a configured blocker fails the gate diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index 1cf6fe245d..70d6f2d6a4 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -440,6 +440,13 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu dryRun: normalizeOptionalBoolean(record.dryRun, "gate.dryRun", warnings), firstTimeContributorGrace: normalizeOptionalBoolean(record.firstTimeContributorGrace, "gate.firstTimeContributorGrace", warnings), }; + // #2266: the flag is parsed, clamped, and threaded end-to-end, but the gate evaluator never reads it — a + // maintainer who sets it to true believing it softens a blocker for newcomers gets no such effect. Surface + // that inertness at parse time rather than leaving it silently no-op; `false`/unset matches the (also inert) + // default, so only an explicit `true` is worth flagging. + if (gate.firstTimeContributorGrace === true) { + warnings.push(`Manifest field "gate.firstTimeContributorGrace" is currently reserved/inert — it does not soften a blocker outcome for first-time contributors.`); + } gate.present = gate.enabled !== null || gate.pack !== null || diff --git a/src/types.ts b/src/types.ts index d1719a72f0..4eb6e3f909 100644 --- a/src/types.ts +++ b/src/types.ts @@ -546,9 +546,10 @@ export type RepositorySettings = { * opened the linked issue (`pr.authorLogin === issue.authorLogin`). Defaults to `advisory` — the finding * is surfaced in the review panel but never blocks unless the maintainer opts in. */ selfAuthoredLinkedIssueGateMode: GateRuleMode; - /** First-time-contributor grace (#552). When true, a would-be BLOCK is softened to a neutral/advisory gate - * for a genuine newcomer (0 merged PRs in this repo) who is NOT a repeat offender (< 3 closed-unmerged PRs). - * Repeat offenders and authors with merge history are gated normally. Default false — opt-in. */ + /** First-time-contributor grace (#552). RESERVED / currently INERT (#2266): parsed, clamped, and threaded + * end-to-end, but the gate evaluator never reads it — a genuine newcomer with a real blocker is still + * one-shot closed exactly like a repeat contributor (blocker findings must remain closure outcomes). + * Setting this true has no runtime effect today; kept for potential future use. Default false. */ firstTimeContributorGrace: boolean; /** Slop-risk threshold (0-100) at/above which `slopGateMode: block` blocks. Default 60 (the `high` band). */ slopGateMinScore?: number | null | undefined; diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index bfc420ec20..4c6fa3d4e9 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -797,6 +797,19 @@ describe("parseFocusManifest gate config", () => { expect(bad.gate.present).toBe(false); }); + it("warns that gate.firstTimeContributorGrace is reserved/inert when explicitly set true (#2266)", () => { + const m = parseFocusManifest({ gate: { firstTimeContributorGrace: true } }); + expect(m.gate.firstTimeContributorGrace).toBe(true); + expect(m.warnings.some((w) => /gate\.firstTimeContributorGrace.*reserved\/inert/i.test(w))).toBe(true); + }); + + it("does not warn about firstTimeContributorGrace when left unset or explicitly false (matches the inert default)", () => { + const unset = parseFocusManifest({ gate: { linkedIssue: "block" } }); + expect(unset.warnings.some((w) => /firstTimeContributorGrace/i.test(w))).toBe(false); + const explicitFalse = parseFocusManifest({ gate: { firstTimeContributorGrace: false } }); + expect(explicitFalse.warnings.some((w) => /firstTimeContributorGrace/i.test(w))).toBe(false); + }); + it("parses gate.selfAuthoredLinkedIssue + settings.selfAuthoredLinkedIssueGateMode, round-trips + resolves them (the gate alias wins)", () => { const m = parseFocusManifest({ gate: { selfAuthoredLinkedIssue: "block" }, settings: { selfAuthoredLinkedIssueGateMode: "advisory" } }); expect(m.gate.present).toBe(true);