Skip to content

Commit 1822fca

Browse files
authored
feat(review): fleet-wide gaming-pattern detector for the self-host orb fleet (#2350) (#5083)
Extends computeFleetAnalytics (src/orb/analytics.ts) with a more targeted detection signal than the existing single-metric outlier check: an instance whose decided-PR volume is unusually high, whose merge-precision is unusually high, AND whose reversal-rate is unusually low -- all three simultaneously -- fits the exact "mass-submitting only trivially-safe PRs to inflate merge-precision" signature the issue describes. A high-precision instance alone isn't suspicious (could be a genuinely careful team); combined with abnormal volume and suspiciously few reversals, it is. Detection only: gamingPatternFlags is a new read-only field surfaced on the existing operator dashboard tile set and the operator-only MCP gittensory_get_fleet_analytics tool summary -- nothing acts on it automatically, nothing here touches the live gate, and instanceId is the same opaque HMAC-derived handle already used throughout this pipeline (orb-collector.ts), never a login or anything more identifying. Scope note (see the module's own doc comment): this flags a self-hosted INSTANCE, never an individual miner -- the fleet pipeline carries no per-actor identity by deliberate, repeatedly-documented design (review_audit has no login column; predicted_gate_calibration_ledger is explicitly never-exported, citing this issue as the reason why). A genuine per-miner detector would require adding a new anonymized per-actor signal to the export pipeline, a separate privacy-sensitive design decision deserving its own focused issue. Also out of scope: "duplicate-claim-election win-rate skew" (isDuplicateClusterWinnerByClaim) is not implemented. Its outcome is never persisted anywhere in this pipeline -- only the losing side of a duplicate cluster produces a finding, with no cluster id and no actor linkage, so there is no winner data to measure a win-rate from. No proxy is implemented; a misleading one would be worse than none. Advances #2350 (does not close it -- the win-rate-skew and per-miner sub-deliverables remain genuinely unbuildable from data that exists today)
1 parent 74afe7d commit 1822fca

5 files changed

Lines changed: 254 additions & 2 deletions

File tree

src/mcp/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2859,7 +2859,7 @@ export class GittensoryMcp {
28592859
const report = await computeFleetAnalytics(this.env, input.windowDays !== undefined ? { windowDays: input.windowDays } : {});
28602860
const merge = report.fleet.mergePrecision !== null ? `${Math.round(report.fleet.mergePrecision * 100)}%` : "n/a";
28612861
return {
2862-
summary: `Fleet calibration over ${report.windowDays}d: ${report.instanceCount} instance(s), median merge precision ${merge}, ${report.outliers.length} outlier(s).`,
2862+
summary: `Fleet calibration over ${report.windowDays}d: ${report.instanceCount} instance(s), median merge precision ${merge}, ${report.outliers.length} outlier(s), ${report.gamingPatternFlags.length} gaming-pattern flag(s).`,
28632863
data: report as unknown as Record<string, unknown>,
28642864
};
28652865
}

src/orb/analytics.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
11
// Gittensory Orb (#1255) — fleet calibration ANALYTICS. Reads the anonymized orb_signals collected from
22
// self-hosted instances and derives gate-accuracy metrics across the fleet. Aggregation is median/percentile
33
// (never mean) so a single instance contributing fabricated data cannot move the fleet numbers.
4+
//
5+
// ANTI-FARMING DETECTION (#2350): gamingPatternFlags below extends the existing outlier check with a more targeted,
6+
// ONE-SIDED signal for the specific "gaming" pattern the issue describes -- an instance mass-submitting only
7+
// trivially-safe PRs to inflate its own merge-precision. mergePrecision alone can't distinguish "gamed" from
8+
// "genuinely excellent" (a careful team also has high precision); combining it with UNUSUALLY HIGH volume and
9+
// UNUSUALLY LOW reversal-rate, all three simultaneously, is the actual farming signature: lots of easy merges,
10+
// nothing risky enough to ever get reverted. Detection only — never an automatic action.
11+
//
12+
// SCOPE (explicit non-goals, read before extending): this flags a self-hosted INSTANCE, never an individual
13+
// miner. The fleet pipeline (orb_signals, review_audit's export) carries NO per-actor identity by deliberate,
14+
// repeatedly-stated design (review_audit has no login column; predicted_gate_calibration_ledger is explicitly
15+
// documented as never-exported, citing THIS issue as the reason) -- a genuine per-miner detector would require
16+
// adding a new anonymized per-actor signal to the export pipeline, which is a privacy-sensitive design
17+
// decision deserving its own focused issue/PR, not a rushed addition here. This module never deanonymizes,
18+
// never auto-bans, and never touches the live gate — instanceId here is the SAME opaque, HMAC-derived handle
19+
// already used everywhere else in this pipeline (see selfhost/orb-collector.ts), nothing more identifying.
20+
//
21+
// OUT OF SCOPE: "duplicate-claim-election win-rate skew" (isDuplicateClusterWinnerByClaim,
22+
// src/signals/duplicate-winner.ts) is NOT implemented here. Its outcome is never persisted anywhere in this
23+
// pipeline — only the LOSING side of a duplicate cluster produces a finding (duplicate_pr_risk), bucketed as
24+
// gate_reasoncode_bucket="duplicate_risk" on export with no cluster id and no actor linkage. There is no
25+
// winner marker to measure a win-rate FROM, and a per-instance duplicate_risk rate would measure something
26+
// different (how often THIS instance's own PRs lose a local collision) than "identities farming wins," so no
27+
// proxy for it is implemented — a misleading proxy would be worse than none.
428

