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