verifiable-thinking-mcp 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/package.json +75 -0
  4. package/src/index.ts +38 -0
  5. package/src/lib/cache.ts +246 -0
  6. package/src/lib/compression.ts +804 -0
  7. package/src/lib/compute/cache.ts +86 -0
  8. package/src/lib/compute/classifier.ts +555 -0
  9. package/src/lib/compute/confidence.ts +79 -0
  10. package/src/lib/compute/context.ts +154 -0
  11. package/src/lib/compute/extract.ts +200 -0
  12. package/src/lib/compute/filter.ts +224 -0
  13. package/src/lib/compute/index.ts +171 -0
  14. package/src/lib/compute/math.ts +247 -0
  15. package/src/lib/compute/patterns.ts +564 -0
  16. package/src/lib/compute/registry.ts +145 -0
  17. package/src/lib/compute/solvers/arithmetic.ts +65 -0
  18. package/src/lib/compute/solvers/calculus.ts +249 -0
  19. package/src/lib/compute/solvers/derivation-core.ts +371 -0
  20. package/src/lib/compute/solvers/derivation-latex.ts +160 -0
  21. package/src/lib/compute/solvers/derivation-mistakes.ts +1046 -0
  22. package/src/lib/compute/solvers/derivation-simplify.ts +451 -0
  23. package/src/lib/compute/solvers/derivation-transform.ts +620 -0
  24. package/src/lib/compute/solvers/derivation.ts +67 -0
  25. package/src/lib/compute/solvers/facts.ts +120 -0
  26. package/src/lib/compute/solvers/formula.ts +728 -0
  27. package/src/lib/compute/solvers/index.ts +36 -0
  28. package/src/lib/compute/solvers/logic.ts +422 -0
  29. package/src/lib/compute/solvers/probability.ts +307 -0
  30. package/src/lib/compute/solvers/statistics.ts +262 -0
  31. package/src/lib/compute/solvers/word-problems.ts +408 -0
  32. package/src/lib/compute/types.ts +107 -0
  33. package/src/lib/concepts.ts +111 -0
  34. package/src/lib/domain.ts +731 -0
  35. package/src/lib/extraction.ts +912 -0
  36. package/src/lib/index.ts +122 -0
  37. package/src/lib/judge.ts +260 -0
  38. package/src/lib/math/ast.ts +842 -0
  39. package/src/lib/math/index.ts +8 -0
  40. package/src/lib/math/operators.ts +171 -0
  41. package/src/lib/math/tokenizer.ts +477 -0
  42. package/src/lib/patterns.ts +200 -0
  43. package/src/lib/session.ts +825 -0
  44. package/src/lib/think/challenge.ts +323 -0
  45. package/src/lib/think/complexity.ts +504 -0
  46. package/src/lib/think/confidence-drift.ts +507 -0
  47. package/src/lib/think/consistency.ts +347 -0
  48. package/src/lib/think/guidance.ts +188 -0
  49. package/src/lib/think/helpers.ts +568 -0
  50. package/src/lib/think/hypothesis.ts +216 -0
  51. package/src/lib/think/index.ts +127 -0
  52. package/src/lib/think/prompts.ts +262 -0
  53. package/src/lib/think/route.ts +358 -0
  54. package/src/lib/think/schema.ts +98 -0
  55. package/src/lib/think/scratchpad-schema.ts +662 -0
  56. package/src/lib/think/spot-check.ts +961 -0
  57. package/src/lib/think/types.ts +93 -0
  58. package/src/lib/think/verification.ts +260 -0
  59. package/src/lib/tokens.ts +177 -0
  60. package/src/lib/verification.ts +620 -0
  61. package/src/prompts/index.ts +10 -0
  62. package/src/prompts/templates.ts +336 -0
  63. package/src/resources/index.ts +8 -0
  64. package/src/resources/sessions.ts +196 -0
  65. package/src/tools/compress.ts +138 -0
  66. package/src/tools/index.ts +5 -0
  67. package/src/tools/scratchpad.ts +2659 -0
  68. package/src/tools/sessions.ts +144 -0