529
const MIN_DECIDED = 5; // an instance needs at least this many decided PRs to count toward the fleet median
630
const OUTLIER_BAND = 0.25; // |instance precision − fleet median| beyond this flags the instance
31+
const GAMING_VOLUME_MULTIPLIER = 2; // an instance's decided count more than this many times the fleet median
32+
const GAMING_PRECISION_BAND = OUTLIER_BAND; // mergePrecision this far ABOVE the fleet median (one-sided)
33+
const GAMING_REVERSAL_RATIO = 0.5; // reversalRate below this fraction of the fleet median
734

835
/** Per-instance confusion-matrix cell as stored. */
936
interface Cell {
@@ -29,6 +56,20 @@ export interface InstanceMetrics {
2956
reversalRate: number; // share of decided PRs a human reversed
3057
}
3158

59+
/** #2350: one self-hosted instance whose combined volume/precision/reversal-rate pattern looks like it is
60+
* gaming the fleet-aggregate accuracy signal (see the module doc comment for the exact signature and its
61+
* scope). Detection only — a human reads this, nothing here takes any action automatically. `instanceId` is
62+
* the same opaque, HMAC-derived handle used throughout this pipeline; nothing more identifying is included. */
63+
export interface GamingPatternFlag {
64+
instanceId: string;
65+
decided: number;
66+
mergePrecision: number;
67+
reversalRate: number;
68+
fleetMedianDecided: number;
69+
fleetMergePrecision: number;
70+
fleetReversalRate: number;
71+
}
72+
3273
export interface FleetAnalytics {
3374
windowDays: number;
3475
instanceCount: number; // instances meeting MIN_DECIDED
@@ -42,6 +83,7 @@ export interface FleetAnalytics {
4283
};
4384
instances: InstanceMetrics[];
4485
outliers: Array<{ instanceId: string; metric: string; value: number; fleetMedian: number }>;
86+
gamingPatternFlags: GamingPatternFlag[];
4587
}
4688

4789
function median(xs: number[]): number | null {
@@ -127,7 +169,7 @@ export async function computeFleetAnalytics(env: Env, opts: { windowDays?: numbe
127169
const reg = await env.DB.prepare(`SELECT instance_id FROM orb_instances WHERE registered = 1`).all<{ instance_id: string }>();
128170
registered = new Set((reg.results ?? []).map((r) => r.instance_id));
129171
} catch {
130-
return { windowDays, instanceCount: 0, fleet: { mergePrecision: null, closePrecision: null, fpRate: null, reversalRate: null, cycleP50Ms: null, cycleP95Ms: null }, instances: [], outliers: [] };
172+
return { windowDays, instanceCount: 0, fleet: { mergePrecision: null, closePrecision: null, fpRate: null, reversalRate: null, cycleP50Ms: null, cycleP95Ms: null }, instances: [], outliers: [], gamingPatternFlags: [] };
131173
}
132174

133175
// Group cells by instance, fold each.
@@ -157,6 +199,31 @@ export async function computeFleetAnalytics(env: Env, opts: { windowDays?: numbe
157199
}
158200
}
159201

202+
// #2350: gamingPatternFlags. Gated on fleetMergeP !== null (at least one eligible instance made a comparable
203+
// merge verdict) — decided/reversalRate are never null per-instance, so once `eligible` is known non-empty
204+
// (implied by fleetMergeP being resolvable), both medians below are guaranteed non-null too.
205+
const gamingPatternFlags: FleetAnalytics["gamingPatternFlags"] = [];
206+
if (fleetMergeP !== null) {
207+
const fleetMedianDecided = median(eligible.map((i) => i.decided))!;
208+
const fleetReversalRate = median(eligible.map((i) => i.reversalRate))!;
209+
for (const i of eligible) {
210+
const highVolume = i.decided > fleetMedianDecided * GAMING_VOLUME_MULTIPLIER;
211+
const highPrecision = i.mergePrecision !== null && i.mergePrecision - fleetMergeP > GAMING_PRECISION_BAND;
212+
const lowReversal = i.reversalRate < fleetReversalRate * GAMING_REVERSAL_RATIO;
213+
if (highVolume && highPrecision && lowReversal) {
214+
gamingPatternFlags.push({
215+
instanceId: i.instanceId,
216+
decided: i.decided,
217+
mergePrecision: i.mergePrecision!,
218+
reversalRate: i.reversalRate,
219+
fleetMedianDecided,
220+
fleetMergePrecision: fleetMergeP,
221+
fleetReversalRate,
222+
});
223+
}
224+
}
225+
}
226+
160227
return {
161228
windowDays,
162229
instanceCount: eligible.length,
@@ -170,5 +237,6 @@ export async function computeFleetAnalytics(env: Env, opts: { windowDays?: numbe
170237
},
171238
instances,
172239
outliers,
240+
gamingPatternFlags,
173241
};
174242
}

src/services/operator-dashboard.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ export async function buildOperatorDashboardPayload(env: Env): Promise<OperatorD
181181
value: fleetMetrics.fleet.mergePrecision !== null ? `${Math.round(fleetMetrics.fleet.mergePrecision * 100)}%` : "—",
182182
delta: "median across the fleet",
183183
},
184+
{
185+
// #2350: human-facing detection signal only — no automatic action reads this value.
186+
label: "Fleet gaming-pattern flags",
187+
value: String(fleetMetrics.gamingPatternFlags.length),
188+
delta: fleetMetrics.gamingPatternFlags.length > 0 ? `${fleetMetrics.gamingPatternFlags.map((f) => f.instanceId).join(", ")}` : "no gaming pattern detected",
189+
},
184190
],
185191
noiseReduction: [
186192
{

test/unit/operator-dashboard.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,37 @@ describe("operator dashboard payload", () => {
127127
expect.arrayContaining([
128128
expect.objectContaining({ label: "Fleet instances", value: "3", delta: "1 outlier(s)" }),
129129
expect.objectContaining({ label: "Fleet merge precision", value: "100%" }),
130+
expect.objectContaining({ label: "Fleet gaming-pattern flags", value: "0", delta: "no gaming pattern detected" }),
130131
]),
131132
);
132133
});
133134

135+
it("surfaces a fleet farming flag (#2350) as a dedicated dashboard tile naming the flagged instance", async () => {
136+
const env = createTestEnv();
137+
let n = 0;
138+
const seed = async (instance: string, count: number, opts: { reversal?: string } = {}): Promise<void> => {
139+
for (let i = 0; i < count; i++) {
140+
await env.DB
141+
.prepare(`INSERT INTO orb_signals (instance_id, repo_hash, pr_hash, gate_verdict, outcome, reversal_flag) VALUES (?, ?, ?, 'merge', 'merged', ?)`)
142+
.bind(instance, `r${n}`, `p${n++}`, opts.reversal ?? "none")
143+
.run();
144+
}
145+
};
146+
// Two normal instances: decided 10, precision 0.7, reversalRate 0.3 (7 confirmed + 3 reverted).
147+
for (const id of ["normal1", "normal2"]) {
148+
await seed(id, 7);
149+
await seed(id, 3, { reversal: "reverted" });
150+
}
151+
// Farmer: decided 30 (> 2x the fleet median volume of 10), precision 1.0 (> 0.7 + 0.25), reversalRate 0.
152+
await seed("farmer", 30);
153+
for (const id of ["normal1", "normal2", "farmer"]) {
154+
await env.DB.prepare(`INSERT INTO orb_instances (instance_id, registered) VALUES (?, 1)`).bind(id).run();
155+
}
156+
const payload = await buildOperatorDashboardPayload(env);
157+
expect(payload.fleetMetrics.gamingPatternFlags.map((f) => f.instanceId)).toEqual(["farmer"]);
158+
expect(payload.metrics).toEqual(expect.arrayContaining([expect.objectContaining({ label: "Fleet gaming-pattern flags", value: "1", delta: "farmer" })]));
159+
});
160+
134161
it("picks the newest rollup day for adoption insights", () => {
135162
const rollups: ProductUsageDailyRollupRecord[] = [
136163
rollup("2026-05-28"),

test/unit/orb-analytics.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,154 @@ describe("computeFleetAnalytics()", () => {
159159
expect(a.fleet.cycleP95Ms).toBe(3000);
160160
});
161161
});
162+
163+
describe("gamingPatternFlags — anti-farming detection (#2350)", () => {
164+
/** 7 confirmed merges + 3 reverted merges: precision 0.7, reversalRate 0.3. */
165+
async function normalInstance(env: Env, id: string): Promise<void> {
166+
await signals(env, id, 7, { verdict: "merge", outcome: "merged", reversal: "none" });
167+
await signals(env, id, 3, { verdict: "merge", outcome: "merged", reversal: "reverted" });
168+
}
169+
170+
it("a normal-distribution fleet (similar volume/precision/reversal everywhere) produces no flag", async () => {
171+
const env = createTestEnv();
172+
await normalInstance(env, "a");
173+
await normalInstance(env, "b");
174+
await normalInstance(env, "c");
175+
await register(env, "a", "b", "c");
176+
const result = await computeFleetAnalytics(env);
177+
expect(result.instanceCount).toBe(3);
178+
expect(result.gamingPatternFlags).toEqual([]);
179+
});
180+
181+
it("an inflated-trivial-volume instance (high volume + high precision + low reversal, all three) flags", async () => {
182+
const env = createTestEnv();
183+
await normalInstance(env, "normal1"); // decided 10, precision 0.7, reversalRate 0.3
184+
await normalInstance(env, "normal2");
185+
await normalInstance(env, "normal3");
186+
// 30 decided (> 2x the fleet median of 10), precision 1.0 (> 0.7 + 0.25), reversalRate 0 (< 0.5 * 0.3).
187+
await signals(env, "farmer", 30, { verdict: "merge", outcome: "merged", reversal: "none" });
188+
await register(env, "normal1", "normal2", "normal3", "farmer");
189+
190+
const result = await computeFleetAnalytics(env);
191+
expect(result.gamingPatternFlags).toHaveLength(1);
192+
const flag = result.gamingPatternFlags[0]!;
193+
expect(flag.instanceId).toBe("farmer");
194+
expect(flag.decided).toBe(30);
195+
expect(flag.mergePrecision).toBe(1);
196+
expect(flag.reversalRate).toBe(0);
197+
expect(flag.fleetMedianDecided).toBe(10);
198+
expect(flag.fleetMergePrecision).toBeCloseTo(0.7);
199+
expect(flag.fleetReversalRate).toBeCloseTo(0.3);
200+
// No identity beyond the same opaque instance handle used everywhere else in the pipeline.
201+
expect(Object.keys(flag).sort()).toEqual(["decided", "fleetMedianDecided", "fleetMergePrecision", "fleetReversalRate", "instanceId", "mergePrecision", "reversalRate"]);
202+
});
203+
204+
it("high precision WITHOUT elevated volume does not flag (precision alone is not the signature)", async () => {
205+
const env = createTestEnv();
206+
await normalInstance(env, "normal1");
207+
await normalInstance(env, "normal2");
208+
// Same volume (10) as the normals, but perfect precision and zero reversals.
209+
await signals(env, "precise", 10, { verdict: "merge", outcome: "merged", reversal: "none" });
210+
await register(env, "normal1", "normal2", "precise");
211+
212+
const result = await computeFleetAnalytics(env);
213+
expect(result.gamingPatternFlags).toEqual([]);
214+
// It IS still caught by the existing, broader outlier check — this test isolates gamingPatternFlags specifically.
215+
expect(result.outliers.map((o) => o.instanceId)).toContain("precise");
216+
});
217+
218+
it("elevated volume WITHOUT elevated precision does not flag (volume alone is not the signature)", async () => {
219+
const env = createTestEnv();
220+
await normalInstance(env, "normal1");
221+
await normalInstance(env, "normal2");
222+
// 30 decided (high volume) but the SAME precision/reversal profile as everyone else.
223+
await signals(env, "busy", 21, { verdict: "merge", outcome: "merged", reversal: "none" });
224+
await signals(env, "busy", 9, { verdict: "merge", outcome: "merged", reversal: "reverted" });
225+
226+
await register(env, "normal1", "normal2", "busy");
227+
228+
const result = await computeFleetAnalytics(env);
229+
expect(result.gamingPatternFlags).toEqual([]);
230+
});
231+
232+
it("elevated volume + elevated precision but NOT a suspiciously low reversal rate does not flag", async () => {
233+
const env = createTestEnv();
234+
await normalInstance(env, "normal1"); // decided 10, precision 0.7, reversalRate 0.3
235+
await normalInstance(env, "normal2");
236+
// 30 decided (high volume). mergePrecision is 25/25 = 1.0 (elevated) -- ONLY the merge-verdict rows count
237+
// toward it. reversalRate is 5/30 ≈ 0.167, from separate close-verdict reopens -- above the 0.5x-fleet-
238+
// median floor (0.15), i.e. NOT suspiciously low, so this must not read as "farming".
239+
await signals(env, "risky", 25, { verdict: "merge", outcome: "merged", reversal: "none" });
240+
await signals(env, "risky", 5, { verdict: "close", outcome: "closed", reversal: "reopened" });
241+
await register(env, "normal1", "normal2", "risky");
242+
243+
const result = await computeFleetAnalytics(env);
244+
const risky = result.instances.find((i) => i.instanceId === "risky")!;
245+
expect(risky.mergePrecision).toBe(1);
246+
expect(risky.reversalRate).toBeCloseTo(5 / 30);
247+
expect(result.gamingPatternFlags).toEqual([]);
248+
});
249+
250+
it("an instance with no merge verdicts at all (null mergePrecision) never flags, even alongside a farmer", async () => {
251+
const env = createTestEnv();
252+
await normalInstance(env, "normal1"); // decided 10
253+
await normalInstance(env, "normal2"); // decided 10
254+
await signals(env, "farmer", 30, { verdict: "merge", outcome: "merged", reversal: "none" });
255+
// Every verdict is "close" — mergePrecision is null, so it cannot be flagged on precision regardless of
256+
// volume. Kept at MIN_DECIDED so it counts toward the fleet without shifting the volume median the farmer
257+
// is measured against.
258+
await signals(env, "close-only", 5, { verdict: "close", outcome: "closed", reversal: "none" });
259+
await register(env, "normal1", "normal2", "farmer", "close-only");
260+
261+
const result = await computeFleetAnalytics(env);
262+
expect(result.gamingPatternFlags.map((f) => f.instanceId)).toEqual(["farmer"]);
263+
});
264+
265+
it("no flags when no eligible instance has any merge verdict (fleetMergeP unresolvable)", async () => {
266+
const env = createTestEnv();
267+
await signals(env, "a", 10, { verdict: "close", outcome: "closed" });
268+
await signals(env, "b", 10, { verdict: "close", outcome: "closed" });
269+
await register(env, "a", "b");
270+
271+
const result = await computeFleetAnalytics(env);
272+
expect(result.fleet.mergePrecision).toBeNull();
273+
expect(result.gamingPatternFlags).toEqual([]);
274+
});
275+
276+
it("an unregistered instance never flags, even with an extreme farming-shaped pattern", async () => {
277+
const env = createTestEnv();
278+
await normalInstance(env, "normal1");
279+
await normalInstance(env, "normal2");
280+
await signals(env, "unregistered-farmer", 30, { verdict: "merge", outcome: "merged", reversal: "none" });
281+
await register(env, "normal1", "normal2"); // deliberately NOT registering the farmer
282+
283+
const result = await computeFleetAnalytics(env);
284+
expect(result.gamingPatternFlags).toEqual([]);
285+
// Still visible per-instance for the operator, same precedent as outliers.
286+
expect(result.instances.map((i) => i.instanceId)).toContain("unregistered-farmer");
287+
});
288+
289+
it("a below-MIN_DECIDED instance never flags, even if registered with an extreme farming-shaped pattern", async () => {
290+
const env = createTestEnv();
291+
await normalInstance(env, "normal1");
292+
await normalInstance(env, "normal2");
293+
// Only 3 decided (< MIN_DECIDED = 5) — excluded from `eligible` regardless of registration.
294+
await signals(env, "tiny-farmer", 3, { verdict: "merge", outcome: "merged", reversal: "none" });
295+
await register(env, "normal1", "normal2", "tiny-farmer");
296+
297+
const result = await computeFleetAnalytics(env);
298+
expect(result.gamingPatternFlags).toEqual([]);
299+
});
300+
301+
it("empty store -> empty gamingPatternFlags (not undefined)", async () => {
302+
const env = createTestEnv();
303+
const result = await computeFleetAnalytics(env);
304+
expect(result.gamingPatternFlags).toEqual([]);
305+
});
306+
307+
it("fail-safe on a DB error -> empty gamingPatternFlags", async () => {
308+
const broken = { DB: { prepare: () => ({ bind: () => ({ all: () => Promise.reject(new Error("boom")) }) }) } } as unknown as Env;
309+
const result = await computeFleetAnalytics(broken);
310+
expect(result.gamingPatternFlags).toEqual([]);
311+
});
312+
});

0 commit comments

Comments
 (0)