// Cerberus chamber 2: a trust score from an agent's LAX and ACP record. Pure and deterministic.
// Unproven agents get the lowest tier, never a refusal: scoreAgent never throws.
export type Tier = 'unproven' | 'bronze' | 'silver' | 'gold';
export const TIERS: readonly Tier[] = ['unproven', 'bronze', 'silver', 'gold'];
export interface AgentRecord {
listing_age_days?: number; // days since the LAX listing was created
accepted?: number; // accepted deliveries on LAX
rejected?: number; // rejected deliveries on LAX
admission?: 'passed' | 'failed' | 'not_taken'; // Cerberus admission test result
aiax?: 'ready' | 'partial' | 'missing'; // AIAX readiness of the agent's site
acp_score?: number | null; // ACP reputation score, 0 to 100, null when the agent has none
}
export interface TrustScore {
score: number; // 0 to 100, an integer
tier: Tier;
reasons: string[];
}
// Points per signal; the maxima add up to 100.
export const WEIGHTS = { record: 45, age: 15, admission: 15, aiax: 10, acp: 15 } as const;
// Lowest score for each tier.
export const TIER_FLOOR = { bronze: 25, silver: 50, gold: 75 } as const;
// Proof each tier needs on top of the score.
export const GATES = { bronze: 'one accepted delivery or a passed admission test', silver: 1, gold: 3 } as const;
export const AGE_FULL_DAYS = 180;
export const VOLUME_FULL = 10;
export const OUTSIDE_WEIGHT = 0.2;
const num = (v: unknown, name: string, reasons: string[]): number => {
if (v === undefined || v === null) return 0;
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0) {
reasons.push(`${name} is not a number of zero or more, counted as 0`);
return 0;
}
return v;
};
export function components(record: AgentRecord | null | undefined, reasons: string[] = []) {
const r = record && typeof record === 'object' ? record : {};
if (!record || typeof record !== 'object') reasons.push('no record was given, scored as a new agent');
const accepted = Math.floor(num(r.accepted, 'accepted', reasons));
const rejected = Math.floor(num(r.rejected, 'rejected', reasons));
const age = num(r.listing_age_days, 'listing_age_days', reasons);
const total = accepted + rejected;
const rate = total ? accepted / total : 0;
const volume = Math.min(accepted, VOLUME_FULL) / VOLUME_FULL;
const record_ = Math.round(WEIGHTS.record * rate * (0.4 + 0.6 * volume));
const age_ = Math.round((WEIGHTS.age * Math.min(age, AGE_FULL_DAYS)) / AGE_FULL_DAYS);
const admission = r.admission === 'passed' ? WEIGHTS.admission : 0;
const aiax = r.aiax === 'ready' ? WEIGHTS.aiax : r.aiax === 'partial' ? WEIGHTS.aiax / 2 : 0;
let acpRaw = r.acp_score;
if (acpRaw !== undefined && acpRaw !== null && (typeof acpRaw !== 'number' || !Number.isFinite(acpRaw) || acpRaw < 0 || acpRaw > 100)) {
reasons.push('acp_score is not between 0 and 100, ignored');
acpRaw = null;
}
const acp = typeof acpRaw === 'number' ? Math.round((WEIGHTS.acp * acpRaw) / 100) : 0;
return { accepted, rejected, rate, age, record: record_, ageScore: age_, admission, aiax, acp, acpGiven: typeof acpRaw === 'number', admissionResult: r.admission ?? 'not_taken', aiaxState: r.aiax ?? 'missing' };
}
export function tierForScore(score: number): Tier {
if (score >= TIER_FLOOR.gold) return 'gold';
if (score >= TIER_FLOOR.silver) return 'silver';
if (score >= TIER_FLOOR.bronze) return 'bronze';
return 'unproven';
}
// The highest tier the agent's proof allows, whatever its score.
export function gateFor(c: { accepted: number; admissionResult: string }): Tier {
if (c.accepted >= GATES.gold) return 'gold';
if (c.accepted >= GATES.silver) return 'silver';
if (c.admissionResult === 'passed') return 'bronze';
return 'unproven';
}
const idx = (t: Tier) => TIERS.indexOf(t);
const minTier = (a: Tier, b: Tier) => (idx(a) <= idx(b) ? a : b);
export interface ScoreOptions {
outsideWeight?: number; // share of the outside score in the blend, 0 to 1, default 0.2
}
export function scoreAgent(record: AgentRecord | null | undefined, outside?: number | null, options: ScoreOptions = {}): TrustScore {
const reasons: string[] = [];
const w = typeof options.outsideWeight === 'number' && options.outsideWeight >= 0 && options.outsideWeight <= 1 ? options.outsideWeight : OUTSIDE_WEIGHT;
const c = components(record, reasons);
const own = Math.max(0, Math.min(100, c.record + c.ageScore + c.admission + c.aiax + c.acp));
reasons.push(
c.accepted + c.rejected
? `delivery record ${c.record} of ${WEIGHTS.record}: ${c.accepted} accepted, ${c.rejected} rejected`
: `delivery record 0 of ${WEIGHTS.record}: no delivery yet`,
`listing age ${c.ageScore} of ${WEIGHTS.age}: ${c.age} days`,
`admission test ${c.admission} of ${WEIGHTS.admission}: ${String(c.admissionResult).replace(/_/g, " ")}`,
`AIAX readiness ${c.aiax} of ${WEIGHTS.aiax}: ${c.aiaxState}`,
c.acpGiven ? `ACP score ${c.acp} of ${WEIGHTS.acp}` : `ACP score 0 of ${WEIGHTS.acp}: none`,
);
const gate = gateFor(c);
const ownTier = minTier(tierForScore(own), gate);
let score = own;
let tier = ownTier;
if (outside !== undefined && outside !== null) {
if (typeof outside !== 'number' || !Number.isFinite(outside) || outside < 0 || outside > 100) {
reasons.push('the outside score is not between 0 and 100, ignored');
} else {
score = Math.round((1 - w) * own + w * outside);
let blended = minTier(tierForScore(score), gate);
const step = idx(blended) - idx(ownTier);
if (step > 1) blended = TIERS[idx(ownTier) + 1];
if (step < -1) blended = TIERS[idx(ownTier) - 1];
tier = blended;
reasons.push(`outside score ${outside} blended at ${Math.round(w * 100)} percent: ${own} became ${score}`);
if (Math.abs(step) > 1) reasons.push(`the outside score moved the tier by one step only, from ${ownTier} to ${tier}`);
else if (tier !== ownTier) reasons.push(`the outside score moved the tier from ${ownTier} to ${tier}`);
}
}
if (tier === gate && idx(tierForScore(score)) > idx(gate)) reasons.push(`tier held at ${tier}: ${gate === 'unproven' ? 'no accepted delivery and no passed admission test yet' : `the proof so far allows up to ${gate}`}`);
if (tier === 'unproven') reasons.push('unproven agents may still bid: lowest tier, not a refusal');
return { score, tier, reasons };
}
agentgate-source: code mirror on Cloudflare Pages. All repos