@@ -0,0 +1,728 @@
1
+ /**
2
+ * Formula solver - tiered pattern matching for mathematical formulas
3
+ */
4
+
5
+ import { type ASTNode, buildAST, simplifyAST, tokenizeMathExpression } from "../../verification.ts";
6
+ import { SolverType } from "../classifier.ts";
7
+ import {
8
+ combinations,
9
+ factorial,
10
+ fibonacci,
11
+ formatResult,
12
+ gcd,
13
+ isPrime,
14
+ lcm,
15
+ permutations,
16
+ } from "../math.ts";
17
+ import { GUARDS, TIER1, TIER2, TIER3, TIER4 } from "../patterns.ts";
18
+ import type { ComputeResult, Solver } from "../types.ts";
19
+
20
+ /** Helper to build a successful ComputeResult @internal */
21
+ function solved(result: string | number, method: string, start: number): ComputeResult {
22
+ return {
23
+ solved: true,
24
+ result,
25
+ method,
26
+ confidence: 1.0,
27
+ time_ms: performance.now() - start,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Matrix determinant using Gaussian elimination
33
+ * O(n³) complexity - works for any NxN matrix
34
+ * @internal
35
+ */
36
+ function matrixDeterminant(matrix: number[][]): number | null {
37
+ const n = matrix.length;
38
+ if (n === 0) return 0;
39
+ if (!matrix.every((row) => row.length === n)) return null; // Must be square
40
+
41
+ // Special cases for small matrices (faster)
42
+ if (n === 1) {
43
+ const val = matrix[0]?.[0];
44
+ return val !== undefined ? val : null;
45
+ }
46
+ if (n === 2) {
47
+ const a = matrix[0]?.[0];
48
+ const b = matrix[0]?.[1];
49
+ const c = matrix[1]?.[0];
50
+ const d = matrix[1]?.[1];
51
+ if (a === undefined || b === undefined || c === undefined || d === undefined) return null;
52
+ return a * d - b * c;
53
+ }
54
+
55
+ // Clone matrix to avoid mutation
56
+ const mat = matrix.map((row) => [...row]);
57
+ let det = 1;
58
+
59
+ // Gaussian elimination to upper triangular form
60
+ for (let col = 0; col < n; col++) {
61
+ // Find pivot (largest absolute value in column for numerical stability)
62
+ let maxRow = col;
63
+ for (let row = col + 1; row < n; row++) {
64
+ const current = mat[row]?.[col];
65
+ const best = mat[maxRow]?.[col];
66
+ if (current !== undefined && best !== undefined && Math.abs(current) > Math.abs(best)) {
67
+ maxRow = row;
68
+ }
69
+ }
70
+
71
+ // Swap rows if needed
72
+ if (maxRow !== col) {
73
+ const rowCol = mat[col];
74
+ const rowMax = mat[maxRow];
75
+ if (rowCol && rowMax) {
76
+ mat[col] = rowMax;
77
+ mat[maxRow] = rowCol;
78
+ det *= -1; // Row swap changes sign
79
+ }
80
+ }
81
+
82
+ // Check for zero pivot (singular matrix)
83
+ const pivot = mat[col]?.[col];
84
+ if (pivot === undefined || Math.abs(pivot) < 1e-10) return 0;
85
+
86
+ // Multiply determinant by pivot
87
+ det *= pivot;
88
+
89
+ // Eliminate column entries below pivot
90
+ for (let row = col + 1; row < n; row++) {
91
+ const rowArr = mat[row];
92
+ const colArr = mat[col];
93
+ if (!rowArr || !colArr) continue;
94
+
95
+ const rowColVal = rowArr[col];
96
+ const pivotVal = colArr[col];
97
+ if (rowColVal === undefined || pivotVal === undefined) continue;
98
+
99
+ const factor = rowColVal / pivotVal;
100
+ for (let k = col; k < n; k++) {
101
+ const colK = colArr[k];
102
+ const rowK = rowArr[k];
103
+ if (colK !== undefined && rowK !== undefined) {
104
+ rowArr[k] = rowK - factor * colK;
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ return det;
111
+ }
112
+
113
+ /**
114
+ * Parse matrix from various string formats:
115
+ * - [[1,2],[3,4]] - JSON-like
116
+ * - [1,2;3,4] - MATLAB-like
117
+ * - |1 2; 3 4| - bar notation
118
+ * @internal
119
+ */
120
+ function parseMatrix(text: string): number[][] | null {
121
+ // Try JSON-like format: [[1,2],[3,4]]
122
+ const jsonMatch = text.match(/\[\s*\[([\d\s,-]+)\]\s*(?:,\s*\[([\d\s,-]+)\]\s*)*\]/);
123
+ if (jsonMatch) {
124
+ try {
125
+ // Extract all rows
126
+ const rowMatches = text.match(/\[([\d\s,-]+)\]/g);
127
+ if (!rowMatches) return null;
128
+
129
+ const matrix = rowMatches.map((rowStr) => {
130
+ const nums = rowStr.match(/-?\d+/g);
131
+ return nums ? nums.map(Number) : [];
132
+ });
133
+
134
+ // Validate: all rows same length, at least 1x1
135
+ const firstRow = matrix[0];
136
+ if (!firstRow || matrix.length === 0 || firstRow.length === 0) return null;
137
+ if (!matrix.every((row) => row.length === firstRow.length)) return null;
138
+
139
+ return matrix;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ // Try semicolon format: [1,2;3,4] or 1,2;3,4
146
+ const semiMatch = text.match(/[[|]?\s*([\d\s,;-]+)\s*[\]|]?/);
147
+ if (semiMatch?.[1]?.includes(";")) {
148
+ const rows = semiMatch[1].split(";").map((row) => {
149
+ const nums = row.match(/-?\d+/g);
150
+ return nums ? nums.map(Number) : [];
151
+ });
152
+
153
+ const firstRow = rows[0];
154
+ if (firstRow && rows.length > 0 && firstRow.length > 0) {
155
+ if (rows.every((row) => row.length === firstRow.length)) {
156
+ return rows;
157
+ }
158
+ }
159
+ }
160
+
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Detect decision theory / expected value contexts where we should NOT
166
+ * extract simple percentages or arithmetic.
167
+ *
168
+ * Examples to SKIP:
169
+ * - "100% chance of $50" (probability context)
170
+ * - "expected value" (EV calculation)
171
+ * - "which has higher" (comparison)
172
+ * - "prefers A or B" (preference question)
173
+ * @internal
174
+ */
175
+ function isDecisionOrExpectedValueContext(lower: string): boolean {
176
+ return (
177
+ lower.includes("expected value") ||
178
+ lower.includes("which has higher") ||
179
+ lower.includes("prefers") ||
180
+ lower.includes("prefer") ||
181
+ lower.includes("gamble") ||
182
+ (lower.includes("chance") && lower.includes("$")) // "X% chance of $Y"
183
+ );
184
+ }
185
+
186
+ /**
187
+ * TIER 1: Ultra-fast formulas (simple patterns, O(1) compute)
188
+ * - Percentage, factorial, modulo, primality, fibonacci
189
+ * @internal
190
+ */
191
+ function tryFormulaTier1(text: string, lower: string): ComputeResult | null {
192
+ const start = performance.now();
193
+
194
+ // PERCENTAGE: "15% of 200" - very common, fast check
195
+ // SKIP: Decision questions like "100% chance of $50" or "expected value" contexts
196
+ if (GUARDS.hasPercent(text) && !isDecisionOrExpectedValueContext(lower)) {
197
+ const percentMatch = text.match(TIER1.percentage);
198
+ if (percentMatch?.[1] && percentMatch[2]) {
199
+ const percent = parseFloat(percentMatch[1]);
200
+ const value = parseFloat(percentMatch[2]);
201
+ return solved(formatResult((percent / 100) * value), "percentage", start);
202
+ }
203
+ }
204
+
205
+ // FACTORIAL: "5!" or "factorial of 5" - guard on "!" or "factorial"
206
+ if ((GUARDS.hasExclaim(text) || lower.includes("factorial")) && !/trailing/i.test(text)) {
207
+ const factMatch = text.match(TIER1.factorial);
208
+ if (factMatch) {
209
+ const nStr = factMatch[1] || factMatch[2];
210
+ if (nStr) {
211
+ const n = parseInt(nStr, 10);
212
+ if (n >= 0 && n <= 170) {
213
+ return solved(factorial(n), "factorial", start);
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ // MODULO: "17 mod 5" - guard on "mod" or "%" or "remainder"
220
+ // BUT skip "X^Y mod 10" which is handled by last_digit in Tier 3
221
+ if (
222
+ (lower.includes("mod") ||
223
+ lower.includes("remainder") ||
224
+ (GUARDS.hasPercent(text) && /\d+\s*%\s*\d+/.test(text))) &&
225
+ !TIER1.moduloLastDigitGuard.test(text)
226
+ ) {
227
+ const modMatch = text.match(TIER1.moduloBasic) || text.match(TIER1.moduloRemainder);
228
+ if (modMatch?.[1] && modMatch[2]) {
229
+ const a = parseInt(modMatch[1], 10);
230
+ const b = parseInt(modMatch[2], 10);
231
+ if (b !== 0) {
232
+ return solved(a % b, "modulo", start);
233
+ }
234
+ }
235
+ }
236
+
237
+ // PRIMALITY: "is 91 prime?" - guard on "prime"
238
+ if (lower.includes("prime")) {
239
+ const primeMatch = text.match(TIER1.prime);
240
+ if (primeMatch?.[1]) {
241
+ const n = parseInt(primeMatch[1], 10);
242
+ if (n <= 1_000_000) {
243
+ return solved(isPrime(n) ? "YES" : "NO", "primality", start);
244
+ }
245
+ }
246
+ }
247
+
248
+ // FIBONACCI: "8th fibonacci" - guard on "fibonacci"
249
+ if (lower.includes("fibonacci")) {
250
+ const fibMatch = lower.match(TIER1.fibonacci);
251
+ if (fibMatch?.[1]) {
252
+ const n = parseInt(fibMatch[1], 10);
253
+ if (n > 0 && n <= 100) {
254
+ return solved(fibonacci(n), "fibonacci", start);
255
+ }
256
+ }
257
+ }
258
+
259
+ return null;
260
+ }
261
+
262
+ /**
263
+ * TIER 2: Fast formulas (simple math operations)
264
+ * - Square root, power, GCD/LCM
265
+ */
266
+ function tryFormulaTier2(text: string, lower: string): ComputeResult | null {
267
+ const start = performance.now();
268
+
269
+ // SQUARE ROOT: √x, sqrt(x) - guard on "sqrt", "√", or "root"
270
+ if (lower.includes("sqrt") || text.includes("\u221A") || lower.includes("root")) {
271
+ const sqrtMatch = text.match(TIER2.sqrt);
272
+ if (sqrtMatch?.[1]) {
273
+ const val = parseFloat(sqrtMatch[1]);
274
+ if (val >= 0) {
275
+ return solved(formatResult(Math.sqrt(val)), "square_root", start);
276
+ }
277
+ }
278
+ }
279
+
280
+ // POWER: x^n, x**n - guard on "^" or "**" or "power"
281
+ if (GUARDS.hasCaret(text) || lower.includes("power")) {
282
+ // Skip if "last digit" or "mod" present (handled elsewhere)
283
+ if (!TIER2.powerLastDigitGuard.test(text) && !TIER2.powerModGuard.test(text)) {
284
+ const powMatch = text.match(TIER2.power);
285
+ if (powMatch?.[1] && powMatch[2]) {
286
+ const base = parseFloat(powMatch[1]);
287
+ const exp = parseFloat(powMatch[2]);
288
+ const result = base ** exp;
289
+ if (Number.isFinite(result)) {
290
+ return solved(formatResult(result), "power", start);
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ // GCD: "gcd of 12 and 18" - guard on "gcd"
297
+ if (lower.includes("gcd") || lower.includes("greatest common")) {
298
+ const gcdMatch = text.match(TIER2.gcd);
299
+ if (gcdMatch?.[1] && gcdMatch[2]) {
300
+ const a = parseInt(gcdMatch[1], 10);
301
+ const b = parseInt(gcdMatch[2], 10);
302
+ return solved(gcd(a, b), "gcd", start);
303
+ }
304
+ }
305
+
306
+ // LCM: "lcm of 12 and 18" - guard on "lcm"
307
+ if (lower.includes("lcm") || lower.includes("least common")) {
308
+ const lcmMatch = text.match(TIER2.lcm);
309
+ if (lcmMatch?.[1] && lcmMatch[2]) {
310
+ const a = parseInt(lcmMatch[1], 10);
311
+ const b = parseInt(lcmMatch[2], 10);
312
+ return solved(lcm(a, b), "lcm", start);
313
+ }
314
+ }
315
+
316
+ return null;
317
+ }
318
+
319
+ // =============================================================================
320
+ // TIER 3 HELPERS (extracted to reduce cognitive complexity)
321
+ // =============================================================================
322
+
323
+ /** Try logarithm patterns: log₁₀(x), ln(x) */
324
+ function tryLogarithm(text: string, lower: string, start: number): ComputeResult | null {
325
+ if (!lower.includes("log") && !lower.includes("ln")) return null;
326
+
327
+ // Base 10 log - need fresh regex each time due to global flag
328
+ const logBase10Pattern = /log[\u2081\u20801]?[\u2080\u20800]?\s*\(?\s*(\d+)\s*\)?/gi;
329
+ const logBase10 = text.match(logBase10Pattern);
330
+ if (logBase10 && logBase10.length > 0) {
331
+ let sum = 0;
332
+ let valid = true;
333
+ for (const match of logBase10) {
334
+ const numMatch = match.match(/\d+/);
335
+ if (numMatch?.[0]) {
336
+ const val = parseInt(numMatch[0], 10);
337
+ if (val > 0) {
338
+ sum += Math.log10(val);
339
+ } else {
340
+ valid = false;
341
+ break;
342
+ }
343
+ }
344
+ }
345
+ if (valid && (lower.includes("+") || logBase10.length === 1)) {
346
+ return {
347
+ solved: true,
348
+ result: formatResult(sum),
349
+ method: "logarithm_base10",
350
+ confidence: 1.0,
351
+ time_ms: performance.now() - start,
352
+ };
353
+ }
354
+ }
355
+
356
+ // Natural log: ln(x)
357
+ const lnMatch = text.match(TIER3.logNatural);
358
+ if (lnMatch?.[1]) {
359
+ const val = parseFloat(lnMatch[1]);
360
+ if (val > 0) {
361
+ return {
362
+ solved: true,
363
+ result: +Math.log(val).toFixed(6),
364
+ method: "natural_log",
365
+ confidence: 1.0,
366
+ time_ms: performance.now() - start,
367
+ };
368
+ }
369
+ }
370
+
371
+ return null;
372
+ }
373
+
374
+ /** Try quadratic equation: ax² + bx + c = 0 */
375
+ function tryQuadraticEq(text: string, lower: string, start: number): ComputeResult | null {
376
+ if (!GUARDS.hasX(text) || !text.includes("0")) return null;
377
+
378
+ const quadMatch = text.match(TIER3.quadratic);
379
+ if (!quadMatch?.[2] || !quadMatch[3] || !quadMatch[4] || !quadMatch[5]) return null;
380
+
381
+ const a = quadMatch[1] ? parseInt(quadMatch[1], 10) : 1;
382
+ const bSign = quadMatch[2] === "-" ? -1 : 1;
383
+ const b = bSign * parseInt(quadMatch[3], 10);
384
+ const cSign = quadMatch[4] === "-" ? -1 : 1;
385
+ const c = cSign * parseInt(quadMatch[5], 10);
386
+ const discriminant = b * b - 4 * a * c;
387
+
388
+ if (discriminant < 0) return null;
389
+
390
+ const r1 = (-b + Math.sqrt(discriminant)) / (2 * a);
391
+ const r2 = (-b - Math.sqrt(discriminant)) / (2 * a);
392
+
393
+ if (lower.includes("larger") || lower.includes("greater") || lower.includes("bigger")) {
394
+ return {
395
+ solved: true,
396
+ result: Math.max(r1, r2),
397
+ method: "quadratic_larger",
398
+ confidence: 1.0,
399
+ time_ms: performance.now() - start,
400
+ };
401
+ }
402
+ if (lower.includes("smaller") || lower.includes("lesser")) {
403
+ return {
404
+ solved: true,
405
+ result: Math.min(r1, r2),
406
+ method: "quadratic_smaller",
407
+ confidence: 1.0,
408
+ time_ms: performance.now() - start,
409
+ };
410
+ }
411
+ return {
412
+ solved: true,
413
+ result: r1 === r2 ? r1 : `${r1}, ${r2}`,
414
+ method: "quadratic",
415
+ confidence: 0.95,
416
+ time_ms: performance.now() - start,
417
+ };
418
+ }
419
+
420
+ /** Try combinations: "10 choose 3", nCr */
421
+ function tryCombinationsFormula(text: string, lower: string, start: number): ComputeResult | null {
422
+ if (!lower.includes("choose") && !/ c /i.test(text) && !lower.includes("combination")) {
423
+ return null;
424
+ }
425
+
426
+ const combMatch =
427
+ text.match(TIER3.combinationsChoose) ||
428
+ text.match(TIER3.combinationsFrom) ||
429
+ text.match(TIER3.combinationsHowMany);
430
+
431
+ if (!combMatch?.[1] || !combMatch[2]) return null;
432
+
433
+ let n = parseInt(combMatch[1], 10);
434
+ let k = parseInt(combMatch[2], 10);
435
+ if (combMatch[0].includes("from") && n < k) {
436
+ [n, k] = [k, n];
437
+ }
438
+
439
+ if (n >= k && k >= 0 && n <= 100) {
440
+ return {
441
+ solved: true,
442
+ result: combinations(n, k),
443
+ method: "combinations",
444
+ confidence: 1.0,
445
+ time_ms: performance.now() - start,
446
+ };
447
+ }
448
+ return null;
449
+ }
450
+
451
+ /** Try permutations: "10 P 3", nPr */
452
+ function tryPermutationsFormula(text: string, lower: string, start: number): ComputeResult | null {
453
+ if (!/ p /i.test(text) && !lower.includes("permutation") && !lower.includes("arrangement")) {
454
+ return null;
455
+ }
456
+
457
+ const permMatch = text.match(TIER3.permutationsP) || text.match(TIER3.permutationsWord);
458
+ if (!permMatch?.[1] || !permMatch[2]) return null;
459
+
460
+ const n = parseInt(permMatch[1], 10);
461
+ const k = parseInt(permMatch[2], 10);
462
+
463
+ if (n >= k && k >= 0 && n <= 100) {
464
+ return {
465
+ solved: true,
466
+ result: permutations(n, k),
467
+ method: "permutations",
468
+ confidence: 1.0,
469
+ time_ms: performance.now() - start,
470
+ };
471
+ }
472
+ return null;
473
+ }
474
+
475
+ /** Try last digit calculation: "7^100 mod 10" */
476
+ function tryLastDigit(text: string, lower: string, start: number): ComputeResult | null {
477
+ if (!lower.includes("last digit") && !/mod\s*10/i.test(text)) return null;
478
+
479
+ const lastDigitMatch = text.match(TIER3.lastDigitMod);
480
+ if (!lastDigitMatch) return null;
481
+
482
+ const base = parseInt(lastDigitMatch[1] || lastDigitMatch[3] || "", 10);
483
+ const exp = parseInt(lastDigitMatch[2] || lastDigitMatch[4] || "", 10);
484
+
485
+ if (Number.isNaN(base) || Number.isNaN(exp) || exp <= 0) return null;
486
+
487
+ const lastDigits = [base % 10];
488
+ let current = base % 10;
489
+ for (let i = 1; i < 4; i++) {
490
+ current = (current * (base % 10)) % 10;
491
+ if (current === lastDigits[0]) break;
492
+ lastDigits.push(current);
493
+ }
494
+
495
+ return {
496
+ solved: true,
497
+ result: lastDigits[(exp - 1) % lastDigits.length],
498
+ method: "last_digit",
499
+ confidence: 1.0,
500
+ time_ms: performance.now() - start,
501
+ };
502
+ }
503
+
504
+ /**
505
+ * TIER 3: Medium-cost formulas (more complex patterns)
506
+ * - Logarithms, quadratic, combinations, permutations, last digit
507
+ */
508
+ function tryFormulaTier3(text: string, lower: string): ComputeResult | null {
509
+ const start = performance.now();
510
+
511
+ return (
512
+ tryLogarithm(text, lower, start) ||
513
+ tryQuadraticEq(text, lower, start) ||
514
+ tryCombinationsFormula(text, lower, start) ||
515
+ tryPermutationsFormula(text, lower, start) ||
516
+ tryLastDigit(text, lower, start)
517
+ );
518
+ }
519
+
520
+ /**
521
+ * TIER 4: Expensive formulas (geometry, series, finance, matrix)
522
+ * - Pythagorean, trailing zeros, geometric series, compound interest, determinant
523
+ */
524
+ function tryFormulaTier4(text: string, lower: string): ComputeResult | null {
525
+ const start = performance.now();
526
+
527
+ // PYTHAGOREAN: guard on "hypoten" (hypotenuse)
528
+ if (lower.includes("hypoten")) {
529
+ for (const pattern of TIER4.pythagorean) {
530
+ const match = text.match(pattern);
531
+ if (match?.[1] && match[2]) {
532
+ const a = parseFloat(match[1]);
533
+ const b = parseFloat(match[2]);
534
+ if (!Number.isNaN(a) && !Number.isNaN(b)) {
535
+ return solved(formatResult(Math.sqrt(a * a + b * b)), "pythagorean", start);
536
+ }
537
+ }
538
+ }
539
+ }
540
+
541
+ // TRAILING ZEROS: guard on "trailing"
542
+ if (lower.includes("trailing")) {
543
+ const trailingMatch = text.match(TIER4.trailingZeros);
544
+ if (trailingMatch?.[1]) {
545
+ const n = parseInt(trailingMatch[1], 10);
546
+ if (n >= 0 && n <= 1_000_000) {
547
+ let zeros = 0;
548
+ let power = 5;
549
+ while (power <= n) {
550
+ zeros += Math.floor(n / power);
551
+ power *= 5;
552
+ }
553
+ return solved(zeros, "trailing_zeros", start);
554
+ }
555
+ }
556
+ }
557
+
558
+ // GEOMETRIC SERIES: guard on "infinite", "series", "sum", or "..."
559
+ if (
560
+ lower.includes("infinite") ||
561
+ lower.includes("series") ||
562
+ text.includes("...") ||
563
+ lower.includes("sum")
564
+ ) {
565
+ for (const pattern of TIER4.geometricSeries) {
566
+ const geoSeriesMatch = text.match(pattern);
567
+ if (geoSeriesMatch?.[1]) {
568
+ const r = 1 / parseInt(geoSeriesMatch[1], 10);
569
+ if (r > 0 && r < 1) {
570
+ return solved(formatResult(1 / (1 - r)), "geometric_series", start);
571
+ }
572
+ }
573
+ }
574
+ }
575
+
576
+ // MATRIX DETERMINANT: guard on "[" and "det"
577
+ if (GUARDS.hasBracket(text) && (lower.includes("det") || lower.includes("determinant"))) {
578
+ const matrix = parseMatrix(text);
579
+ if (matrix && matrix.length > 0) {
580
+ const det = matrixDeterminant(matrix);
581
+ if (det !== null) {
582
+ return solved(formatResult(det), `determinant_${matrix.length}x${matrix.length}`, start);
583
+ }
584
+ }
585
+ }
586
+
587
+ // COMPOUND INTEREST: guard on "$" or "interest"
588
+ if (GUARDS.hasDollar(text) || lower.includes("interest")) {
589
+ const compoundMatch = text.match(TIER4.compoundInterest);
590
+ if (compoundMatch?.[1] && compoundMatch[2] && compoundMatch[3]) {
591
+ const P = parseFloat(compoundMatch[1].replace(/,/g, ""));
592
+ const r = parseFloat(compoundMatch[2]) / 100;
593
+ const t = parseInt(compoundMatch[3], 10);
594
+ return solved(Math.round(P * (1 + r) ** t), "compound_interest", start);
595
+ }
596
+ }
597
+
598
+ return null;
599
+ }
600
+
601
+ // =============================================================================
602
+ // EXPRESSION CANONICALIZATION
603
+ // =============================================================================
604
+
605
+ /**
606
+ * Extract and canonicalize a math expression from text
607
+ * Uses simplifyAST to normalize expressions before pattern matching
608
+ *
609
+ * Benefits:
610
+ * - "x + 0" → "x" (identity removal)
611
+ * - "2 + 3" → "5" (constant folding)
612
+ * - "x * 1" → "x" (multiplication identity)
613
+ *
614
+ * @returns Canonicalized expression string, or null if not parseable
615
+ */
616
+ export function canonicalizeExpression(expr: string): string | null {
617
+ const { tokens, errors } = tokenizeMathExpression(expr);
618
+ if (errors.length > 0) return null;
619
+
620
+ const { ast, error } = buildAST(tokens);
621
+ if (error || !ast) return null;
622
+
623
+ const simplified = simplifyAST(ast);
624
+ return astToString(simplified);
625
+ }
626
+
627
+ /**
628
+ * Convert an AST node back to a string representation
629
+ * @internal
630
+ */
631
+ function astToString(node: ASTNode): string {
632
+ switch (node.type) {
633
+ case "number":
634
+ return String(node.value);
635
+ case "variable":
636
+ return node.name;
637
+ case "unary":
638
+ if (node.operator === "²" || node.operator === "³") {
639
+ return `(${astToString(node.operand)})${node.operator}`;
640
+ }
641
+ return `${node.operator}(${astToString(node.operand)})`;
642
+ case "binary": {
643
+ const left = astToString(node.left);
644
+ const right = astToString(node.right);
645
+ // Add parentheses for clarity
646
+ return `(${left} ${node.operator} ${right})`;
647
+ }
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Try to simplify an expression and check if it reduces to a constant
653
+ * Useful for detecting expressions like "x - x" → 0 or "x/x" → 1
654
+ *
655
+ * @returns The constant value if fully reducible, null otherwise
656
+ */
657
+ export function trySimplifyToConstant(expr: string): number | null {
658
+ const { tokens, errors } = tokenizeMathExpression(expr);
659
+ if (errors.length > 0) return null;
660
+
661
+ const { ast, error } = buildAST(tokens);
662
+ if (error || !ast) return null;
663
+
664
+ const simplified = simplifyAST(ast);
665
+
666
+ if (simplified.type === "number") {
667
+ return simplified.value;
668
+ }
669
+
670
+ return null;
671
+ }
672
+
673
+ /**
674
+ * Formula-based computation
675
+ * Recognizes common mathematical formulas from natural language
676
+ * Uses tiered pattern matching: cheap checks first, expensive last
677
+ */
678
+ export function tryFormula(text: string): ComputeResult {
679
+ const lower = text.toLowerCase();
680
+
681
+ // Tier 1: Ultra-fast (percentage, factorial, modulo, prime, fibonacci)
682
+ const t1 = tryFormulaTier1(text, lower);
683
+ if (t1) return t1;
684
+
685
+ // Tier 2: Fast (sqrt, power, gcd, lcm)
686
+ const t2 = tryFormulaTier2(text, lower);
687
+ if (t2) return t2;
688
+
689
+ // Tier 3: Medium (log, quadratic, combinations, permutations, last digit)
690
+ const t3 = tryFormulaTier3(text, lower);
691
+ if (t3) return t3;
692
+
693
+ // Tier 4: Expensive (pythagorean, trailing zeros, series, matrix, interest)
694
+ const t4 = tryFormulaTier4(text, lower);
695
+ if (t4) return t4;
696
+
697
+ // Tier 5: Try algebraic simplification as last resort
698
+ // Detects patterns like "x - x = ?", "x/x = ?", "x + 0 = ?"
699
+ const simplifyMatch = text.match(
700
+ /(?:what is|simplify|evaluate)?\s*([\w\d.+\-*/^×÷−·√²³()\s]+)\s*[=?]/i,
701
+ );
702
+ if (simplifyMatch?.[1]) {
703
+ const start = performance.now();
704
+ const constant = trySimplifyToConstant(simplifyMatch[1].trim());
705
+ if (constant !== null) {
706
+ return solved(constant, "algebraic_simplification", start);
707
+ }
708
+ }
709
+
710
+ return { solved: false, confidence: 0 };
711
+ }
712
+
713
+ // =============================================================================
714
+ // SOLVER REGISTRATION
715
+ // =============================================================================
716
+
717
+ export const solver: Solver = {
718
+ name: "formula",
719
+ description:
720
+ "Mathematical formulas: %, factorial, sqrt, gcd, lcm, log, quadratic, combinations, permutations, series, matrix determinant",
721
+ types:
722
+ SolverType.FORMULA_TIER1 |
723
+ SolverType.FORMULA_TIER2 |
724
+ SolverType.FORMULA_TIER3 |
725
+ SolverType.FORMULA_TIER4,
726
+ priority: 20,
727
+ solve: (text, _lower) => tryFormula(text),
728
+ };