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
14 changes: 14 additions & 0 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ gate:
# the gate. Bool. Default: false.
aiAdvisory: false

# Copycat/plagiarism detection (#1969). CURRENTLY INERT — this config is parsed
# and threaded end-to-end, but the containment/similarity detection engine that
# would actually compute a copycat finding does not exist yet (tracked as
# separate, later PRs against #1969). Setting this today has no observable
# effect; it exists so an operator's config can already declare intent.
copycat:
# off | warn | label | block. Default: off. A dedicated 4-tier scale (not the
# shared off/advisory/block used elsewhere) — a further "strikes" escalation
# beyond block reuses the existing cross-repo banned-contributors ledger.
mode: off
# Containment/similarity threshold at/above which mode acts, once the
# detection engine exists. Number 0–100, or null for the engine default.
minScore: null

# Oversized-PR gate. A PR at/above EITHER the file-count or line-count threshold
# (engine defaults, not configurable here) gets a manual-review HOLD finding —
# never a hard blocker; mode only turns this hold signal on or off.
Expand Down
13 changes: 13 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9304,6 +9304,19 @@
"advisory",
"block"
]
},
"copycatGateMode": {
"type": "string",
"enum": [
"off",
"warn",
"label",
"block"
]
},
"copycatGateMinScore": {
"type": "number",
"nullable": true
}
},
"required": [
Expand Down
7 changes: 7 additions & 0 deletions apps/gittensory-ui/src/routes/docs.how-reviews-work.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ function HowReviewsWork() {
warnings; <code>block</code> also hard-blocks at or above <code>slopGateMinScore</code>{" "}
(engine default band <code>60</code>).
</li>
<li>
<strong>Copycat / plagiarism gate</strong> (<code>copycatGateMode</code>, default{" "}
<code>off</code>) — a code containment/similarity check against prior art (repo history,
other PRs). Escalating tiers: <code>warn</code>, <code>label</code>, <code>block</code>,
plus a further strikes escalation for repeat offenders. Config only today — the detection
engine itself has not shipped yet, so setting this has no effect until it does.
</li>
<li>
<strong>Merge-readiness gate</strong> (<code>mergeReadinessGateMode</code>, default{" "}
<code>off</code>) — a composite readiness check.
Expand Down
7 changes: 7 additions & 0 deletions apps/gittensory-ui/src/routes/docs.tuning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,13 @@ function Tuning() {
a free advisory-only <code>ai_slop_advisory</code> finding — it never feeds the slop score
or the gate.
</li>
<li>
<code>gate.copycat.mode</code> — code containment/similarity gate against prior art (repo
history, other PRs). Default <code>off</code>. Escalating tiers: <code>warn</code>,{" "}
<code>label</code>, <code>block</code>, plus a further strikes escalation for repeat
offenders. Pair it with <code>gate.copycat.minScore</code> (0–100). Config only today —
the detection engine has not shipped yet, so this has no effect until it does.
</li>
<li>
<code>gate.mergeReadiness</code> — composite merge-readiness gate. Default{" "}
<code>off</code>, no min score.
Expand Down
14 changes: 14 additions & 0 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ gate:
# the gate. Bool. Default: false.
aiAdvisory: false

# Copycat/plagiarism detection (#1969). CURRENTLY INERT — this config is parsed
# and threaded end-to-end, but the containment/similarity detection engine that
# would actually compute a copycat finding does not exist yet (tracked as
# separate, later PRs against #1969). Setting this today has no observable
# effect; it exists so an operator's config can already declare intent.
copycat:
# off | warn | label | block. Default: off. A dedicated 4-tier scale (not the
# shared off/advisory/block used elsewhere) — a further "strikes" escalation
# beyond block reuses the existing cross-repo banned-contributors ledger.
mode: off
# Containment/similarity threshold at/above which mode acts, once the
# detection engine exists. Number 0–100, or null for the engine default.
minScore: null

# Oversized-PR gate. A PR at/above EITHER the file-count or line-count threshold
# (engine defaults, not configurable here) gets a manual-review HOLD finding —
# never a hard blocker; mode only turns this hold signal on or off.
Expand Down
36 changes: 35 additions & 1 deletion packages/gittensory-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,25 @@ export type FocusManifestGateConfig = {
* hallucination can one-shot-close a structurally-clean PR) as an explicit, per-repo, documented
* trade-off — never the default. */
aiJudgmentBlockersMode: "gate" | "advisory" | null;
/** `gate.copycat.mode` (#1969): off|warn|label|block, off by default. Config-as-code only -- no DB column
* or dashboard toggle. Deliberately a DEDICATED 4-value enum, not the shared `GateRuleMode` tri-state: the
* issue's tiered response is warn -> label -> block -> strikes, where "strikes" is a separate escalation
* action (reusing the existing cross-repo banned-contributors ledger once wired) rather than a 5th mode
* value. THIS FIELD IS CURRENTLY INERT -- the similarity/containment detection engine that would actually
* compute a copycat finding does not exist yet (tracked as later, separate PRs against #1969); parsing and
* threading this config end-to-end first proves the plumbing and lets an operator's `.gittensory.yml`
* already declare intent without waiting on the detection engine. */
copycatMode: CopycatGateMode | null;
/** `gate.copycat.minScore` (#1969): containment/similarity score (0-100) at/above which `copycatMode` acts.
* null (unset) ⇒ the (also currently inert) engine's own default threshold once it exists. Same 0-100
* clamp-and-round normalization as `slopMinScore`/`readinessMinScore` above. */
copycatMinScore: number | null;
};

/** `gate.copycat.mode` (#1969) -- see {@link FocusManifestGateConfig.copycatMode}'s doc comment for why this
* is a dedicated enum rather than the shared `GateRuleMode`. */
export type CopycatGateMode = "off" | "warn" | "label" | "block";

// The converged per-PR review features a self-host operator toggles PER-REPO under `features:` in the private
// `.gittensory.yml`. Each feature ALSO has a GLOBAL env flag (GITTENSORY_REVIEW_*) that stays a master
// kill-switch (the feature never runs when its env flag is off, regardless of this block). See
Expand Down Expand Up @@ -857,6 +874,8 @@ const EMPTY_GATE_CONFIG: FocusManifestGateConfig = {
claCheckRunAppSlug: null,
expectedCiContexts: null,
aiJudgmentBlockersMode: null,
copycatMode: null,
copycatMinScore: null,
};

const EMPTY_FEATURES_CONFIG: FocusManifestFeaturesConfig = {
Expand Down Expand Up @@ -1142,6 +1161,11 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
if (slop !== undefined && slop !== null && slopRecord === undefined) {
warnings.push(`Manifest gate field "gate.slop" must be a mapping; ignoring it.`);
}
const copycat = record.copycat;
const copycatRecord = copycat !== null && typeof copycat === "object" && !Array.isArray(copycat) ? (copycat as Record<string, JsonValue>) : undefined;
if (copycat !== undefined && copycat !== null && copycatRecord === undefined) {
warnings.push(`Manifest gate field "gate.copycat" must be a mapping; ignoring it.`);
}
const size = record.size;
const sizeRecord = size !== null && typeof size === "object" && !Array.isArray(size) ? (size as Record<string, JsonValue>) : undefined;
if (size !== undefined && size !== null && sizeRecord === undefined) {
Expand Down Expand Up @@ -1189,6 +1213,8 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
claCheckRunAppSlug: parsePublicSafeText(claRecord?.checkRunAppSlug, "gate.cla.checkRunAppSlug", warnings),
expectedCiContexts: normalizeOptionalStringList(record.expectedCiContexts, "gate.expectedCiContexts", warnings),
aiJudgmentBlockersMode: normalizeOptionalEnum(record.aiJudgmentBlockers, "gate.aiJudgmentBlockers", ["gate", "advisory"] as const, warnings),
copycatMode: normalizeOptionalEnum(copycatRecord?.mode, "gate.copycat.mode", ["off", "warn", "label", "block"] as const, warnings),
copycatMinScore: normalizeOptionalScore(copycatRecord?.minScore, "gate.copycat.minScore", 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
Expand Down Expand Up @@ -1232,7 +1258,9 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
gate.claCheckRunName !== null ||
gate.claCheckRunAppSlug !== null ||
gate.expectedCiContexts !== null ||
gate.aiJudgmentBlockersMode !== null;
gate.aiJudgmentBlockersMode !== null ||
gate.copycatMode !== null ||
gate.copycatMinScore !== null;
return gate;
}

Expand Down Expand Up @@ -1308,6 +1336,12 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue {
}
if (gate.expectedCiContexts !== null) out.expectedCiContexts = gate.expectedCiContexts as JsonValue;
if (gate.aiJudgmentBlockersMode !== null) out.aiJudgmentBlockers = gate.aiJudgmentBlockersMode;
if (gate.copycatMode !== null || gate.copycatMinScore !== null) {
const copycat: Record<string, JsonValue> = {};
if (gate.copycatMode !== null) copycat.mode = gate.copycatMode;
if (gate.copycatMinScore !== null) copycat.minScore = gate.copycatMinScore;
out.copycat = copycat;
}
return out;
}

Expand Down
8 changes: 8 additions & 0 deletions packages/gittensory-engine/src/types/manifest-deps-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ export type RepositorySettings = {
/** `gate.cla.checkRunAppSlug`: the trusted GitHub App slug that must have produced `claCheckRunName`. Required
* for check-run detection so contributor-controlled same-name runs cannot satisfy a blocking CLA gate. */
claCheckRunAppSlug?: string | null | undefined;
/** Copycat/plagiarism detection (#1969). `off` (default/absent) = no check; `warn`/`label`/`block` are
* escalating tiers a future containment/similarity engine would act on. Config-as-code only — no DB column
* or dashboard toggle; set via `.gittensory.yml gate.copycat.mode`. CURRENTLY INERT: parsed and threaded
* end-to-end, but no detection engine reads it yet. */
copycatGateMode?: "off" | "warn" | "label" | "block" | undefined;
/** `gate.copycat.minScore`: containment/similarity score (0-100) at/above which `copycatGateMode` would act,
* once the detection engine exists. Config-as-code only, alongside {@link copycatGateMode}. */
copycatGateMinScore?: number | null | undefined;
/** `gate.expectedCiContexts` (#selfhost-ci-verification): maintainer-declared CI check/status context names to
* treat as required when GitHub branch protection returns no readable required-status-checks (unconfigured,
* or a 403 from a token lacking `administration:read` — common for GitHub App installations). Merged with any
Expand Down
1 change: 1 addition & 0 deletions scripts/check-docs-drift.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const GATE_MODE_MANIFEST = [
{ field: "duplicatePrGateMode", aliases: ["duplicatePrGateMode", "gate.duplicates"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx"] },
{ field: "qualityGateMode", aliases: ["qualityGateMode", "gate.readiness.mode"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx"] },
{ field: "slopGateMode", aliases: ["slopGateMode", "gate.slop.mode"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx"] },
{ field: "copycatGateMode", aliases: ["copycatGateMode", "gate.copycat.mode"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx"] },
{ field: "sizeGateMode", aliases: ["sizeGateMode", "gate.size"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx", "docs.github-app.tsx"] },
{ field: "lockfileIntegrityGateMode", aliases: ["lockfileIntegrityGateMode", "gate.lockfileIntegrity"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx", "docs.github-app.tsx"] },
{ field: "claGateMode", aliases: ["claGateMode", "gate.claMode"], pages: ["docs.how-reviews-work.tsx", "docs.tuning.tsx", "docs.github-app.tsx"] },
Expand Down
2 changes: 2 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,8 @@ export const RepositorySettingsSchema = z
claCheckRunName: z.string().nullable().optional(),
claCheckRunAppSlug: z.string().nullable().optional(),
expectedCiContexts: z.array(z.string()).optional(),
copycatGateMode: z.enum(["off", "warn", "label", "block"]).optional(),
copycatGateMinScore: z.number().nullable().optional(),
gateDryRun: z.boolean().optional(),
premergeContentRecheck: z.boolean().optional(),
requireFreshRebaseWindowMinutes: z.number().int().positive().nullable().optional(),
Expand Down
2 changes: 2 additions & 0 deletions src/signals/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,8 @@ function applyGateConfigOverrides(effective: RepositorySettings, gate: FocusMani
if (gate.claCheckRunName !== null) effective.claCheckRunName = gate.claCheckRunName;
if (gate.claCheckRunAppSlug !== null) effective.claCheckRunAppSlug = gate.claCheckRunAppSlug;
if (gate.expectedCiContexts !== null) effective.expectedCiContexts = gate.expectedCiContexts;
if (gate.copycatMode !== null) effective.copycatGateMode = gate.copycatMode;
if (gate.copycatMinScore !== null) effective.copycatGateMinScore = gate.copycatMinScore;
}

/**
Expand Down
17 changes: 17 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,12 @@ export type BountyRecord = {

export type GateRuleMode = "off" | "advisory" | "block";

/** `gate.copycat.mode` (#1969) -- a dedicated 4-value enum rather than the shared {@link GateRuleMode}
* tri-state, since the issue's tiered response is warn -> label -> block -> strikes (where "strikes" is a
* separate escalation action reusing the existing cross-repo banned-contributors ledger, not a 5th mode
* value). See {@link RepositorySettings.copycatGateMode}'s doc comment for the currently-inert status. */
export type CopycatGateMode = "off" | "warn" | "label" | "block";

/** Review-check publish surface (#2852). Controls ONLY whether/how the "Gittensory Orb Review Agent" check-run
* is created/updated -- never the underlying gate evaluation, disposition, comments, labels, audit, or
* autonomous merge/close, all of which run identically in every mode (the autonomous decision engine already
Expand Down Expand Up @@ -726,6 +732,17 @@ export type RepositorySettings = {
/** `gate.cla.checkRunAppSlug`: the trusted GitHub App slug that must have produced `claCheckRunName`. Required
* for check-run detection so contributor-controlled same-name runs cannot satisfy a blocking CLA gate. */
claCheckRunAppSlug?: string | null | undefined;
/** Copycat/plagiarism detection (#1969). `off` (default/absent) = no check; `warn`/`label`/`block` are
* escalating tiers a future containment/similarity engine would act on (`block` additionally hard-blocks;
* a further "strikes" escalation reuses the existing cross-repo banned-contributors ledger once wired).
* Config-as-code only — no DB column or dashboard toggle; set via `.gittensory.yml gate.copycat.mode`.
* CURRENTLY INERT: this field is parsed and threaded end-to-end, but no detection engine reads it yet —
* see {@link CopycatGateMode}'s doc comment in packages/gittensory-engine for the tracked follow-up plan. */
copycatGateMode?: CopycatGateMode | undefined;
/** `gate.copycat.minScore`: containment/similarity score (0-100) at/above which `copycatGateMode` would act,
* once the detection engine exists. `null`/absent ⇒ the engine's own default threshold. Config-as-code
* only, alongside {@link copycatGateMode}. */
copycatGateMinScore?: number | null | undefined;
/** `gate.expectedCiContexts` (#selfhost-ci-verification): maintainer-declared CI check/status context names to
* treat as required when GitHub branch protection returns no readable required-status-checks (unconfigured,
* or a 403 from a token lacking `administration:read` — common for GitHub App installations). Merged with any
Expand Down
4 changes: 2 additions & 2 deletions test/unit/check-docs-drift-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ describe("check-docs-drift script", () => {
const result = checkDocsDrift({ root: "/fake", readFile: makeReadFile(files) });

expect(result.failures).toEqual([]);
// gateModes bumped 11 -> 12 for linkedIssueSatisfactionGateMode (#1961/#3906).
expect(result.counts).toEqual({ flags: 10, commands: 19, gateModes: 12 });
// gateModes bumped 12 -> 13 for copycatGateMode (#1969, currently inert config scaffold).
expect(result.counts).toEqual({ flags: 10, commands: 19, gateModes: 13 });
});

it("catches an unmapped *GateMode field missing from GATE_MODE_MANIFEST", () => {
Expand Down
Loading
Loading