neuronlayer 0.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.
Files changed (78) hide show
  1. package/CONTRIBUTING.md +127 -0
  2. package/LICENSE +21 -0
  3. package/README.md +305 -0
  4. package/dist/index.js +38016 -0
  5. package/esbuild.config.js +26 -0
  6. package/package.json +63 -0
  7. package/src/cli/commands.ts +382 -0
  8. package/src/core/adr-exporter.ts +253 -0
  9. package/src/core/architecture/architecture-enforcement.ts +228 -0
  10. package/src/core/architecture/duplicate-detector.ts +288 -0
  11. package/src/core/architecture/index.ts +6 -0
  12. package/src/core/architecture/pattern-learner.ts +306 -0
  13. package/src/core/architecture/pattern-library.ts +403 -0
  14. package/src/core/architecture/pattern-validator.ts +324 -0
  15. package/src/core/change-intelligence/bug-correlator.ts +444 -0
  16. package/src/core/change-intelligence/change-intelligence.ts +221 -0
  17. package/src/core/change-intelligence/change-tracker.ts +334 -0
  18. package/src/core/change-intelligence/fix-suggester.ts +340 -0
  19. package/src/core/change-intelligence/index.ts +5 -0
  20. package/src/core/code-verifier.ts +843 -0
  21. package/src/core/confidence/confidence-scorer.ts +251 -0
  22. package/src/core/confidence/conflict-checker.ts +289 -0
  23. package/src/core/confidence/index.ts +5 -0
  24. package/src/core/confidence/source-tracker.ts +263 -0
  25. package/src/core/confidence/warning-detector.ts +241 -0
  26. package/src/core/context-rot/compaction.ts +284 -0
  27. package/src/core/context-rot/context-health.ts +243 -0
  28. package/src/core/context-rot/context-rot-prevention.ts +213 -0
  29. package/src/core/context-rot/critical-context.ts +221 -0
  30. package/src/core/context-rot/drift-detector.ts +255 -0
  31. package/src/core/context-rot/index.ts +7 -0
  32. package/src/core/context.ts +263 -0
  33. package/src/core/decision-extractor.ts +339 -0
  34. package/src/core/decisions.ts +69 -0
  35. package/src/core/deja-vu.ts +421 -0
  36. package/src/core/engine.ts +1455 -0
  37. package/src/core/feature-context.ts +726 -0
  38. package/src/core/ghost-mode.ts +412 -0
  39. package/src/core/learning.ts +485 -0
  40. package/src/core/living-docs/activity-tracker.ts +296 -0
  41. package/src/core/living-docs/architecture-generator.ts +428 -0
  42. package/src/core/living-docs/changelog-generator.ts +348 -0
  43. package/src/core/living-docs/component-generator.ts +230 -0
  44. package/src/core/living-docs/doc-engine.ts +110 -0
  45. package/src/core/living-docs/doc-validator.ts +282 -0
  46. package/src/core/living-docs/index.ts +8 -0
  47. package/src/core/project-manager.ts +297 -0
  48. package/src/core/summarizer.ts +267 -0
  49. package/src/core/test-awareness/change-validator.ts +499 -0
  50. package/src/core/test-awareness/index.ts +5 -0
  51. package/src/index.ts +49 -0
  52. package/src/indexing/ast.ts +563 -0
  53. package/src/indexing/embeddings.ts +85 -0
  54. package/src/indexing/indexer.ts +245 -0
  55. package/src/indexing/watcher.ts +78 -0
  56. package/src/server/gateways/aggregator.ts +374 -0
  57. package/src/server/gateways/index.ts +473 -0
  58. package/src/server/gateways/memory-ghost.ts +343 -0
  59. package/src/server/gateways/memory-query.ts +452 -0
  60. package/src/server/gateways/memory-record.ts +346 -0
  61. package/src/server/gateways/memory-review.ts +410 -0
  62. package/src/server/gateways/memory-status.ts +517 -0
  63. package/src/server/gateways/memory-verify.ts +392 -0
  64. package/src/server/gateways/router.ts +434 -0
  65. package/src/server/gateways/types.ts +610 -0
  66. package/src/server/mcp.ts +154 -0
  67. package/src/server/resources.ts +85 -0
  68. package/src/server/tools.ts +2261 -0
  69. package/src/storage/database.ts +262 -0
  70. package/src/storage/tier1.ts +135 -0
  71. package/src/storage/tier2.ts +764 -0
  72. package/src/storage/tier3.ts +123 -0
  73. package/src/types/documentation.ts +619 -0
  74. package/src/types/index.ts +222 -0
  75. package/src/utils/config.ts +193 -0
  76. package/src/utils/files.ts +117 -0
  77. package/src/utils/time.ts +37 -0
  78. package/src/utils/tokens.ts +52 -0
