@@ -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
850869const 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
0 commit comments