@felkot/think-mcp 1.1.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 +165 -0
- package/dist/constants/index.d.ts +5 -0
- package/dist/constants/index.js +5 -0
- package/dist/constants/patterns.d.ts +8 -0
- package/dist/constants/patterns.js +34 -0
- package/dist/constants/prompts.d.ts +1 -0
- package/dist/constants/prompts.js +44 -0
- package/dist/constants/thresholds.d.ts +39 -0
- package/dist/constants/thresholds.js +54 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +711 -0
- package/dist/services/burst.service.d.ts +39 -0
- package/dist/services/burst.service.js +215 -0
- package/dist/services/coaching.service.d.ts +38 -0
- package/dist/services/coaching.service.js +258 -0
- package/dist/services/consolidate.service.d.ts +17 -0
- package/dist/services/consolidate.service.js +236 -0
- package/dist/services/context.service.d.ts +15 -0
- package/dist/services/context.service.js +113 -0
- package/dist/services/export.service.d.ts +33 -0
- package/dist/services/export.service.js +129 -0
- package/dist/services/insights.service.d.ts +103 -0
- package/dist/services/insights.service.js +216 -0
- package/dist/services/logic.service.d.ts +28 -0
- package/dist/services/logic.service.js +467 -0
- package/dist/services/nudge.service.d.ts +20 -0
- package/dist/services/nudge.service.js +114 -0
- package/dist/services/recall.service.d.ts +38 -0
- package/dist/services/recall.service.js +188 -0
- package/dist/services/stagnation.service.d.ts +14 -0
- package/dist/services/stagnation.service.js +48 -0
- package/dist/services/thinking.service.d.ts +263 -0
- package/dist/services/thinking.service.js +1048 -0
- package/dist/services/validation.service.d.ts +45 -0
- package/dist/services/validation.service.js +260 -0
- package/dist/services/visualization.service.d.ts +23 -0
- package/dist/services/visualization.service.js +211 -0
- package/dist/types/thought.types.d.ts +486 -0
- package/dist/types/thought.types.js +5 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/sensitive-data.d.ts +2 -0
- package/dist/utils/sensitive-data.js +28 -0
- package/dist/utils/storage-paths.d.ts +9 -0
- package/dist/utils/storage-paths.js +28 -0
- package/dist/utils/text-analysis.d.ts +30 -0
- package/dist/utils/text-analysis.js +92 -0
- package/package.json +66 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ValidationService - Thought sequence and path validation
|
|
3
|
+
* Stateless service - receives data as parameters
|
|
4
|
+
*/
|
|
5
|
+
import type { ThoughtInput, ThoughtRecord, ValidationResult, PathConnectivityResult } from '../types/thought.types.js';
|
|
6
|
+
export declare class ValidationService {
|
|
7
|
+
/**
|
|
8
|
+
* Validate thought sequence - prevent skipping steps and invalid revisions
|
|
9
|
+
* Also validates revision content is meaningfully different
|
|
10
|
+
* @param input - The thought input to validate
|
|
11
|
+
* @param sessionThoughts - Current session thoughts
|
|
12
|
+
* @param lastThoughtNumber - Last recorded thought number
|
|
13
|
+
*/
|
|
14
|
+
validateSequence(input: ThoughtInput, sessionThoughts: ThoughtRecord[], lastThoughtNumber: number): ValidationResult;
|
|
15
|
+
/**
|
|
16
|
+
* Validate thought quality - enforce substance and depth
|
|
17
|
+
* (v5.3.0) - Rejects lazy thoughts
|
|
18
|
+
*/
|
|
19
|
+
validateThoughtQuality(input: ThoughtInput, isComplexTask: boolean): ValidationResult;
|
|
20
|
+
/**
|
|
21
|
+
* Check semantic quality using entropy and pattern matching
|
|
22
|
+
* Acts as a lightweight "LLM-Validator"
|
|
23
|
+
*/
|
|
24
|
+
private checkSemanticQuality;
|
|
25
|
+
/**
|
|
26
|
+
* HARD duplicate check - returns error message if duplicate found
|
|
27
|
+
* Used for strict rejection before adding to history
|
|
28
|
+
* @param input - The thought input to check
|
|
29
|
+
* @param sessionThoughts - Current session thoughts
|
|
30
|
+
*/
|
|
31
|
+
checkDuplicateStrict(input: ThoughtInput, sessionThoughts: ThoughtRecord[]): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Validate branch source - reject if branchFromThought references non-existent thought
|
|
34
|
+
* @param input - The thought input to validate
|
|
35
|
+
* @param sessionThoughts - Current session thoughts
|
|
36
|
+
*/
|
|
37
|
+
validateBranchSource(input: ThoughtInput, sessionThoughts: ThoughtRecord[]): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Validate path connectivity - ensure thoughts in winningPath are logically connected
|
|
40
|
+
* Each thought must be reachable from its predecessor via sequence, branch, or revision
|
|
41
|
+
* @param winningPath - Array of thought numbers in the winning path
|
|
42
|
+
* @param sessionThoughts - Current session thoughts
|
|
43
|
+
*/
|
|
44
|
+
validatePathConnectivity(winningPath: number[], sessionThoughts: ThoughtRecord[]): PathConnectivityResult;
|
|
45
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ValidationService - Thought sequence and path validation
|
|
3
|
+
* Stateless service - receives data as parameters
|
|
4
|
+
*/
|
|
5
|
+
import { calculateJaccardSimilarity, calculateWordEntropy } from '../utils/index.js';
|
|
6
|
+
import { LAZY_THINKING_PATTERNS } from '../constants/patterns.js';
|
|
7
|
+
export class ValidationService {
|
|
8
|
+
/**
|
|
9
|
+
* Validate thought sequence - prevent skipping steps and invalid revisions
|
|
10
|
+
* Also validates revision content is meaningfully different
|
|
11
|
+
* @param input - The thought input to validate
|
|
12
|
+
* @param sessionThoughts - Current session thoughts
|
|
13
|
+
* @param lastThoughtNumber - Last recorded thought number
|
|
14
|
+
*/
|
|
15
|
+
validateSequence(input, sessionThoughts, lastThoughtNumber) {
|
|
16
|
+
// Validate revision target - can't revise future or non-existent thoughts
|
|
17
|
+
if (input.isRevision && input.revisesThought !== undefined) {
|
|
18
|
+
const targetThought = sessionThoughts.find((t) => t.thoughtNumber === input.revisesThought);
|
|
19
|
+
if (!targetThought) {
|
|
20
|
+
return {
|
|
21
|
+
valid: false,
|
|
22
|
+
warning: `🚫 INVALID REVISION: Cannot revise #${input.revisesThought}. Available: ${sessionThoughts.map((t) => t.thoughtNumber).join(', ')}`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Check revision is meaningfully different from original
|
|
26
|
+
const similarity = calculateJaccardSimilarity(input.thought, targetThought.thought);
|
|
27
|
+
if (similarity > 0.85) {
|
|
28
|
+
return {
|
|
29
|
+
valid: false,
|
|
30
|
+
warning: `⚠️ SHALLOW: ${Math.round(similarity * 100)}% similar. Rewrite substantially.`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Check for circular revision (revision text similar to even earlier thought)
|
|
34
|
+
const earlierThoughts = sessionThoughts.filter((t) => t.thoughtNumber < input.revisesThought && !t.isRevision);
|
|
35
|
+
for (const earlier of earlierThoughts) {
|
|
36
|
+
const circularSimilarity = calculateJaccardSimilarity(input.thought, earlier.thought);
|
|
37
|
+
if (circularSimilarity > 0.8) {
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
warning: `🔄 CIRCULAR: ${Math.round(circularSimilarity * 100)}% similar to #${earlier.thoughtNumber}. New approach needed.`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Allow revisions and branches to jump in sequence
|
|
46
|
+
if (input.isRevision || input.branchFromThought) {
|
|
47
|
+
return { valid: true };
|
|
48
|
+
}
|
|
49
|
+
// First thought is always valid
|
|
50
|
+
if (lastThoughtNumber === 0) {
|
|
51
|
+
return { valid: true };
|
|
52
|
+
}
|
|
53
|
+
const expectedNext = lastThoughtNumber + 1;
|
|
54
|
+
if (input.thoughtNumber > expectedNext) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
warning: `⚠️ Sequence break: expected #${expectedNext}, got #${input.thoughtNumber}.`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { valid: true };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate thought quality - enforce substance and depth
|
|
64
|
+
* (v5.3.0) - Rejects lazy thoughts
|
|
65
|
+
*/
|
|
66
|
+
validateThoughtQuality(input, isComplexTask) {
|
|
67
|
+
const isHeavyTask = input.totalThoughts >= 8;
|
|
68
|
+
// 1. Check Phase Logic
|
|
69
|
+
if (input.phase) {
|
|
70
|
+
if (input.phase === 'execution' && input.thoughtNumber === 1) {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
warning: `⚠️ ILLEGAL PHASE JUMP: You cannot start with 'execution'. Begin with 'initialization' or 'analysis'.`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (isHeavyTask &&
|
|
78
|
+
input.nextThoughtNeeded &&
|
|
79
|
+
input.thoughtNumber > 1 &&
|
|
80
|
+
(!input.subSteps || input.subSteps.length === 0) &&
|
|
81
|
+
(!input.alternatives || input.alternatives.length === 0)) {
|
|
82
|
+
return {
|
|
83
|
+
valid: false,
|
|
84
|
+
warning: '⚠️ HEAVY TASK: Add subSteps or alternatives for this step to avoid shallow linear reasoning.'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (isHeavyTask &&
|
|
88
|
+
!input.nextThoughtNeeded &&
|
|
89
|
+
input.thoughtNumber < Math.ceil(input.totalThoughts * 0.7)) {
|
|
90
|
+
return {
|
|
91
|
+
valid: false,
|
|
92
|
+
warning: '⚠️ PREMATURE FINISH: You are ending too early for a heavy task. Continue reasoning or lower totalThoughts with justification.'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// 1. Minimum Length Check
|
|
96
|
+
if (input.thought.length < 20) {
|
|
97
|
+
return {
|
|
98
|
+
valid: false,
|
|
99
|
+
warning: `⚠️ TOO SHORT: Thought is too brief (${input.thought.length} chars). Expand your reasoning.`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// 2. Forbidden Weak Phrases
|
|
103
|
+
const weakPhrases = ['i hope', 'maybe', 'probably', 'might work', 'guess'];
|
|
104
|
+
const foundWeakness = weakPhrases.find((phrase) => input.thought.toLowerCase().includes(phrase));
|
|
105
|
+
if (foundWeakness) {
|
|
106
|
+
return {
|
|
107
|
+
valid: false,
|
|
108
|
+
warning: `⚠️ WEAK LANGUAGE: Avoid "${foundWeakness}". Be precise or use 'think_diverge' to explore uncertainty.`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// 3. Substeps for Complex Tasks (Middle of chain)
|
|
112
|
+
// If it's a complex task, and we are not in the first or last thought, require substeps for detailed execution
|
|
113
|
+
if (isComplexTask &&
|
|
114
|
+
input.thoughtNumber > 1 &&
|
|
115
|
+
!input.nextThoughtNeeded &&
|
|
116
|
+
(!input.subSteps || input.subSteps.length === 0)) {
|
|
117
|
+
// This is a heuristic: if we are finishing a complex task, we should have had substeps in previous thoughts.
|
|
118
|
+
// But if this SPECIFIC thought is a summary, it might be fine.
|
|
119
|
+
// Let's be less strict here to avoid false positives, but still warn.
|
|
120
|
+
// Actually, let's strictly require substeps if confidence is high (>8) to prove the confidence.
|
|
121
|
+
if (input.confidence && input.confidence > 8) {
|
|
122
|
+
return {
|
|
123
|
+
valid: false,
|
|
124
|
+
warning: `⚠️ UNJUSTIFIED CONFIDENCE: High confidence (${input.confidence}) requires subSteps to prove the logic. Add subSteps.`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 4. Semantic Heuristics (v5.5.0) - Rejects "lazy" thinking and gibberish
|
|
129
|
+
const semanticCheck = this.checkSemanticQuality(input);
|
|
130
|
+
if (!semanticCheck.valid) {
|
|
131
|
+
return semanticCheck;
|
|
132
|
+
}
|
|
133
|
+
// 5. Complexity Budget Check (Game Theory)
|
|
134
|
+
// If thought proposes new files/libs without justification in "planning" phase
|
|
135
|
+
if (input.phase === 'strategy') {
|
|
136
|
+
const lowerT = input.thought.toLowerCase();
|
|
137
|
+
if ((lowerT.includes('create new file') || lowerT.includes('add library')) &&
|
|
138
|
+
input.thought.length < 100) {
|
|
139
|
+
return {
|
|
140
|
+
valid: false,
|
|
141
|
+
warning: `⚠️ COMPLEXITY BUDGET: Creating files/libs costs tokens. Justify WHY this is needed in detail (>100 chars).`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { valid: true };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check semantic quality using entropy and pattern matching
|
|
149
|
+
* Acts as a lightweight "LLM-Validator"
|
|
150
|
+
*/
|
|
151
|
+
checkSemanticQuality(input) {
|
|
152
|
+
// A. Entropy Check (detects gibberish or extreme repetition)
|
|
153
|
+
const entropy = calculateWordEntropy(input.thought);
|
|
154
|
+
// Threshold 0.2: Relaxed to allow simple commands
|
|
155
|
+
if (entropy < 0.2 && input.thought.length > 50) {
|
|
156
|
+
return {
|
|
157
|
+
valid: false,
|
|
158
|
+
warning: `⚠️ LOW ENTROPY (${entropy.toFixed(2)}): Thought is too repetitive. Elaborate with diverse vocabulary.`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// B. Lazy Thinking Detection (e.g. "I will just simply...")
|
|
162
|
+
const lowerThought = input.thought.toLowerCase();
|
|
163
|
+
const foundLazyPattern = LAZY_THINKING_PATTERNS.find((pattern) => lowerThought.includes(pattern));
|
|
164
|
+
if (foundLazyPattern) {
|
|
165
|
+
// Allow if confidence is low (honest uncertainty)
|
|
166
|
+
if (!input.confidence || input.confidence > 5) {
|
|
167
|
+
return {
|
|
168
|
+
valid: false,
|
|
169
|
+
warning: `⚠️ LAZY THINKING DETECTED: Avoid terms like "${foundLazyPattern}". Be specific about HOW and WHY.`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// C. Context Awareness Check
|
|
174
|
+
// If files are provided but not mentioned, warn?
|
|
175
|
+
// (Disabled for now as it might be too strict, but good for future)
|
|
176
|
+
return { valid: true };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* HARD duplicate check - returns error message if duplicate found
|
|
180
|
+
* Used for strict rejection before adding to history
|
|
181
|
+
* @param input - The thought input to check
|
|
182
|
+
* @param sessionThoughts - Current session thoughts
|
|
183
|
+
*/
|
|
184
|
+
checkDuplicateStrict(input, sessionThoughts) {
|
|
185
|
+
if (input.isRevision)
|
|
186
|
+
return undefined; // Revisions are allowed to reuse numbers
|
|
187
|
+
const exists = sessionThoughts.some((t) => t.thoughtNumber === input.thoughtNumber);
|
|
188
|
+
if (exists) {
|
|
189
|
+
return `🚫 REJECTED: #${input.thoughtNumber} exists. Use isRevision:true or quickExtension.`;
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Validate branch source - reject if branchFromThought references non-existent thought
|
|
195
|
+
* @param input - The thought input to validate
|
|
196
|
+
* @param sessionThoughts - Current session thoughts
|
|
197
|
+
*/
|
|
198
|
+
validateBranchSource(input, sessionThoughts) {
|
|
199
|
+
if (!input.branchFromThought)
|
|
200
|
+
return undefined;
|
|
201
|
+
const sourceExists = sessionThoughts.some((t) => t.thoughtNumber === input.branchFromThought);
|
|
202
|
+
if (!sourceExists) {
|
|
203
|
+
return `🚫 INVALID BRANCH: Cannot branch from #${input.branchFromThought}. Available: ${sessionThoughts.map((t) => t.thoughtNumber).join(', ') || 'none'}`;
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Validate path connectivity - ensure thoughts in winningPath are logically connected
|
|
209
|
+
* Each thought must be reachable from its predecessor via sequence, branch, or revision
|
|
210
|
+
* @param winningPath - Array of thought numbers in the winning path
|
|
211
|
+
* @param sessionThoughts - Current session thoughts
|
|
212
|
+
*/
|
|
213
|
+
validatePathConnectivity(winningPath, sessionThoughts) {
|
|
214
|
+
if (winningPath.length <= 1)
|
|
215
|
+
return { valid: true };
|
|
216
|
+
const thoughtMap = new Map(sessionThoughts.map((t) => [t.thoughtNumber, t]));
|
|
217
|
+
for (let i = 1; i < winningPath.length; i++) {
|
|
218
|
+
const current = winningPath[i];
|
|
219
|
+
const previous = winningPath[i - 1];
|
|
220
|
+
const currentThought = thoughtMap.get(current);
|
|
221
|
+
if (!currentThought) {
|
|
222
|
+
return {
|
|
223
|
+
valid: false,
|
|
224
|
+
error: `Thought #${current} not found`,
|
|
225
|
+
disconnectedAt: current
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Build set of valid predecessors for current thought
|
|
229
|
+
const validPredecessors = new Set();
|
|
230
|
+
// Sequential predecessor (N can follow N-1)
|
|
231
|
+
validPredecessors.add(current - 1);
|
|
232
|
+
// Branch source (if this thought branches from another)
|
|
233
|
+
if (currentThought.branchFromThought) {
|
|
234
|
+
validPredecessors.add(currentThought.branchFromThought);
|
|
235
|
+
}
|
|
236
|
+
// Revision target (revision can follow the thought it revises)
|
|
237
|
+
if (currentThought.isRevision && currentThought.revisesThought) {
|
|
238
|
+
validPredecessors.add(currentThought.revisesThought);
|
|
239
|
+
// Also allow revision to follow the thought BEFORE the one it revises
|
|
240
|
+
validPredecessors.add(currentThought.revisesThought - 1);
|
|
241
|
+
}
|
|
242
|
+
// Special case: if previous thought was revised, current can follow the revision
|
|
243
|
+
const previousThought = thoughtMap.get(previous);
|
|
244
|
+
if (previousThought?.isRevision && previousThought.revisesThought) {
|
|
245
|
+
// Allow next sequential after revision target
|
|
246
|
+
validPredecessors.add(previousThought.revisesThought + 1);
|
|
247
|
+
}
|
|
248
|
+
if (!validPredecessors.has(previous)) {
|
|
249
|
+
return {
|
|
250
|
+
valid: false,
|
|
251
|
+
error: `Path discontinuity: #${current} cannot logically follow #${previous}. Valid predecessors for #${current}: [${Array.from(validPredecessors)
|
|
252
|
+
.sort((a, b) => a - b)
|
|
253
|
+
.join(', ')}]`,
|
|
254
|
+
disconnectedAt: current
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { valid: true };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VisualizationService - ASCII tree and Mermaid diagram generation
|
|
3
|
+
* Stateless service - receives data as parameters
|
|
4
|
+
* v4.3.0 - Improved visualization with confidence icons and truncation
|
|
5
|
+
*/
|
|
6
|
+
import type { ThoughtRecord } from '../types/thought.types.js';
|
|
7
|
+
export declare class VisualizationService {
|
|
8
|
+
/**
|
|
9
|
+
* Generate ASCII tree visualization of thought structure
|
|
10
|
+
* v4.3.0: Added confidence icons, truncation for long sessions
|
|
11
|
+
* @param sessionThoughts - thoughts from current session
|
|
12
|
+
* @param branches - Map of branch ID to branch thoughts
|
|
13
|
+
*/
|
|
14
|
+
generateAsciiTree(sessionThoughts: ThoughtRecord[], branches: Map<string, ThoughtRecord[]>): string;
|
|
15
|
+
/**
|
|
16
|
+
* Generate Mermaid.js graph visualization
|
|
17
|
+
* @param sessionThoughts - thoughts from current session
|
|
18
|
+
* @param branches - Map of branch ID to branch thoughts
|
|
19
|
+
* @param thoughtHistory - full thought history for branch filtering
|
|
20
|
+
* @param sessionStartIndex - index where current session starts
|
|
21
|
+
*/
|
|
22
|
+
generateMermaid(sessionThoughts: ThoughtRecord[], branches: Map<string, ThoughtRecord[]>, thoughtHistory: ThoughtRecord[], sessionStartIndex: number): string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VisualizationService - ASCII tree and Mermaid diagram generation
|
|
3
|
+
* Stateless service - receives data as parameters
|
|
4
|
+
* v4.3.0 - Improved visualization with confidence icons and truncation
|
|
5
|
+
*/
|
|
6
|
+
import { sanitizeForMermaid } from '../utils/index.js';
|
|
7
|
+
// v4.3.0: Max thoughts to show in tree (older ones collapsed)
|
|
8
|
+
const MAX_TREE_THOUGHTS = 5;
|
|
9
|
+
/**
|
|
10
|
+
* Get confidence icon based on score
|
|
11
|
+
*/
|
|
12
|
+
function getConfidenceIcon(confidence) {
|
|
13
|
+
if (!confidence)
|
|
14
|
+
return '○';
|
|
15
|
+
if (confidence >= 8)
|
|
16
|
+
return '✅';
|
|
17
|
+
if (confidence >= 5)
|
|
18
|
+
return '⚠️';
|
|
19
|
+
return '❌';
|
|
20
|
+
}
|
|
21
|
+
export class VisualizationService {
|
|
22
|
+
/**
|
|
23
|
+
* Generate ASCII tree visualization of thought structure
|
|
24
|
+
* v4.3.0: Added confidence icons, truncation for long sessions
|
|
25
|
+
* @param sessionThoughts - thoughts from current session
|
|
26
|
+
* @param branches - Map of branch ID to branch thoughts
|
|
27
|
+
*/
|
|
28
|
+
generateAsciiTree(sessionThoughts, branches) {
|
|
29
|
+
if (sessionThoughts.length === 0)
|
|
30
|
+
return '(empty)';
|
|
31
|
+
const mainThoughts = sessionThoughts.filter((t) => !t.branchFromThought && !t.isRevision);
|
|
32
|
+
// v4.3.0: Truncate long sessions
|
|
33
|
+
const shouldTruncate = mainThoughts.length > MAX_TREE_THOUGHTS;
|
|
34
|
+
const hiddenCount = shouldTruncate ? mainThoughts.length - MAX_TREE_THOUGHTS : 0;
|
|
35
|
+
const visibleThoughts = shouldTruncate
|
|
36
|
+
? mainThoughts.slice(-MAX_TREE_THOUGHTS)
|
|
37
|
+
: mainThoughts;
|
|
38
|
+
const lines = ['📊 Thought Tree:'];
|
|
39
|
+
// Show truncation notice
|
|
40
|
+
if (shouldTruncate) {
|
|
41
|
+
lines.push(`│ ... ${hiddenCount} earlier thought(s) hidden`);
|
|
42
|
+
}
|
|
43
|
+
for (let i = 0; i < visibleThoughts.length; i++) {
|
|
44
|
+
const thought = visibleThoughts[i];
|
|
45
|
+
const isLast = i === visibleThoughts.length - 1;
|
|
46
|
+
const prefix = isLast ? '└──' : '├──';
|
|
47
|
+
const childPrefix = isLast ? ' ' : '│ ';
|
|
48
|
+
// v4.3.0: Confidence icon instead of [N]
|
|
49
|
+
const confIcon = getConfidenceIcon(thought.confidence);
|
|
50
|
+
const preview = thought.thought.substring(0, 35);
|
|
51
|
+
lines.push(`${prefix} ${confIcon} #${thought.thoughtNumber}: ${preview}...`);
|
|
52
|
+
// Show subSteps (compact)
|
|
53
|
+
if (thought.subSteps && thought.subSteps.length > 0) {
|
|
54
|
+
lines.push(`${childPrefix}📋 [${thought.subSteps.length} steps]`);
|
|
55
|
+
}
|
|
56
|
+
// Show alternatives (compact)
|
|
57
|
+
if (thought.alternatives && thought.alternatives.length > 0) {
|
|
58
|
+
lines.push(`${childPrefix}⚖️ [${thought.alternatives.length} alts]`);
|
|
59
|
+
}
|
|
60
|
+
// Show extensions (compact - only count)
|
|
61
|
+
if (thought.extensions && thought.extensions.length > 0) {
|
|
62
|
+
const blockers = thought.extensions.filter(e => e.impact === 'blocker').length;
|
|
63
|
+
const extInfo = blockers > 0 ? `${thought.extensions.length} ext, ${blockers}🚫` : `${thought.extensions.length} ext`;
|
|
64
|
+
lines.push(`${childPrefix}🔍 [${extInfo}]`);
|
|
65
|
+
}
|
|
66
|
+
// Show revisions (compact)
|
|
67
|
+
const revisions = sessionThoughts.filter((t) => t.isRevision && t.revisesThought === thought.thoughtNumber);
|
|
68
|
+
if (revisions.length > 0) {
|
|
69
|
+
lines.push(`${childPrefix}🔄 [${revisions.length} revision(s)]`);
|
|
70
|
+
}
|
|
71
|
+
// Show branches (compact)
|
|
72
|
+
for (const [branchId, branchThoughts] of branches) {
|
|
73
|
+
const fromThis = branchThoughts.filter((t) => t.branchFromThought === thought.thoughtNumber);
|
|
74
|
+
if (fromThis.length > 0) {
|
|
75
|
+
lines.push(`${childPrefix}🌿 [${branchId}]: ${fromThis.length} thought(s)`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Generate Mermaid.js graph visualization
|
|
83
|
+
* @param sessionThoughts - thoughts from current session
|
|
84
|
+
* @param branches - Map of branch ID to branch thoughts
|
|
85
|
+
* @param thoughtHistory - full thought history for branch filtering
|
|
86
|
+
* @param sessionStartIndex - index where current session starts
|
|
87
|
+
*/
|
|
88
|
+
generateMermaid(sessionThoughts, branches, thoughtHistory, sessionStartIndex) {
|
|
89
|
+
if (sessionThoughts.length === 0)
|
|
90
|
+
return '';
|
|
91
|
+
const lines = ['graph TD;'];
|
|
92
|
+
const mainThoughts = sessionThoughts.filter((t) => !t.branchFromThought && !t.isRevision);
|
|
93
|
+
// Build set of revised thoughts (thoughts that have been superseded)
|
|
94
|
+
const revisedThoughts = new Set(sessionThoughts
|
|
95
|
+
.filter((t) => t.isRevision && t.revisesThought)
|
|
96
|
+
.map((t) => t.revisesThought));
|
|
97
|
+
// Build set of thoughts with blocker extensions
|
|
98
|
+
const blockerThoughts = new Set(sessionThoughts
|
|
99
|
+
.filter((t) => t.extensions?.some((e) => e.impact === 'blocker'))
|
|
100
|
+
.map((t) => t.thoughtNumber));
|
|
101
|
+
// Main flow subgraph
|
|
102
|
+
lines.push(' subgraph MainFlow["🧠 Main Reasoning"]');
|
|
103
|
+
// Add start node
|
|
104
|
+
if (mainThoughts.length > 0) {
|
|
105
|
+
lines.push(` start((Start)) --> ${mainThoughts[0].thoughtNumber};`);
|
|
106
|
+
}
|
|
107
|
+
// Process each main thought
|
|
108
|
+
for (let i = 0; i < mainThoughts.length; i++) {
|
|
109
|
+
const t = mainThoughts[i];
|
|
110
|
+
const label = sanitizeForMermaid(t.thought.substring(0, 25));
|
|
111
|
+
const confLabel = t.confidence ? `<br/>conf:${t.confidence}` : '';
|
|
112
|
+
const subStepsLabel = t.subSteps && t.subSteps.length > 0 ? `<br/>📋${t.subSteps.length} steps` : '';
|
|
113
|
+
const altsLabel = t.alternatives && t.alternatives.length > 0 ? `<br/>⚖️${t.alternatives.length} alts` : '';
|
|
114
|
+
// Determine style class with priority: blocker > revised > lowConf > highConf > normal
|
|
115
|
+
let styleClass = 'normal';
|
|
116
|
+
if (blockerThoughts.has(t.thoughtNumber)) {
|
|
117
|
+
styleClass = 'blocker';
|
|
118
|
+
}
|
|
119
|
+
else if (revisedThoughts.has(t.thoughtNumber)) {
|
|
120
|
+
styleClass = 'revised';
|
|
121
|
+
}
|
|
122
|
+
else if (t.confidence && t.confidence < 5) {
|
|
123
|
+
styleClass = 'lowConf';
|
|
124
|
+
}
|
|
125
|
+
else if (t.confidence && t.confidence >= 8) {
|
|
126
|
+
styleClass = 'highConf';
|
|
127
|
+
}
|
|
128
|
+
lines.push(` ${t.thoughtNumber}["#${t.thoughtNumber}: ${label}...${confLabel}${subStepsLabel}${altsLabel}"]:::${styleClass};`);
|
|
129
|
+
// Edge to next thought
|
|
130
|
+
if (i < mainThoughts.length - 1) {
|
|
131
|
+
lines.push(` ${t.thoughtNumber} --> ${mainThoughts[i + 1].thoughtNumber};`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
lines.push(' end');
|
|
135
|
+
// Extensions subgraph (if any)
|
|
136
|
+
const hasExtensions = mainThoughts.some((t) => t.extensions && t.extensions.length > 0);
|
|
137
|
+
if (hasExtensions) {
|
|
138
|
+
lines.push(' subgraph Extensions["🔍 Deep Analysis"]');
|
|
139
|
+
for (const t of mainThoughts) {
|
|
140
|
+
if (t.extensions && t.extensions.length > 0) {
|
|
141
|
+
t.extensions.forEach((ext, idx) => {
|
|
142
|
+
const extId = `ext_${t.thoughtNumber}_${idx}`;
|
|
143
|
+
const extLabel = sanitizeForMermaid(ext.content.substring(0, 20));
|
|
144
|
+
const extClass = ext.impact === 'blocker' ? 'blocker' : ext.impact === 'high' ? 'highImpact' : 'ext';
|
|
145
|
+
const icon = ext.impact === 'blocker' ? '🚫' : ext.impact === 'high' ? '⚠️' : '📝';
|
|
146
|
+
lines.push(` ${extId}[/"${icon} ${ext.type}: ${extLabel}..."/]:::${extClass};`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
lines.push(' end');
|
|
151
|
+
// Connect extensions to main thoughts
|
|
152
|
+
for (const t of mainThoughts) {
|
|
153
|
+
if (t.extensions && t.extensions.length > 0) {
|
|
154
|
+
t.extensions.forEach((_, idx) => {
|
|
155
|
+
const extId = `ext_${t.thoughtNumber}_${idx}`;
|
|
156
|
+
lines.push(` ${t.thoughtNumber} -.-> ${extId};`);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Revisions subgraph (if any)
|
|
162
|
+
const revisions = sessionThoughts.filter((t) => t.isRevision);
|
|
163
|
+
if (revisions.length > 0) {
|
|
164
|
+
lines.push(' subgraph Revisions["🔄 Revisions"]');
|
|
165
|
+
revisions.forEach((rev, idx) => {
|
|
166
|
+
const revId = `rev_${rev.revisesThought}_${idx}`;
|
|
167
|
+
const revLabel = sanitizeForMermaid(rev.thought.substring(0, 20));
|
|
168
|
+
lines.push(` ${revId}["🔄 ${revLabel}..."]:::revision;`);
|
|
169
|
+
});
|
|
170
|
+
lines.push(' end');
|
|
171
|
+
// Connect revisions to targets
|
|
172
|
+
revisions.forEach((rev, idx) => {
|
|
173
|
+
const revId = `rev_${rev.revisesThought}_${idx}`;
|
|
174
|
+
lines.push(` ${revId} ==> ${rev.revisesThought};`);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// Branch subgraphs
|
|
178
|
+
for (const [branchId, branchThoughts] of branches) {
|
|
179
|
+
const sessionBranchThoughts = branchThoughts.filter((bt) => {
|
|
180
|
+
return thoughtHistory.indexOf(bt) >= sessionStartIndex;
|
|
181
|
+
});
|
|
182
|
+
if (sessionBranchThoughts.length > 0) {
|
|
183
|
+
lines.push(` subgraph Branch_${branchId}["🌿 Branch: ${branchId}"]`);
|
|
184
|
+
sessionBranchThoughts.forEach((bt, idx) => {
|
|
185
|
+
const branchNodeId = `branch_${branchId}_${idx}`;
|
|
186
|
+
const btLabel = sanitizeForMermaid(bt.thought.substring(0, 20));
|
|
187
|
+
lines.push(` ${branchNodeId}["${btLabel}..."]:::branch;`);
|
|
188
|
+
});
|
|
189
|
+
lines.push(' end');
|
|
190
|
+
// Connect branches to source thoughts
|
|
191
|
+
sessionBranchThoughts.forEach((bt, idx) => {
|
|
192
|
+
if (bt.branchFromThought) {
|
|
193
|
+
const branchNodeId = `branch_${branchId}_${idx}`;
|
|
194
|
+
lines.push(` ${bt.branchFromThought} -.->|${branchId}| ${branchNodeId};`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Style definitions with visual intelligence
|
|
200
|
+
lines.push(' classDef normal fill:#e1f5fe,stroke:#01579b;');
|
|
201
|
+
lines.push(' classDef highConf fill:#e1f5fe,stroke:#ffd700,stroke-width:3px;');
|
|
202
|
+
lines.push(' classDef lowConf fill:#ffecb3,stroke:#ff6f00;');
|
|
203
|
+
lines.push(' classDef blocker fill:#ffcdd2,stroke:#b71c1c,stroke-width:3px;');
|
|
204
|
+
lines.push(' classDef revised fill:#e0e0e0,stroke:#9e9e9e,stroke-dasharray:5 5;');
|
|
205
|
+
lines.push(' classDef highImpact fill:#fff3e0,stroke:#e65100;');
|
|
206
|
+
lines.push(' classDef ext fill:#f3e5f5,stroke:#7b1fa2;');
|
|
207
|
+
lines.push(' classDef revision fill:#e8f5e9,stroke:#2e7d32;');
|
|
208
|
+
lines.push(' classDef branch fill:#e0f2f1,stroke:#00695c;');
|
|
209
|
+
return lines.join('\n');
|
|
210
|
+
}
|
|
211
|
+
}
|