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,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
+ };