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,408 @@
1
+ /**
2
+ * Word problem solvers - extract math from natural language
3
+ */
4
+
5
+ import { SolverType } from "../classifier.ts";
6
+ import { formatResult } from "../math.ts";
7
+ import { MULTI_STEP, WORD_PROBLEM_PATTERNS } from "../patterns.ts";
8
+ import type { ComputeResult, Entity, Solver } from "../types.ts";
9
+
10
+ // ============================================================================
11
+ // COGNITIVE REFLECTION TEST (CRT) PATTERNS
12
+ // These are classic "trap" problems that require algebraic setup
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Bat and Ball problem:
17
+ * "A bat and ball cost $X total. The bat costs $Y more than the ball.
18
+ * How much does the ball cost?"
19
+ *
20
+ * Setup: ball = x, bat = x + Y, total = X
21
+ * x + (x + Y) = X => 2x = X - Y => x = (X - Y) / 2
22
+ */
23
+ const BAT_BALL_PATTERN =
24
+ /(?:bat|racket|stick)\s+and\s+(?:a\s+)?ball\s+cost\s+\$?([\d.]+).*?(?:bat|racket|stick)\s+costs?\s+\$?([\d.]+)\s+more\s+than\s+(?:the\s+)?ball/i;
25
+
26
+ /**
27
+ * Lily pad doubling problem:
28
+ * "Lily pad doubles every day. Takes N days to cover lake.
29
+ * How many days to cover half?"
30
+ *
31
+ * Answer: N - 1 (if doubles on day N, half on day N-1)
32
+ */
33
+ const LILY_PAD_PATTERN =
34
+ /(?:lily\s*pad|patch|area)\s+(?:doubles|grows\s+twice).*?(\d+)\s+days?\s+to\s+cover\s+(?:the\s+)?(?:entire\s+)?(?:lake|pond|pool).*?(?:how\s+many\s+days?|when).*?(?:half|50%)/i;
35
+
36
+ /**
37
+ * Widget machine problem:
38
+ * "N machines take M minutes to make N widgets. How long for X machines to make X widgets?"
39
+ *
40
+ * Each machine makes 1 widget in M minutes. So X machines make X widgets in M minutes.
41
+ * Answer: M (not X)
42
+ */
43
+ const WIDGET_MACHINE_PATTERN =
44
+ /(\d+)\s+machines?\s+(?:take|takes?)\s+(\d+)\s+minutes?\s+to\s+(?:make|produce)\s+\1\s+widgets?.*?(\d+)\s+machines?\s+(?:to\s+)?(?:make|produce)\s+\3\s+widgets?/i;
45
+
46
+ /**
47
+ * Harmonic mean (round trip average speed):
48
+ * "Goes A→B at X mph, returns at Y mph. Average speed?"
49
+ *
50
+ * Answer: 2*X*Y / (X+Y) (harmonic mean, not arithmetic mean)
51
+ */
52
+ const HARMONIC_SPEED_PATTERN =
53
+ /(?:goes|travels?|drives?)\s+.*?at\s+(\d+)\s*(?:mph|km\/h|kmh).*?(?:returns?|back)\s+(?:at\s+)?(\d+)\s*(?:mph|km\/h|kmh).*?average\s+speed/i;
54
+
55
+ /**
56
+ * Achilles and Tortoise (catch-up problem):
57
+ * "A moves at X m/s, B moves at Y m/s with Z head start. When does A catch B?"
58
+ *
59
+ * Time = head_start / (speed_A - speed_B)
60
+ */
61
+ const CATCHUP_PATTERN =
62
+ /(\d+)\s*(?:m\/s|mph|kmh).*?(\d+)\s*(?:m\/s|mph|kmh).*?(\d+)\s*(?:m|meter|mile|km)\s*(?:head\s*start|ahead)/i;
63
+
64
+ /**
65
+ * Pigeonhole/sock drawer problem:
66
+ * "N items of type A, M items of type B. Minimum draws to guarantee a pair?"
67
+ *
68
+ * Answer: number_of_types + 1
69
+ */
70
+ const SOCK_DRAWER_PATTERN =
71
+ /(\d+)\s+(\w+)\s+(?:socks?|balls?|items?)\s+and\s+(\d+)\s+(\w+)\s+(?:socks?|balls?|items?).*?(?:minimum|least|fewest).*?(?:guarantee|ensure|certain)/i;
72
+
73
+ /**
74
+ * Try Cognitive Reflection Test style problems
75
+ */
76
+ export function tryCRTProblem(text: string): ComputeResult {
77
+ const start = performance.now();
78
+ const lower = text.toLowerCase();
79
+
80
+ // Bat and Ball
81
+ const batBall = text.match(BAT_BALL_PATTERN);
82
+ if (batBall?.[1] && batBall[2]) {
83
+ const total = parseFloat(batBall[1]);
84
+ const diff = parseFloat(batBall[2]);
85
+ // ball + (ball + diff) = total => ball = (total - diff) / 2
86
+ const ballCost = (total - diff) / 2;
87
+ if (ballCost >= 0 && Number.isFinite(ballCost)) {
88
+ // Check if asking for cents
89
+ const wantsCents = /cents?|¢/i.test(text);
90
+ const result = wantsCents ? Math.round(ballCost * 100) : ballCost;
91
+ return {
92
+ solved: true,
93
+ result: formatResult(result),
94
+ method: "crt_bat_ball",
95
+ confidence: 0.98,
96
+ time_ms: performance.now() - start,
97
+ };
98
+ }
99
+ }
100
+
101
+ // Lily Pad doubling
102
+ const lilyPad = text.match(LILY_PAD_PATTERN);
103
+ if (lilyPad?.[1]) {
104
+ const totalDays = parseInt(lilyPad[1], 10);
105
+ // Half coverage is one day before full coverage
106
+ const halfDays = totalDays - 1;
107
+ return {
108
+ solved: true,
109
+ result: String(halfDays),
110
+ method: "crt_lily_pad",
111
+ confidence: 0.98,
112
+ time_ms: performance.now() - start,
113
+ };
114
+ }
115
+
116
+ // Widget Machine
117
+ const widget = text.match(WIDGET_MACHINE_PATTERN);
118
+ if (widget?.[2]) {
119
+ // N machines make N widgets in M minutes means 1 machine makes 1 widget in M minutes
120
+ // So X machines make X widgets in M minutes (not X minutes!)
121
+ const minutes = parseInt(widget[2], 10);
122
+ return {
123
+ solved: true,
124
+ result: String(minutes),
125
+ method: "crt_widget",
126
+ confidence: 0.98,
127
+ time_ms: performance.now() - start,
128
+ };
129
+ }
130
+
131
+ // Harmonic mean (round trip speed)
132
+ const harmonic = text.match(HARMONIC_SPEED_PATTERN);
133
+ if (harmonic?.[1] && harmonic[2]) {
134
+ const speed1 = parseFloat(harmonic[1]);
135
+ const speed2 = parseFloat(harmonic[2]);
136
+ // Harmonic mean: 2 * s1 * s2 / (s1 + s2)
137
+ const avgSpeed = (2 * speed1 * speed2) / (speed1 + speed2);
138
+ return {
139
+ solved: true,
140
+ result: formatResult(avgSpeed),
141
+ method: "crt_harmonic",
142
+ confidence: 0.98,
143
+ time_ms: performance.now() - start,
144
+ };
145
+ }
146
+
147
+ // Catch-up problem (Achilles/Tortoise style)
148
+ const catchup = text.match(CATCHUP_PATTERN);
149
+ if (catchup?.[1] && catchup[2] && catchup[3]) {
150
+ const speed1 = parseFloat(catchup[1]);
151
+ const speed2 = parseFloat(catchup[2]);
152
+ const headStart = parseFloat(catchup[3]);
153
+ if (speed1 > speed2) {
154
+ const time = headStart / (speed1 - speed2);
155
+ // Round to nearest integer if close
156
+ const result = Math.abs(time - Math.round(time)) < 0.01 ? Math.round(time) : time;
157
+ return {
158
+ solved: true,
159
+ result: formatResult(result),
160
+ method: "crt_catchup",
161
+ confidence: 0.95,
162
+ time_ms: performance.now() - start,
163
+ };
164
+ }
165
+ }
166
+
167
+ // Sock drawer / pigeonhole
168
+ const sockMatch = text.match(SOCK_DRAWER_PATTERN);
169
+ if (sockMatch?.[2] && sockMatch[4] && /pair|matching/i.test(lower)) {
170
+ // With N types of items, need N+1 draws to guarantee a pair
171
+ const type1 = sockMatch[2].toLowerCase();
172
+ const type2 = sockMatch[4].toLowerCase();
173
+ // Count distinct types
174
+ const types = new Set([type1, type2]);
175
+ const minDraws = types.size + 1;
176
+ return {
177
+ solved: true,
178
+ result: String(minDraws),
179
+ method: "crt_pigeonhole",
180
+ confidence: 0.95,
181
+ time_ms: performance.now() - start,
182
+ };
183
+ }
184
+
185
+ return { solved: false, confidence: 0 };
186
+ }
187
+
188
+ /**
189
+ * Try to solve a word problem using pattern matching
190
+ */
191
+ export function tryWordProblem(text: string): ComputeResult {
192
+ const start = performance.now();
193
+
194
+ for (const { pattern, compute, method } of WORD_PROBLEM_PATTERNS) {
195
+ const match = text.match(pattern);
196
+ if (match) {
197
+ const result = compute(match);
198
+ if (result !== null && Number.isFinite(result)) {
199
+ const time_ms = performance.now() - start;
200
+ return {
201
+ solved: true,
202
+ result: formatResult(result),
203
+ method,
204
+ confidence: 0.95, // Slightly lower confidence for word problems
205
+ time_ms,
206
+ };
207
+ }
208
+ }
209
+ }
210
+
211
+ return { solved: false, confidence: 0 };
212
+ }
213
+
214
+ // =============================================================================
215
+ // MULTI-STEP WORD PROBLEM HELPERS (extracted to reduce cognitive complexity)
216
+ // =============================================================================
217
+
218
+ type OperationFn = (x: number) => number;
219
+
220
+ /** Extract simple binary relations (twice, half, triple) */
221
+ function extractSimpleRelation(
222
+ text: string,
223
+ pattern: RegExp,
224
+ operation: OperationFn,
225
+ entities: Map<string, Entity>,
226
+ ): void {
227
+ const regex = new RegExp(pattern.source, "gi");
228
+ let match: RegExpExecArray | null;
229
+ while ((match = regex.exec(text)) !== null) {
230
+ const name1 = match[1]?.toLowerCase();
231
+ const name2 = match[2]?.toLowerCase();
232
+ if (name1 && name2 && !entities.has(name1)) {
233
+ entities.set(name1, { name: name1, value: null, dependsOn: name2, operation });
234
+ if (!entities.has(name2)) {
235
+ entities.set(name2, { name: name2, value: null, dependsOn: null, operation: null });
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ /** Extract delta relations (more, less) where match[2] contains the delta */
242
+ function extractDeltaRelation(
243
+ text: string,
244
+ pattern: RegExp,
245
+ sign: 1 | -1,
246
+ entities: Map<string, Entity>,
247
+ ): void {
248
+ const regex = new RegExp(pattern.source, "gi");
249
+ let match: RegExpExecArray | null;
250
+ while ((match = regex.exec(text)) !== null) {
251
+ const name1 = match[1]?.toLowerCase();
252
+ const deltaStr = match[2];
253
+ const name2 = match[3]?.toLowerCase();
254
+ if (name1 && deltaStr && name2 && !entities.has(name1)) {
255
+ const delta = parseFloat(deltaStr);
256
+ entities.set(name1, {
257
+ name: name1,
258
+ value: null,
259
+ dependsOn: name2,
260
+ operation: (x) => x + sign * delta,
261
+ });
262
+ if (!entities.has(name2)) {
263
+ entities.set(name2, { name: name2, value: null, dependsOn: null, operation: null });
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ /** Extract direct values "[Name] has [number]" */
270
+ function extractDirectValues(text: string, entities: Map<string, Entity>): void {
271
+ const regex = new RegExp(MULTI_STEP.directValue.source, "gi");
272
+ let match: RegExpExecArray | null;
273
+ while ((match = regex.exec(text)) !== null) {
274
+ const name = match[1]?.toLowerCase();
275
+ const valueStr = match[2];
276
+ if (name && valueStr) {
277
+ const value = parseFloat(valueStr);
278
+ const existing = entities.get(name);
279
+ if (!existing) {
280
+ entities.set(name, { name, value, dependsOn: null, operation: null });
281
+ } else if (existing.value === null && !existing.dependsOn) {
282
+ existing.value = value;
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ /** Resolve entity dependencies via topological iteration */
289
+ function resolveDependencies(entities: Map<string, Entity>): void {
290
+ let changed = true;
291
+ let iterations = 0;
292
+ while (changed && iterations < 10) {
293
+ changed = false;
294
+ iterations++;
295
+ for (const entity of entities.values()) {
296
+ if (entity.value === null && entity.dependsOn && entity.operation) {
297
+ const dep = entities.get(entity.dependsOn);
298
+ if (dep?.value !== null && dep?.value !== undefined) {
299
+ entity.value = entity.operation(dep.value);
300
+ changed = true;
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ /** Try to answer a specific entity question */
308
+ function tryAnswerEntityQuestion(
309
+ text: string,
310
+ entities: Map<string, Entity>,
311
+ start: number,
312
+ ): ComputeResult | null {
313
+ const questionMatch = text.match(MULTI_STEP.question);
314
+ if (questionMatch?.[1]) {
315
+ const entity = entities.get(questionMatch[1].toLowerCase());
316
+ if (entity?.value !== null && entity?.value !== undefined) {
317
+ return {
318
+ solved: true,
319
+ result: formatResult(entity.value),
320
+ method: "multi_step_word",
321
+ confidence: 0.9,
322
+ time_ms: performance.now() - start,
323
+ };
324
+ }
325
+ }
326
+ return null;
327
+ }
328
+
329
+ /** Try to answer a "total" question */
330
+ function tryAnswerTotalQuestion(
331
+ lower: string,
332
+ entities: Map<string, Entity>,
333
+ start: number,
334
+ ): ComputeResult | null {
335
+ if (!/total|altogether|combined|sum/i.test(lower) || entities.size === 0) return null;
336
+
337
+ let total = 0;
338
+ for (const entity of entities.values()) {
339
+ if (entity.value === null) return null; // Not all resolved
340
+ total += entity.value;
341
+ }
342
+
343
+ return {
344
+ solved: true,
345
+ result: formatResult(total),
346
+ method: "multi_step_total",
347
+ confidence: 0.85,
348
+ time_ms: performance.now() - start,
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Try to solve multi-step word problems by extracting entities and resolving dependencies
354
+ * E.g., "John has twice as many as Mary, who has 5. How many does John have?"
355
+ */
356
+ export function tryMultiStepWordProblem(text: string): ComputeResult {
357
+ const start = performance.now();
358
+ const lower = text.toLowerCase();
359
+ const entities: Map<string, Entity> = new Map();
360
+
361
+ // Extract relations (order matters: specific patterns first)
362
+ extractSimpleRelation(text, MULTI_STEP.twice, (x) => x * 2, entities);
363
+ extractSimpleRelation(text, MULTI_STEP.half, (x) => x / 2, entities);
364
+ extractSimpleRelation(text, MULTI_STEP.triple, (x) => x * 3, entities);
365
+ extractDeltaRelation(text, MULTI_STEP.more, 1, entities);
366
+ extractDeltaRelation(text, MULTI_STEP.less, -1, entities);
367
+
368
+ // Extract direct values last
369
+ extractDirectValues(text, entities);
370
+
371
+ // Resolve dependencies
372
+ resolveDependencies(entities);
373
+
374
+ // Try to answer
375
+ return (
376
+ tryAnswerEntityQuestion(text, entities, start) ||
377
+ tryAnswerTotalQuestion(lower, entities, start) || { solved: false, confidence: 0 }
378
+ );
379
+ }
380
+
381
+ // =============================================================================
382
+ // SOLVER REGISTRATION
383
+ // =============================================================================
384
+
385
+ export const solvers: Solver[] = [
386
+ {
387
+ name: "crt_word",
388
+ description:
389
+ "Cognitive Reflection Test traps: bat-ball, lily pad doubling, widget machines, harmonic mean, catch-up, pigeonhole",
390
+ types: SolverType.WORD_PROBLEM,
391
+ priority: 25, // After formula, before regular word problems (more specific)
392
+ solve: (text, _lower) => tryCRTProblem(text),
393
+ },
394
+ {
395
+ name: "word_problem",
396
+ description: "Simple word problems: age, distance, percentage increase/decrease, profit",
397
+ types: SolverType.WORD_PROBLEM,
398
+ priority: 30,
399
+ solve: (text, _lower) => tryWordProblem(text),
400
+ },
401
+ {
402
+ name: "multi_step_word",
403
+ description: "Multi-step word problems with entity relationships (twice as many, N more than)",
404
+ types: SolverType.MULTI_STEP,
405
+ priority: 40,
406
+ solve: (text, _lower) => tryMultiStepWordProblem(text),
407
+ },
408
+ ];
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Type definitions for the compute module
3
+ */
4
+
5
+ export interface ComputeResult {
6
+ solved: boolean;
7
+ result?: string | number;
8
+ method?: string;
9
+ confidence: number;
10
+ time_ms?: number;
11
+ }
12
+
13
+ export interface ExtractedComputation {
14
+ original: string;
15
+ result: number | string;
16
+ method: string;
17
+ start: number;
18
+ end: number;
19
+ }
20
+
21
+ export interface AugmentedResult {
22
+ /** Original text with computations injected */
23
+ augmented: string;
24
+ /** List of all computations found and solved */
25
+ computations: ExtractedComputation[];
26
+ /** Whether any computations were found */
27
+ hasComputations: boolean;
28
+ /** Time taken in ms */
29
+ time_ms: number;
30
+ }
31
+
32
+ export interface CacheEntry {
33
+ result: ComputeResult;
34
+ timestamp: number;
35
+ }
36
+
37
+ export interface CacheStats {
38
+ hits: number;
39
+ misses: number;
40
+ size: number;
41
+ hitRate: number;
42
+ }
43
+
44
+ export interface WordProblemMatch {
45
+ pattern: RegExp;
46
+ compute: (match: RegExpMatchArray) => number | null;
47
+ method: string;
48
+ }
49
+
50
+ export interface Entity {
51
+ name: string;
52
+ value: number | null;
53
+ dependsOn: string | null;
54
+ operation: ((x: number) => number) | null;
55
+ }
56
+
57
+ export interface PolyTerm {
58
+ coeff: number;
59
+ exp: number;
60
+ }
61
+
62
+ export interface ComputeConfidence {
63
+ /** Overall confidence score (0-1) */
64
+ score: number;
65
+ /** Breakdown of what matched */
66
+ signals: {
67
+ positive: string[];
68
+ negative: string[];
69
+ };
70
+ /** Recommended action */
71
+ recommendation: "skip" | "try_local" | "try_local_first" | "local_only";
72
+ }
73
+
74
+ /** Weighted signal for confidence calculation */
75
+ export interface WeightedSignal {
76
+ pattern: RegExp;
77
+ weight: number;
78
+ name: string;
79
+ }
80
+
81
+ /** Negative signal for confidence calculation */
82
+ export interface NegativeSignal {
83
+ pattern: RegExp;
84
+ penalty: number;
85
+ name: string;
86
+ }
87
+
88
+ // =============================================================================
89
+ // SOLVER REGISTRY TYPES
90
+ // =============================================================================
91
+
92
+ /** Bitmask for solver types (imported from classifier in practice) */
93
+ export type SolverMask = number;
94
+
95
+ /** Solver registration interface - solvers export this */
96
+ export interface Solver {
97
+ /** Unique name for this solver */
98
+ name: string;
99
+ /** Human-readable description of what this solver handles */
100
+ description?: string;
101
+ /** Bitmask of solver types this handles */
102
+ types: SolverMask;
103
+ /** Priority within type (lower = run first) */
104
+ priority: number;
105
+ /** The solve function */
106
+ solve: (text: string, lower: string) => ComputeResult;
107
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Concept Tracker - Lightweight concept extraction and tracking
3
+ * Simplified from full ConceptWeb (no graph metrics for speed)
4
+ */
5
+
6
+ export interface Concept {
7
+ id: string;
8
+ name: string;
9
+ domain: "math" | "logic" | "code" | "language" | "general";
10
+ first_seen_step: number;
11
+ count: number;
12
+ }
13
+
14
+ // Domain-specific keyword patterns
15
+ const DOMAIN_KEYWORDS: Record<Concept["domain"], RegExp> = {
16
+ math: /\b(equation|variable|function|derivative|integral|sum|product|matrix|vector|polynomial|coefficient|algebra|calculus|theorem|proof|solve|calculate|compute)\b/gi,
17
+ logic:
18
+ /\b(therefore|implies|if|then|because|hence|thus|conclude|assume|given|premise|proposition|valid|invalid|fallacy|deduction|induction|axiom)\b/gi,
19
+ code: /\b(function|class|method|variable|loop|array|object|string|boolean|integer|algorithm|complexity|runtime|memory|pointer|reference|async|await|promise|callback)\b/gi,
20
+ language:
21
+ /\b(syntax|grammar|semantic|parse|token|lexer|compiler|interpreter|expression|statement|declaration)\b/gi,
22
+ general:
23
+ /\b(problem|solution|step|approach|strategy|method|technique|process|result|outcome|goal|objective)\b/gi,
24
+ };
25
+
26
+ export class ConceptTracker {
27
+ private concepts: Map<string, Concept> = new Map();
28
+
29
+ extract(thought: string, stepNumber: number): Concept[] {
30
+ const extracted: Concept[] = [];
31
+ const seen = new Set<string>();
32
+
33
+ for (const [domain, pattern] of Object.entries(DOMAIN_KEYWORDS)) {
34
+ const matches = thought.match(pattern) || [];
35
+
36
+ for (const match of matches) {
37
+ const name = match.toLowerCase();
38
+ if (seen.has(name)) continue;
39
+ seen.add(name);
40
+
41
+ const id = `${domain}:${name}`;
42
+ const existing = this.concepts.get(id);
43
+
44
+ if (existing) {
45
+ existing.count++;
46
+ } else {
47
+ const concept: Concept = {
48
+ id,
49
+ name,
50
+ domain: domain as Concept["domain"],
51
+ first_seen_step: stepNumber,
52
+ count: 1,
53
+ };
54
+ this.concepts.set(id, concept);
55
+ extracted.push(concept);
56
+ }
57
+ }
58
+ }
59
+
60
+ return extracted;
61
+ }
62
+
63
+ getAll(): Concept[] {
64
+ return Array.from(this.concepts.values());
65
+ }
66
+
67
+ getByDomain(domain: Concept["domain"]): Concept[] {
68
+ return this.getAll().filter((c) => c.domain === domain);
69
+ }
70
+
71
+ getTopConcepts(n: number = 5): Concept[] {
72
+ return this.getAll()
73
+ .sort((a, b) => b.count - a.count)
74
+ .slice(0, n);
75
+ }
76
+
77
+ getSummary(): { total: number; by_domain: Record<string, number>; top: string[] } {
78
+ const all = this.getAll();
79
+ const byDomain: Record<string, number> = {};
80
+
81
+ for (const c of all) {
82
+ byDomain[c.domain] = (byDomain[c.domain] || 0) + 1;
83
+ }
84
+
85
+ return {
86
+ total: all.length,
87
+ by_domain: byDomain,
88
+ top: this.getTopConcepts(5).map((c) => c.name),
89
+ };
90
+ }
91
+
92
+ clear(): void {
93
+ this.concepts.clear();
94
+ }
95
+ }
96
+
97
+ // Per-session concept trackers
98
+ const trackers: Map<string, ConceptTracker> = new Map();
99
+
100
+ export function getTracker(sessionId: string): ConceptTracker {
101
+ let tracker = trackers.get(sessionId);
102
+ if (!tracker) {
103
+ tracker = new ConceptTracker();
104
+ trackers.set(sessionId, tracker);
105
+ }
106
+ return tracker;
107
+ }
108
+
109
+ export function clearTracker(sessionId: string): void {
110
+ trackers.delete(sessionId);
111
+ }