vaticinus's picture
Deploy reviewed keyless workbench; fit Space metadata limits
22a56cb verified
Raw History Blame Contribute Delete
2.62 kB
// A well-formed forecast is a candidate for manual resolution. Source prose is not a
// resolver, and no automatic resolver is registered for generic chat cards. Keep the
// candidate checks aligned with engine/experimental_p/ingest_cards.py.
export type NeedleCardLike = {
kind?: string;
resolution_date?: string | null;
dated_metric?: string | null;
threshold?: number | null;
threshold_dir?: string | null;
probability?: number | null;
};
export type NeedleVerdict = {
// Reserved for a server-verified outcome/resolver contract. Never inferred from model text.
scorable: false;
structured: boolean;
reason: string;
};
const VAGUE_METRIC = /^(n\/?a|tbd|to be determined|unknown|various|multiple( sources)?|the market|market data|news|reports?|analysts?|sources?|public data|official data)\.?$/i;
export function validResolutionDate(value: unknown): value is string {
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
const time = Date.parse(`${value}T00:00:00Z`);
return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === value;
}
/** Assess structure only. A future date, source URL, resolver name or claimed outcome supplied
* by the model cannot make a card automatically scorable. Manual review remains available. */
export function needleVerdict(card: NeedleCardLike, now: Date = new Date()): NeedleVerdict {
const no = (reason: string): NeedleVerdict => ({scorable: false, structured: false, reason});
if (!card || typeof card !== 'object') return no('no forecast');
const rd = typeof card.resolution_date === 'string' ? card.resolution_date.trim() : '';
if (!validResolutionDate(rd)) return no('no valid dated resolution');
if (rd <= now.toISOString().slice(0, 10)) return no('resolution date is not in the future');
const metric = typeof card.dated_metric === 'string' ? card.dated_metric.trim() : '';
if (metric.length < 8 || VAGUE_METRIC.test(metric)) return no('no proposed resolution source');
if (card.kind !== 'binary' && card.kind !== 'conditional' && card.kind !== 'bayes') {
if (typeof card.threshold !== 'number' || !Number.isFinite(card.threshold)) return no('no numeric threshold');
if (!['>=', '>', '<=', '<'].includes(card.threshold_dir ?? '')) return no('no threshold direction');
}
if (typeof card.probability !== 'number' || !Number.isFinite(card.probability) || card.probability < 0 || card.probability > 1) return no('probability must be between 0 and 1');
return {scorable: false, structured: true, reason: 'manual resolution required; source and outcome are unverified'};
}