Skip to content

Commit e86bed2

Browse files
committed
feat(review): add gate.copycat.mode config scaffold for copycat detection
Parses and threads a new gate.copycat.mode (off/warn/label/block) and gate.copycat.minScore end-to-end through the manifest, effective-settings resolver, and OpenAPI schema -- config-as-code only, no DB column. The similarity/containment detection engine that would consume this config does not exist yet; this field is deliberately inert (parsed and threaded, never read by any decision path) so an operator's .gittensory.yml can already declare intent ahead of the detection engine landing in a later PR.
1 parent b8a1ba4 commit e86bed2

9 files changed

Lines changed: 161 additions & 3 deletions

File tree

.gittensory.yml.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,20 @@ gate:
179179
# the gate. Bool. Default: false.
180180
aiAdvisory: false
181181

182+
# Copycat/plagiarism detection (#1969). CURRENTLY INERT — this config is parsed
183+
# and threaded end-to-end, but the containment/similarity detection engine that
184+
# would actually compute a copycat finding does not exist yet (tracked as
185+
# separate, later PRs against #1969). Setting this today has no observable
186+
# effect; it exists so an operator's config can already declare intent.
187+
copycat:
188+
# off | warn | label | block. Default: off. A dedicated 4-tier scale (not the
189+
# shared off/advisory/block used elsewhere) — a further "strikes" escalation
190+
# beyond block reuses the existing cross-repo banned-contributors ledger.
191+
mode: off
192+
# Containment/similarity threshold at/above which mode acts, once the
193+
# detection engine exists. Number 0–100, or null for the engine default.
194+
minScore: null
195+
182196
# Oversized-PR gate. A PR at/above EITHER the file-count or line-count threshold
183197
# (engine defaults, not configurable here) gets a manual-review HOLD finding —
184198
# never a hard blocker; mode only turns this hold signal on or off.

