@@ -173,8 +173,25 @@ export type FocusManifestGateConfig = {
173173 * hallucination can one-shot-close a structurally-clean PR) as an explicit, per-repo, documented
174174 * trade-off — never the default. */
175175 aiJudgmentBlockersMode : "gate" | "advisory" | null ;
176+ /** `gate.copycat.mode` (#1969): off|warn|label|block, off by default. Config-as-code only -- no DB column
177+ * or dashboard toggle. Deliberately a DEDICATED 4-value enum, not the shared `GateRuleMode` tri-state: the
178+ * issue's tiered response is warn -> label -> block -> strikes, where "strikes" is a separate escalation
179+ * action (reusing the existing cross-repo banned-contributors ledger once wired) rather than a 5th mode
180+ * value. THIS FIELD IS CURRENTLY INERT -- the similarity/containment detection engine that would actually
181+ * compute a copycat finding does not exist yet (tracked as later, separate PRs against #1969); parsing and
182+ * threading this config end-to-end first proves the plumbing and lets an operator's `.gittensory.yml`
183+ * already declare intent without waiting on the detection engine. */
184+ copycatMode : CopycatGateMode | null ;
185+ /** `gate.copycat.minScore` (#1969): containment/similarity score (0-100) at/above which `copycatMode` acts.
186+ * null (unset) ⇒ the (also currently inert) engine's own default threshold once it exists. Same 0-100
187+ * clamp-and-round normalization as `slopMinScore`/`readinessMinScore` above. */
188+ copycatMinScore : number | null ;
176189} ;
177190
191+ /** `gate.copycat.mode` (#1969) -- see {@link FocusManifestGateConfig.copycatMode}'s doc comment for why this
192+ * is a dedicated enum rather than the shared `GateRuleMode`. */
193+ export type CopycatGateMode = "off" | "warn" | "label" | "block" ;
194+
178195// The converged per-PR review features a self-host operator toggles PER-REPO under `features:` in the private
179196// `.gittensory.yml`. Each feature ALSO has a GLOBAL env flag (GITTENSORY_REVIEW_*) that stays a master
180197// kill-switch (the feature never runs when its env flag is off, regardless of this block). See
@@ -857,6 +874,8 @@ const EMPTY_GATE_CONFIG: FocusManifestGateConfig = {
857874 claCheckRunAppSlug : null ,
858875 expectedCiContexts : null ,
859876 aiJudgmentBlockersMode : null ,
877+ copycatMode : null ,
878+ copycatMinScore : null ,
860879} ;
861880
862881const EMPTY_FEATURES_CONFIG : FocusManifestFeaturesConfig = {
@@ -1142,6 +1161,11 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
11421161 if ( slop !== undefined && slop !== null && slopRecord === undefined ) {
11431162 warnings . push ( `Manifest gate field "gate.slop" must be a mapping; ignoring it.` ) ;
11441163 }
1164+ const copycat = record . copycat ;
1165+ const copycatRecord = copycat !== null && typeof copycat === "object" && ! Array . isArray ( copycat ) ? ( copycat as Record < string , JsonValue > ) : undefined ;
1166+ if ( copycat !== undefined && copycat !== null && copycatRecord === undefined ) {
1167+ warnings . push ( `Manifest gate field "gate.copycat" must be a mapping; ignoring it.` ) ;
1168+ }
11451169 const size = record . size ;
11461170 const sizeRecord = size !== null && typeof size === "object" && ! Array . isArray ( size ) ? ( size as Record < string , JsonValue > ) : undefined ;
11471171 if ( size !== undefined && size !== null && sizeRecord === undefined ) {
@@ -1189,6 +1213,8 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
11891213 claCheckRunAppSlug : parsePublicSafeText ( claRecord ?. checkRunAppSlug , "gate.cla.checkRunAppSlug" , warnings ) ,
11901214 expectedCiContexts : normalizeOptionalStringList ( record . expectedCiContexts , "gate.expectedCiContexts" , warnings ) ,
11911215 aiJudgmentBlockersMode : normalizeOptionalEnum ( record . aiJudgmentBlockers , "gate.aiJudgmentBlockers" , [ "gate" , "advisory" ] as const , warnings ) ,
1216+ copycatMode : normalizeOptionalEnum ( copycatRecord ?. mode , "gate.copycat.mode" , [ "off" , "warn" , "label" , "block" ] as const , warnings ) ,
1217+ copycatMinScore : normalizeOptionalScore ( copycatRecord ?. minScore , "gate.copycat.minScore" , warnings ) ,
11921218 } ;
11931219 // #2266: the flag is parsed, clamped, and threaded end-to-end, but the gate evaluator never reads it — a
11941220 // maintainer who sets it to true believing it softens a blocker for newcomers gets no such effect. Surface
@@ -1232,7 +1258,9 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
12321258 gate . claCheckRunName !== null ||
12331259 gate . claCheckRunAppSlug !== null ||
12341260 gate . expectedCiContexts !== null ||
1235- gate . aiJudgmentBlockersMode !== null ;
1261+ gate . aiJudgmentBlockersMode !== null ||
1262+ gate . copycatMode !== null ||
1263+ gate . copycatMinScore !== null ;
12361264 return gate ;
12371265}
12381266
@@ -1308,6 +1336,12 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue {
13081336 }
13091337 if ( gate . expectedCiContexts !== null ) out . expectedCiContexts = gate . expectedCiContexts as JsonValue ;
13101338 if ( gate . aiJudgmentBlockersMode !== null ) out . aiJudgmentBlockers = gate . aiJudgmentBlockersMode ;
1339+ if ( gate . copycatMode !== null || gate . copycatMinScore !== null ) {
1340+ const copycat : Record < string , JsonValue > = { } ;
1341+ if ( gate . copycatMode !== null ) copycat . mode = gate . copycatMode ;
1342+ if ( gate . copycatMinScore !== null ) copycat . minScore = gate . copycatMinScore ;
1343+ out . copycat = copycat ;
1344+ }
13111345 return out ;
13121346}
13131347
0 commit comments