← 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`;
  });
}