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.
- package/LICENSE +21 -0
- package/README.md +339 -0
- package/package.json +75 -0
- package/src/index.ts +38 -0
- package/src/lib/cache.ts +246 -0
- package/src/lib/compression.ts +804 -0
- package/src/lib/compute/cache.ts +86 -0
- package/src/lib/compute/classifier.ts +555 -0
- package/src/lib/compute/confidence.ts +79 -0
- package/src/lib/compute/context.ts +154 -0
- package/src/lib/compute/extract.ts +200 -0
- package/src/lib/compute/filter.ts +224 -0
- package/src/lib/compute/index.ts +171 -0
- package/src/lib/compute/math.ts +247 -0
- package/src/lib/compute/patterns.ts +564 -0
- package/src/lib/compute/registry.ts +145 -0
- package/src/lib/compute/solvers/arithmetic.ts +65 -0
- package/src/lib/compute/solvers/calculus.ts +249 -0
- package/src/lib/compute/solvers/derivation-core.ts +371 -0
- package/src/lib/compute/solvers/derivation-latex.ts +160 -0
- package/src/lib/compute/solvers/derivation-mistakes.ts +1046 -0
- package/src/lib/compute/solvers/derivation-simplify.ts +451 -0
- package/src/lib/compute/solvers/derivation-transform.ts +620 -0
- package/src/lib/compute/solvers/derivation.ts +67 -0
- package/src/lib/compute/solvers/facts.ts +120 -0
- package/src/lib/compute/solvers/formula.ts +728 -0
- package/src/lib/compute/solvers/index.ts +36 -0
- package/src/lib/compute/solvers/logic.ts +422 -0
- package/src/lib/compute/solvers/probability.ts +307 -0
- package/src/lib/compute/solvers/statistics.ts +262 -0
- package/src/lib/compute/solvers/word-problems.ts +408 -0
- package/src/lib/compute/types.ts +107 -0
- package/src/lib/concepts.ts +111 -0
- package/src/lib/domain.ts +731 -0
- package/src/lib/extraction.ts +912 -0
- package/src/lib/index.ts +122 -0
- package/src/lib/judge.ts +260 -0
- package/src/lib/math/ast.ts +842 -0
- package/src/lib/math/index.ts +8 -0
- package/src/lib/math/operators.ts +171 -0
- package/src/lib/math/tokenizer.ts +477 -0
- package/src/lib/patterns.ts +200 -0
- package/src/lib/session.ts +825 -0
- package/src/lib/think/challenge.ts +323 -0
- package/src/lib/think/complexity.ts +504 -0
- package/src/lib/think/confidence-drift.ts +507 -0
- package/src/lib/think/consistency.ts +347 -0
- package/src/lib/think/guidance.ts +188 -0
- package/src/lib/think/helpers.ts +568 -0
- package/src/lib/think/hypothesis.ts +216 -0
- package/src/lib/think/index.ts +127 -0
- package/src/lib/think/prompts.ts +262 -0
- package/src/lib/think/route.ts +358 -0
- package/src/lib/think/schema.ts +98 -0
- package/src/lib/think/scratchpad-schema.ts +662 -0
- package/src/lib/think/spot-check.ts +961 -0
- package/src/lib/think/types.ts +93 -0
- package/src/lib/think/verification.ts +260 -0
- package/src/lib/tokens.ts +177 -0
- package/src/lib/verification.ts +620 -0
- package/src/prompts/index.ts +10 -0
- package/src/prompts/templates.ts +336 -0
- package/src/resources/index.ts +8 -0
- package/src/resources/sessions.ts +196 -0
- package/src/tools/compress.ts +138 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/scratchpad.ts +2659 -0
- 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
|
+
};
|