← back to Govarbitrage
src/agents/skeptic.ts
94 lines
import { prisma } from "@/lib/db";
import { runAgent, type AgentRunResult } from "./framework";
import { topRankedOpportunities } from "@/lib/digest-top";
import { findCloneGroups, isClosingStale, isLowConfidence, type ValuationTuple } from "./skeptic-rules";
// The Skeptic is the adversarial digest gate. It loads the top-ranked
// opportunities exactly the way the digest does (shared helper in
// src/lib/digest-top.ts) and flags what a buyer should NOT trust:
// CLONE_VALUATION (CRITICAL) — >=3 listings with dollar-identical
// (newRetail, expectedNetProfit, roi): the placeholder-heuristic artifact.
// LOW_CONFIDENCE (WARN) — identification/valuation confidence < 0.4.
// CLOSING_DATA_STALE (WARN) — closingAt in the past but still marked open.
const num = (d: unknown): number | null => (d == null ? null : Number(d));
export async function runSkeptic(topN = 30): Promise<AgentRunResult> {
return runAgent("skeptic", async (ctx) => {
// openOnly:false so stale-but-still-ranked rows are audited too.
const rows = await topRankedOpportunities({ limit: topN, openOnly: false });
const ids = rows.map((r) => r.id);
const listings = await prisma.listing.findMany({
where: { id: { in: ids } },
include: { research: true, costBreakdown: true },
});
const byId = new Map(listings.map((l) => [l.id, l]));
const flagged = new Set<string>();
ctx.count(rows.length);
// ── CLONE_VALUATION ──
const tuples: ValuationTuple[] = [];
for (const r of rows) {
const l = byId.get(r.id);
if (!l) continue;
tuples.push({
listingId: l.id,
title: l.title,
newRetail: num(l.research?.newRetail),
expectedNetProfit: num(l.costBreakdown?.expectedNetProfit),
roi: num(l.costBreakdown?.roi),
});
}
for (const group of findCloneGroups(tuples)) {
for (const m of group.members) {
flagged.add(m.listingId);
await ctx.finding({
kind: "CLONE_VALUATION",
severity: "CRITICAL",
listingId: m.listingId,
listingTitle: m.title,
title: `Clone valuation: ${group.members.length} top listings share ${group.key}`,
detail:
`Identical-to-the-dollar valuation tuple across ${group.members.length} listings — ` +
`placeholder heuristic, not item-specific research. Members: ` +
group.members.map((x) => x.title.slice(0, 50)).join(" | "),
});
}
}
// ── LOW_CONFIDENCE + CLOSING_DATA_STALE ──
for (const r of rows) {
const l = byId.get(r.id);
if (!l) continue;
const confidence = num(l.research?.confidenceScore);
if (isLowConfidence(confidence)) {
flagged.add(l.id);
await ctx.finding({
kind: "LOW_CONFIDENCE",
severity: "WARN",
listingId: l.id,
listingTitle: l.title,
title: `Low confidence (${confidence == null ? "none" : Math.round(confidence) + "/100"}) on "${l.title.slice(0, 70)}"`,
detail: "Identification/valuation confidence below 0.4 — verify comps before trusting the numbers.",
});
}
if (isClosingStale(l.closingAt, l.listingStatus)) {
flagged.add(l.id);
await ctx.finding({
kind: "CLOSING_DATA_STALE",
severity: "WARN",
listingId: l.id,
listingTitle: l.title,
title: `Closing data stale: "${l.title.slice(0, 70)}" closed ${l.closingAt?.toISOString().slice(0, 10)} but is still ACTIVE`,
detail: "closingAt is in the past while listingStatus=ACTIVE — the liveness sweep missed it or the source changed.",
});
}
}
return `${flagged.size}/${rows.length} top opportunities flagged`;
});
}