import { parseFreeformFactV1 } from './truthtrance-freeform-facts'; import type { HandInventoryFact } from './truthtrance-hand-inventory'; import type { TruthFact } from './truthtrance'; type V1Fact = Extract; export type FreeformFactV2 = V1Fact | HandInventoryFact | { kind: 'and' | 'or'; terms: [V1Fact, V1Fact] }; /** * Version 2 preserves version 1 and recognizes a small additional set of * complete current facts. Do not change this grammar for saved v2 questions. * Callers must still restrict interpretation to fact-scoped questions. */ export function parseFreeformFactV2(text: string): FreeformFactV2 | null { if (typeof text !== 'string' || text.length > 500) return null; const input = text.trim(); // Check both whole-name and compound parses before deciding; a future // printed name might itself contain a conjunction between known names. const existing = parseFreeformFactV1(input); const count = /^do[ \t]+you[ \t]+have[ \t]+(at[ \t]+least|exactly|at[ \t]+most)[ \t]+([0-9]+)[ \t]+cards?[ \t]+in[ \t]+your[ \t]+hand[ \t]*\??$/i.exec(input); if (count) { const value = Number(count[2]); if (!Number.isSafeInteger(value) || value < 0) return null; const relation = count[1].toLowerCase().replace(/[ \t]+/g, ' '); return { kind: 'handInventory', category: 'all', compare: relation === 'at least' ? 'gte' : relation === 'at most' ? 'lte' : 'eq', value }; } const compound = /^do[ \t]+you[ \t]+have[ \t]+(.+?)[ \t]*\??$/i.exec(input); if (!compound) return existing; const body = compound[1]; const separator = /[ \t]+(and|or)[ \t]+/gi; const matches: Array> = []; for (const match of body.matchAll(separator)) { const index = match.index ?? 0; const left = body.slice(0, index); const right = body.slice(index + match[0].length); const first = parseFreeformFactV1(`Do you have ${left}`); const second = parseFreeformFactV1(/^do[ \t]+you[ \t]+have[ \t]+/i.test(right) ? right : `Do you have ${right}`); if (first && second) matches.push({ kind: match[1].toLowerCase() as 'and' | 'or', terms: [first, second] }); } // A whole printed name and a compound are also ambiguous if both fit. if (matches.length > 1 || existing && matches.length > 0) return null; return existing ?? matches[0] ?? null; }