prjct-cli 0.9.2 → 0.10.2
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/CHANGELOG.md +142 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +299 -17
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/context-filter.js +83 -83
- package/core/agentic/ground-truth.js +591 -0
- package/core/agentic/loop-detector.js +406 -0
- package/core/agentic/memory-system.js +850 -0
- package/core/agentic/parallel-tools.js +366 -0
- package/core/agentic/plan-mode.js +572 -0
- package/core/agentic/prompt-builder.js +127 -2
- package/core/agentic/response-templates.js +290 -0
- package/core/agentic/semantic-compression.js +517 -0
- package/core/agentic/think-blocks.js +657 -0
- package/core/agentic/tool-registry.js +32 -0
- package/core/agentic/validation-rules.js +380 -0
- package/core/command-registry.js +48 -0
- package/core/commands.js +128 -60
- package/core/context-sync.js +183 -0
- package/core/domain/agent-generator.js +77 -46
- package/core/domain/agent-loader.js +183 -0
- package/core/domain/agent-matcher.js +217 -0
- package/core/domain/agent-validator.js +217 -0
- package/core/domain/context-estimator.js +175 -0
- package/core/domain/product-standards.js +92 -0
- package/core/domain/smart-cache.js +157 -0
- package/core/domain/task-analyzer.js +353 -0
- package/core/domain/tech-detector.js +365 -0
- package/package.json +8 -16
- package/templates/commands/done.md +7 -0
- package/templates/commands/feature.md +8 -0
- package/templates/commands/ship.md +8 -0
- package/templates/commands/spec.md +128 -0
- package/templates/global/CLAUDE.md +17 -0
- package/core/__tests__/agentic/agent-router.test.js +0 -398
- package/core/__tests__/agentic/command-executor.test.js +0 -223
- package/core/__tests__/agentic/context-builder.test.js +0 -160
- package/core/__tests__/agentic/context-filter.test.js +0 -494
- package/core/__tests__/agentic/prompt-builder.test.js +0 -212
- package/core/__tests__/agentic/template-loader.test.js +0 -164
- package/core/__tests__/agentic/tool-registry.test.js +0 -243
- package/core/__tests__/domain/agent-generator.test.js +0 -296
- package/core/__tests__/domain/analyzer.test.js +0 -324
- package/core/__tests__/infrastructure/author-detector.test.js +0 -103
- package/core/__tests__/infrastructure/config-manager.test.js +0 -454
- package/core/__tests__/infrastructure/path-manager.test.js +0 -412
- package/core/__tests__/setup.test.js +0 -15
- package/core/__tests__/utils/date-helper.test.js +0 -169
- package/core/__tests__/utils/file-helper.test.js +0 -258
- package/core/__tests__/utils/jsonl-helper.test.js +0 -387
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Compression
|
|
3
|
+
*
|
|
4
|
+
* Compresses raw data into semantic summaries to reduce token usage.
|
|
5
|
+
* Instead of loading full files, provides insights.
|
|
6
|
+
*
|
|
7
|
+
* OPTIMIZATION (P2.3): Semantic Compression
|
|
8
|
+
* - Summarizers for each data type
|
|
9
|
+
* - 70% less tokens for same insight
|
|
10
|
+
* - Metrics tracking
|
|
11
|
+
*
|
|
12
|
+
* Source: Cursor, Windsurf, Kiro patterns
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
class SemanticCompression {
|
|
16
|
+
constructor() {
|
|
17
|
+
// Track compression metrics
|
|
18
|
+
this.metrics = {
|
|
19
|
+
totalOriginalTokens: 0,
|
|
20
|
+
totalCompressedTokens: 0,
|
|
21
|
+
compressions: 0
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compress any content based on type
|
|
27
|
+
* @param {string} content - Raw content
|
|
28
|
+
* @param {string} type - Content type (session, shipped, metrics, queue, ideas, roadmap)
|
|
29
|
+
* @returns {Object} Compressed summary
|
|
30
|
+
*/
|
|
31
|
+
compress(content, type) {
|
|
32
|
+
if (!content || content.trim() === '') {
|
|
33
|
+
return { summary: 'Empty', tokens: { original: 0, compressed: 0 } }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const compressor = this._getCompressor(type)
|
|
37
|
+
const result = compressor.call(this, content)
|
|
38
|
+
|
|
39
|
+
// Track metrics
|
|
40
|
+
const originalTokens = this._estimateTokens(content)
|
|
41
|
+
const compressedTokens = this._estimateTokens(
|
|
42
|
+
typeof result === 'string' ? result : JSON.stringify(result)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
this.metrics.totalOriginalTokens += originalTokens
|
|
46
|
+
this.metrics.totalCompressedTokens += compressedTokens
|
|
47
|
+
this.metrics.compressions++
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...result,
|
|
51
|
+
tokens: {
|
|
52
|
+
original: originalTokens,
|
|
53
|
+
compressed: compressedTokens,
|
|
54
|
+
reduction: Math.round((1 - compressedTokens / originalTokens) * 100)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get appropriate compressor for type
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
_getCompressor(type) {
|
|
64
|
+
const compressors = {
|
|
65
|
+
session: this._compressSession,
|
|
66
|
+
shipped: this._compressShipped,
|
|
67
|
+
metrics: this._compressMetrics,
|
|
68
|
+
queue: this._compressQueue,
|
|
69
|
+
ideas: this._compressIdeas,
|
|
70
|
+
roadmap: this._compressRoadmap,
|
|
71
|
+
now: this._compressNow,
|
|
72
|
+
history: this._compressHistory,
|
|
73
|
+
spec: this._compressSpec,
|
|
74
|
+
default: this._compressGeneric
|
|
75
|
+
}
|
|
76
|
+
return compressors[type] || compressors.default
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compress session JSONL data
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
_compressSession(content) {
|
|
84
|
+
const lines = content.split('\n').filter(l => l.trim())
|
|
85
|
+
const entries = lines.map(l => {
|
|
86
|
+
try { return JSON.parse(l) }
|
|
87
|
+
catch { return null }
|
|
88
|
+
}).filter(Boolean)
|
|
89
|
+
|
|
90
|
+
if (entries.length === 0) {
|
|
91
|
+
return { summary: 'No session data', entries: 0 }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Group by type
|
|
95
|
+
const byType = {}
|
|
96
|
+
entries.forEach(e => {
|
|
97
|
+
const type = e.type || 'unknown'
|
|
98
|
+
byType[type] = (byType[type] || 0) + 1
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Calculate time range
|
|
102
|
+
const timestamps = entries.map(e => new Date(e.ts)).filter(d => !isNaN(d))
|
|
103
|
+
const earliest = timestamps.length ? new Date(Math.min(...timestamps)) : null
|
|
104
|
+
const latest = timestamps.length ? new Date(Math.max(...timestamps)) : null
|
|
105
|
+
|
|
106
|
+
// Calculate duration
|
|
107
|
+
let duration = 'N/A'
|
|
108
|
+
if (earliest && latest) {
|
|
109
|
+
const diffMs = latest - earliest
|
|
110
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
111
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
|
|
112
|
+
duration = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
summary: `${entries.length} entries over ${duration}`,
|
|
117
|
+
entries: entries.length,
|
|
118
|
+
byType,
|
|
119
|
+
timeRange: { start: earliest?.toISOString(), end: latest?.toISOString() },
|
|
120
|
+
duration
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Compress shipped.md content
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
_compressShipped(content) {
|
|
129
|
+
const lines = content.split('\n')
|
|
130
|
+
|
|
131
|
+
// Count shipped items
|
|
132
|
+
const shipped = lines.filter(l => l.match(/^[-*]\s+/)).length
|
|
133
|
+
|
|
134
|
+
// Extract dates
|
|
135
|
+
const dateMatches = content.match(/\d{4}-\d{2}-\d{2}/g) || []
|
|
136
|
+
const uniqueDates = [...new Set(dateMatches)].sort()
|
|
137
|
+
|
|
138
|
+
// Detect recent activity
|
|
139
|
+
const today = new Date().toISOString().split('T')[0]
|
|
140
|
+
const hasToday = uniqueDates.includes(today)
|
|
141
|
+
|
|
142
|
+
// Calculate velocity (ships per week)
|
|
143
|
+
let velocity = 0
|
|
144
|
+
if (uniqueDates.length > 0) {
|
|
145
|
+
const firstDate = new Date(uniqueDates[0])
|
|
146
|
+
const lastDate = new Date(uniqueDates[uniqueDates.length - 1])
|
|
147
|
+
const weeks = Math.max(1, (lastDate - firstDate) / (1000 * 60 * 60 * 24 * 7))
|
|
148
|
+
velocity = Math.round(shipped / weeks * 10) / 10
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
summary: `${shipped} shipped | ${velocity}/week`,
|
|
153
|
+
shipped,
|
|
154
|
+
dates: uniqueDates.length,
|
|
155
|
+
velocity,
|
|
156
|
+
recentActivity: hasToday,
|
|
157
|
+
dateRange: {
|
|
158
|
+
first: uniqueDates[0] || null,
|
|
159
|
+
last: uniqueDates[uniqueDates.length - 1] || null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Compress metrics.md content
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
_compressMetrics(content) {
|
|
169
|
+
// Extract key metrics using patterns
|
|
170
|
+
const patterns = {
|
|
171
|
+
totalTasks: /total[:\s]+(\d+)/i,
|
|
172
|
+
completed: /completed[:\s]+(\d+)/i,
|
|
173
|
+
velocity: /velocity[:\s]+([\d.]+)/i,
|
|
174
|
+
streak: /streak[:\s]+(\d+)/i,
|
|
175
|
+
avgTime: /average[:\s]+([\d.]+\s*[hm])/i
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const metrics = {}
|
|
179
|
+
for (const [key, pattern] of Object.entries(patterns)) {
|
|
180
|
+
const match = content.match(pattern)
|
|
181
|
+
if (match) {
|
|
182
|
+
metrics[key] = match[1]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Determine momentum
|
|
187
|
+
let momentum = 'medium'
|
|
188
|
+
if (metrics.streak && parseInt(metrics.streak) > 5) momentum = 'high'
|
|
189
|
+
else if (metrics.streak && parseInt(metrics.streak) < 2) momentum = 'low'
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
summary: `${metrics.completed || '?'} done | Momentum: ${momentum}`,
|
|
193
|
+
metrics,
|
|
194
|
+
momentum
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Compress queue (next.md) content
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
_compressQueue(content) {
|
|
203
|
+
const lines = content.split('\n')
|
|
204
|
+
|
|
205
|
+
// Count tasks by status
|
|
206
|
+
const pending = (content.match(/- \[ \]/g) || []).length
|
|
207
|
+
const done = (content.match(/- \[x\]/gi) || []).length
|
|
208
|
+
const total = pending + done
|
|
209
|
+
|
|
210
|
+
// Extract priorities
|
|
211
|
+
const urgent = lines.filter(l =>
|
|
212
|
+
l.toLowerCase().includes('urgent') ||
|
|
213
|
+
l.toLowerCase().includes('critical') ||
|
|
214
|
+
l.includes('🔥') ||
|
|
215
|
+
l.includes('🚨')
|
|
216
|
+
).length
|
|
217
|
+
|
|
218
|
+
// Get top 3 items
|
|
219
|
+
const taskLines = lines.filter(l => l.match(/^[-*]\s+\[[ x]\]/))
|
|
220
|
+
const top3 = taskLines.slice(0, 3).map(l =>
|
|
221
|
+
l.replace(/^[-*]\s+\[[ x]\]\s*/, '').trim().substring(0, 40)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
summary: `${pending} pending | ${urgent} urgent`,
|
|
226
|
+
total,
|
|
227
|
+
pending,
|
|
228
|
+
done,
|
|
229
|
+
urgent,
|
|
230
|
+
top3,
|
|
231
|
+
percentComplete: total > 0 ? Math.round(done / total * 100) : 0
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Compress ideas.md content
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
_compressIdeas(content) {
|
|
240
|
+
// Count ideas by section
|
|
241
|
+
const sections = content.split(/^##\s+/m).filter(Boolean)
|
|
242
|
+
const ideaCount = (content.match(/^###\s+/gm) || []).length
|
|
243
|
+
|
|
244
|
+
// Detect priorities
|
|
245
|
+
const highPriority = (content.match(/🔥|HIGH|CRITICAL|URGENT/gi) || []).length
|
|
246
|
+
const hasRecent = content.includes(new Date().toISOString().split('T')[0])
|
|
247
|
+
|
|
248
|
+
// Extract categories
|
|
249
|
+
const categories = sections.map(s => s.split('\n')[0].trim()).filter(Boolean)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
summary: `${ideaCount} ideas | ${highPriority} high priority`,
|
|
253
|
+
ideaCount,
|
|
254
|
+
highPriority,
|
|
255
|
+
categories: categories.slice(0, 5),
|
|
256
|
+
hasRecentIdeas: hasRecent
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compress roadmap.md content
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
_compressRoadmap(content) {
|
|
265
|
+
// Extract phases
|
|
266
|
+
const phases = content.match(/^##\s+Phase\s+\d+/gim) || []
|
|
267
|
+
|
|
268
|
+
// Detect status markers
|
|
269
|
+
const completed = (content.match(/✅|COMPLETED?|DONE/gi) || []).length
|
|
270
|
+
const inProgress = (content.match(/🔄|IN.?PROGRESS|CURRENT/gi) || []).length
|
|
271
|
+
const planned = (content.match(/📋|PLANNED|TODO/gi) || []).length
|
|
272
|
+
|
|
273
|
+
// Extract current phase
|
|
274
|
+
let currentPhase = 'Unknown'
|
|
275
|
+
const currentMatch = content.match(/##\s+(Phase\s+\d+[^#\n]*)/i)
|
|
276
|
+
if (currentMatch) {
|
|
277
|
+
currentPhase = currentMatch[1].trim()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Calculate progress
|
|
281
|
+
const total = completed + inProgress + planned
|
|
282
|
+
const progress = total > 0 ? Math.round(completed / total * 100) : 0
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
summary: `${phases.length} phases | ${progress}% complete`,
|
|
286
|
+
phases: phases.length,
|
|
287
|
+
status: { completed, inProgress, planned },
|
|
288
|
+
currentPhase,
|
|
289
|
+
progress
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Compress now.md content
|
|
295
|
+
* @private
|
|
296
|
+
*/
|
|
297
|
+
_compressNow(content) {
|
|
298
|
+
if (!content || content.trim() === '') {
|
|
299
|
+
return { summary: 'No active task', hasTask: false }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Extract task name
|
|
303
|
+
let taskName = ''
|
|
304
|
+
const boldMatch = content.match(/\*\*([^*]+)\*\*/)
|
|
305
|
+
const headerMatch = content.match(/^#\s+(.+)$/m)
|
|
306
|
+
|
|
307
|
+
if (boldMatch) taskName = boldMatch[1]
|
|
308
|
+
else if (headerMatch) taskName = headerMatch[1]
|
|
309
|
+
else taskName = content.split('\n')[0].substring(0, 50)
|
|
310
|
+
|
|
311
|
+
// Extract start time
|
|
312
|
+
const timeMatch = content.match(/Started:\s*(\d{4}-\d{2}-\d{2}T[\d:]+)/i)
|
|
313
|
+
let duration = ''
|
|
314
|
+
if (timeMatch) {
|
|
315
|
+
const start = new Date(timeMatch[1])
|
|
316
|
+
const now = new Date()
|
|
317
|
+
const diffMs = now - start
|
|
318
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
319
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
|
|
320
|
+
duration = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Extract agent if present
|
|
324
|
+
const agentMatch = content.match(/Agent:\s*(\w+)/i)
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
summary: `🎯 ${taskName.substring(0, 30)}${duration ? ` (${duration})` : ''}`,
|
|
328
|
+
hasTask: true,
|
|
329
|
+
taskName,
|
|
330
|
+
duration,
|
|
331
|
+
agent: agentMatch ? agentMatch[1] : null
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Compress JSONL history content
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
_compressHistory(content) {
|
|
340
|
+
const lines = content.split('\n').filter(l => l.trim())
|
|
341
|
+
const entries = lines.map(l => {
|
|
342
|
+
try { return JSON.parse(l) }
|
|
343
|
+
catch { return null }
|
|
344
|
+
}).filter(Boolean)
|
|
345
|
+
|
|
346
|
+
if (entries.length === 0) {
|
|
347
|
+
return { summary: 'No history', entries: 0 }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Get last 5 entries summary
|
|
351
|
+
const recent = entries.slice(-5).map(e => ({
|
|
352
|
+
type: e.type,
|
|
353
|
+
ts: e.ts
|
|
354
|
+
}))
|
|
355
|
+
|
|
356
|
+
// Count by type
|
|
357
|
+
const byType = {}
|
|
358
|
+
entries.forEach(e => {
|
|
359
|
+
const type = e.type || 'unknown'
|
|
360
|
+
byType[type] = (byType[type] || 0) + 1
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
summary: `${entries.length} events`,
|
|
365
|
+
entries: entries.length,
|
|
366
|
+
recent,
|
|
367
|
+
byType,
|
|
368
|
+
lastEntry: entries[entries.length - 1]?.ts || null
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Compress spec file content
|
|
374
|
+
* @private
|
|
375
|
+
*/
|
|
376
|
+
_compressSpec(content) {
|
|
377
|
+
if (!content || content.trim() === '') {
|
|
378
|
+
return { summary: 'No spec content', hasSpec: false }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const lines = content.split('\n')
|
|
382
|
+
|
|
383
|
+
// Extract spec name from header
|
|
384
|
+
const headerMatch = content.match(/^#\s+(.+)$/m)
|
|
385
|
+
const specName = headerMatch ? headerMatch[1] : 'Unnamed Spec'
|
|
386
|
+
|
|
387
|
+
// Count sections
|
|
388
|
+
const sections = (content.match(/^##\s+/gm) || []).length
|
|
389
|
+
|
|
390
|
+
// Extract requirements count
|
|
391
|
+
const requirements = lines.filter(l =>
|
|
392
|
+
l.match(/^[-*]\s+/) && !l.match(/\[[ x]\]/)
|
|
393
|
+
).length
|
|
394
|
+
|
|
395
|
+
// Extract tasks count (checkboxes)
|
|
396
|
+
const tasks = (content.match(/- \[ \]/g) || []).length
|
|
397
|
+
const completedTasks = (content.match(/- \[x\]/gi) || []).length
|
|
398
|
+
const totalTasks = tasks + completedTasks
|
|
399
|
+
|
|
400
|
+
// Detect status markers
|
|
401
|
+
const hasApproval = content.toLowerCase().includes('approved')
|
|
402
|
+
const hasDraft = content.toLowerCase().includes('draft')
|
|
403
|
+
const hasBlocked = content.toLowerCase().includes('blocked')
|
|
404
|
+
|
|
405
|
+
// Determine status
|
|
406
|
+
let status = 'draft'
|
|
407
|
+
if (hasApproval) status = 'approved'
|
|
408
|
+
else if (hasBlocked) status = 'blocked'
|
|
409
|
+
else if (hasDraft) status = 'draft'
|
|
410
|
+
|
|
411
|
+
// Extract design decisions count
|
|
412
|
+
const decisions = (content.match(/^###\s+/gm) || []).length
|
|
413
|
+
|
|
414
|
+
// Calculate progress
|
|
415
|
+
const progress = totalTasks > 0
|
|
416
|
+
? Math.round(completedTasks / totalTasks * 100)
|
|
417
|
+
: 0
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
summary: `📋 ${specName} | ${sections} sections | ${totalTasks} tasks (${progress}%)`,
|
|
421
|
+
hasSpec: true,
|
|
422
|
+
specName,
|
|
423
|
+
sections,
|
|
424
|
+
requirements,
|
|
425
|
+
tasks: totalTasks,
|
|
426
|
+
completedTasks,
|
|
427
|
+
decisions,
|
|
428
|
+
status,
|
|
429
|
+
progress
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Generic compression for unknown types
|
|
435
|
+
* @private
|
|
436
|
+
*/
|
|
437
|
+
_compressGeneric(content) {
|
|
438
|
+
const lines = content.split('\n')
|
|
439
|
+
const nonEmpty = lines.filter(l => l.trim()).length
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
summary: `${nonEmpty} lines`,
|
|
443
|
+
lines: lines.length,
|
|
444
|
+
nonEmpty,
|
|
445
|
+
preview: content.substring(0, 100).replace(/\n/g, ' ')
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Estimate token count (rough approximation)
|
|
451
|
+
* ~4 characters per token for English text
|
|
452
|
+
* @private
|
|
453
|
+
*/
|
|
454
|
+
_estimateTokens(text) {
|
|
455
|
+
if (!text) return 0
|
|
456
|
+
return Math.ceil(text.length / 4)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get compression metrics
|
|
461
|
+
* @returns {Object}
|
|
462
|
+
*/
|
|
463
|
+
getMetrics() {
|
|
464
|
+
const reduction = this.metrics.totalOriginalTokens > 0
|
|
465
|
+
? Math.round((1 - this.metrics.totalCompressedTokens / this.metrics.totalOriginalTokens) * 100)
|
|
466
|
+
: 0
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
...this.metrics,
|
|
470
|
+
reductionPercent: reduction,
|
|
471
|
+
averageReduction: this.metrics.compressions > 0
|
|
472
|
+
? Math.round(reduction / this.metrics.compressions)
|
|
473
|
+
: 0
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Reset metrics
|
|
479
|
+
*/
|
|
480
|
+
resetMetrics() {
|
|
481
|
+
this.metrics = {
|
|
482
|
+
totalOriginalTokens: 0,
|
|
483
|
+
totalCompressedTokens: 0,
|
|
484
|
+
compressions: 0
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Compress multiple contents at once
|
|
490
|
+
* @param {Object} contents - Map of type -> content
|
|
491
|
+
* @returns {Object} Map of type -> compressed
|
|
492
|
+
*/
|
|
493
|
+
compressAll(contents) {
|
|
494
|
+
const result = {}
|
|
495
|
+
for (const [type, content] of Object.entries(contents)) {
|
|
496
|
+
result[type] = this.compress(content, type)
|
|
497
|
+
}
|
|
498
|
+
return result
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Format compressed data for context
|
|
503
|
+
* @param {Object} compressed - Result from compress()
|
|
504
|
+
* @returns {string}
|
|
505
|
+
*/
|
|
506
|
+
format(compressed) {
|
|
507
|
+
if (!compressed) return ''
|
|
508
|
+
|
|
509
|
+
if (typeof compressed.summary === 'string') {
|
|
510
|
+
return compressed.summary
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return JSON.stringify(compressed, null, 2)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
module.exports = new SemanticCompression()
|