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
10 changes: 6 additions & 4 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions src/rules/advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/signals/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down
7 changes: 4 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading