Skip to content

Commit 6ecacec

Browse files
authored
fix(review): mark gate.firstTimeContributorGrace as reserved/inert (#2411)
evaluateGateCheckCore in src/rules/advisory.ts never reads policy.firstTimeContributorGrace (or its authorMergedPrCount / authorClosedUnmergedPrCount companions) — a deliberate removal ("blocker findings must remain closure/rejection outcomes") that the public-facing docs never caught up with. A maintainer reading .gittensory.yml.example would believe setting this true softens a blocker for newcomers; nothing changes, and a first-time contributor with a real blocker is still one-shot closed like anyone else. - Rewrite the .gittensory.yml.example entry, the RepositorySettings field doc in types.ts, and the GateCheckPolicy field docs in advisory.ts to state the flag (and its two companion counts) are reserved/inert rather than describing behavior that no longer runs. - Emit a manifest warning when a repo explicitly sets gate.firstTimeContributorGrace: true, so a maintainer relying on it gets a visible signal instead of silent inertness. Unset/false (the default, and also the effective behavior) does not warn. This is not a guard weakening — the actual behavior was already stricter than documented. Only the docs and warning are new.
1 parent 28ae561 commit 6ecacec

5 files changed

Lines changed: 37 additions & 12 deletions

File tree

.gittensory.yml.example

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,12 @@ gate:
145145
# off | advisory | block. Default: off.
146146
manifestPolicy: off
147147

148-
# First-time-contributor grace. When true, softens a would-be block to
149-
# advisory for a genuine newcomer (0 merged PRs, < 3 closed-unmerged PRs).
150-
# Repeat offenders and authors with merge history are gated normally.
151-
# Bool. Default: false.
148+
# First-time-contributor grace. RESERVED / currently INERT: this value is
149+
# parsed and stored, but the gate does not read it — a first-time
150+
# contributor with a real blocker is one-shot closed the same as a
151+
# repeat contributor (blocker findings must remain closure outcomes).
152+
# Setting this to true has no effect today; kept for potential future
153+
# use. Bool. Default: false.
152154
firstTimeContributorGrace: false
153155

154156
# AI maintainer review. Opt-in; the AI capabilities are switched on at the

src/rules/advisory.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ export type GateCheckPolicy = {
5050
* the PR author also filed the linked issue — becomes a hard blocker. Defaults to `advisory` — the
5151
* finding is surfaced but never blocks unless the maintainer opts in. */
5252
selfAuthoredLinkedIssueGateMode?: GateRuleMode | undefined;
53-
/** First-time-contributor grace (#552). When true AND the author is a genuine newcomer (0 merged PRs in
54-
* this repo) who is NOT a repeat offender (< 3 closed-unmerged PRs), a would-be BLOCK is softened to a
55-
* neutral/advisory gate. `undefined`/false = the grace rule does not apply and blockers gate normally. */
53+
/** First-time-contributor grace (#552). RESERVED / currently INERT (#2266): threaded through from config,
54+
* but evaluateGateCheckCore never reads it (see the removal note below) — a would-be blocker gates a
55+
* genuine newcomer exactly like a repeat contributor. Kept for potential future use. */
5656
firstTimeContributorGrace?: boolean | undefined;
57-
/** The PR author's merged PR count in THIS repo (newcomer = 0). Used only by the grace rule. */
57+
/** The PR author's merged PR count in THIS repo. RESERVED / currently INERT (#2266) alongside
58+
* firstTimeContributorGrace above — populated but never read by the gate evaluator today. */
5859
authorMergedPrCount?: number | undefined;
59-
/** The PR author's closed-unmerged PR count in THIS repo (repeat offender = >= 3). Used only by grace. */
60+
/** The PR author's closed-unmerged PR count in THIS repo. RESERVED / currently INERT (#2266) alongside
61+
* firstTimeContributorGrace above — populated but never read by the gate evaluator today. */
6062
authorClosedUnmergedPrCount?: number | undefined;
6163
/** The PR author's confirmed-Gittensor status. Carried for context/telemetry only — it no longer
6264
* changes the gate verdict (every author is gated identically; a configured blocker fails the gate

src/signals/focus-manifest.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,13 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
455455
dryRun: normalizeOptionalBoolean(record.dryRun, "gate.dryRun", warnings),
456456
firstTimeContributorGrace: normalizeOptionalBoolean(record.firstTimeContributorGrace, "gate.firstTimeContributorGrace", warnings),
457457
};
458+
// #2266: the flag is parsed, clamped, and threaded end-to-end, but the gate evaluator never reads it — a
459+
// maintainer who sets it to true believing it softens a blocker for newcomers gets no such effect. Surface
460+
// that inertness at parse time rather than leaving it silently no-op; `false`/unset matches the (also inert)
461+
// default, so only an explicit `true` is worth flagging.
462+
if (gate.firstTimeContributorGrace === true) {
463+
warnings.push(`Manifest field "gate.firstTimeContributorGrace" is currently reserved/inert — it does not soften a blocker outcome for first-time contributors.`);
464+
}
458465
gate.present =
459466
gate.enabled !== null ||
460467
gate.pack !== null ||

src/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -546,9 +546,10 @@ export type RepositorySettings = {
546546
* opened the linked issue (`pr.authorLogin === issue.authorLogin`). Defaults to `advisory` — the finding
547547
* is surfaced in the review panel but never blocks unless the maintainer opts in. */
548548
selfAuthoredLinkedIssueGateMode: GateRuleMode;
549-
/** First-time-contributor grace (#552). When true, a would-be BLOCK is softened to a neutral/advisory gate
550-
* for a genuine newcomer (0 merged PRs in this repo) who is NOT a repeat offender (< 3 closed-unmerged PRs).
551-
* Repeat offenders and authors with merge history are gated normally. Default false — opt-in. */
549+
/** First-time-contributor grace (#552). RESERVED / currently INERT (#2266): parsed, clamped, and threaded
550+
* end-to-end, but the gate evaluator never reads it — a genuine newcomer with a real blocker is still
551+
* one-shot closed exactly like a repeat contributor (blocker findings must remain closure outcomes).
552+
* Setting this true has no runtime effect today; kept for potential future use. Default false. */
552553
firstTimeContributorGrace: boolean;
553554
/** Slop-risk threshold (0-100) at/above which `slopGateMode: block` blocks. Default 60 (the `high` band). */
554555
slopGateMinScore?: number | null | undefined;

test/unit/focus-manifest.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,19 @@ describe("parseFocusManifest gate config", () => {
799799
expect(bad.gate.present).toBe(false);
800800
});
801801

802+
it("warns that gate.firstTimeContributorGrace is reserved/inert when explicitly set true (#2266)", () => {
803+
const m = parseFocusManifest({ gate: { firstTimeContributorGrace: true } });
804+
expect(m.gate.firstTimeContributorGrace).toBe(true);
805+
expect(m.warnings.some((w) => /gate\.firstTimeContributorGrace.*reserved\/inert/i.test(w))).toBe(true);
806+
});
807+
808+
it("does not warn about firstTimeContributorGrace when left unset or explicitly false (matches the inert default)", () => {
809+
const unset = parseFocusManifest({ gate: { linkedIssue: "block" } });
810+
expect(unset.warnings.some((w) => /firstTimeContributorGrace/i.test(w))).toBe(false);
811+
const explicitFalse = parseFocusManifest({ gate: { firstTimeContributorGrace: false } });
812+
expect(explicitFalse.warnings.some((w) => /firstTimeContributorGrace/i.test(w))).toBe(false);
813+
});
814+
802815
it("parses gate.selfAuthoredLinkedIssue + settings.selfAuthoredLinkedIssueGateMode, round-trips + resolves them (the gate alias wins)", () => {
803816
const m = parseFocusManifest({ gate: { selfAuthoredLinkedIssue: "block" }, settings: { selfAuthoredLinkedIssueGateMode: "advisory" } });
804817
expect(m.gate.present).toBe(true);

0 commit comments

Comments
 (0)