import { describe, it, expect } from 'vitest'; import { extractClaimedSpecifics, checkGrounding } from '../src/grounding-check.js'; // The real LOCKED-frame memory the live agent recalled (abridged). const MEMORY = `Marko Markovic is founder and CEO of Egzakta Group. Working on Waggle OS launch project. LOCKED DECISIONS: 1. Pricing: Pro at $19/month, Teams at $49/seat/month 2. Public launch gated on beating Mem0's LoCoMo memory benchmark TEAM: - Ivan: owns LM TEK and GPU hardware, default rack is eight H200s - Mihail: owns GAPA+BPMN prompt architecture`; describe('extractClaimedSpecifics', () => { it('extracts money, percent, duration, and stat-noun counts', () => { const s = extractClaimedSpecifics('We have $19/month pricing, a 73% gap, 4 months runway, and 227 entities.'); const kinds = s.map((x) => x.kind).sort(); expect(kinds).toContain('money'); expect(kinds).toContain('percent'); expect(kinds).toContain('duration'); expect(kinds).toContain('count'); }); it('does NOT flag benign advice quantities (non-stat nouns)', () => { const s = extractClaimedSpecifics('Ask Ivan 3 questions, pick 2 options, try 5 ways.'); // "questions"/"options"/"ways" are not stat nouns → no count specifics expect(s.filter((x) => x.kind === 'count')).toHaveLength(0); }); it('does not interpret an opening arithmetic parenthesis as negative money', () => { const money = extractClaimedSpecifics( 'A lower burn extends runway to 5 months ($40,000.00 ÷ $8,000.00).', ).filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.text)).toEqual(['$40,000.00', '$8,000.00']); expect(money.map(specific => specific.number)).toEqual(['40000', '8000']); }); it('preserves balanced accounting and explicit negative money', () => { const money = extractClaimedSpecifics( 'Accounting loss: ($40,000.00). Cash deficit: -$10,000.00.', ).filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.text)).toEqual(['($40,000.00)', '-$10,000.00']); expect(money.map(specific => specific.number)).toEqual(['-40000', '-10000']); }); it('treats a parenthesized divisor as arithmetic grouping, not accounting-negative money', () => { const result = checkGrounding( 'Runway = $40,000 / ($10,000).', 'Cash is $40,000. Monthly burn is $10,000.', ); expect(result.specifics.map(specific => specific.number)).toEqual(['40000', '10000']); expect(result.ungrounded).toEqual([]); expect(result.score).toBe(1); }); it('preserves accounting negativity when a currency label precedes the closing parenthesis', () => { const result = checkGrounding( 'Revenue was ($10,000 USD).', 'Revenue was $10,000.', ); expect(result.specifics.map(specific => specific.number)).toEqual(['-10000']); expect(result.ungrounded.map(specific => specific.number)).toEqual(['-10000']); }); it('treats a parenthesized money apposition as positive', () => { const result = checkGrounding( 'Cash on hand ($40,000) is available.', 'Cash is $40,000.', ); expect(result.specifics.map(specific => specific.number)).toEqual(['40000']); expect(result.ungrounded).toEqual([]); }); it.each([ ['subtraction', 'Remaining = $40,000 - ($10,000).', ['40000', '10000']], ['multiplication', 'Twelve months is 12 × ($10,000).', ['10000']], ['burn apposition', 'Monthly burn ($10,000).', ['10000']], ['cost apposition', 'Cost ($5,000).', ['5000']], ])('treats %s grouping or apposition as positive', (_label, text, expected) => { const money = extractClaimedSpecifics(text).filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.number)).toEqual(expected); }); it('extracts currency-prefixed and Unicode negative signs', () => { const money = extractClaimedSpecifics( 'First loss was $-10,000; second loss was −$20,000.', ).filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.text)).toEqual(['$-10,000', '−$20,000']); expect(money.map(specific => specific.number)).toEqual(['-10000', '-20000']); }); it('preserves explicit negative signs inside arithmetic groups and appositions', () => { const money = extractClaimedSpecifics( 'Total = $40,000 + (-$10,000). Monthly burn (-$10,000).', ).filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.number)).toEqual(['40000', '-10000', '-10000']); }); it('recognizes an ASCII negative sign separated from the currency symbol', () => { const money = extractClaimedSpecifics('Revenue is - $10,000. Pricing: Pro - $19/month.') .filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.number)).toEqual(['-10000', '19']); }); it('does not backtrack to a partial amount before an unsupported cadence', () => { const money = extractClaimedSpecifics('Teams is $49/seat/weekly.') .filter(specific => specific.kind === 'money'); expect(money).toEqual([]); }); it('does not partially parse magnitude suffixes', () => { const supported = extractClaimedSpecifics('Revenue is $19B.') .filter(specific => specific.kind === 'money'); const unsupported = extractClaimedSpecifics('Revenue is $19Q.') .filter(specific => specific.kind === 'money'); expect(supported.map(specific => specific.number)).toEqual(['19000000000']); expect(unsupported).toEqual([]); }); it('normalizes non-breaking spaces inside money', () => { for (const text of [ 'Revenue is $\u00a010,000.', 'Revenue is $\u200710,000.', 'Revenue is $\u200910,000.', 'Revenue is $\u200a10,000.', 'Revenue is $\u202f10,000.', 'Revenue is $\u205f10,000.', ]) { expect(extractClaimedSpecifics(text).filter(specific => specific.kind === 'money')) .toMatchObject([{ number: '10000', subject: 'revenue' }]); } }); it('extracts currency labels before cadence without dropping the cadence', () => { const money = extractClaimedSpecifics( 'Pricing is $19 USD per month. Price is $19 dollars per year. Pricing is $49 USD per seat per month. Price is $29 monthly. Pricing is $39 annually.', ).filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.text)).toEqual([ '$19 USD per month', '$19 dollars per year', '$49 USD per seat per month', '$29 monthly', '$39 annually', ]); expect(money.map(specific => specific.cadence)).toEqual([ 'month', 'year', 'seat/month', 'month', 'year', ]); }); it('preserves source order across mixed-currency claims', () => { const money = extractClaimedSpecifics('Pricing is €19 and support costs $20.') .filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.number)).toEqual(['19', '20']); expect(money.map(specific => specific.currency)).toEqual(['eur', 'usd']); }); it('coalesces overlapping foreign-currency claims and preserves signs', () => { const cad = extractClaimedSpecifics('Pricing is $19 CAD per month.') .filter(specific => specific.kind === 'money'); const negatives = ['-€10,000', '€-10,000', 'EUR -10,000', '(€10,000)'] .flatMap(text => extractClaimedSpecifics(`Revenue is ${text}.`)) .filter(specific => specific.kind === 'money'); expect(cad).toMatchObject([{ number: '19', currency: 'cad', cadence: 'month' }]); expect(negatives.map(specific => specific.number)).toEqual([ '-10000', '-10000', '-10000', '-10000', ]); }); it('preserves signs and currencies on regional-dollar prefixes', () => { const money = extractClaimedSpecifics( 'Canadian losses were -CA$19 and (CA$20); Australian loss was AU$-21.', ).filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.number)).toEqual(['-19', '-20', '-21']); expect(money.map(specific => specific.currency)).toEqual(['cad', 'cad', 'aud']); }); it('extracts explicit currency syntax without a dollar sign and ignores ambiguous prose', () => { for (const reply of [ 'Revenue is 10000 dollars.', 'Revenue is USD 10000.', 'Revenue is 10000€.', ]) { const result = checkGrounding(reply, 'Revenue is unavailable.'); expect(result.specifics.filter(specific => specific.kind === 'money'), reply).toHaveLength(1); expect(result.ungrounded.filter(specific => specific.kind === 'money'), reply).toHaveLength(1); } expect(extractClaimedSpecifics('We won 10 customers.').filter(specific => specific.kind === 'money')) .toEqual([]); expect(extractClaimedSpecifics('The package weighs 10 pounds.').filter(specific => specific.kind === 'money')) .toEqual([]); for (const prose of [ 'Try 3 options.', 'Use PHP 8.3 for this project.', 'Rub 10 records before continuing.', 'A sterling 10 performance score.', ]) { expect(extractClaimedSpecifics(prose).filter(specific => specific.kind === 'money'), prose) .toEqual([]); } expect(extractClaimedSpecifics('Pricing is TRY 3.').filter(specific => specific.kind === 'money')) .toMatchObject([{ number: '3', currency: 'try', subject: 'price' }]); for (const prose of [ 'Revenue dashboard uses PHP 8.3.', 'The pricing parser should try 3 alternatives.', 'CAD 2024 pricing software is installed.', 'Rub 10 revenue records before continuing.', ]) { expect(extractClaimedSpecifics(prose).filter(specific => specific.kind === 'money'), prose) .toEqual([]); } }); it('extracts strongly-bound compact and magnitude finance values', () => { for (const [text, expected] of [ ['Revenue is USD19.', { number: '19', currency: 'usd', subject: 'revenue' }], ['Revenue is 19EUR.', { number: '19', currency: 'eur', subject: 'revenue' }], ['ARR is 5M.', { number: '5000000', subject: 'annual recurring revenue' }], ['Valuation is 2 billion.', { number: '2000000000', subject: 'valuation' }], ] as const) { expect(extractClaimedSpecifics(text).filter(specific => specific.kind === 'money'), text) .toMatchObject([expected]); } expect(extractClaimedSpecifics('Margin is 5 per cent; uplift is 7 pct.') .filter(specific => specific.kind === 'percent')) .toMatchObject([{ number: '5', unit: 'percent' }, { number: '7', unit: 'percent' }]); }); it('extracts explicit suffix currencies instead of passing them through unchecked', () => { for (const [text, expected] of [ ['Revenue is 10000 EUR.', { number: '10000', currency: 'eur', subject: 'revenue' }], ['Revenue is 10,000 EUR.', { number: '10000', currency: 'eur', subject: 'revenue' }], ['Revenue is 10,000€.', { number: '10000', currency: 'eur', subject: 'revenue' }], ['Pricing is 19 AUD/month.', { number: '19', currency: 'aud', subject: 'price', cadence: 'month' }], ] as const) { const money = extractClaimedSpecifics(text).filter(specific => specific.kind === 'money'); expect(money, text).toMatchObject([expected]); expect(checkGrounding(text, 'No matching finance fact was supplied.').ungrounded, text) .toHaveLength(1); } }); it('does not interpret alphabetic currency codes in code declarations as money', () => { for (const code of [ 'const revenue = PHP 8.3;', 'const revenue = CAD 2024;', 'let pricing = TRY 3;', 'var revenue = RUB 10;', ]) { expect(extractClaimedSpecifics(code).filter(specific => specific.kind === 'money'), code) .toEqual([]); } }); it('extracts digit-bearing stat nouns and magnitude counts as counts', () => { expect(extractClaimedSpecifics('We operate 8 H200s.')) .toMatchObject([{ kind: 'count', number: '8', unit: 'h200' }]); for (const text of ['We have 5 million users.', 'We have 5M users.']) { const specifics = extractClaimedSpecifics(text); expect(specifics.filter(specific => specific.kind === 'count'), text) .toMatchObject([{ number: '5000000', unit: 'user' }]); expect(specifics.filter(specific => specific.kind === 'money'), text).toEqual([]); expect(checkGrounding(text, text).ungrounded, text).toEqual([]); } }); it('normalizes full-width decimal characters without bypassing extraction', () => { const claim = 'Revenue is $10,000.'; expect(extractClaimedSpecifics(claim).filter(specific => specific.kind === 'money')) .toMatchObject([{ number: '10000', subject: 'revenue', currency: 'usd' }]); expect(checkGrounding(claim, 'Revenue is $10,000.').ungrounded).toEqual([]); expect(checkGrounding(claim, 'Revenue was not supplied.').ungrounded).toHaveLength(1); }); it('normalizes Unicode thousands separators inside money values', () => { const reply = 'Revenue is $10\u202f000.'; expect(extractClaimedSpecifics(reply).filter(specific => specific.kind === 'money')) .toMatchObject([{ number: '10000', subject: 'revenue' }]); expect(checkGrounding(reply, 'Revenue is $10,000.').ungrounded).toEqual([]); expect(checkGrounding('Revenue is $10,000.', reply).ungrounded).toEqual([]); }); it('normalizes explicit European decimal money', () => { const claim = extractClaimedSpecifics('Pricing is €19,99.') .find(specific => specific.kind === 'money'); expect(claim).toMatchObject({ number: '19.99', currency: 'eur', subject: 'price' }); expect(checkGrounding('Pricing is €19,99.', 'Pricing is EUR 19.99.').ungrounded) .toEqual([]); const grouped = extractClaimedSpecifics('Pricing is €1.234,56.') .filter(specific => specific.kind === 'money'); expect(grouped).toMatchObject([{ number: '1234.56', currency: 'eur', subject: 'price' }]); expect(checkGrounding('Pricing is €1.234,56.', 'Pricing is EUR 1234.56.').ungrounded).toEqual([]); expect(checkGrounding('Pricing is EUR 1234.56.', 'Pricing is €1.234,56.').ungrounded).toEqual([]); }); it('normalizes apostrophe grouping and regional currency symbols', () => { expect(checkGrounding('Revenue is CHF 1\'234.56.', 'Revenue is CHF 1234.56.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is C$1,000.', 'Revenue is CAD 1000.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is R$1,000.', 'Revenue is BRL 1000.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is S$1,000.', 'Revenue is SGD 1000.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is NT$1,000.', 'Revenue is TWD 1000.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is CN¥19.', 'Revenue is CNY 19.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is RMB ¥19.', 'Revenue is CNY 19.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is CN¥19.', 'Revenue is JPY 19.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is INR 1,23,456.78.', 'Revenue is INR 123456.78.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is C$1,000.', 'Revenue is USD 1000.').ungrounded) .toHaveLength(1); }); it('preserves sign, grouping, and unit semantics across other quantitative kinds', () => { const specifics = extractClaimedSpecifics( 'Net margin is -5%; uplift is 5 pp; history spans 1,000 months; membership is -5 users.', ); expect(specifics).toEqual(expect.arrayContaining([ expect.objectContaining({ kind: 'percent', number: '-5', unit: 'percent' }), expect.objectContaining({ kind: 'percent', number: '5', unit: 'percentage-point' }), expect.objectContaining({ kind: 'duration', number: '1000', unit: 'month' }), expect.objectContaining({ kind: 'count', number: '-5', unit: 'user' }), ])); }); it('preserves plus and accounting signs without treating Markdown bullets as negatives', () => { expect(extractClaimedSpecifics('Revenue is +$10,000.').filter(specific => specific.kind === 'money')) .toMatchObject([{ number: '10000', subject: 'revenue' }]); expect(extractClaimedSpecifics('Revenue is +USD 10,000.').filter(specific => specific.kind === 'money')) .toMatchObject([{ number: '10000', subject: 'revenue', currency: 'usd' }]); expect(extractClaimedSpecifics('Revenue is USD -$19.').filter(specific => specific.kind === 'money')) .toMatchObject([{ number: '-19', subject: 'revenue', currency: 'usd' }]); expect(extractClaimedSpecifics('Revenue is CHF (1,234.56).').filter(specific => specific.kind === 'money')) .toMatchObject([{ number: '-1234.56', subject: 'revenue', currency: 'chf' }]); expect(extractClaimedSpecifics('Net margin is (5%).').filter(specific => specific.kind === 'percent')) .toMatchObject([{ number: '-5', subject: 'net margin' }]); for (const text of ['- 5 users are active.', '- 4 months runway.', '- 73% gross margin.', '- 5 pp uplift.']) { expect(checkGrounding(text, text).ungrounded, text).toEqual([]); } }); it('keeps sentence commas outside money values and finance-subject binding', () => { const money = extractClaimedSpecifics( 'Revenue is $49, burn is unknown. Revenue later reached $49,000, burn unchanged.', ).filter(specific => specific.kind === 'money'); expect(money.map(specific => specific.text)).toEqual(['$49', '$49,000']); expect(money.map(specific => specific.subject)).toEqual(['revenue', 'revenue']); expect(checkGrounding('Revenue is $49.', 'Revenue is $49, burn is unknown.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $49, burn is unknown.', 'Revenue is unknown; burn is $49.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $49,000.', 'Revenue is $49,000, burn is unknown.').ungrounded) .toEqual([]); }); }); describe('checkGrounding — the live confabulation cases', () => { it('flags "4 months runway" as ungrounded (not in memory)', () => { const r = checkGrounding('You are pre-revenue with 4 months runway.', MEMORY); expect(r.ungrounded.some((s) => s.text.includes('4 month'))).toBe(true); }); it('flags "227 entities" as ungrounded (real count is not in memory)', () => { const r = checkGrounding('I have 227 entities tracked.', MEMORY); expect(r.ungrounded.some((s) => s.number === '227')).toBe(true); }); it('grounds "$19/month" (it IS in the LOCKED decisions)', () => { const r = checkGrounding('Your pricing is Pro at $19/month.', MEMORY); expect(r.grounded.some((s) => s.number === '19')).toBe(true); expect(r.ungrounded.some((s) => s.number === '19')).toBe(false); expect(checkGrounding('Your pricing is Pro at $19/month.', 'Pricing: Pro — $19/month.').ungrounded) .toEqual([]); for (const source of [ 'Pricing — Pro at $19/month.', 'Pricing - Pro at $19/month.', 'Pricing: Pro - $19/month.', 'Pricing: Pro -$19/month.', 'Pricing: Pro-$19/month.', 'Pricing: Pro—$19/month.', 'Pricing: Pro–$19/month.', ]) { expect(checkGrounding('Your pricing is Pro at $19/month.', source).ungrounded, source).toEqual([]); } expect(checkGrounding('Revenue is $10,000.', 'Revenue came in at $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $10,000.', 'Revenue reached $10,000.').ungrounded) .toEqual([]); }); it('grounds a percentage that appears in sources, flags one that does not', () => { expect(checkGrounding('a 73% lift', 'we measured a 73% lift').ungrounded).toHaveLength(0); expect(checkGrounding('a 99% lift', MEMORY).ungrounded.some((s) => s.number === '99')).toBe(true); }); it('grounds formatted money from equivalent user-supplied numbers', () => { const sources = 'Cash is 40000 dollars, monthly burn is 10000 dollars, and revenue is zero.'; const reply = 'Cash is $40,000.00, monthly burn is $10,000.00, and revenue is $0.00.'; const result = checkGrounding(reply, sources); expect(result.ungrounded).toEqual([]); expect(result.grounded.map(specific => specific.number)).toEqual(['40000', '10000', '0']); expect(checkGrounding('Revenue is $0.00.', 'No revenue figure was supplied.').ungrounded) .toHaveLength(1); const unrelatedZero = 'There were zero failed tests; no revenue figure was supplied.'; expect(checkGrounding('Revenue is $0.00.', unrelatedZero).ungrounded).toHaveLength(1); expect(checkGrounding('Conversion was 0%.', unrelatedZero).ungrounded).toHaveLength(1); expect(checkGrounding('Revenue is $0.00.', 'Revenue is not zero.').ungrounded).toHaveLength(1); expect(checkGrounding('Revenue is $0.00.', 'Without zero revenue, the model cannot proceed.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $0.00.', 'Cash is zero; no revenue figure was supplied.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $0.00.', 'Cash is zero and revenue was not supplied.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'Burn is 10000 dollars, and revenue was not supplied.').ungrounded) .toHaveLength(1); expect(checkGrounding('Cash is $0.00.', 'Cash is zero.').ungrounded).toEqual([]); expect(checkGrounding('$19 pricing.', 'Pricing is 19.').ungrounded).toEqual([]); const postfixedLabels = checkGrounding( 'Cash is $40,000; $10,000 is monthly burn; revenue is $0.', 'Cash is 40000 dollars, monthly burn is 10000 dollars, and revenue is zero.', ); expect(postfixedLabels.ungrounded).toEqual([]); const repeatedFormula = checkGrounding( 'Cash: $40,000.00\nMonthly burn: $10,000.00\nRevenue: $0.00\n\nRunway = $40,000.00 ÷ $10,000.00 = 4 months.', sources, ); expect(repeatedFormula.ungrounded.filter(specific => specific.kind === 'money')).toEqual([]); expect(checkGrounding('Monthly burn is $10,000.', '10000 dollars in monthly burn.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $10,000.', 'Revenue is pending; 10000 dollars in monthly burn.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'Revenue TBD. 10000 dollars in monthly burn.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is -$10,000.', 'Revenue is $10,000.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is −$10,000.', 'Revenue is −10000 dollars.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is −$10,000.', 'Revenue is −10k.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $10,000.', 'Not 10000 dollars in revenue.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'There was no revenue of $10,000.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'No revenue reached $10,000.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'This is not an estimate: revenue is $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $10,000.', 'Do not omit this — revenue is $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $10,000.', 'No revenue: $10,000.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10.', 'Revenue is 10%.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'Revenue is 10000 customers.').ungrounded) .toHaveLength(1); expect(checkGrounding('Sales are $10,000.', 'Sales are 10,000 units.').ungrounded) .toHaveLength(1); expect(checkGrounding('Sales are $10,000.', 'Sales are 10,000 orders.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'Revenue is 10000 euros.').ungrounded) .toHaveLength(1); for (const source of [ 'Revenue is 10000 shares.', 'Revenue is 10000 CAD.', 'Revenue is $10,000 CAD.', 'EUR 10000 is revenue.', 'GBP 10000 revenue.', '€10000 revenue.', '10000 AUD revenue.', '10000 CHF revenue.', ]) { expect(checkGrounding('Revenue is $10,000.', source).ungrounded, source).toHaveLength(1); } for (const source of [ 'widgets 10000 revenue.', 'shares 10000 is revenue.', 'kg 10000 revenue.', 'Revenue is 10000 in units.', 'Revenue is 10000 in CAD.', 'Revenue is 10000 total units.', ]) { expect(checkGrounding('Revenue is $10,000.', source).ungrounded, source).toHaveLength(1); } expect(checkGrounding('Revenue is $10,000.', 'Approximately 10000 revenue.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $19m.', 'Revenue is $19.').ungrounded).toHaveLength(1); expect(checkGrounding('Revenue is $19k.', 'Revenue is $19.').ungrounded).toHaveLength(1); expect(checkGrounding('Revenue is $19.', 'Revenue is $19m.').ungrounded).toHaveLength(1); expect(checkGrounding('Revenue is $19m.', 'Revenue is $19 million.').ungrounded).toEqual([]); expect(checkGrounding('Revenue is $19B.', 'Revenue is $19.').ungrounded).toHaveLength(1); expect(checkGrounding('Revenue is $9007199254740992.', 'Revenue is $9007199254740993.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $9007199254740993.', 'Revenue is $9007199254740993.').ungrounded) .toEqual([]); expect(checkGrounding('Runway input: $10,000.', 'Cash deficit: -10000 dollars.').ungrounded) .toHaveLength(1); const negativeFormulaSource = checkGrounding( 'Runway = $10,000 / $5,000.', 'Cash is -10000 dollars; burn is 5000 dollars.', ); expect(negativeFormulaSource.ungrounded.map(specific => specific.number)).toContain('10000'); expect(negativeFormulaSource.grounded.map(specific => specific.number)).toContain('5000'); expect(checkGrounding('Revenue is -$10,000.', 'Revenue is - 10000 dollars.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $10,000.', 'Revenue is - 10000 dollars.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'Revenue: - 10000 dollars.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is -$10,000.', 'Revenue: - 10000 dollars.').ungrounded) .toEqual([]); }); it('binds a claim to the nearest finance subject in a multi-subject sentence', () => { const reply = 'Revenue is unknown while burn is $10,000.'; const mismatched = 'Cash is $10,000; revenue is unknown; burn is unknown.'; expect(checkGrounding(reply, mismatched).ungrounded.map(specific => specific.number)) .toEqual(['10000']); expect(checkGrounding(reply, 'Revenue is unknown while burn is $10,000.').ungrounded) .toEqual([]); }); it('prefers an explicit postfixed finance subject over an earlier subject', () => { const reply = 'Revenue is unknown but $10,000 is monthly burn.'; expect(checkGrounding(reply, 'Revenue is $10,000; burn is unknown.').ungrounded) .toHaveLength(1); expect(checkGrounding(reply, 'Revenue is unknown; burn is $10,000.').ungrounded) .toEqual([]); }); it('keeps a clear preceding subject when a weak postfixed phrase names another field', () => { const reply = 'Revenue is $10,000 in cash.'; expect(checkGrounding(reply, 'Revenue is unknown; cash is $10,000.').ungrounded) .toHaveLength(1); expect(checkGrounding(reply, 'Revenue is $10,000; cash is unknown.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $10,000.', 'Revenue is $10,000 in cash.').ungrounded) .toEqual([]); expect(checkGrounding('Burn is $10,000.', '$10,000 is monthly burn.').ungrounded) .toEqual([]); expect(checkGrounding('Cash is $10,000.', 'Revenue is $10,000 in cash.').ungrounded) .toHaveLength(1); expect(checkGrounding('Burn is $10,000.', 'Revenue is $10,000 for burn.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'We budgeted $10,000 for revenue.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', 'Allocated: $10,000 for revenue.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue is $10,000.', '- $10,000 for revenue.').ungrounded) .toHaveLength(1); for (const source of [ 'Approximately 10000 dollars in revenue.', 'The company has 10000 dollars in revenue.', 'We recorded 10000 dollars of revenue.', ]) { expect(checkGrounding('Revenue is $10,000.', source).ungrounded, source).toEqual([]); } }); it('keeps common finance subjects isolated and canonical', () => { expect(checkGrounding('Sales are $10,000.', 'Sales are $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Profit is $10,000.', 'Revenue is $10,000; profit unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding('Profit is $10,000.', 'Profit is $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Monthly costs are $10,000.', 'Revenue is $10,000; costs unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding('Prices are $10,000.', 'Revenue is $10,000; prices unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding('Monthly costs are $10,000.', 'Monthly costs are $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Prices are $10,000.', 'Prices are $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('ARR is $10,000.', 'Burn is $10,000; ARR unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding('ARR is $10,000.', 'ARR is $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue:\r\n$10,000.', 'Burn is $10,000.').ungrounded) .toHaveLength(1); expect(checkGrounding('Revenue:\r\n$10,000.', 'Revenue is $10,000.').ungrounded) .toEqual([]); for (const subject of ['EBITDA', 'Payroll', 'Debt']) { expect(checkGrounding(`${subject} is $10,000.`, `Burn is $10,000; ${subject} unavailable.`).ungrounded) .toHaveLength(1); expect(checkGrounding(`${subject} is $10,000.`, `${subject} is $10,000.`).ungrounded) .toEqual([]); } for (const subject of [ 'Valuation', 'Salary', 'Capex', 'Opex', 'Assets', 'Liabilities', 'COGS', 'Loss', ]) { expect(checkGrounding(`${subject} is $10,000.`, `Revenue is $10,000; ${subject} unavailable.`).ungrounded) .toHaveLength(1); expect(checkGrounding(`${subject} is $10,000.`, `${subject} is $10,000.`).ungrounded) .toEqual([]); } for (const reply of [ 'Revenue, not cash, is $10,000.', 'Revenue (not cash) is $10,000.', 'Revenue, excluding cash, is $10,000.', 'Revenue rather than cash is $10,000.', 'Revenue, excluding the cash, is $10,000.', 'Revenue rather than current cash is $10,000.', 'Revenue, not available cash, is $10,000.', ]) { expect(checkGrounding(reply, 'Burn is $10,000.').ungrounded).toHaveLength(1); expect(checkGrounding(reply, 'Revenue is $10,000; cash unavailable.').ungrounded).toEqual([]); expect(checkGrounding(reply, 'Cash is $10,000; revenue unavailable.').ungrounded).toHaveLength(1); } }); it('keeps qualified generic finance subjects distinct', () => { const claim = extractClaimedSpecifics('Gross margin is $10,000.') .find(specific => specific.kind === 'money'); expect(claim?.subject).toBe('gross margin'); expect(checkGrounding('Gross margin is $10,000.', 'Gross margin is $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Gross margin is $10,000.', 'Net margin is $10,000.').ungrounded) .toHaveLength(1); for (const [claimSubject, otherSubject] of [ ['Gross profit', 'Net profit'], ['Gross revenue', 'Net revenue'], ]) { expect(checkGrounding( `${claimSubject} is $10,000.`, `${otherSubject} is $10,000; ${claimSubject} unavailable.`, ).ungrounded, `${claimSubject} vs ${otherSubject}`).toHaveLength(1); } expect(checkGrounding('Bookings: $10,000.', 'Revenue is $10,000; bookings unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding('Bookings: $10,000.', 'Bookings: $10,000.').ungrounded).toEqual([]); for (const [claim, wrongSource] of [ ['Revenue including cash is $10,000.', 'Cash is $10,000; revenue unavailable.'], ['Revenue excluding all cash and debt is $10,000.', 'Debt is $10,000; revenue unavailable.'], ['Cost of sales is $10,000.', 'Sales are $10,000; cost of sales unavailable.'], ['Cash balance is $10,000.', 'Balance is $10,000; cash balance unavailable.'], ['Adjusted EBITDA is $10,000.', 'Reported EBITDA is $10,000; adjusted unavailable.'], ['Current assets are $10,000.', 'Fixed assets are $10,000; current unavailable.'], ['Annual recurring revenue is $10,000.', 'Monthly recurring revenue is $10,000.'], ['Operating expenses are $10,000.', 'Capital expenses are $10,000.'], ]) { expect(checkGrounding(claim, wrongSource).ungrounded, claim).toHaveLength(1); expect(checkGrounding(claim, claim).ungrounded, claim).toEqual([]); } expect(checkGrounding('GMV = $10,000.', 'Revenue = $10,000; GMV unavailable.').ungrounded) .toHaveLength(1); for (const [claim, source] of [ ['Annual revenue is $10,000.', 'Monthly revenue is $10,000.'], ['Annual costs are $10,000.', 'Monthly costs are $10,000.'], ['Annual payroll is $10,000.', 'Monthly payroll is $10,000.'], ['Recurring revenue is $10,000.', 'Revenue is $10,000.'], ['Operating margin is $10,000.', 'Gross margin is $10,000.'], ['Contribution margin is $10,000.', 'Margin is $10,000.'], ]) { expect(checkGrounding(claim, source).ungrounded, `${claim} <- ${source}`).toHaveLength(1); expect(checkGrounding(claim, claim).ungrounded, claim).toEqual([]); } expect(checkGrounding('Revenue is $10,000.', 'Actual revenue is $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Actual revenue is $10,000.', 'Revenue is $10,000.').ungrounded) .toHaveLength(1); }); it('binds finance percentages to their qualified subject', () => { expect(checkGrounding('Gross margin is 73%.', 'Gross margin is 73%.').ungrounded) .toEqual([]); expect(checkGrounding( 'Gross margin is 73%.', 'Net margin is 73%; gross margin unavailable.', ).ungrounded).toHaveLength(1); expect(checkGrounding('Gross margin 73%.', 'Net margin 73%; gross unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding('Gross margin rose 73%.', 'Gross margin rose 73%.').ungrounded) .toEqual([]); expect(checkGrounding('Net margin is -5%.', 'Net margin is 5%.').ungrounded) .toHaveLength(1); expect(checkGrounding('Net margin is 5 pp.', 'Net margin is 5%.').ungrounded) .toHaveLength(1); expect(checkGrounding('Net margin is 1,000%.', 'Net margin is 0%.').ungrounded) .toHaveLength(1); expect(checkGrounding('Net margin is (5%).', 'Net margin is 5%.').ungrounded) .toHaveLength(1); expect(checkGrounding('Operating margin is 73%.', 'Gross margin is 73%.').ungrounded) .toHaveLength(1); expect(checkGrounding( 'Revenue was $10k and margin was $10k.', 'Revenue was $10k and margin unavailable.', ).ungrounded).toHaveLength(1); }); it('rejects negated, unavailable, inequality, and malformed source values', () => { for (const source of [ 'Revenue has not reached $10k.', 'Revenue was unavailable at $10k.', 'Revenue was below $10k.', 'Revenue is $1 234.', 'Revenue is $12-34.', 'Revenue is $12 -34.', 'Revenue is $12 - 34.', ]) { expect(checkGrounding('Revenue is $1234.', source).ungrounded, source).toHaveLength(1); } expect(checkGrounding('Margin is 1234%.', 'Margin is 12-34%.').ungrounded).toHaveLength(1); expect(checkGrounding('Runway is 1234 months.', 'Runway is 12-34 months.').ungrounded) .toHaveLength(1); expect(checkGrounding('There are 1234 users.', 'There are 12-34 users.').ungrounded) .toHaveLength(1); for (const malformed of [ 'Revenue is $1_000.', 'Revenue is USD 1_000.', 'Revenue is EUR 1_000.', 'Revenue is C$1_000.', 'Revenue is $1,000foo.', ]) { expect(extractClaimedSpecifics(malformed).filter(specific => specific.kind === 'money'), malformed) .toEqual([]); } expect(checkGrounding('Revenue stands at $10k.', 'Revenue stands at $10k.').ungrounded) .toEqual([]); expect(checkGrounding('Gross margin stands at 73%.', 'Gross margin stands at 73%.').ungrounded) .toEqual([]); }); it('rejects non-assertive and malformed non-money source occurrences', () => { for (const [reply, source] of [ ['There are 227 users.', 'There are not 227 users.'], ['Runway is 4 months.', 'We do not have 4 months of runway.'], ['Runway is 4 months.', 'Runway is under 4 months.'], ['A 73% lift occurred.', 'It was not a 73% lift.'], ['There are 0 users.', 'There are 1_000 users.'], ['Runway is 0 months.', 'Runway is 1_000 months.'], ['Gross margin is 5%.', 'Gross margin is 5%foo.'], ['Gross margin is 5%.', 'Gross margin is 5%%.'], ]) { expect(checkGrounding(reply, source).ungrounded, `${reply} <- ${source}`).toHaveLength(1); } }); it('requires lexical count and duration unit boundaries', () => { for (const [reply, source] of [ ['There are 4 users.', 'There are 4 usernames.'], ['There are 4 users.', 'There are abuser 4 records.'], ['There are 4 agents.', 'There are 4 agentic workflows.'], ['There are 4 agents.', 'Reagent 4 was selected.'], ['There are 4 frames.', 'iframe 4 was rendered.'], ['Runway is 4 days.', 'The daydream 4 prototype shipped.'], ]) { expect(checkGrounding(reply, source).ungrounded, `${reply} <- ${source}`).toHaveLength(1); } }); it('keeps finance ownership, time, epistemic, plan, delta, and compound identity isolated', () => { for (const [reply, source] of [ ['Our revenue is $10,000.', 'Their revenue is $10,000.'], ['Revenue is $10,000.', 'Competitor revenue is $10,000.'], ['Projected revenue is $10,000.', 'Actual revenue is $10,000.'], ['Target revenue is $10,000.', 'Revenue is $10,000.'], ['Revenue is $10,000.', 'Last year revenue is $10,000.'], ['Pricing: Pro — $49/month.', 'Pricing: Teams — $49/month; Pro unavailable.'], ['Pro pricing is $49/month.', 'Pro pricing is $19/month. Teams pricing is $49/month.'], ['Revenue declined by $10,000.', 'Revenue is $10,000.'], ['Cash and revenue are $10,000.', 'Revenue is $10,000; cash unavailable.'], ]) { expect(checkGrounding(reply, source).ungrounded, `${reply} <- ${source}`).toHaveLength(1); } }); it('accepts asserted finance predicates and isolates comma-separated facts', () => { for (const source of [ 'Revenue totaled $10,000.', 'Revenue recorded $10,000.', 'Revenue is approximately $10,000.', 'Revenue is about $10,000.', 'Revenue is estimated at $10,000.', ]) { expect(checkGrounding('Revenue is $10,000.', source).ungrounded, source).toEqual([]); } expect(checkGrounding('Cash is $10,000.', 'Revenue is unavailable, cash is $10,000.').ungrounded) .toEqual([]); }); it('rejects interrogative and hypothetical source mentions', () => { for (const source of [ 'Is revenue $10,000?', 'If revenue is $10,000, we can hire.', 'Whether revenue is $10,000 remains unknown.', ]) { expect(checkGrounding('Revenue is $10,000.', source).ungrounded, source).toHaveLength(1); } }); it('rejects adjacent non-money units in grounding sources', () => { for (const source of [ 'Sales are 10000orders.', 'Sales are $10000orders.', 'Sales are 10,000-unit.', 'Sales are 10,000/orders.', 'Sales are 10,000 (units).', 'Sales are 10,000 [units].', 'Sales are 10,000: units.', ]) { expect(checkGrounding('Sales are $10,000.', source).ungrounded, source).toHaveLength(1); } for (const source of [ '10000 revenue widgets.', '10000 revenue in widgets.', ]) { expect(checkGrounding('Revenue is $10,000.', source).ungrounded, source).toHaveLength(1); } }); it('does not bind a value to a finance subject across a newline', () => { expect(checkGrounding('Burn is $10,000.', 'Revenue is $10,000\nburn is unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding('Burn is $10,000.', 'Burn\n$10,000\nRevenue unavailable.').ungrounded) .toEqual([]); for (const newline of ['\n', '\r\n']) { const reply = `Revenue is unknown${newline}$10,000 in cash.`; const source = `Revenue is unknown${newline}10000 dollars in cash.`; expect(checkGrounding(reply, 'Revenue is $10,000; cash unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding(reply, 'Revenue unavailable; cash is $10,000.').ungrounded) .toEqual([]); expect(checkGrounding('Cash is $10,000.', source).ungrounded).toEqual([]); } expect(checkGrounding( 'Revenue is $5,000 but $10,000 in cash.', 'Revenue is $5,000 but $10,000 in cash.', ).ungrounded).toEqual([]); expect(checkGrounding('Cash is $10,000.', 'Revenue unavailable but 10000 dollars in cash.').ungrounded) .toEqual([]); }); it('requires a claimed pricing cadence to match the source cadence', () => { expect(checkGrounding('Pricing is $19/year.', 'Pricing is $19/month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is $19/year.', 'Pricing is $19/year.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19.', 'Pricing is $19/month.').ungrounded) .toEqual([]); expect(checkGrounding('Teams is $49/seat/year.', 'Teams is $49/seat/month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Teams is $49/seat/month.', 'Teams is $49/seat/month.').ungrounded) .toEqual([]); expect(checkGrounding('Teams is $49/seat/monthly.', 'Teams is $49/seat/year.').ungrounded) .toHaveLength(1); expect(checkGrounding('Teams is $49/seat/monthly.', 'Teams is $49/seat/month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19 per year.', 'Pricing is $19/month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is $19/month.', 'Pricing is $19 per month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19 USD per year.', 'Pricing is $19/month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is $19 annually.', 'Pricing is $19/month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is $19 monthly.', 'Pricing is $19/month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19/month.', 'Pricing is 19 dollars per month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19/month.', 'Pricing:\n- 19 dollars per month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19/month.', 'Pricing: - 19 dollars per month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is -$19/month.', 'Pricing: - 19 dollars per month.').ungrounded) .toEqual([]); for (const claim of [ 'Pricing is $19 weekly.', 'Pricing is $19 quarterly.', 'Pricing is $19 annual.', 'Pricing is $19 a year.', 'Pricing is $19 each year.', 'Pricing is $19 every year.', 'Pricing is $19 per annum.', ]) { expect(checkGrounding(claim, 'Pricing is $19/month.').ungrounded, claim).toHaveLength(1); } for (const claim of [ 'Pricing is $19 billed per year.', 'Pricing is $19 charged per year.', 'Pricing is $19 billed every year.', 'Pricing is $19 payable annually.', ]) { expect(checkGrounding(claim, 'Pricing is $19/month.').ungrounded, claim).toHaveLength(1); expect(checkGrounding(claim, 'Pricing is $19/year.').ungrounded, claim).toEqual([]); } expect(checkGrounding('Pricing is $19 every month.', 'Pricing is $19/month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19 every month.', 'Pricing is $19/year.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is $19 every year.', 'Pricing is $19/year.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19 per annum.', 'Pricing is $19/year.').ungrounded) .toEqual([]); for (const claim of [ 'Pricing is $19 daily.', 'Pricing is $19 biweekly.', 'Pricing is $19 hourly.', 'Pricing is $19 nightly.', 'Pricing is $19 semimonthly.', 'Pricing is $19 semi-monthly.', 'Pricing is $19 bi-weekly.', 'Pricing is $19 every two weeks.', 'Pricing is $19 every other week.', 'Pricing is $19 twice monthly.', 'Pricing is $19 twice a month.', 'Pricing is $19 p.a.', 'Pricing is $19 qtrly.', 'Pricing is $19 each two weeks.', 'Pricing is $19 every fortnight.', 'Pricing is $19 once per month.', 'Pricing is $19 every day.', 'Pricing is $19 pa.', 'Pricing is $19 p.m.', 'Pricing is $19 pcm.', ]) { expect(extractClaimedSpecifics(claim).filter(specific => specific.kind === 'money'), claim) .toEqual([]); } for (const claim of ['Pricing is $19 CAD.', 'Pricing is CA$19.', 'Pricing is €19.']) { expect(checkGrounding(claim, 'Pricing is $19 USD.').ungrounded, claim).toHaveLength(1); } expect(checkGrounding('Pricing is $19 CAD.', 'Pricing is $19 CAD.').ungrounded).toEqual([]); expect(checkGrounding('Revenue is CA$10,000.', 'Revenue is 10000 in CAD.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is CA$10,000.', 'Revenue is 10000 in AUD.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is CA$19/month.', 'Pricing is 19 in CAD per month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is €19.', 'Pricing is EUR 19.').ungrounded).toEqual([]); expect(checkGrounding('Pricing is $19 CAD.', 'Pricing is 19.').ungrounded).toHaveLength(1); for (const [claim, source] of [ ['Pricing is €19.', 'Pricing is €19.'], ['Pricing is $19 CAD per month.', 'Pricing is $19 CAD per month.'], ['Pricing is 19 AUD/month.', 'Pricing is 19 AUD/month.'], ['Pricing is €19/month.', 'Pricing is 19 EUR per month.'], ['Pricing is CAD $19/month.', 'Pricing is CAD $19/month.'], ]) { expect(checkGrounding(claim, source).ungrounded, `${claim} <- ${source}`).toEqual([]); } expect(checkGrounding('Pricing is $19 CAD/month.', 'Pricing is $19 CAD/year.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is $19/years.', 'Pricing is $19/years.').ungrounded) .toEqual([]); expect(checkGrounding('Monthly pricing is $19.', 'Annual pricing is $19.').ungrounded) .toHaveLength(1); expect(checkGrounding('Monthly pricing is $19.', 'Monthly pricing is $19.').ungrounded) .toEqual([]); expect(checkGrounding('Price per month is $19.', 'Price per year is $19.').ungrounded) .toHaveLength(1); expect(checkGrounding( 'Pricing is $19/month.', 'Monthly meeting notes: pricing is $19.', ).ungrounded).toHaveLength(1); expect(checkGrounding('Pricing is $19 billed annually.', 'Pricing is $19/month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is $19 (monthly).', 'Pricing is $19/year.').ungrounded) .toHaveLength(1); expect(checkGrounding('Pricing is $19 per calendar month.', 'Pricing is $19/month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19 every calendar month.', 'Pricing is $19/month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $19 every calendar month.', 'Pricing is $19/year.').ungrounded) .toHaveLength(1); for (const claim of [ 'Pricing is $19, billed monthly.', 'Pricing is $19 (billed monthly).', 'Pricing is $19 charged monthly.', 'Pricing is monthly at $19.', ]) { expect(checkGrounding(claim, 'Pricing is $19/year.').ungrounded, claim).toHaveLength(1); expect(checkGrounding(claim, 'Pricing is $19/month.').ungrounded, claim).toEqual([]); } expect(checkGrounding('Pricing is $19 (monthly).', 'Pricing is $19 (monthly).').ungrounded) .toEqual([]); for (const claim of ['Revenue is -€10,000.', 'Revenue is €-10,000.', 'Revenue is EUR -10,000.']) { expect(checkGrounding(claim, 'Revenue is 10000 EUR.').ungrounded, claim).toHaveLength(1); expect(checkGrounding(claim, 'Revenue is -10000 EUR.').ungrounded, claim).toEqual([]); } expect(checkGrounding('Pricing is $19 per year.', 'Pricing is 19 per year.').ungrounded) .toEqual([]); expect(checkGrounding('Revenue is $10,000/month.', '10000 dollars per month in revenue.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $49/seat/month.', 'Pricing is 49 dollars per seat per month.').ungrounded) .toEqual([]); expect(checkGrounding('Pricing is $49/seat/year.', 'Pricing is 49 dollars per seat per month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Teams is $49 per seat per year.', 'Teams is $49/seat/month.').ungrounded) .toHaveLength(1); expect(checkGrounding('Teams is $49 per seat per year.', 'Teams is $49/seat/year.').ungrounded) .toEqual([]); }); it('keeps named owners and material finance qualifiers isolated', () => { for (const [claim, source] of [ ["Acme's revenue is $10,000.", "Beta's revenue is $10,000."], ['Average revenue is $10,000.', 'Total revenue is $10,000.'], ['Non-recurring revenue is $10,000.', 'Recurring revenue is $10,000.'], [ 'Your Pro pricing is $49/month.', 'Your Pro pricing is $19/month. Your Teams pricing is $49/month.', ], ]) { expect(checkGrounding(claim, source).ungrounded, `${claim} <- ${source}`).toHaveLength(1); expect(checkGrounding(claim, claim).ungrounded, claim).toEqual([]); } }); it('does not treat coder assignments as finance claims', () => { for (const code of [ 'config.revenue = PHP 8.3;', 'const cfg = { revenue: CAD 2024 };', 'revenue = PHP 8.3;', 'pricing = TRY 3;', 'revenue = PHP 8.3 // runtime version', 'pricing = TRY 3 # config', 'revenue = PHP 8.3 /* runtime version */', 'pricing = TRY 3 -- config', '/* revenue = PHP 8.3; */', '-- pricing = TRY 3', '// revenue = PHP 8.3', '# pricing = TRY 3', '# revenue = PHP 8.3', ]) { expect(extractClaimedSpecifics(code).filter(specific => specific.kind === 'money'), code) .toEqual([]); } expect(extractClaimedSpecifics('Revenue = PHP 8.3.').filter(specific => specific.kind === 'money')) .toHaveLength(1); expect(extractClaimedSpecifics('# Revenue is $10,000.').filter(specific => specific.kind === 'money')) .toHaveLength(1); expect(extractClaimedSpecifics('## Revenue is $10,000.').filter(specific => specific.kind === 'money')) .toHaveLength(1); }); it('separates deltas, levels, and multi-metric compound subjects', () => { for (const claim of [ 'Revenue decreased by $10,000.', 'Revenue dropped by $10,000.', 'Revenue dropped $10,000.', 'Revenue grew $10,000.', 'Revenue dropped about $10,000.', 'Revenue grew approximately $10,000.', 'Revenue rose by $10,000.', 'Revenue increased $10,000.', 'Revenue fell $10,000.', ]) { expect(checkGrounding(claim, 'Cash is $10,000; revenue unavailable.').ungrounded, claim) .toHaveLength(1); expect(checkGrounding(claim, 'Revenue is $10,000.').ungrounded, claim).toHaveLength(1); expect(checkGrounding(claim, claim).ungrounded, claim).toEqual([]); } expect(checkGrounding('Revenue increased to $10,000.', 'Revenue is $10,000.').ungrounded) .toEqual([]); const compound = 'Cash, revenue, and profit are $10,000.'; expect(checkGrounding(compound, 'Profit is $10,000; cash and revenue unavailable.').ungrounded) .toHaveLength(1); expect(checkGrounding(compound, compound).ungrounded).toEqual([]); }); it('preserves invoiced cadence instead of accepting a conflicting period', () => { const claim = 'Pricing is $19 invoiced annually.'; expect(checkGrounding(claim, 'Pricing is $19/month.').ungrounded).toHaveLength(1); expect(checkGrounding(claim, 'Pricing is $19/year.').ungrounded).toEqual([]); }); it('rejects hypothetical and comma-qualified questions as source facts', () => { for (const source of [ 'Hypothetical: revenue is $10,000.', 'Assume revenue is $10,000.', 'For example, revenue is $10,000.', 'For instance, revenue is $10,000.', 'Example: revenue is $10,000.', 'Revenue is $10,000, correct?', 'Is revenue $10,000, yes or no?', ]) { expect(checkGrounding('Revenue is $10,000.', source).ungrounded, source).toHaveLength(1); } expect(checkGrounding('There are 227 users.', 'Are there 227 users, currently?').ungrounded) .toHaveLength(1); expect(checkGrounding( 'Revenue is $10,000.', 'The example is illustrative, but revenue is $10,000.', ).ungrounded).toEqual([]); for (const source of [ 'For example, cash could be $5,000, but revenue is $10,000.', 'For instance, cash could be $5,000, but revenue is $10,000.', 'For example, cash may vary, but actual revenue is $10,000.', ]) { expect(checkGrounding('Revenue is $10,000.', source).ungrounded, source).toEqual([]); } }); it('does not truncate malformed quantitative claims into different values', () => { for (const malformed of [ 'Revenue is $12-34.', `Revenue is $1\u200b000.`, 'There are 1_000 users.', 'Runway is 1_000 months.', 'Margin is 1_000%.', ]) { expect(extractClaimedSpecifics(malformed), malformed).toEqual([]); } }); it('normalizes full-width currency and percent symbols with full-width digits', () => { const money = extractClaimedSpecifics('Revenue is $10,000.') .filter(specific => specific.kind === 'money'); expect(money).toHaveLength(1); expect(money[0]?.number).toBe('10000'); expect(checkGrounding('Revenue is $10,000.', 'Revenue is $10,000.').ungrounded) .toEqual([]); const percent = extractClaimedSpecifics('Margin is 73%.') .filter(specific => specific.kind === 'percent'); expect(percent).toHaveLength(1); expect(percent[0]?.number).toBe('73'); expect(checkGrounding('Margin is 73%.', 'Margin is 73%.').ungrounded).toEqual([]); }); it('keeps repeated subject/value binding within a linear-time budget', () => { const sources = `${'revenue '.repeat(5_000)}${'$10,000 '.repeat(5_000)}`; const startedAt = performance.now(); const result = checkGrounding('Revenue is $10,000.', sources); const elapsedMs = performance.now() - startedAt; expect(result.ungrounded).toEqual([]); expect(elapsedMs).toBeLessThan(500); }); it('rejects a huge invalid money token within a bounded time', () => { const sources = `$${'9'.repeat(50_000)}/weekly`; const startedAt = performance.now(); const result = checkGrounding('Revenue is $19.', sources); const elapsedMs = performance.now() - startedAt; expect(result.ungrounded).toHaveLength(1); expect(elapsedMs).toBeLessThan(500); }); it('score is 1 when there are no quantitative specifics', () => { const r = checkGrounding('Let me help you think this through.', MEMORY); expect(r.specifics).toHaveLength(0); expect(r.score).toBe(1); }); it('score reflects grounded ratio on a mixed reply', () => { // "$19/month" grounded; "4 months runway" + "227 entities" ungrounded → 1/3 const r = checkGrounding('Pro at $19/month, 4 months runway, 227 entities.', MEMORY); expect(r.specifics.length).toBeGreaterThanOrEqual(3); expect(r.score).toBeLessThan(0.5); expect(r.ungrounded.length).toBeGreaterThanOrEqual(2); }); });