@@ -0,0 +1,284 @@
1
+ import type {
2
+ ContextChunk,
3
+ CompactionResult,
4
+ CompactionOptions,
5
+ CompactionSuggestion
6
+ } from '../../types/documentation.js';
7
+ import { ContextHealthMonitor } from './context-health.js';
8
+ import { CriticalContextManager } from './critical-context.js';
9
+
10
+ // Thresholds for different strategies
11
+ const RELEVANCE_THRESHOLD_SUMMARIZE = 0.5;
12
+ const RELEVANCE_THRESHOLD_SELECTIVE = 0.3;
13
+ const RELEVANCE_THRESHOLD_AGGRESSIVE = 0.2;
14
+
15
+ export class CompactionEngine {
16
+ private healthMonitor: ContextHealthMonitor;
17
+ private criticalManager: CriticalContextManager;
18
+
19
+ constructor(healthMonitor: ContextHealthMonitor, criticalManager: CriticalContextManager) {
20
+ this.healthMonitor = healthMonitor;
21
+ this.criticalManager = criticalManager;
22
+ }
23
+
24
+ suggestCompaction(): CompactionSuggestion {
25
+ const chunks = this.healthMonitor.getChunks();
26
+ const tokenLimit = this.healthMonitor.getTokenLimit();
27
+ const currentTokens = this.healthMonitor.getCurrentTokens();
28
+
29
+ const critical: ContextChunk[] = [];
30
+ const summarizable: ContextChunk[] = [];
31
+ const removable: ContextChunk[] = [];
32
+
33
+ for (const chunk of chunks) {
34
+ if (chunk.isCritical || chunk.relevanceScore >= RELEVANCE_THRESHOLD_SUMMARIZE) {
35
+ critical.push(chunk);
36
+ } else if (chunk.relevanceScore >= RELEVANCE_THRESHOLD_SELECTIVE) {
37
+ summarizable.push(chunk);
38
+ } else {
39
+ removable.push(chunk);
40
+ }
41
+ }
42
+
43
+ const removableTokens = removable.reduce((sum, c) => sum + c.tokens, 0);
44
+ const summarizableTokens = summarizable.reduce((sum, c) => sum + c.tokens, 0);
45
+
46
+ // Estimate tokens after summarization (assume 70% compression)
47
+ const summarizedTokens = Math.ceil(summarizableTokens * 0.3);
48
+ const tokensSaved = removableTokens + (summarizableTokens - summarizedTokens);
49
+
50
+ const newTokens = currentTokens - tokensSaved;
51
+ const newUtilization = (newTokens / tokenLimit) * 100;
52
+
53
+ return {
54
+ critical,
55
+ summarizable,
56
+ removable,
57
+ tokensSaved,
58
+ newUtilization: Math.round(newUtilization * 10) / 10
59
+ };
60
+ }
61
+
62
+ compact(options: CompactionOptions): CompactionResult {
63
+ const { strategy, preserveRecent = 5, targetUtilization, preserveCritical = true } = options;
64
+
65
+ const chunks = this.healthMonitor.getChunks();
66
+ const tokensBefore = this.healthMonitor.getCurrentTokens();
67
+
68
+ // Separate chunks by type
69
+ const recentChunks = chunks.slice(-preserveRecent);
70
+ const olderChunks = chunks.slice(0, -preserveRecent);
71
+
72
+ let relevanceThreshold: number;
73
+ switch (strategy) {
74
+ case 'aggressive':
75
+ relevanceThreshold = RELEVANCE_THRESHOLD_AGGRESSIVE;
76
+ break;
77
+ case 'selective':
78
+ relevanceThreshold = RELEVANCE_THRESHOLD_SELECTIVE;
79
+ break;
80
+ case 'summarize':
81
+ default:
82
+ relevanceThreshold = RELEVANCE_THRESHOLD_SUMMARIZE;
83
+ }
84
+
85
+ const toKeep: ContextChunk[] = [];
86
+ const toSummarize: ContextChunk[] = [];
87
+ const toRemove: ContextChunk[] = [];
88
+
89
+ for (const chunk of olderChunks) {
90
+ // Always preserve critical if flag is set
91
+ if (preserveCritical && chunk.isCritical) {
92
+ toKeep.push(chunk);
93
+ } else if (chunk.relevanceScore >= relevanceThreshold) {
94
+ if (strategy === 'aggressive') {
95
+ toSummarize.push(chunk);
96
+ } else {
97
+ toKeep.push(chunk);
98
+ }
99
+ } else if (chunk.relevanceScore >= relevanceThreshold * 0.5 && strategy !== 'aggressive') {
100
+ toSummarize.push(chunk);
101
+ } else {
102
+ toRemove.push(chunk);
103
+ }
104
+ }
105
+
106
+ // Generate summaries for chunks to summarize
107
+ const summaries = this.generateSummaries(toSummarize);
108
+
109
+ // Calculate new token count
110
+ const keptTokens = toKeep.reduce((sum, c) => sum + c.tokens, 0);
111
+ const recentTokens = recentChunks.reduce((sum, c) => sum + c.tokens, 0);
112
+ const summaryTokens = summaries.reduce((sum, s) => sum + this.estimateTokens(s), 0);
113
+
114
+ const tokensAfter = keptTokens + recentTokens + summaryTokens;
115
+ const tokensSaved = tokensBefore - tokensAfter;
116
+
117
+ // Update the health monitor with new chunks
118
+ this.healthMonitor.clearChunks();
119
+
120
+ // Re-add kept chunks
121
+ for (const chunk of toKeep) {
122
+ this.healthMonitor.addChunk(chunk);
123
+ }
124
+
125
+ // Add summary chunks
126
+ for (const summary of summaries) {
127
+ this.healthMonitor.addChunk({
128
+ content: summary,
129
+ tokens: this.estimateTokens(summary),
130
+ timestamp: new Date(),
131
+ type: 'message'
132
+ });
133
+ }
134
+
135
+ // Re-add recent chunks
136
+ for (const chunk of recentChunks) {
137
+ this.healthMonitor.addChunk(chunk);
138
+ }
139
+
140
+ // Check if we hit target utilization
141
+ if (targetUtilization) {
142
+ const currentUtilization = (tokensAfter / this.healthMonitor.getTokenLimit()) * 100;
143
+ if (currentUtilization > targetUtilization && strategy !== 'aggressive') {
144
+ // Recursively compact with more aggressive strategy
145
+ return this.compact({
146
+ ...options,
147
+ strategy: strategy === 'summarize' ? 'selective' : 'aggressive'
148
+ });
149
+ }
150
+ }
151
+
152
+ return {
153
+ success: true,
154
+ strategy,
155
+ tokensBefore,
156
+ tokensAfter,
157
+ tokensSaved,
158
+ preservedCritical: toKeep.filter(c => c.isCritical).length,
159
+ summarizedChunks: toSummarize.length,
160
+ removedChunks: toRemove.length,
161
+ summaries
162
+ };
163
+ }
164
+
165
+ private generateSummaries(chunks: ContextChunk[]): string[] {
166
+ if (chunks.length === 0) return [];
167
+
168
+ // Group chunks by type
169
+ const grouped: Record<string, ContextChunk[]> = {};
170
+ for (const chunk of chunks) {
171
+ if (!grouped[chunk.type]) {
172
+ grouped[chunk.type] = [];
173
+ }
174
+ grouped[chunk.type]!.push(chunk);
175
+ }
176
+
177
+ const summaries: string[] = [];
178
+
179
+ for (const [type, typeChunks] of Object.entries(grouped)) {
180
+ if (typeChunks.length === 0) continue;
181
+
182
+ // Simple extractive summary: take key sentences
183
+ const allContent = typeChunks.map(c => c.content).join(' ');
184
+ const summary = this.extractiveSummarize(allContent, type);
185
+ summaries.push(summary);
186
+ }
187
+
188
+ return summaries;
189
+ }
190
+
191
+ private extractiveSummarize(content: string, type: string): string {
192
+ // Split into sentences
193
+ const sentences = content.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 10);
194
+
195
+ if (sentences.length === 0) {
196
+ return `[${type}]: ${content.slice(0, 100)}`;
197
+ }
198
+
199
+ // Score sentences by importance
200
+ const scored = sentences.map(sentence => ({
201
+ sentence,
202
+ score: this.scoreSentence(sentence)
203
+ }));
204
+
205
+ // Sort by score and take top sentences
206
+ scored.sort((a, b) => b.score - a.score);
207
+ const topSentences = scored.slice(0, Math.min(3, sentences.length));
208
+
209
+ // Sort back by original order for coherence
210
+ const originalOrder = topSentences.sort((a, b) => {
211
+ return sentences.indexOf(a.sentence) - sentences.indexOf(b.sentence);
212
+ });
213
+
214
+ const summary = originalOrder.map(s => s.sentence).join('. ') + '.';
215
+
216
+ return `[Summary - ${type}]: ${summary}`;
217
+ }
218
+
219
+ private scoreSentence(sentence: string): number {
220
+ let score = 0;
221
+
222
+ // Longer sentences might have more info (but not too long)
223
+ const wordCount = sentence.split(/\s+/).length;
224
+ if (wordCount >= 5 && wordCount <= 30) {
225
+ score += 1;
226
+ }
227
+
228
+ // Contains important keywords
229
+ const importantWords = [
230
+ 'decided', 'choose', 'use', 'implement', 'because', 'important',
231
+ 'must', 'should', 'require', 'need', 'critical', 'key'
232
+ ];
233
+ for (const word of importantWords) {
234
+ if (sentence.toLowerCase().includes(word)) {
235
+ score += 0.5;
236
+ }
237
+ }
238
+
239
+ // Contains technical terms (likely important)
240
+ const technicalPatterns = [
241
+ /\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\b/, // CamelCase
242
+ /\b\w+\(\)/, // Function calls
243
+ /`[^`]+`/ // Code markers
244
+ ];
245
+ for (const pattern of technicalPatterns) {
246
+ if (pattern.test(sentence)) {
247
+ score += 0.3;
248
+ }
249
+ }
250
+
251
+ return score;
252
+ }
253
+
254
+ private estimateTokens(text: string): number {
255
+ // Rough estimation: ~4 characters per token
256
+ return Math.ceil(text.length / 4);
257
+ }
258
+
259
+ autoCompact(): CompactionResult {
260
+ const health = this.healthMonitor.getHealth();
261
+
262
+ // Determine strategy based on health
263
+ let strategy: CompactionOptions['strategy'];
264
+ let targetUtilization: number;
265
+
266
+ if (health.health === 'critical') {
267
+ strategy = 'aggressive';
268
+ targetUtilization = 40;
269
+ } else if (health.health === 'warning') {
270
+ strategy = 'selective';
271
+ targetUtilization = 50;
272
+ } else {
273
+ strategy = 'summarize';
274
+ targetUtilization = 60;
275
+ }
276
+
277
+ return this.compact({
278
+ strategy,
279
+ preserveRecent: 10,
280
+ targetUtilization,
281
+ preserveCritical: true
282
+ });
283
+ }
284
+ }
@@ -0,0 +1,243 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { ContextHealth, ContextChunk } from '../../types/documentation.js';
3
+ import { CriticalContextManager } from './critical-context.js';
4
+
5
+ // Default token limits (can be overridden)
6
+ const DEFAULT_TOKEN_LIMIT = 100000;
7
+
8
+ // Health thresholds
9
+ const UTILIZATION_WARNING = 0.7; // 70%
10
+ const UTILIZATION_CRITICAL = 0.85; // 85%
11
+ const DRIFT_WARNING = 0.3;
12
+ const DRIFT_CRITICAL = 0.5;
13
+
14
+ export class ContextHealthMonitor {
15
+ private db: Database.Database;
16
+ private criticalManager: CriticalContextManager;
17
+ private tokenLimit: number;
18
+
19
+ // In-memory tracking for current session
20
+ private contextChunks: ContextChunk[] = [];
21
+ private currentTokens: number = 0;
22
+
23
+ constructor(db: Database.Database, criticalManager: CriticalContextManager, tokenLimit?: number) {
24
+ this.db = db;
25
+ this.criticalManager = criticalManager;
26
+ this.tokenLimit = tokenLimit || DEFAULT_TOKEN_LIMIT;
27
+ }
28
+
29
+ setTokenLimit(limit: number): void {
30
+ this.tokenLimit = limit;
31
+ }
32
+
33
+ getTokenLimit(): number {
34
+ return this.tokenLimit;
35
+ }
36
+
37
+ addChunk(chunk: Omit<ContextChunk, 'id' | 'relevanceScore' | 'isCritical'>): ContextChunk {
38
+ const id = `chunk_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
39
+ const isCritical = this.criticalManager.isCritical(chunk.content);
40
+
41
+ const fullChunk: ContextChunk = {
42
+ ...chunk,
43
+ id,
44
+ relevanceScore: 1.0, // New chunks start with full relevance
45
+ isCritical
46
+ };
47
+
48
+ this.contextChunks.push(fullChunk);
49
+ this.currentTokens += chunk.tokens;
50
+
51
+ // Decay relevance of older chunks
52
+ this.decayRelevance();
53
+
54
+ return fullChunk;
55
+ }
56
+
57
+ removeChunk(id: string): boolean {
58
+ const index = this.contextChunks.findIndex(c => c.id === id);
59
+ if (index === -1) return false;
60
+
61
+ const chunk = this.contextChunks[index]!;
62
+ this.currentTokens -= chunk.tokens;
63
+ this.contextChunks.splice(index, 1);
64
+
65
+ return true;
66
+ }
67
+
68
+ getChunks(): ContextChunk[] {
69
+ return [...this.contextChunks];
70
+ }
71
+
72
+ clearChunks(): void {
73
+ this.contextChunks = [];
74
+ this.currentTokens = 0;
75
+ }
76
+
77
+ setCurrentTokens(tokens: number): void {
78
+ this.currentTokens = tokens;
79
+ }
80
+
81
+ getCurrentTokens(): number {
82
+ return this.currentTokens;
83
+ }
84
+
85
+ getHealth(driftScore: number = 0): ContextHealth {
86
+ const tokensUsed = this.currentTokens;
87
+ const utilizationPercent = (tokensUsed / this.tokenLimit) * 100;
88
+
89
+ // Calculate relevance score (average of all chunks)
90
+ const relevanceScore = this.contextChunks.length > 0
91
+ ? this.contextChunks.reduce((sum, c) => sum + c.relevanceScore, 0) / this.contextChunks.length
92
+ : 1.0;
93
+
94
+ // Determine health status
95
+ let health: ContextHealth['health'] = 'good';
96
+ if (utilizationPercent >= UTILIZATION_CRITICAL * 100 || driftScore >= DRIFT_CRITICAL) {
97
+ health = 'critical';
98
+ } else if (utilizationPercent >= UTILIZATION_WARNING * 100 || driftScore >= DRIFT_WARNING) {
99
+ health = 'warning';
100
+ }
101
+
102
+ // Check if compaction is needed
103
+ const compactionNeeded = health !== 'good';
104
+ const driftDetected = driftScore >= DRIFT_WARNING;
105
+
106
+ // Generate suggestions
107
+ const suggestions = this.generateSuggestions(health, utilizationPercent, driftScore);
108
+
109
+ // Get critical context count
110
+ const criticalContextCount = this.criticalManager.getCriticalCount();
111
+
112
+ const healthResult: ContextHealth = {
113
+ tokensUsed,
114
+ tokensLimit: this.tokenLimit,
115
+ utilizationPercent: Math.round(utilizationPercent * 10) / 10,
116
+ health,
117
+ relevanceScore: Math.round(relevanceScore * 100) / 100,
118
+ driftScore: Math.round(driftScore * 100) / 100,
119
+ criticalContextCount,
120
+ driftDetected,
121
+ compactionNeeded,
122
+ suggestions
123
+ };
124
+
125
+ // Log to history
126
+ this.logHealthCheck(healthResult);
127
+
128
+ return healthResult;
129
+ }
130
+
131
+ private generateSuggestions(
132
+ health: ContextHealth['health'],
133
+ utilization: number,
134
+ driftScore: number
135
+ ): string[] {
136
+ const suggestions: string[] = [];
137
+
138
+ if (health === 'good') {
139
+ suggestions.push('Context is healthy, no action needed');
140
+ return suggestions;
141
+ }
142
+
143
+ if (utilization >= UTILIZATION_CRITICAL * 100) {
144
+ suggestions.push('Context nearly full - compaction strongly recommended');
145
+ suggestions.push('Consider using "aggressive" compaction strategy');
146
+ } else if (utilization >= UTILIZATION_WARNING * 100) {
147
+ suggestions.push('Context getting large - consider compaction');
148
+ suggestions.push('Use "summarize" strategy to compress old context');
149
+ }
150
+
151
+ if (driftScore >= DRIFT_CRITICAL) {
152
+ suggestions.push('Significant drift detected - AI may be ignoring earlier instructions');
153
+ suggestions.push('Review critical context and add reminders if needed');
154
+ } else if (driftScore >= DRIFT_WARNING) {
155
+ suggestions.push('Some drift detected - consider marking critical items');
156
+ }
157
+
158
+ const criticalCount = this.criticalManager.getCriticalCount();
159
+ if (criticalCount === 0) {
160
+ suggestions.push('No critical context marked - consider marking important decisions/requirements');
161
+ }
162
+
163
+ return suggestions;
164
+ }
165
+
166
+ private decayRelevance(): void {
167
+ // Decay relevance of chunks based on position (older = lower relevance)
168
+ const totalChunks = this.contextChunks.length;
169
+
170
+ for (let i = 0; i < totalChunks; i++) {
171
+ const chunk = this.contextChunks[i]!;
172
+
173
+ // Critical chunks decay slower
174
+ const decayRate = chunk.isCritical ? 0.98 : 0.95;
175
+
176
+ // Position-based decay (older chunks have lower position)
177
+ const positionFactor = (i + 1) / totalChunks;
178
+
179
+ // New relevance is combination of decay and position
180
+ chunk.relevanceScore = Math.max(0.1, chunk.relevanceScore * decayRate * (0.5 + 0.5 * positionFactor));
181
+ }
182
+ }
183
+
184
+ private logHealthCheck(health: ContextHealth): void {
185
+ try {
186
+ const stmt = this.db.prepare(`
187
+ INSERT INTO context_health_history
188
+ (tokens_used, tokens_limit, utilization_percent, drift_score, relevance_score, health, compaction_triggered)
189
+ VALUES (?, ?, ?, ?, ?, ?, ?)
190
+ `);
191
+
192
+ stmt.run(
193
+ health.tokensUsed,
194
+ health.tokensLimit,
195
+ health.utilizationPercent,
196
+ health.driftScore,
197
+ health.relevanceScore,
198
+ health.health,
199
+ 0
200
+ );
201
+ } catch {
202
+ // Ignore logging errors
203
+ }
204
+ }
205
+
206
+ getHealthHistory(limit: number = 20): Array<{
207
+ timestamp: Date;
208
+ health: ContextHealth['health'];
209
+ utilizationPercent: number;
210
+ driftScore: number;
211
+ }> {
212
+ try {
213
+ const stmt = this.db.prepare(`
214
+ SELECT timestamp, health, utilization_percent, drift_score
215
+ FROM context_health_history
216
+ ORDER BY timestamp DESC
217
+ LIMIT ?
218
+ `);
219
+
220
+ const rows = stmt.all(limit) as Array<{
221
+ timestamp: number;
222
+ health: string;
223
+ utilization_percent: number;
224
+ drift_score: number;
225
+ }>;
226
+
227
+ return rows.map(row => ({
228
+ timestamp: new Date(row.timestamp * 1000),
229
+ health: row.health as ContextHealth['health'],
230
+ utilizationPercent: row.utilization_percent,
231
+ driftScore: row.drift_score
232
+ }));
233
+ } catch {
234
+ return [];
235
+ }
236
+ }
237
+
238
+ estimateTokens(text: string): number {
239
+ // Rough estimation: ~4 characters per token for English
240
+ // This is a simple heuristic; real implementation would use a proper tokenizer
241
+ return Math.ceil(text.length / 4);
242
+ }
243
+ }