apps/gittensory-ui/public/openapi.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9304,6 +9304,19 @@
93049304
"advisory",
93059305
"block"
93069306
]
9307+
},
9308+
"copycatGateMode": {
9309+
"type": "string",
9310+
"enum": [
9311+
"off",
9312+
"warn",
9313+
"label",
9314+
"block"
9315+
]
9316+
},
9317+
"copycatGateMinScore": {
9318+
"type": "number",
9319+
"nullable": true
93079320
}
93089321
},
93099322
"required": [

config/examples/gittensory.full.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,20 @@ gate:
192192
# the gate. Bool. Default: false.
193193
aiAdvisory: false
194194

195+
# Copycat/plagiarism detection (#1969). CURRENTLY INERT — this config is parsed
196+
# and threaded end-to-end, but the containment/similarity detection engine that
197+
# would actually compute a copycat finding does not exist yet (tracked as
198+
# separate, later PRs against #1969). Setting this today has no observable
199+
# effect; it exists so an operator's config can already declare intent.
200+
copycat:
201+
# off | warn | label | block. Default: off. A dedicated 4-tier scale (not the
202+
# shared off/advisory/block used elsewhere) — a further "strikes" escalation
203+
# beyond block reuses the existing cross-repo banned-contributors ledger.
204+
mode: off
205+
# Containment/similarity threshold at/above which mode acts, once the
206+
# detection engine exists. Number 0–100, or null for the engine default.
207+
minScore: null
208+
195209
# Oversized-PR gate. A PR at/above EITHER the file-count or line-count threshold
196210
# (engine defaults, not configurable here) gets a manual-review HOLD finding —
197211
# never a hard blocker; mode only turns this hold signal on or off.

packages/gittensory-engine/src/focus-manifest.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,25 @@ export type FocusManifestGateConfig = {
162162
* (unset) ⇒ no generic fallback configured — the live-CI aggregate keeps today's fold-all behavior
163163
* when branch protection is also unreadable. See {@link RepositorySettings.expectedCiContexts}. */
164164
expectedCiContexts: ReadonlyArray<string> | null;
165+
/** `gate.copycat.mode` (#1969): off|warn|label|block, off by default. Config-as-code only -- no DB column
166+
* or dashboard toggle. Deliberately a DEDICATED 4-value enum, not the shared `GateRuleMode` tri-state: the
167+
* issue's tiered response is warn -> label -> block -> strikes, where "strikes" is a separate escalation
168+
* action (reusing the existing cross-repo banned-contributors ledger once wired) rather than a 5th mode
169+
* value. THIS FIELD IS CURRENTLY INERT -- the similarity/containment detection engine that would actually
170+
* compute a copycat finding does not exist yet (tracked as later, separate PRs against #1969); parsing and
171+
* threading this config end-to-end first proves the plumbing and lets an operator's `.gittensory.yml`
172+
* already declare intent without waiting on the detection engine. */
173+
copycatMode: CopycatGateMode | null;
174+
/** `gate.copycat.minScore` (#1969): containment/similarity score (0-100) at/above which `copycatMode` acts.
175+
* null (unset) ⇒ the (also currently inert) engine's own default threshold once it exists. Same 0-100
176+
* clamp-and-round normalization as `slopMinScore`/`readinessMinScore` above. */
177+
copycatMinScore: number | null;
165178
};
166179

180+
/** `gate.copycat.mode` (#1969) -- see {@link FocusManifestGateConfig.copycatMode}'s doc comment for why this
181+
* is a dedicated enum rather than the shared `GateRuleMode`. */
182+
export type CopycatGateMode = "off" | "warn" | "label" | "block";
183+
167184
// The converged per-PR review features a self-host operator toggles PER-REPO under `features:` in the private
168185
// `.gittensory.yml`. Each feature ALSO has a GLOBAL env flag (GITTENSORY_REVIEW_*) that stays a master
169186
// kill-switch (the feature never runs when its env flag is off, regardless of this block). See
@@ -845,6 +862,8 @@ const EMPTY_GATE_CONFIG: FocusManifestGateConfig = {
845862
claCheckRunName: null,
846863
claCheckRunAppSlug: null,
847864
expectedCiContexts: null,
865+
copycatMode: null,
866+
copycatMinScore: null,
848867
};
849868

850869
const EMPTY_FEATURES_CONFIG: FocusManifestFeaturesConfig = {
@@ -1130,6 +1149,11 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
11301149
if (slop !== undefined && slop !== null && slopRecord === undefined) {
11311150
warnings.push(`Manifest gate field "gate.slop" must be a mapping; ignoring it.`);
11321151
}
1152+
const copycat = record.copycat;
1153+
const copycatRecord = copycat !== null && typeof copycat === "object" && !Array.isArray(copycat) ? (copycat as Record<string, JsonValue>) : undefined;
1154+
if (copycat !== undefined && copycat !== null && copycatRecord === undefined) {
1155+
warnings.push(`Manifest gate field "gate.copycat" must be a mapping; ignoring it.`);
1156+
}
11331157
const size = record.size;
11341158
const sizeRecord = size !== null && typeof size === "object" && !Array.isArray(size) ? (size as Record<string, JsonValue>) : undefined;
11351159
if (size !== undefined && size !== null && sizeRecord === undefined) {
@@ -1176,6 +1200,8 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
11761200
claCheckRunName: parsePublicSafeText(claRecord?.checkRunName, "gate.cla.checkRunName", warnings),
11771201
claCheckRunAppSlug: parsePublicSafeText(claRecord?.checkRunAppSlug, "gate.cla.checkRunAppSlug", warnings),
11781202
expectedCiContexts: normalizeOptionalStringList(record.expectedCiContexts, "gate.expectedCiContexts", warnings),
1203+
copycatMode: normalizeOptionalEnum(copycatRecord?.mode, "gate.copycat.mode", ["off", "warn", "label", "block"] as const, warnings),
1204+
copycatMinScore: normalizeOptionalScore(copycatRecord?.minScore, "gate.copycat.minScore", warnings),
11791205
};
11801206
// #2266: the flag is parsed, clamped, and threaded end-to-end, but the gate evaluator never reads it — a
11811207
// maintainer who sets it to true believing it softens a blocker for newcomers gets no such effect. Surface
@@ -1218,7 +1244,9 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
12181244
gate.claConsentPhrase !== null ||
12191245
gate.claCheckRunName !== null ||
12201246
gate.claCheckRunAppSlug !== null ||
1221-
gate.expectedCiContexts !== null;
1247+
gate.expectedCiContexts !== null ||
1248+
gate.copycatMode !== null ||
1249+
gate.copycatMinScore !== null;
12221250
return gate;
12231251
}
12241252

@@ -1293,6 +1321,12 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue {
12931321
out.cla = cla;
12941322
}
12951323
if (gate.expectedCiContexts !== null) out.expectedCiContexts = gate.expectedCiContexts as JsonValue;
1324+
if (gate.copycatMode !== null || gate.copycatMinScore !== null) {
1325+
const copycat: Record<string, JsonValue> = {};
1326+
if (gate.copycatMode !== null) copycat.mode = gate.copycatMode;
1327+
if (gate.copycatMinScore !== null) copycat.minScore = gate.copycatMinScore;
1328+
out.copycat = copycat;
1329+
}
12961330
return out;
12971331
}
12981332

