← back to Govarbitrage

src/agents/skeptic-rules.ts

70 lines

// Pure, testable rule logic for the skeptic agent. No DB, no I/O.

export interface ValuationTuple {
  listingId: string;
  title: string;
  /** Research.newRetail, USD */
  newRetail: number | null;
  /** CostBreakdown.expectedNetProfit, USD */
  expectedNetProfit: number | null;
  /** CostBreakdown.roi, ratio (0.42 = 42%) */
  roi: number | null;
}

export interface CloneGroup {
  /** Human-readable tuple key, e.g. "$900 retail / $189 net / 214% ROI" */
  key: string;
  members: ValuationTuple[];
}

/**
 * Detect the placeholder-valuation artifact: groups of >= minGroup listings
 * whose (newRetail, expectedNetProfit, roi) tuple is identical to the dollar
 * (ROI compared to 0.1%). Distinct items genuinely never share all three.
 */
export function findCloneGroups(items: ValuationTuple[], minGroup = 3): CloneGroup[] {
  const buckets = new Map<string, ValuationTuple[]>();
  for (const it of items) {
    if (it.newRetail == null || it.expectedNetProfit == null || it.roi == null) continue;
    const key = [
      Math.round(it.newRetail),
      Math.round(it.expectedNetProfit),
      Math.round(it.roi * 1000), // 0.1% resolution
    ].join("|");
    const arr = buckets.get(key);
    if (arr) arr.push(it);
    else buckets.set(key, [it]);
  }

  const groups: CloneGroup[] = [];
  for (const members of buckets.values()) {
    if (members.length < minGroup) continue;
    const m = members[0];
    groups.push({
      key: `$${Math.round(m.newRetail!)} retail / $${Math.round(m.expectedNetProfit!)} net / ${Math.round(m.roi! * 100)}% ROI`,
      members,
    });
  }
  // Biggest artifact first.
  groups.sort((a, b) => b.members.length - a.members.length);
  return groups;
}

/** Identification/valuation confidence below the floor (both scales accepted). */
export function isLowConfidence(confidence: number | null | undefined, floor = 0.4): boolean {
  if (confidence == null) return true; // no confidence recorded at all
  // Research.confidenceScore is 0..100; identificationConfidence is 0..1.
  const ratio = confidence > 1 ? confidence / 100 : confidence;
  return ratio < floor;
}

/** closesAt in the past but the listing still marked open/ACTIVE. */
export function isClosingStale(
  closingAt: Date | string | null | undefined,
  listingStatus: string,
  now: Date = new Date(),
): boolean {
  if (!closingAt) return false;
  return listingStatus === "ACTIVE" && new Date(closingAt).getTime() < now.getTime();
}