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
9 changes: 9 additions & 0 deletions .loopover.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,15 @@ settings:
# inherit | off | enabled. Default: inherit.
# duplicateWinnerMode: inherit

# Open-PR file-path collision annotations (#2653): enriches preflight/queue-health
# output with which open PRs touch overlapping files -- display-only, never a
# close/gate blocker. Costs an extra GitHub API round-trip per open PR, so this is
# opt-in and off by default. "inherit" defers to the self-host operator's
# LOOPOVER_OPEN_PR_FILE_COLLISION env default (itself off by default); "off"/"enabled"
# override that default in either direction for this repo.
# inherit | off | enabled. Default: inherit.
# openPrFileCollisionMode: inherit

# Hard manual-review path guardrails are config-as-code only. Safe by default (#3943): whatever you list
# here is ADDED to a fixed, built-in invariant set (CI workflows/scripts, deploy config, config-as-code
# files, core engine-decision paths — see DEFAULT_HARD_GUARDRAIL_GLOBS in src/review/guardrail-config.ts)
Expand Down
8 changes: 8 additions & 0 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9673,6 +9673,14 @@
"updatedAt": {
"type": "string",
"nullable": true
},
"openPrFileCollisionMode": {
"type": "string",
"enum": [
"inherit",
"off",
"enabled"
]
}
},
"required": [
Expand Down
9 changes: 9 additions & 0 deletions config/examples/loopover.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,15 @@ settings:
# inherit | off | enabled. Default: inherit.
# duplicateWinnerMode: inherit

# Open-PR file-path collision annotations (#2653): enriches preflight/queue-health
# output with which open PRs touch overlapping files -- display-only, never a
# close/gate blocker. Costs an extra GitHub API round-trip per open PR, so this is
# opt-in and off by default. "inherit" defers to the self-host operator's
# LOOPOVER_OPEN_PR_FILE_COLLISION env default (itself off by default); "off"/"enabled"
# override that default in either direction for this repo.
# inherit | off | enabled. Default: inherit.
# openPrFileCollisionMode: inherit

# Hard manual-review path guardrails are config-as-code only. Safe by default (#3943): whatever you list
# here is ADDED to a fixed, built-in invariant set (CI workflows/scripts, deploy config, config-as-code
# files, core engine-decision paths — see DEFAULT_HARD_GUARDRAIL_GLOBS in src/review/guardrail-config.ts)
Expand Down
5 changes: 5 additions & 0 deletions packages/loopover-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ export type FocusManifestSettings = Partial<
| "closeOwnerAuthors"
| "skipAutomationBotAuthors"
| "duplicateWinnerMode"
| "openPrFileCollisionMode"
| "autoLabelEnabled"
| "typeLabelsEnabled"
| "badgeEnabled"
Expand Down Expand Up @@ -2243,6 +2244,10 @@ function parseSettingsOverride(value: JsonValue | undefined, warnings: string[])
// default -- "inherit" defers to it, "off"/"enabled" override in either direction for this repo.
const duplicateWinnerMode = normalizeOptionalEnum(r.duplicateWinnerMode, "settings.duplicateWinnerMode", ["inherit", "off", "enabled"] as const, warnings);
if (duplicateWinnerMode !== null) out.duplicateWinnerMode = duplicateWinnerMode;
// Open-PR file-path collision annotations (#2653): per-repo override of the global LOOPOVER_OPEN_PR_FILE_COLLISION
// default -- "inherit" defers to it, "off"/"enabled" override in either direction for this repo.
const openPrFileCollisionMode = normalizeOptionalEnum(r.openPrFileCollisionMode, "settings.openPrFileCollisionMode", ["inherit", "off", "enabled"] as const, warnings);
if (openPrFileCollisionMode !== null) out.openPrFileCollisionMode = openPrFileCollisionMode;
// Moderation-rules engine (#selfhost-mod-engine): per-repo override of the global moderation config.
const moderationGateMode = normalizeOptionalEnum(r.moderationGateMode, "settings.moderationGateMode", ["inherit", "off", "enabled"] as const, warnings);
if (moderationGateMode !== null) out.moderationGateMode = moderationGateMode;
Expand Down
6 changes: 6 additions & 0 deletions packages/loopover-engine/src/types/manifest-deps-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,12 @@ export type RepositorySettings = {
* default-OFF); `"off"`/`"enabled"` fully override the global default in either direction for this repo. No
* DB column -- config-as-code only, set via `.loopover.yml settings.duplicateWinnerMode`. */
duplicateWinnerMode?: "inherit" | "off" | "enabled" | undefined;
/** Open-PR file-path collision annotations (#2653): enriches preflight/queue-health output with which open
* PRs touch overlapping files -- display-only, never a close/gate blocker. `"inherit"` defers to the
* `LOOPOVER_OPEN_PR_FILE_COLLISION` global env default (itself default-OFF); `"off"`/`"enabled"` fully
* override the global default in either direction for this repo. No DB column -- config-as-code only, set
* via `.loopover.yml settings.openPrFileCollisionMode`. */
openPrFileCollisionMode?: "inherit" | "off" | "enabled" | undefined;
autoLabelEnabled: boolean;
gittensorLabel: string;
createMissingLabel: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,7 @@ export const RepositorySettingsSchema = z
moderationBannedLabel: z.string().optional(),
skipAutomationBotAuthors: z.enum(["inherit", "off", "enabled"]).optional(),
duplicateWinnerMode: z.enum(["inherit", "off", "enabled"]).optional(),
openPrFileCollisionMode: z.enum(["inherit", "off", "enabled"]).optional(),
reviewEvasionProtection: z
.enum(["off", "close"])
.optional()
Expand Down
3 changes: 2 additions & 1 deletion src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ import {
} from "../signals/engine";
import { isDuplicateClusterWinnerByClaim } from "../signals/duplicate-winner";
import { isDuplicateWinnerEnabledGlobally, resolveDuplicateWinnerEnabled } from "../settings/duplicate-winner-mode";
import { isOpenPrFileCollisionEnabledGlobally, resolveOpenPrFileCollisionEnabled } from "../settings/open-pr-file-collision-mode";
import { buildAiReviewDiff, buildSecretScanDiff, buildUnifiedReviewDiff, totalAddedLineCount } from "../review/review-diff";
// #4013 step 4 (prep): buildAiReviewDiff/buildSecretScanDiff moved to review-diff.ts (a natural existing
// home -- both already wrapped buildUnifiedReviewDiff there) rather than staying here, since keeping them
Expand Down Expand Up @@ -8380,7 +8381,7 @@ async function maybePublishPrPublicSurface(
// Scoped to collision/preflight/queue-health inputs only — every OTHER use of repoPullRequests below (e.g. the
// duplicate-winner adjudication, which is same-linked-issue-based, not path-based) keeps reading the un-enriched array.
const collisionPullRequests =
env.LOOPOVER_OPEN_PR_FILE_COLLISION === "true"
resolveOpenPrFileCollisionEnabled(isOpenPrFileCollisionEnabledGlobally(env), settings.openPrFileCollisionMode)
? await enrichOpenPullRequestsWithChangedFiles(env, repoFullName, repoPullRequests)
: repoPullRequests;
collisions = buildCollisionReport(
Expand Down
20 changes: 20 additions & 0 deletions src/settings/open-pr-file-collision-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type OpenPrFileCollisionMode = "inherit" | "off" | "enabled";

/** Truthy convention matches the rest of this codebase's `LOOPOVER_*` flags (exact `"true"` string) -- opt-in
* and default OFF: the enrichment call this gates costs an extra GitHub API round-trip per open PR
* (enrichOpenPullRequestsWithChangedFiles), so an operator should deliberately turn it on rather than pay
* that cost by default. */
export function isOpenPrFileCollisionEnabledGlobally(env: { LOOPOVER_OPEN_PR_FILE_COLLISION?: string | undefined }): boolean {
return env.LOOPOVER_OPEN_PR_FILE_COLLISION === "true";
}

/** Per-repo override resolved against the global default. Mirrors `resolveDuplicateWinnerEnabled`'s
* inherit/off/enabled shape (settings/duplicate-winner-mode.ts) -- symmetric: "off" and "enabled" both fully
* override the global default in either direction, so a repo that wants the extra file-collision annotation
* cost isn't blocked by a globally-off default, and a repo that wants to opt OUT of the extra API calls can
* do so even when the fleet default is on. */
export function resolveOpenPrFileCollisionEnabled(globalDefault: boolean, mode: OpenPrFileCollisionMode | null | undefined): boolean {
if (mode === "off") return false;
if (mode === "enabled") return true;
return globalDefault;
}
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,13 @@ export type RepositorySettings = {
* default in either direction for this repo. No DB column -- config-as-code only, set via `.loopover.yml
* settings.duplicateWinnerMode`. */
duplicateWinnerMode?: "inherit" | "off" | "enabled" | undefined;
/** Open-PR file-path collision annotations (#2653): enriches preflight/queue-health output with which open
* PRs touch overlapping files -- display-only, never a close/gate blocker. Costs an extra GitHub API
* round-trip per open PR to fetch changed files, so this is opt-in and default-OFF, unlike most settings
* here. `"inherit"` (the default) defers to the `LOOPOVER_OPEN_PR_FILE_COLLISION` global env default
* (itself default-OFF); `"off"`/`"enabled"` fully override the global default in either direction for this
* repo. No DB column -- config-as-code only, set via `.loopover.yml settings.openPrFileCollisionMode`. */
openPrFileCollisionMode?: "inherit" | "off" | "enabled" | undefined;
/** Review-evasion protection (#review-evasion-protection): a contributor closing or converting their OWN
* PR to draft while loopover has an ACTIVE review pass running against it is dodging the one-shot
* review process. The effective default is `"close"` as of #4011 (see `normalizeReviewEvasionProtection`
Expand Down
16 changes: 16 additions & 0 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ describe(".loopover.yml.example field-exhaustiveness (#1670)", () => {
autoProjectMilestoneMatchBackend: "autoProjectMilestoneMatchBackend:",
closeOwnerAuthors: "closeOwnerAuthors:",
duplicateWinnerMode: "duplicateWinnerMode:",
openPrFileCollisionMode: "openPrFileCollisionMode:",
autoLabelEnabled: "autoLabelEnabled:",
typeLabelsEnabled: "typeLabelsEnabled:",
badgeEnabled: "badgeEnabled:",
Expand Down Expand Up @@ -2532,6 +2533,21 @@ describe("parseFocusManifest settings override + resolveEffectiveSettings", () =
expect(invalid.warnings.some((w) => /settings\.duplicateWinnerMode/.test(w))).toBe(true);
});

it("parses + resolves openPrFileCollisionMode from the settings: block, overlaying the DB (#2653)", () => {
const manifest = parseFocusManifest({ settings: { openPrFileCollisionMode: "enabled" } });
expect(manifest.settings.openPrFileCollisionMode).toBe("enabled");
// yml overlays (replaces) the DB-configured value.
const eff = resolveEffectiveSettings({ openPrFileCollisionMode: "off" } as unknown as RepositorySettings, manifest);
expect(eff.openPrFileCollisionMode).toBe("enabled");
// Omitted in yml ⇒ the DB-configured value survives untouched.
const noOverride = resolveEffectiveSettings({ openPrFileCollisionMode: "off" } as unknown as RepositorySettings, parseFocusManifest({}));
expect(noOverride.openPrFileCollisionMode).toBe("off");
// An invalid enum is dropped with a warning rather than silently coerced.
const invalid = parseFocusManifest({ settings: { openPrFileCollisionMode: "sometimes" as never } });
expect(invalid.settings.openPrFileCollisionMode).toBeUndefined();
expect(invalid.warnings.some((w) => /settings\.openPrFileCollisionMode/.test(w))).toBe(true);
});

it("moderationRules accepts review_evasion alongside the original three rule types (#review-evasion-protection)", () => {
const manifest = parseFocusManifest({ settings: { moderationRules: ["review_evasion", "not-a-rule" as never] } });
expect(manifest.settings.moderationRules).toEqual(["review_evasion"]);
Expand Down
40 changes: 40 additions & 0 deletions test/unit/open-pr-file-collision-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { isOpenPrFileCollisionEnabledGlobally, resolveOpenPrFileCollisionEnabled } from "../../src/settings/open-pr-file-collision-mode";

describe("isOpenPrFileCollisionEnabledGlobally", () => {
it("defaults OFF when unset", () => {
expect(isOpenPrFileCollisionEnabledGlobally({})).toBe(false);
expect(isOpenPrFileCollisionEnabledGlobally({ LOOPOVER_OPEN_PR_FILE_COLLISION: undefined })).toBe(false);
expect(isOpenPrFileCollisionEnabledGlobally({ LOOPOVER_OPEN_PR_FILE_COLLISION: "" })).toBe(false);
});

it("is ON only for the exact string \"true\"", () => {
expect(isOpenPrFileCollisionEnabledGlobally({ LOOPOVER_OPEN_PR_FILE_COLLISION: "true" })).toBe(true);
});

it("stays OFF for any other value, including truthy-looking ones", () => {
for (const value of ["1", "yes", "on", "True", "TRUE", " true "]) {
expect(isOpenPrFileCollisionEnabledGlobally({ LOOPOVER_OPEN_PR_FILE_COLLISION: value })).toBe(false);
}
});
});

describe("resolveOpenPrFileCollisionEnabled", () => {
it("inherit defers to the global default in both directions", () => {
expect(resolveOpenPrFileCollisionEnabled(true, "inherit")).toBe(true);
expect(resolveOpenPrFileCollisionEnabled(false, "inherit")).toBe(false);
});

it("null/undefined mode behaves the same as inherit", () => {
expect(resolveOpenPrFileCollisionEnabled(true, null)).toBe(true);
expect(resolveOpenPrFileCollisionEnabled(false, undefined)).toBe(false);
});

it("off fully overrides a globally-ON default", () => {
expect(resolveOpenPrFileCollisionEnabled(true, "off")).toBe(false);
});

it("enabled fully overrides a globally-OFF default (symmetric)", () => {
expect(resolveOpenPrFileCollisionEnabled(false, "enabled")).toBe(true);
});
});
Loading