packages/gittensory-engine/src/types/manifest-deps-types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ export type RepositorySettings = {
171171
/** `gate.cla.checkRunAppSlug`: the trusted GitHub App slug that must have produced `claCheckRunName`. Required
172172
* for check-run detection so contributor-controlled same-name runs cannot satisfy a blocking CLA gate. */
173173
claCheckRunAppSlug?: string | null | undefined;
174+
/** Copycat/plagiarism detection (#1969). `off` (default/absent) = no check; `warn`/`label`/`block` are
175+
* escalating tiers a future containment/similarity engine would act on. Config-as-code only — no DB column
176+
* or dashboard toggle; set via `.gittensory.yml gate.copycat.mode`. CURRENTLY INERT: parsed and threaded
177+
* end-to-end, but no detection engine reads it yet. */
178+
copycatGateMode?: "off" | "warn" | "label" | "block" | undefined;
179+
/** `gate.copycat.minScore`: containment/similarity score (0-100) at/above which `copycatGateMode` would act,
180+
* once the detection engine exists. Config-as-code only, alongside {@link copycatGateMode}. */
181+
copycatGateMinScore?: number | null | undefined;
174182
/** `gate.expectedCiContexts` (#selfhost-ci-verification): maintainer-declared CI check/status context names to
175183
* treat as required when GitHub branch protection returns no readable required-status-checks (unconfigured,
176184
* or a 403 from a token lacking `administration:read` — common for GitHub App installations). Merged with any

src/openapi/schemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,8 @@ export const RepositorySettingsSchema = z
651651
claCheckRunName: z.string().nullable().optional(),
652652
claCheckRunAppSlug: z.string().nullable().optional(),
653653
expectedCiContexts: z.array(z.string()).optional(),
654+
copycatGateMode: z.enum(["off", "warn", "label", "block"]).optional(),
655+
copycatGateMinScore: z.number().nullable().optional(),
654656
gateDryRun: z.boolean().optional(),
655657
premergeContentRecheck: z.boolean().optional(),
656658
requireFreshRebaseWindowMinutes: z.number().int().positive().nullable().optional(),

src/signals/focus-manifest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ function applyGateConfigOverrides(effective: RepositorySettings, gate: FocusMani
469469
if (gate.claCheckRunName !== null) effective.claCheckRunName = gate.claCheckRunName;
470470
if (gate.claCheckRunAppSlug !== null) effective.claCheckRunAppSlug = gate.claCheckRunAppSlug;
471471
if (gate.expectedCiContexts !== null) effective.expectedCiContexts = gate.expectedCiContexts;
472+
if (gate.copycatMode !== null) effective.copycatGateMode = gate.copycatMode;
473+
if (gate.copycatMinScore !== null) effective.copycatGateMinScore = gate.copycatMinScore;
472474
}
473475

474476
/**

src/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,12 @@ export type BountyRecord = {
586586

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

589+
/** `gate.copycat.mode` (#1969) -- a dedicated 4-value enum rather than the shared {@link GateRuleMode}
590+
* tri-state, since the issue's tiered response is warn -> label -> block -> strikes (where "strikes" is a
591+
* separate escalation action reusing the existing cross-repo banned-contributors ledger, not a 5th mode
592+
* value). See {@link RepositorySettings.copycatGateMode}'s doc comment for the currently-inert status. */
593+
export type CopycatGateMode = "off" | "warn" | "label" | "block";
594+
589595
/** Review-check publish surface (#2852). Controls ONLY whether/how the "Gittensory Orb Review Agent" check-run
590596
* is created/updated -- never the underlying gate evaluation, disposition, comments, labels, audit, or
591597
* autonomous merge/close, all of which run identically in every mode (the autonomous decision engine already
@@ -726,6 +732,17 @@ export type RepositorySettings = {
726732
/** `gate.cla.checkRunAppSlug`: the trusted GitHub App slug that must have produced `claCheckRunName`. Required
727733
* for check-run detection so contributor-controlled same-name runs cannot satisfy a blocking CLA gate. */
728734
claCheckRunAppSlug?: string | null | undefined;
735+
/** Copycat/plagiarism detection (#1969). `off` (default/absent) = no check; `warn`/`label`/`block` are
736+
* escalating tiers a future containment/similarity engine would act on (`block` additionally hard-blocks;
737+
* a further "strikes" escalation reuses the existing cross-repo banned-contributors ledger once wired).
738+
* Config-as-code only — no DB column or dashboard toggle; set via `.gittensory.yml gate.copycat.mode`.
739+
* CURRENTLY INERT: this field is parsed and threaded end-to-end, but no detection engine reads it yet —
740+
* see {@link CopycatGateMode}'s doc comment in packages/gittensory-engine for the tracked follow-up plan. */
741+
copycatGateMode?: CopycatGateMode | undefined;
742+
/** `gate.copycat.minScore`: containment/similarity score (0-100) at/above which `copycatGateMode` would act,
743+
* once the detection engine exists. `null`/absent ⇒ the engine's own default threshold. Config-as-code
744+
* only, alongside {@link copycatGateMode}. */
745+
copycatGateMinScore?: number | null | undefined;
729746
/** `gate.expectedCiContexts` (#selfhost-ci-verification): maintainer-declared CI check/status context names to
730747
* treat as required when GitHub branch protection returns no readable required-status-checks (unconfigured,
731748
* or a 403 from a token lacking `administration:read` — common for GitHub App installations). Merged with any

0 commit comments

Comments
 (0)