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
19 changes: 10 additions & 9 deletions .loopover.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -187,18 +187,19 @@ 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/plagiarism detection (#1969): a deterministic code-containment check against prior art
# (earlier open or recently merged PRs on the same repo), direction resolved by submission
# timestamp so the earlier (original) author is never flagged.
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.
# shared off/advisory/block used elsewhere): warn surfaces an advisory finding
# only; label also applies a label; block also closes the PR and counts toward
# a further "strikes" escalation that reuses the existing moderation-rules
# violation ledger (escalating to the global contributor blacklist at the
# configured ban threshold).
mode: off
# Containment/similarity threshold at/above which mode acts, once the
# detection engine exists. Number 0–100, or null for the engine default.
# Containment/similarity threshold (0-100) at/above which mode acts.
# null uses the engine default (85).
minScore: null

# Oversized-PR gate. A PR at/above EITHER the file-count or line-count threshold
Expand Down
9 changes: 5 additions & 4 deletions apps/loopover-ui/src/routes/docs.how-reviews-work.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ function HowReviewsWork() {
</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.
<code>off</code>) — a code containment/similarity check against prior art (earlier open or
recently merged PRs on the same repo). Escalating tiers: <code>warn</code> surfaces an
advisory finding only; <code>label</code> also applies a label; <code>block</code> also
closes the PR and counts toward the repeat-offender strikes ledger. The direction is
always by submission timestamp, so the earlier (original) author is never flagged.
</li>
<li>
<strong>Merge-readiness gate</strong> (<code>mergeReadinessGateMode</code>, default{" "}
Expand Down
12 changes: 7 additions & 5 deletions apps/loopover-ui/src/routes/docs.tuning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,13 @@ function Tuning() {
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.
<code>gate.copycat.mode</code> — code containment/similarity gate against prior art
(earlier open or recently merged PRs on the same repo). Default <code>off</code>.
Escalating tiers: <code>warn</code> surfaces an advisory finding only; <code>label</code>{" "}
also applies a label; <code>block</code> also closes the PR and counts toward the
repeat-offender strikes ledger. Pair it with <code>gate.copycat.minScore</code> (0–100;{" "}
<code>null</code> uses the engine default, <code>85</code>). Direction is always by
submission timestamp, so the earlier (original) author is never flagged.
</li>
<li>
<code>gate.mergeReadiness</code> — composite merge-readiness gate. Default{" "}
Expand Down
19 changes: 10 additions & 9 deletions config/examples/loopover.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,18 +201,19 @@ 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/plagiarism detection (#1969): a deterministic code-containment check against prior art
# (earlier open or recently merged PRs on the same repo), direction resolved by submission
# timestamp so the earlier (original) author is never flagged.
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.
# shared off/advisory/block used elsewhere): warn surfaces an advisory finding
# only; label also applies a label; block also closes the PR and counts toward
# a further "strikes" escalation that reuses the existing moderation-rules
# violation ledger (escalating to the global contributor blacklist at the
# configured ban threshold).
mode: off
# Containment/similarity threshold at/above which mode acts, once the
# detection engine exists. Number 0–100, or null for the engine default.
# Containment/similarity threshold (0-100) at/above which mode acts.
# null uses the engine default (85).
minScore: null

# Oversized-PR gate. A PR at/above EITHER the file-count or line-count threshold
Expand Down
5 changes: 5 additions & 0 deletions migrations/0151_pull_request_copycat_assessment.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Latest deterministic copycat/plagiarism containment assessment (#1969), persisted separately from the
-- GitHub sync so a later maintenance pass (which re-reads the stored PR row rather than re-running the
-- containment engine) can act on it -- mirrors slop_risk/slop_band (see the pull_requests table's own comment).
ALTER TABLE pull_requests ADD COLUMN copycat_score INTEGER;
ALTER TABLE pull_requests ADD COLUMN copycat_matched_pull_number INTEGER;
13 changes: 6 additions & 7 deletions packages/loopover-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,14 @@ export type FocusManifestGateConfig = {
/** `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 `.loopover.yml`
* already declare intent without waiting on the detection engine. */
* action (reusing the existing moderation-rules violation ledger -- a `block`-tier close is tagged
* `closeKind: "copycat"`, counted the same way as blacklist/contributor_cap/review_nag) rather than a 5th
* mode value. Read by src/queue/copycat-detection.ts's deterministic containment engine, evaluated
* alongside slop in src/queue/processors.ts. */
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. */
* null (unset) ⇒ the engine's own default threshold (85). Same 0-100 clamp-and-round normalization as
* `slopMinScore`/`readinessMinScore` above. */
copycatMinScore: number | null;
};

Expand Down
10 changes: 7 additions & 3 deletions packages/loopover-engine/src/settings/moderation-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@

/** The anti-abuse mechanisms this engine can count violations from -- the three ORIGINAL mechanisms
* (contributor cap, blacklist, review-nag) plus review-evasion (#review-evasion-protection: a contributor
* closing/converting-to-draft their own PR to dodge an active review). Kept as a closed union (not an open
* closing/converting-to-draft their own PR to dodge an active review) and copycat/plagiarism containment
* (#1969: the "strikes" tier of the copycat gate's own warn -> label -> block -> strikes response reuses
* this SAME generic violation ledger rather than a bespoke counter -- a `block`-tier copycat close is tagged
* `closeKind: "copycat"`, counted exactly like any other rule here). Kept as a closed union (not an open
* string) so an unrecognized value is always a normalization error, never silently accepted. */
export type ModerationRuleType = "contributor_cap" | "blacklist" | "review_nag" | "review_evasion";
export type ModerationRuleType = "contributor_cap" | "blacklist" | "review_nag" | "review_evasion" | "copycat";

const ALL_MODERATION_RULE_TYPES: readonly ModerationRuleType[] = ["contributor_cap", "blacklist", "review_nag", "review_evasion"];
const ALL_MODERATION_RULE_TYPES: readonly ModerationRuleType[] = ["contributor_cap", "blacklist", "review_nag", "review_evasion", "copycat"];

/** The `audit_events.event_type` recorded for each rule's violation -- namespaced under `moderation.violation.*`
* so a cross-eventType, cross-repo count query (see `db/repositories.ts`) can scope to exactly this family. */
Expand All @@ -26,6 +29,7 @@ export const MODERATION_VIOLATION_EVENT_TYPE: Record<ModerationRuleType, string>
blacklist: "moderation.violation.blacklist",
review_nag: "moderation.violation.review_nag",
review_evasion: "moderation.violation.review_evasion",
copycat: "moderation.violation.copycat",
};

export const DEFAULT_MODERATION_WARNING_LABEL = "mod:warning";
Expand Down
Loading
Loading