diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index cd4f0f0706..122f1cb510 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -7956,6 +7956,13 @@ "qualityGateMinScore": { "type": "number", "nullable": true + }, + "gatePack": { + "type": "string", + "enum": [ + "gittensor", + "oss-anti-slop" + ] } }, "required": [ @@ -7966,6 +7973,7 @@ "checkRunMode", "checkRunDetailLevel", "gateCheckMode", + "gatePack", "linkedIssueGateMode", "duplicatePrGateMode", "qualityGateMode", @@ -8528,6 +8536,13 @@ "qualityGateMinScore": { "type": "number", "nullable": true + }, + "gatePack": { + "type": "string", + "enum": [ + "gittensor", + "oss-anti-slop" + ] } }, "required": [ @@ -8538,6 +8553,7 @@ "checkRunMode", "checkRunDetailLevel", "gateCheckMode", + "gatePack", "linkedIssueGateMode", "duplicatePrGateMode", "qualityGateMode", diff --git a/migrations/0032_repository_gate_pack.sql b/migrations/0032_repository_gate_pack.sql new file mode 100644 index 0000000000..0c7c928410 --- /dev/null +++ b/migrations/0032_repository_gate_pack.sql @@ -0,0 +1,4 @@ +-- Policy-pack-pluggable gate (#692). `gittensor` (default) = the full Gittensor policy (confirmed- +-- contributor-gated, registry-aware). `oss-anti-slop` = the deterministic rules against any author on any +-- repo, with no Gittensor coupling. Default preserves existing behavior for every current repo. +ALTER TABLE repository_settings ADD COLUMN gate_pack TEXT NOT NULL DEFAULT 'gittensor'; diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 0dafdc7c99..a9a6bc8450 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -393,6 +393,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "block", qualityGateMode: "advisory", @@ -420,6 +421,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise checkRunMode: parseCheckRunMode(row.checkRunMode), checkRunDetailLevel: parseCheckRunDetailLevel(row.checkRunDetailLevel), gateCheckMode: parseGateCheckMode(row.gateCheckMode), + gatePack: parseGatePack(row.gatePack), linkedIssueGateMode: parseGateRuleMode(row.linkedIssueGateMode), duplicatePrGateMode: parseGateRuleMode(row.duplicatePrGateMode), qualityGateMode: parseGateRuleMode(row.qualityGateMode), @@ -451,6 +453,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial DB > defaults), resolved upstream by // resolveRepositorySettings, so the blocker modes here reflect the repo's config file directly. - // confirmedContributor governs WHO can be blocked, downstream in evaluateGateCheck. + // The `oss-anti-slop` pack (#692) is repo-agnostic: it blocks ANY author whose PR trips an opted-in + // deterministic rule, so it drops the confirmed-contributor gate entirely (no Gittensor coupling). The + // `gittensor` pack keeps the contributor gate — only confirmed contributors are hard-blocked. + const confirmedContributorForPack = settings.gatePack === "oss-anti-slop" ? undefined : confirmedContributor; return { linkedIssueGateMode: settings.linkedIssueGateMode, duplicatePrGateMode: settings.duplicatePrGateMode, @@ -825,7 +828,7 @@ export function gateCheckPolicy(settings: RepositorySettings, readinessScore?: n qualityGateMinScore: settings.qualityGateMinScore ?? null, aiReviewGateMode: settings.aiReviewMode, readinessScore: readinessScore ?? null, - confirmedContributor, + confirmedContributor: confirmedContributorForPack, }; } diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index 04e7e7e136..af87b312d7 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -1,5 +1,5 @@ import { parse as parseYaml } from "yaml"; -import type { GateRuleMode, JsonValue, RepositorySettings } from "../types"; +import type { GatePolicyPack, GateRuleMode, JsonValue, RepositorySettings } from "../types"; export type FocusManifestSource = "repo_file" | "api_record" | "none"; export type FocusManifestLinkedIssuePolicy = "required" | "preferred" | "optional"; @@ -17,6 +17,7 @@ export type FocusManifestIssueDiscoveryPolicy = "encouraged" | "neutral" | "disc export type FocusManifestGateConfig = { present: boolean; enabled: boolean | null; + pack: GatePolicyPack | null; linkedIssue: GateRuleMode | null; duplicates: GateRuleMode | null; readinessMode: GateRuleMode | null; @@ -141,6 +142,7 @@ export const MAX_FOCUS_MANIFEST_BYTES = 64 * 1024; const EMPTY_GATE_CONFIG: FocusManifestGateConfig = { present: false, enabled: null, + pack: null, linkedIssue: null, duplicates: null, readinessMode: null, @@ -272,6 +274,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu const gate: FocusManifestGateConfig = { present: false, enabled: normalizeOptionalBoolean(record.enabled, "gate.enabled", warnings), + pack: normalizeOptionalEnum(record.pack, "gate.pack", ["gittensor", "oss-anti-slop"] as const, warnings), linkedIssue: normalizeOptionalGateMode(record.linkedIssue, "gate.linkedIssue", warnings), duplicates: normalizeOptionalGateMode(record.duplicates, "gate.duplicates", warnings), readinessMode: normalizeOptionalGateMode(readinessRecord?.mode, "gate.readiness.mode", warnings), @@ -283,6 +286,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu }; gate.present = gate.enabled !== null || + gate.pack !== null || gate.linkedIssue !== null || gate.duplicates !== null || gate.readinessMode !== null || @@ -302,6 +306,7 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue { if (!gate.present) return null; const out: Record = {}; if (gate.enabled !== null) out.enabled = gate.enabled; + if (gate.pack !== null) out.pack = gate.pack; if (gate.linkedIssue !== null) out.linkedIssue = gate.linkedIssue; if (gate.duplicates !== null) out.duplicates = gate.duplicates; if (gate.readinessMode !== null || gate.readinessMinScore !== null) { @@ -451,6 +456,7 @@ export function resolveEffectiveSettings(dbSettings: RepositorySettings, manifes const effective: RepositorySettings = { ...dbSettings, ...manifest.settings }; const gate = manifest.gate; if (gate.enabled !== null) effective.gateCheckMode = gate.enabled ? "enabled" : "off"; + if (gate.pack !== null) effective.gatePack = gate.pack; if (gate.linkedIssue !== null) effective.linkedIssueGateMode = gate.linkedIssue; if (gate.duplicates !== null) effective.duplicatePrGateMode = gate.duplicates; if (gate.readinessMode !== null) effective.qualityGateMode = gate.readinessMode; diff --git a/src/signals/settings-preview.ts b/src/signals/settings-preview.ts index 5bf85a2dec..7c04acceea 100644 --- a/src/signals/settings-preview.ts +++ b/src/signals/settings-preview.ts @@ -182,6 +182,7 @@ export type RepoSettingsPreview = { checkRunMode: RepositorySettings["checkRunMode"]; checkRunDetailLevel: RepositorySettings["checkRunDetailLevel"]; gateCheckMode: RepositorySettings["gateCheckMode"]; + gatePack: RepositorySettings["gatePack"]; linkedIssueGateMode: RepositorySettings["linkedIssueGateMode"]; duplicatePrGateMode: RepositorySettings["duplicatePrGateMode"]; qualityGateMode: RepositorySettings["qualityGateMode"]; @@ -293,6 +294,7 @@ export function buildRepoSettingsPreview(args: { checkRunMode: settings.checkRunMode, checkRunDetailLevel: settings.checkRunDetailLevel, gateCheckMode: settings.gateCheckMode, + gatePack: settings.gatePack, linkedIssueGateMode: settings.linkedIssueGateMode, duplicatePrGateMode: settings.duplicatePrGateMode, qualityGateMode: settings.qualityGateMode, diff --git a/src/types.ts b/src/types.ts index b3a3443b62..48d1df7fc1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -370,6 +370,12 @@ export type BountyRecord = { export type GateRuleMode = "off" | "advisory" | "block"; +/** Which policy pack the gate runs under (#692). `gittensor` = the full Gittensor policy: only confirmed + * Gittensor contributors are hard-blocked (registry/emissions-aware). `oss-anti-slop` = a general, repo- + * agnostic pack: the same deterministic rules (slop/duplicate/linked-issue/readiness/AI-consensus) block + * ANY author, with no emissions/registry/confirmed-contributor coupling — so the gate runs on any repo. */ +export type GatePolicyPack = "gittensor" | "oss-anti-slop"; + export type RepositorySettings = { repoFullName: string; commentMode: "off" | "detected_contributors_only" | "all_prs"; @@ -378,6 +384,9 @@ export type RepositorySettings = { checkRunMode: "off" | "enabled"; checkRunDetailLevel: "minimal" | "standard" | "deep"; gateCheckMode: "off" | "enabled"; + /** Policy pack the gate evaluates under (#692). Default `gittensor` (confirmed-contributor-gated, + * registry-aware). `oss-anti-slop` runs the deterministic rules against any author on any repo. */ + gatePack: GatePolicyPack; linkedIssueGateMode: GateRuleMode; duplicatePrGateMode: GateRuleMode; qualityGateMode: GateRuleMode; diff --git a/test/unit/data-spine.test.ts b/test/unit/data-spine.test.ts index 8ec3758785..7d13ded517 100644 --- a/test/unit/data-spine.test.ts +++ b/test/unit/data-spine.test.ts @@ -39,6 +39,7 @@ import { upsertRepoLabel, upsertRepoSyncState, upsertRepositoryFromGitHub, + upsertRepositorySettings, } from "../../src/db/repositories"; import { createTestEnv } from "../helpers/d1"; @@ -234,7 +235,13 @@ describe("data spine repositories", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", publicSurface: "comment_and_label", + gatePack: "gittensor", }); + // gatePack (#692) round-trips and defaults to gittensor. + await upsertRepositorySettings(env, { repoFullName: "owner/repo", gatePack: "oss-anti-slop" }); + expect((await getRepositorySettings(env, "owner/repo")).gatePack).toBe("oss-anti-slop"); + await upsertRepositorySettings(env, { repoFullName: "owner/defaultpack" }); + expect((await getRepositorySettings(env, "owner/defaultpack")).gatePack).toBe("gittensor"); expect(await getRepoSyncState(env, "missing/repo")).toBeNull(); expect(await getPullRequest(env, "owner/repo", 404)).toBeNull(); expect(await getIssue(env, "owner/repo", 404)).toBeNull(); diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index fd781b543e..3e5e48996e 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -400,7 +400,7 @@ describe("compileFocusManifestPolicy", () => { issueDiscoveryPolicy: "neutral", maintainerNotes: [], publicNotes: ["Keep PRs focused.", "Maximize your reward payout"], - gate: { present: false, enabled: null, linkedIssue: null, duplicates: null, readinessMode: null, readinessMinScore: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null }, + gate: { present: false, enabled: null, pack: null, linkedIssue: null, duplicates: null, readinessMode: null, readinessMinScore: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null }, settings: {}, review: { present: false, footerText: null, note: null, fields: {} }, warnings: [], @@ -688,7 +688,16 @@ describe("parseFocusManifest gate config", () => { it("parses a full gate section including the readiness block", () => { const m = parseFocusManifest({ gate: { linkedIssue: "block", duplicates: "advisory", readiness: { mode: "block", minScore: 70 } } }); expect(m.present).toBe(true); - expect(m.gate).toEqual({ present: true, enabled: null, linkedIssue: "block", duplicates: "advisory", readinessMode: "block", readinessMinScore: 70, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null }); + expect(m.gate).toEqual({ present: true, enabled: null, pack: null, linkedIssue: "block", duplicates: "advisory", readinessMode: "block", readinessMinScore: 70, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null }); + }); + + it("parses gate.pack and ignores an unknown pack with a warning (#692)", () => { + expect(parseFocusManifest({ gate: { pack: "oss-anti-slop" } }).gate.pack).toBe("oss-anti-slop"); + expect(parseFocusManifest({ gate: { pack: "gittensor" } }).gate.pack).toBe("gittensor"); + expect(parseFocusManifest({ gate: { pack: "oss-anti-slop" } }).gate.present).toBe(true); + const bad = parseFocusManifest({ gate: { pack: "nonsense" } }); + expect(bad.gate.pack).toBeNull(); + expect(bad.warnings.some((w) => /gate\.pack/.test(w))).toBe(true); }); it("parses gate.enabled (on/off) and ignores non-boolean values with a warning", () => { diff --git a/test/unit/gate-check-policy.test.ts b/test/unit/gate-check-policy.test.ts index 866c2843e2..722a55d3b7 100644 --- a/test/unit/gate-check-policy.test.ts +++ b/test/unit/gate-check-policy.test.ts @@ -84,6 +84,36 @@ describe(".gittensory.yml settings override (resolveEffectiveSettings)", () => { }); }); +describe("policy pack (#692)", () => { + it("gittensor pack hard-blocks only confirmed contributors", () => { + const gittensor = settings({ gatePack: "gittensor", linkedIssueGateMode: "block" }); + expect(evaluateGateCheck(missingIssueAdvisory(), gateCheckPolicy(gittensor, null, false)).conclusion).toBe("neutral"); + expect(evaluateGateCheck(missingIssueAdvisory(), gateCheckPolicy(gittensor, null, true)).conclusion).toBe("failure"); + }); + + it("oss-anti-slop pack blocks ANY author whose PR trips an opted-in rule (no confirmed-contributor gate)", () => { + const oss = settings({ gatePack: "oss-anti-slop", linkedIssueGateMode: "block" }); + const blocked = evaluateGateCheck(missingIssueAdvisory(), gateCheckPolicy(oss, null, false)); + expect(blocked.conclusion).toBe("failure"); + expect(blocked.blockers.map((finding) => finding.code)).toEqual(["missing_linked_issue"]); + // Still passes a clean PR (no opted-in blocker) regardless of author. + expect(evaluateGateCheck(missingIssueAdvisory(), gateCheckPolicy(settings({ gatePack: "oss-anti-slop", linkedIssueGateMode: "advisory" }), null, false)).conclusion).toBe("success"); + }); + + it("gateCheckPolicy drops confirmedContributor under oss-anti-slop and keeps it (incl. default) under gittensor", () => { + expect(gateCheckPolicy(settings({ gatePack: "oss-anti-slop" }), null, false).confirmedContributor).toBeUndefined(); + expect(gateCheckPolicy(settings({ gatePack: "oss-anti-slop" }), null, true).confirmedContributor).toBeUndefined(); + expect(gateCheckPolicy(settings({ gatePack: "gittensor" }), null, false).confirmedContributor).toBe(false); + expect(gateCheckPolicy(settings({ gatePack: "gittensor" }), null, true).confirmedContributor).toBe(true); + }); + + it(".gittensory.yml gate.pack overlays the pack and flips the gate to block any author end-to-end", () => { + const eff = resolveEffectiveSettings(settings({ gatePack: "gittensor", linkedIssueGateMode: "block" }), parseFocusManifest({ gate: { pack: "oss-anti-slop" } })); + expect(eff.gatePack).toBe("oss-anti-slop"); + expect(evaluateGateCheck(missingIssueAdvisory(), gateCheckPolicy(eff, null, false)).conclusion).toBe("failure"); + }); +}); + describe("AI consensus defect gate blocker", () => { function aiDefectAdvisory(): Advisory { return { ...missingIssueAdvisory(), findings: [{ code: "ai_consensus_defect", title: "AI reviewers agree on a likely critical defect", severity: "critical", detail: "Both models flagged a null deref.", action: "Resolve it." }] }; diff --git a/test/unit/maintainer-activation.test.ts b/test/unit/maintainer-activation.test.ts index 9f467fa279..e9ebe57f5e 100644 --- a/test/unit/maintainer-activation.test.ts +++ b/test/unit/maintainer-activation.test.ts @@ -28,6 +28,7 @@ function settings(overrides: Partial = {}): RepositorySettin checkRunMode: "off", checkRunDetailLevel: "standard", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/policy-sanitizer.test.ts b/test/unit/policy-sanitizer.test.ts index 01231b672e..0a2ac36c36 100644 --- a/test/unit/policy-sanitizer.test.ts +++ b/test/unit/policy-sanitizer.test.ts @@ -62,6 +62,7 @@ function settingsFor(repoFullName: string, overrides: Partial = {}): RepositorySettin checkRunMode: "enabled", checkRunDetailLevel: "standard", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/self-dogfood-registration-pack.test.ts b/test/unit/self-dogfood-registration-pack.test.ts index e91d28341c..b4b287f3f0 100644 --- a/test/unit/self-dogfood-registration-pack.test.ts +++ b/test/unit/self-dogfood-registration-pack.test.ts @@ -55,6 +55,7 @@ function settingsFor(repoFullName: string, overrides: Partial = {}): RepositorySettin checkRunMode: "off", checkRunDetailLevel: "standard", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 02bde68a0e..069162830d 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -1525,6 +1525,7 @@ function repoSettings(repoFullName: string): RepositorySettings { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 39270c90ae..307bfbb254 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -1613,6 +1613,7 @@ describe("v2 signal builders", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", diff --git a/test/unit/signals.test.ts b/test/unit/signals.test.ts index da8960d5fd..931eee3630 100644 --- a/test/unit/signals.test.ts +++ b/test/unit/signals.test.ts @@ -385,6 +385,7 @@ describe("world-class backend signals", () => { checkRunMode: "off" as const, checkRunDetailLevel: "minimal" as const, gateCheckMode: "off" as const, + gatePack: "gittensor" as const, linkedIssueGateMode: "advisory" as const, duplicatePrGateMode: "advisory" as const, qualityGateMode: "advisory" as const, @@ -430,6 +431,7 @@ describe("world-class backend signals", () => { checkRunMode: "off" as const, checkRunDetailLevel: "minimal" as const, gateCheckMode: "off" as const, + gatePack: "gittensor" as const, linkedIssueGateMode: "advisory" as const, duplicatePrGateMode: "advisory" as const, qualityGateMode: "advisory" as const, @@ -495,6 +497,7 @@ describe("world-class backend signals", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", @@ -581,6 +584,7 @@ describe("world-class backend signals", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory", @@ -642,6 +646,7 @@ describe("world-class backend signals", () => { checkRunMode: "off", checkRunDetailLevel: "minimal", gateCheckMode: "off", + gatePack: "gittensor", linkedIssueGateMode: "advisory", duplicatePrGateMode: "advisory", qualityGateMode: "advisory",