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,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probability Solver - Handles simple probability patterns
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Independent events: "fair coin landed heads N times, probability of heads?" → 50%
|
|
6
|
+
* - Gambler's fallacy detection: Previous outcomes don't affect independent events
|
|
7
|
+
* - Hot hand fallacy: Streak doesn't change underlying probability
|
|
8
|
+
* - Birthday paradox: "N people, probability at least 2 share birthday?"
|
|
9
|
+
*
|
|
10
|
+
* O(n) pattern matching - no backtracking, single-pass regex
|
|
11
|
+
*
|
|
12
|
+
* Key mathematical insight (from research):
|
|
13
|
+
* For independent events, P(A_n | A_1 ∩ A_2 ∩ ... ∩ A_{n-1}) = P(A_n)
|
|
14
|
+
* A fair coin has P(heads) = 0.5 regardless of previous outcomes.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { SolverType } from "../classifier.ts";
|
|
18
|
+
import type { ComputeResult, Solver } from "../types.ts";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// PATTERNS
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
const PATTERNS = {
|
|
25
|
+
// Fair coin after streak: "fair coin landed heads N times, probability of heads?"
|
|
26
|
+
// Captures: the number of times (optional), "heads" or "tails", asking about next flip
|
|
27
|
+
fairCoinStreak:
|
|
28
|
+
/fair\s+coin.*(?:landed|flipped|come\s+up|gotten?)\s+(?:heads|tails)\s*(\d+)?\s*(?:times?)?\s*(?:in\s+a\s+row)?[^.?]*(?:probability|chance|what['']?s|likely)/i,
|
|
29
|
+
|
|
30
|
+
// Independent events with stated probability
|
|
31
|
+
// "independent with 50% success rate" or "each shot is independent with 50%"
|
|
32
|
+
independentEvent:
|
|
33
|
+
/(?:independent|each\s+(?:shot|flip|roll|event)\s+is\s+independent).*?(\d+(?:\.\d+)?)\s*%\s*(?:success|rate|chance|probability)/i,
|
|
34
|
+
|
|
35
|
+
// Gambler's fallacy explicit: "due for a win" patterns
|
|
36
|
+
gamblersFallacy: /(?:due\s+for|must\s+(?:be|get)|bound\s+to|has\s+to|overdue)/i,
|
|
37
|
+
|
|
38
|
+
// Hot hand patterns: "on a streak" + asking about probability
|
|
39
|
+
hotHand:
|
|
40
|
+
/(?:made|hit|scored|succeeded)\s+(\d+)\s+(?:shots?|times?|in\s+a\s+row).*?(?:probability|chance|what['']?s|likely).*?(?:next|following)/i,
|
|
41
|
+
|
|
42
|
+
// Direct probability question for fair coin
|
|
43
|
+
directFairCoin:
|
|
44
|
+
/(?:probability|chance)\s+(?:of\s+)?(?:the\s+)?next\s+(?:flip|toss|coin).*(?:heads|tails)/i,
|
|
45
|
+
|
|
46
|
+
// "What's the probability" patterns for independent events
|
|
47
|
+
whatProbability:
|
|
48
|
+
/what['']?s?\s+(?:the\s+)?(?:probability|chance).*(?:next|following)\s+(?:flip|shot|roll|toss)/i,
|
|
49
|
+
|
|
50
|
+
// Birthday paradox: "N people in a room, probability at least 2 share birthday"
|
|
51
|
+
birthdayParadox:
|
|
52
|
+
/(\d+)\s*(?:people|persons?|students?|guests?|employees?|members?).*?(?:probability|chance|what['']?s|likely).*?(?:at\s+least\s+(?:two|2)|two\s+or\s+more|same\s+birthday|share\s+(?:a\s+)?birthday)/i,
|
|
53
|
+
|
|
54
|
+
// Alternative birthday pattern: "probability that at least 2 of N people share a birthday"
|
|
55
|
+
birthdayParadoxAlt:
|
|
56
|
+
/(?:probability|chance).*?(?:at\s+least\s+(?:two|2)|two\s+or\s+more).*?(\d+)\s*(?:people|persons?|students?|guests?).*?(?:same\s+birthday|share\s+(?:a\s+)?birthday)/i,
|
|
57
|
+
|
|
58
|
+
// Birthday pattern starting with "In a room of N people"
|
|
59
|
+
birthdayRoom:
|
|
60
|
+
/(?:in\s+a\s+)?(?:room|group|class|team)\s+(?:of\s+)?(\d+)\s*(?:people|persons?|students?|guests?).*?(?:birthday|born\s+on\s+the\s+same)/i,
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// GUARDS (cheap detection before expensive regex)
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/** @internal */
|
|
68
|
+
function hasFairCoin(lower: string): boolean {
|
|
69
|
+
return lower.includes("fair") && lower.includes("coin");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @internal */
|
|
73
|
+
function hasIndependent(lower: string): boolean {
|
|
74
|
+
return lower.includes("independent");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect if this is a birthday paradox question
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
function hasBirthdayContext(lower: string): boolean {
|
|
82
|
+
return (
|
|
83
|
+
lower.includes("birthday") ||
|
|
84
|
+
(lower.includes("share") && lower.includes("born")) ||
|
|
85
|
+
(lower.includes("same day") && lower.includes("people"))
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detect if this is asking about probability of next event
|
|
91
|
+
* NOT just mentioning "chance" or "%" in context of expected value
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
function hasProbabilityQuestion(lower: string): boolean {
|
|
95
|
+
// Exclude expected value / decision questions
|
|
96
|
+
if (lower.includes("expected value") || lower.includes("which has higher")) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
lower.includes("probability") ||
|
|
102
|
+
// "chance" must be about asking probability, not stating odds like "100% chance of $50"
|
|
103
|
+
(lower.includes("chance") && (lower.includes("next") || lower.includes("at least"))) ||
|
|
104
|
+
lower.includes("what's the prob") ||
|
|
105
|
+
lower.includes("what is the prob")
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @internal */
|
|
110
|
+
function hasStreakContext(lower: string): boolean {
|
|
111
|
+
return (
|
|
112
|
+
lower.includes("in a row") ||
|
|
113
|
+
lower.includes("times") ||
|
|
114
|
+
lower.includes("made") ||
|
|
115
|
+
lower.includes("landed") ||
|
|
116
|
+
lower.includes("flipped")
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// HELPERS
|
|
122
|
+
// =============================================================================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract stated probability from text (e.g., "50% success rate" → 50)
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
function extractStatedProbability(text: string): number | null {
|
|
129
|
+
// Look for explicit percentage
|
|
130
|
+
const match = text.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
131
|
+
if (match?.[1]) {
|
|
132
|
+
return parseFloat(match[1]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Look for "1/2" or "50-50" patterns
|
|
136
|
+
if (/50[\s-]*50|1\/2|one\s+half/i.test(text)) {
|
|
137
|
+
return 50;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if question asks about percentage vs decimal
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
function wantsPercentage(text: string): boolean {
|
|
148
|
+
const lower = text.toLowerCase();
|
|
149
|
+
return (
|
|
150
|
+
lower.includes("percent") ||
|
|
151
|
+
lower.includes("%") ||
|
|
152
|
+
lower.includes("as a percentage") ||
|
|
153
|
+
lower.includes("answer as percent")
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Calculate birthday paradox probability
|
|
159
|
+
* P(at least 2 share) = 1 - P(all different)
|
|
160
|
+
* P(all different) = 365/365 * 364/365 * ... * (365-n+1)/365
|
|
161
|
+
*
|
|
162
|
+
* @param n - Number of people
|
|
163
|
+
* @returns Probability as percentage (0-100)
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
function birthdayParadoxProbability(n: number): number {
|
|
167
|
+
if (n <= 1) return 0;
|
|
168
|
+
if (n >= 365) return 100;
|
|
169
|
+
|
|
170
|
+
// Calculate P(all different) = ∏(365-i)/365 for i=0 to n-1
|
|
171
|
+
let pAllDifferent = 1;
|
|
172
|
+
for (let i = 0; i < n; i++) {
|
|
173
|
+
pAllDifferent *= (365 - i) / 365;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// P(at least 2 share) = 1 - P(all different)
|
|
177
|
+
// Clamp to 100 to guard against floating-point precision loss
|
|
178
|
+
return Math.min(100, (1 - pAllDifferent) * 100);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// SOLVER
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
export function tryProbability(text: string): ComputeResult {
|
|
186
|
+
const start = performance.now();
|
|
187
|
+
const lower = text.toLowerCase();
|
|
188
|
+
|
|
189
|
+
// Quick exit if no probability-related keywords
|
|
190
|
+
if (!hasProbabilityQuestion(lower) && !hasBirthdayContext(lower)) {
|
|
191
|
+
return { solved: false, confidence: 0 };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// BIRTHDAY PARADOX: "N people, probability at least 2 share birthday?"
|
|
195
|
+
if (hasBirthdayContext(lower)) {
|
|
196
|
+
let n: number | null = null;
|
|
197
|
+
|
|
198
|
+
// Try multiple patterns to extract N
|
|
199
|
+
const match1 = text.match(PATTERNS.birthdayParadox);
|
|
200
|
+
if (match1?.[1]) n = parseInt(match1[1], 10);
|
|
201
|
+
|
|
202
|
+
if (n === null) {
|
|
203
|
+
const match2 = text.match(PATTERNS.birthdayParadoxAlt);
|
|
204
|
+
if (match2?.[1]) n = parseInt(match2[1], 10);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (n === null) {
|
|
208
|
+
const match3 = text.match(PATTERNS.birthdayRoom);
|
|
209
|
+
if (match3?.[1]) n = parseInt(match3[1], 10);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (n !== null && n > 0 && n <= 1000) {
|
|
213
|
+
const probability = birthdayParadoxProbability(n);
|
|
214
|
+
// Round to nearest integer for percentage answers
|
|
215
|
+
// Default to percentage for birthday paradox (common convention)
|
|
216
|
+
const rounded = Math.round(probability);
|
|
217
|
+
// Only return decimal if explicitly asking for decimal/fraction
|
|
218
|
+
const wantsDecimal =
|
|
219
|
+
lower.includes("decimal") || lower.includes("fraction") || lower.includes("0.");
|
|
220
|
+
const result = wantsDecimal ? (probability / 100).toFixed(4) : String(rounded);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
solved: true,
|
|
224
|
+
result,
|
|
225
|
+
method: "birthday_paradox",
|
|
226
|
+
confidence: 1.0,
|
|
227
|
+
time_ms: performance.now() - start,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// FAIR COIN: Independent events with known 50% probability
|
|
233
|
+
// "A fair coin has landed heads 10 times in a row. What's the probability the next flip is heads?"
|
|
234
|
+
if (hasFairCoin(lower) && hasStreakContext(lower)) {
|
|
235
|
+
const match = text.match(PATTERNS.fairCoinStreak);
|
|
236
|
+
if (match) {
|
|
237
|
+
// Fair coin = 50% regardless of previous outcomes
|
|
238
|
+
const result = wantsPercentage(text) ? "50" : "0.5";
|
|
239
|
+
return {
|
|
240
|
+
solved: true,
|
|
241
|
+
result,
|
|
242
|
+
method: "fair_coin_independence",
|
|
243
|
+
confidence: 1.0,
|
|
244
|
+
time_ms: performance.now() - start,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Also catch simpler patterns
|
|
249
|
+
if (PATTERNS.directFairCoin.test(text) || PATTERNS.whatProbability.test(text)) {
|
|
250
|
+
const result = wantsPercentage(text) ? "50" : "0.5";
|
|
251
|
+
return {
|
|
252
|
+
solved: true,
|
|
253
|
+
result,
|
|
254
|
+
method: "fair_coin_direct",
|
|
255
|
+
confidence: 1.0,
|
|
256
|
+
time_ms: performance.now() - start,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// INDEPENDENT EVENTS WITH STATED PROBABILITY
|
|
262
|
+
// "shots are independent with 50% success rate, what's probability of next shot?"
|
|
263
|
+
if (hasIndependent(lower) && hasProbabilityQuestion(lower)) {
|
|
264
|
+
const statedProb = extractStatedProbability(text);
|
|
265
|
+
if (statedProb !== null) {
|
|
266
|
+
// For independent events, probability stays the same
|
|
267
|
+
const result = wantsPercentage(text) ? String(statedProb) : String(statedProb / 100);
|
|
268
|
+
return {
|
|
269
|
+
solved: true,
|
|
270
|
+
result,
|
|
271
|
+
method: "independent_event",
|
|
272
|
+
confidence: 1.0,
|
|
273
|
+
time_ms: performance.now() - start,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// HOT HAND: "made 5 shots in a row, assuming independent with 50%..."
|
|
279
|
+
if (hasStreakContext(lower) && hasIndependent(lower)) {
|
|
280
|
+
const statedProb = extractStatedProbability(text);
|
|
281
|
+
if (statedProb !== null) {
|
|
282
|
+
const result = wantsPercentage(text) ? String(statedProb) : String(statedProb / 100);
|
|
283
|
+
return {
|
|
284
|
+
solved: true,
|
|
285
|
+
result,
|
|
286
|
+
method: "hot_hand_independence",
|
|
287
|
+
confidence: 1.0,
|
|
288
|
+
time_ms: performance.now() - start,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { solved: false, confidence: 0 };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// =============================================================================
|
|
297
|
+
// SOLVER REGISTRATION
|
|
298
|
+
// =============================================================================
|
|
299
|
+
|
|
300
|
+
export const solver: Solver = {
|
|
301
|
+
name: "probability",
|
|
302
|
+
description:
|
|
303
|
+
"Independent events, fair coin, gambler's fallacy, hot hand fallacy, birthday paradox",
|
|
304
|
+
types: SolverType.PROBABILITY,
|
|
305
|
+
priority: 12, // After facts and arithmetic, before logic
|
|
306
|
+
solve: (text, _lower) => tryProbability(text),
|
|
307
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statistics Solver - Mean, median, standard error, expected value
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Mean/average of a list of numbers
|
|
6
|
+
* - Median of a list
|
|
7
|
+
* - Standard error (SE = SD / sqrt(n))
|
|
8
|
+
* - Expected value calculations
|
|
9
|
+
* - Permutations with repetition (MISSISSIPPI problem)
|
|
10
|
+
* - Handshake/combination counting
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { SolverType } from "../classifier.ts";
|
|
14
|
+
import { factorial } from "../math.ts";
|
|
15
|
+
import type { ComputeResult, Solver } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// HELPERS
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
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
|
+
* Extract numbers from text, handling various formats
|
|
33
|
+
* - "$30k" → 30000
|
|
34
|
+
* - "$1M" → 1000000
|
|
35
|
+
* - "30,000" → 30000
|
|
36
|
+
*/
|
|
37
|
+
function extractNumbers(text: string): number[] {
|
|
38
|
+
const numbers: number[] = [];
|
|
39
|
+
|
|
40
|
+
// Match currency with k/M suffix: $30k, $1M
|
|
41
|
+
const currencyMatches = text.matchAll(/\$(\d+(?:\.\d+)?)\s*([kKmMbB])?/g);
|
|
42
|
+
for (const m of currencyMatches) {
|
|
43
|
+
let val = parseFloat(m[1] || "0");
|
|
44
|
+
const suffix = m[2]?.toLowerCase();
|
|
45
|
+
if (suffix === "k") val *= 1000;
|
|
46
|
+
else if (suffix === "m") val *= 1000000;
|
|
47
|
+
else if (suffix === "b") val *= 1000000000;
|
|
48
|
+
numbers.push(val);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If we found currency, return those
|
|
52
|
+
if (numbers.length > 0) return numbers;
|
|
53
|
+
|
|
54
|
+
// Match plain numbers (with optional commas)
|
|
55
|
+
const plainMatches = text.matchAll(/(?<!\$)(\d{1,3}(?:,\d{3})*(?:\.\d+)?|\d+(?:\.\d+)?)/g);
|
|
56
|
+
for (const m of plainMatches) {
|
|
57
|
+
const val = parseFloat((m[1] || "0").replace(/,/g, ""));
|
|
58
|
+
if (!Number.isNaN(val)) numbers.push(val);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return numbers;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// MEAN / AVERAGE
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
const MEAN_PATTERNS = [
|
|
69
|
+
/(?:mean|average)\s+(?:of\s+)?(?:income|value|number)s?[:\s]*(.+?)(?:\?|$)/i,
|
|
70
|
+
/(?:what\s+is\s+the\s+)?(?:mean|average)[:\s]+(.+?)(?:\?|$)/i,
|
|
71
|
+
/incomes?[:\s]+(.+?)\.\s*(?:mean|average)/i,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function tryMean(text: string, lower: string): ComputeResult | null {
|
|
75
|
+
if (!lower.includes("mean") && !lower.includes("average")) return null;
|
|
76
|
+
|
|
77
|
+
const start = performance.now();
|
|
78
|
+
|
|
79
|
+
for (const pattern of MEAN_PATTERNS) {
|
|
80
|
+
const match = text.match(pattern);
|
|
81
|
+
if (match?.[1]) {
|
|
82
|
+
const numbers = extractNumbers(match[1]);
|
|
83
|
+
if (numbers.length >= 2) {
|
|
84
|
+
const mean = numbers.reduce((a, b) => a + b, 0) / numbers.length;
|
|
85
|
+
// Check if asking for thousands
|
|
86
|
+
if (lower.includes("thousand") || lower.includes("in thousands")) {
|
|
87
|
+
return solved(Math.round(mean / 1000), "mean_thousands", start);
|
|
88
|
+
}
|
|
89
|
+
return solved(Math.round(mean * 100) / 100, "mean", start);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback: extract all numbers from text if "mean" is mentioned
|
|
95
|
+
const numbers = extractNumbers(text);
|
|
96
|
+
if (numbers.length >= 2 && (lower.includes("mean") || lower.includes("average"))) {
|
|
97
|
+
const mean = numbers.reduce((a, b) => a + b, 0) / numbers.length;
|
|
98
|
+
if (lower.includes("thousand") || lower.includes("in thousands")) {
|
|
99
|
+
return solved(Math.round(mean / 1000), "mean_thousands", start);
|
|
100
|
+
}
|
|
101
|
+
return solved(Math.round(mean * 100) / 100, "mean", start);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// STANDARD ERROR
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
function tryStandardError(text: string, lower: string): ComputeResult | null {
|
|
112
|
+
if (!lower.includes("standard error")) return null;
|
|
113
|
+
|
|
114
|
+
const start = performance.now();
|
|
115
|
+
|
|
116
|
+
// Pattern: "SD 10, n=100" or "standard deviation 10, sample size 100"
|
|
117
|
+
const sdMatch = text.match(/(?:sd|standard\s+deviation)[:\s=]*(\d+(?:\.\d+)?)/i);
|
|
118
|
+
// Must match "n=" or "n:" or standalone "n " at word boundary, or "sample size"
|
|
119
|
+
const nMatch = text.match(/(?:\bn\s*[=:]\s*|sample\s+size[:\s=]*)(\d+)/i);
|
|
120
|
+
|
|
121
|
+
if (sdMatch?.[1] && nMatch?.[1]) {
|
|
122
|
+
const sd = parseFloat(sdMatch[1]);
|
|
123
|
+
const n = parseInt(nMatch[1], 10);
|
|
124
|
+
if (n > 0) {
|
|
125
|
+
const se = sd / Math.sqrt(n);
|
|
126
|
+
return solved(Math.round(se * 1000) / 1000, "standard_error", start);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// =============================================================================
|
|
134
|
+
// EXPECTED VALUE
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
function tryExpectedValue(text: string, lower: string): ComputeResult | null {
|
|
138
|
+
if (!lower.includes("expected value") && !lower.includes("expected")) return null;
|
|
139
|
+
|
|
140
|
+
const start = performance.now();
|
|
141
|
+
|
|
142
|
+
// Lottery pattern: "1/N chance of $X" → EV = X/N
|
|
143
|
+
const lotteryMatch = text.match(/1\/(\d+(?:,\d{3})*)\s*chance\s+(?:of\s+)?\$?(\d+(?:,\d{3})*)/i);
|
|
144
|
+
if (lotteryMatch?.[1] && lotteryMatch[2]) {
|
|
145
|
+
const odds = parseInt(lotteryMatch[1].replace(/,/g, ""), 10);
|
|
146
|
+
const prize = parseInt(lotteryMatch[2].replace(/,/g, ""), 10);
|
|
147
|
+
const ev = prize / odds;
|
|
148
|
+
|
|
149
|
+
// Check for unit conversion
|
|
150
|
+
if (lower.includes("cents") || lower.includes("in cents")) {
|
|
151
|
+
return solved(Math.round(ev * 100), "expected_value_cents", start);
|
|
152
|
+
}
|
|
153
|
+
return solved(Math.round(ev * 100) / 100, "expected_value", start);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// =============================================================================
|
|
160
|
+
// PERMUTATIONS WITH REPETITION (MISSISSIPPI)
|
|
161
|
+
// =============================================================================
|
|
162
|
+
|
|
163
|
+
function tryPermutationsWithRepetition(text: string, lower: string): ComputeResult | null {
|
|
164
|
+
if (!lower.includes("arrange") && !lower.includes("permutation") && !lower.includes("ways")) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const start = performance.now();
|
|
169
|
+
|
|
170
|
+
// Pattern: "arrange the letters in WORD" or "arrangements of WORD"
|
|
171
|
+
const wordMatch = text.match(
|
|
172
|
+
/(?:arrange|arrangement|permutation|ways\s+to\s+arrange).*?(?:letters?\s+(?:in|of)\s+)?([A-Z]{4,})/i,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (wordMatch?.[1]) {
|
|
176
|
+
const word = wordMatch[1].toUpperCase();
|
|
177
|
+
const n = word.length;
|
|
178
|
+
|
|
179
|
+
// Count letter frequencies
|
|
180
|
+
const freq: Record<string, number> = {};
|
|
181
|
+
for (const char of word) {
|
|
182
|
+
freq[char] = (freq[char] || 0) + 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Formula: n! / (n1! * n2! * ... * nk!)
|
|
186
|
+
let denominator = 1;
|
|
187
|
+
for (const count of Object.values(freq)) {
|
|
188
|
+
denominator *= factorial(count);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = factorial(n) / denominator;
|
|
192
|
+
return solved(result, "permutations_repetition", start);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// =============================================================================
|
|
199
|
+
// HANDSHAKE PROBLEM
|
|
200
|
+
// =============================================================================
|
|
201
|
+
|
|
202
|
+
function tryHandshake(text: string, lower: string): ComputeResult | null {
|
|
203
|
+
if (!lower.includes("handshake") && !lower.includes("shakes hands")) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const start = performance.now();
|
|
208
|
+
|
|
209
|
+
// Pattern: "N people, each shakes hands with everyone else"
|
|
210
|
+
const match = text.match(
|
|
211
|
+
/(\d+)\s*(?:people|persons?|guests?|members?).*?(?:handshake|shakes?\s+hands)/i,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (match?.[1]) {
|
|
215
|
+
const n = parseInt(match[1], 10);
|
|
216
|
+
// Handshake formula: n choose 2 = n(n-1)/2
|
|
217
|
+
const result = (n * (n - 1)) / 2;
|
|
218
|
+
return solved(result, "handshake", start);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// =============================================================================
|
|
225
|
+
// MAIN SOLVER
|
|
226
|
+
// =============================================================================
|
|
227
|
+
|
|
228
|
+
export function tryStatistics(text: string): ComputeResult {
|
|
229
|
+
const lower = text.toLowerCase();
|
|
230
|
+
|
|
231
|
+
// Try standard error FIRST (before mean, since SE questions often contain "mean")
|
|
232
|
+
const se = tryStandardError(text, lower);
|
|
233
|
+
if (se) return se;
|
|
234
|
+
|
|
235
|
+
// Then try mean/average
|
|
236
|
+
const mean = tryMean(text, lower);
|
|
237
|
+
if (mean) return mean;
|
|
238
|
+
|
|
239
|
+
const ev = tryExpectedValue(text, lower);
|
|
240
|
+
if (ev) return ev;
|
|
241
|
+
|
|
242
|
+
const perm = tryPermutationsWithRepetition(text, lower);
|
|
243
|
+
if (perm) return perm;
|
|
244
|
+
|
|
245
|
+
const handshake = tryHandshake(text, lower);
|
|
246
|
+
if (handshake) return handshake;
|
|
247
|
+
|
|
248
|
+
return { solved: false, confidence: 0 };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// =============================================================================
|
|
252
|
+
// SOLVER REGISTRATION
|
|
253
|
+
// =============================================================================
|
|
254
|
+
|
|
255
|
+
export const solver: Solver = {
|
|
256
|
+
name: "statistics",
|
|
257
|
+
description:
|
|
258
|
+
"Statistics: mean, standard error, expected value, permutations with repetition, handshake problem",
|
|
259
|
+
types: SolverType.FORMULA_TIER3 | SolverType.WORD_PROBLEM,
|
|
260
|
+
priority: 25, // After formula, before word problems
|
|
261
|
+
solve: (text, _lower) => tryStatistics(text),
|
|
262
|
+
};
|