k0ntext 3.3.0 → 3.3.1

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.
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Drift Detection Panel
3
+ *
4
+ * Enhanced drift detection with detailed analysis and visualization
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import chalk from 'chalk';
10
+ import { K0NTEXT_THEME } from '../theme.js';
11
+ import { DatabaseClient } from '../../../../db/client.js';
12
+
13
+ /**
14
+ * Drift type
15
+ */
16
+ export type DriftType = 'file_dates' | 'structure' | 'git_diff';
17
+
18
+ /**
19
+ * Drift severity
20
+ */
21
+ export type DriftSeverity = 'low' | 'medium' | 'high' | 'critical';
22
+
23
+ /**
24
+ * File drift information
25
+ */
26
+ interface FileDrift {
27
+ filePath: string;
28
+ fileType: 'doc' | 'code' | 'config';
29
+ lastModified: Date;
30
+ lastIndexed: Date;
31
+ daysSince: number;
32
+ severity: DriftSeverity;
33
+ }
34
+
35
+ /**
36
+ * Structure drift information
37
+ */
38
+ interface StructureDrift {
39
+ addedFiles: string[];
40
+ removedFiles: string[];
41
+ severity: DriftSeverity;
42
+ }
43
+
44
+ /**
45
+ * Git drift information
46
+ */
47
+ interface GitDrift {
48
+ committedChanges: number;
49
+ uncommittedChanges: string[];
50
+ severity: DriftSeverity;
51
+ }
52
+
53
+ /**
54
+ * Combined drift report
55
+ */
56
+ export interface DriftReport {
57
+ fileDrifts: FileDrift[];
58
+ structureDrift: StructureDrift | null;
59
+ gitDrift: GitDrift | null;
60
+ overallSeverity: DriftSeverity;
61
+ summary: string;
62
+ }
63
+
64
+ /**
65
+ * Drift Detection Panel
66
+ */
67
+ export class DriftDetectionPanel {
68
+ private projectRoot: string;
69
+
70
+ constructor(projectRoot: string) {
71
+ this.projectRoot = projectRoot;
72
+ }
73
+
74
+ /**
75
+ * Run complete drift analysis
76
+ */
77
+ async analyze(): Promise<DriftReport> {
78
+ const [fileDrifts, structureDrift, gitDrift] = await Promise.all([
79
+ this.analyzeFileDates(),
80
+ this.analyzeStructure(),
81
+ this.analyzeGitDiff()
82
+ ]);
83
+
84
+ // Determine overall severity
85
+ const severities: DriftSeverity[] = [
86
+ this.getDriftSeverity(fileDrifts),
87
+ structureDrift?.severity || 'low',
88
+ gitDrift?.severity || 'low'
89
+ ];
90
+
91
+ const overallSeverity: DriftSeverity = severities.includes('critical')
92
+ ? 'critical'
93
+ : severities.includes('high')
94
+ ? 'high'
95
+ : severities.includes('medium')
96
+ ? 'medium'
97
+ : 'low';
98
+
99
+ const summary = this.generateSummary(fileDrifts, structureDrift, gitDrift);
100
+
101
+ return {
102
+ fileDrifts,
103
+ structureDrift,
104
+ gitDrift,
105
+ overallSeverity,
106
+ summary
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Analyze file date drifts
112
+ */
113
+ private async analyzeFileDates(): Promise<FileDrift[]> {
114
+ const drifts: FileDrift[] = [];
115
+ const thresholdDays = 7; // Files not updated in 7 days are considered drifted
116
+
117
+ try {
118
+ const db = new DatabaseClient(this.projectRoot);
119
+ const items = db.getAllItems();
120
+
121
+ const now = new Date();
122
+
123
+ for (const item of items) {
124
+ if (!item.updatedAt) continue;
125
+
126
+ const lastIndexed = new Date(item.updatedAt);
127
+ const daysSince = Math.floor((now.getTime() - lastIndexed.getTime()) / (1000 * 60 * 60 * 24));
128
+
129
+ // Only include items that are drifted (> 7 days old)
130
+ if (daysSince <= thresholdDays) continue;
131
+
132
+ // Determine file type from item type
133
+ let fileType: 'doc' | 'code' | 'config' = 'doc';
134
+ if (item.type === 'code' || item.type === 'command' || item.type === 'commit') {
135
+ fileType = 'code';
136
+ } else if (item.type === 'config' || item.type === 'tool_config') {
137
+ fileType = 'config';
138
+ } else {
139
+ fileType = 'doc';
140
+ }
141
+
142
+ // Determine severity based on days since update
143
+ let severity: DriftSeverity = 'medium';
144
+ if (daysSince > 30) {
145
+ severity = 'critical';
146
+ } else if (daysSince > 14) {
147
+ severity = 'high';
148
+ } else {
149
+ severity = 'medium';
150
+ }
151
+
152
+ drifts.push({
153
+ filePath: item.filePath || item.id,
154
+ fileType,
155
+ lastModified: lastIndexed,
156
+ lastIndexed,
157
+ daysSince,
158
+ severity
159
+ });
160
+ }
161
+
162
+ db.close();
163
+ } catch (error) {
164
+ // If database is not available, return empty array
165
+ console.error('Failed to analyze file dates:', error);
166
+ }
167
+
168
+ return drifts;
169
+ }
170
+
171
+ /**
172
+ * Analyze structure changes
173
+ */
174
+ private async analyzeStructure(): Promise<StructureDrift | null> {
175
+ const addedFiles: string[] = [];
176
+ const removedFiles: string[] = [];
177
+
178
+ // Check for .k0ntext directory
179
+ const k0ntextDir = path.join(this.projectRoot, '.k0ntext');
180
+ if (!fs.existsSync(k0ntextDir)) {
181
+ return null;
182
+ }
183
+
184
+ // Could scan for untracked files
185
+ // This is a simplified implementation
186
+
187
+ let severity: DriftSeverity = 'low';
188
+ const totalChanges = addedFiles.length + removedFiles.length;
189
+
190
+ if (totalChanges > 50) severity = 'critical';
191
+ else if (totalChanges > 20) severity = 'high';
192
+ else if (totalChanges > 5) severity = 'medium';
193
+
194
+ return {
195
+ addedFiles,
196
+ removedFiles,
197
+ severity
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Analyze git diff for changes
203
+ */
204
+ private async analyzeGitDiff(): Promise<GitDrift | null> {
205
+ const gitDir = path.join(this.projectRoot, '.git');
206
+ if (!fs.existsSync(gitDir)) {
207
+ return null;
208
+ }
209
+
210
+ // Check for uncommitted changes
211
+ const uncommittedChanges: string[] = [];
212
+
213
+ try {
214
+ // Use git diff to check for changes
215
+ const { execSync } = require('child_process');
216
+
217
+ // Check for modified files
218
+ const modified = execSync('git diff --name-only', {
219
+ cwd: this.projectRoot,
220
+ encoding: 'utf-8',
221
+ stdio: 'pipe'
222
+ }) as string;
223
+
224
+ if (modified.trim()) {
225
+ uncommittedChanges.push(...modified.trim().split('\n').filter(Boolean));
226
+ }
227
+
228
+ // Check for untracked files
229
+ const untracked = execSync('git ls-files --others --exclude-standard', {
230
+ cwd: this.projectRoot,
231
+ encoding: 'utf-8',
232
+ stdio: 'pipe'
233
+ }) as string;
234
+
235
+ if (untracked.trim()) {
236
+ uncommittedChanges.push(...untracked.trim().split('\n').filter(Boolean));
237
+ }
238
+
239
+ // Get commit count
240
+ const commitCountStr = execSync('git rev-list --count HEAD', {
241
+ cwd: this.projectRoot,
242
+ encoding: 'utf-8',
243
+ stdio: 'pipe'
244
+ }) as string;
245
+
246
+ const commitCount = parseInt(commitCountStr.trim() || '0');
247
+
248
+ let severity: DriftSeverity = 'low';
249
+ const totalChanges = uncommittedChanges.length;
250
+
251
+ if (totalChanges > 20) severity = 'critical';
252
+ else if (totalChanges > 10) severity = 'high';
253
+ else if (totalChanges > 5) severity = 'medium';
254
+
255
+ return {
256
+ committedChanges: commitCount,
257
+ uncommittedChanges,
258
+ severity
259
+ };
260
+ } catch {
261
+ // Git not available or error
262
+ return null;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Get drift severity from file drifts
268
+ */
269
+ private getDriftSeverity(drifts: FileDrift[]): DriftSeverity {
270
+ if (drifts.length === 0) return 'low';
271
+
272
+ const criticalDrifts = drifts.filter(d => d.severity === 'critical').length;
273
+ const highDrifts = drifts.filter(d => d.severity === 'high').length;
274
+ const mediumDrifts = drifts.filter(d => d.severity === 'medium').length;
275
+
276
+ if (criticalDrifts > 5) return 'critical';
277
+ if (criticalDrifts > 2 || highDrifts > 10) return 'high';
278
+ if (mediumDrifts > 10 || highDrifts > 3) return 'medium';
279
+ return 'low';
280
+ }
281
+
282
+ /**
283
+ * Generate summary text
284
+ */
285
+ private generateSummary(
286
+ fileDrifts: FileDrift[],
287
+ structureDrift: StructureDrift | null,
288
+ gitDrift: GitDrift | null
289
+ ): string {
290
+ const parts: string[] = [];
291
+
292
+ const fileCount = fileDrifts.length;
293
+ const structChanges = structureDrift ? structureDrift.addedFiles.length + structureDrift.removedFiles.length : 0;
294
+ const gitChanges = gitDrift ? gitDrift.uncommittedChanges.length : 0;
295
+
296
+ if (fileCount === 0 && structChanges === 0 && gitChanges === 0) {
297
+ return 'All context files are up to date with your codebase.';
298
+ }
299
+
300
+ if (fileCount > 0) {
301
+ parts.push(`${fileCount} files may be outdated`);
302
+ }
303
+
304
+ if (structChanges > 0) {
305
+ parts.push(`${structChanges} structural changes detected`);
306
+ }
307
+
308
+ if (gitChanges > 0) {
309
+ parts.push(`${gitChanges} uncommitted changes`);
310
+ }
311
+
312
+ return parts.join(', ') || 'No drift detected';
313
+ }
314
+
315
+ /**
316
+ * Display drift report
317
+ */
318
+ displayReport(report: DriftReport): string {
319
+ const lines: string[] = [];
320
+
321
+ lines.push('');
322
+ lines.push(K0NTEXT_THEME.header('━━━ Documentation Drift Analysis ━━━'));
323
+ lines.push('');
324
+
325
+ // Overall severity
326
+ const severityEmoji = {
327
+ critical: K0NTEXT_THEME.error('🔴 Critical'),
328
+ high: K0NTEXT_THEME.warning('🟠 High'),
329
+ medium: K0NTEXT_THEME.warning('🟡 Medium'),
330
+ low: K0NTEXT_THEME.success('🟢 Good')
331
+ }[report.overallSeverity];
332
+
333
+ lines.push(` Overall Status: ${severityEmoji}`);
334
+ lines.push(` Summary: ${report.summary}`);
335
+ lines.push('');
336
+
337
+ // File drifts
338
+ if (report.fileDrifts.length > 0) {
339
+ lines.push(K0NTEXT_THEME.header('━━━ File Date Drifts ━──'));
340
+
341
+ const bySeverity = this.groupBySeverity(report.fileDrifts);
342
+
343
+ for (const [severity, drifts] of Object.entries(bySeverity)) {
344
+ if (drifts.length === 0) continue;
345
+
346
+ const severityLabel = {
347
+ critical: '🔴 Critical',
348
+ high: '🟠 High',
349
+ medium: '🟡 Medium',
350
+ low: '🟢 Low'
351
+ }[severity as DriftSeverity];
352
+
353
+ lines.push(` ${severityLabel} (${drifts.length} files):`);
354
+
355
+ for (const drift of drifts.slice(0, 5)) {
356
+ const icon = this.getFileTypeIcon(drift.fileType);
357
+ const days = drift.daysSince;
358
+ lines.push(` ${icon} ${K0NTEXT_THEME.dim(drift.filePath)}`);
359
+ lines.push(` ${K0NTEXT_THEME.dim(`not updated in ${days} days`)}`);
360
+ }
361
+
362
+ if (drifts.length > 5) {
363
+ lines.push(` ${K0NTEXT_THEME.dim(`... and ${drifts.length - 5} more`)}`);
364
+ }
365
+
366
+ lines.push('');
367
+ }
368
+ }
369
+
370
+ // Structure drifts
371
+ if (report.structureDrift) {
372
+ lines.push(K0NTEXT_THEME.header('━━━ Structure Changes ━──'));
373
+
374
+ if (report.structureDrift.addedFiles.length > 0) {
375
+ lines.push(` ${K0NTEXT_THEME.success('+')} New files: ${report.structureDrift.addedFiles.length}`);
376
+ for (const file of report.structureDrift.addedFiles.slice(0, 5)) {
377
+ lines.push(` ${K0NTEXT_THEME.dim(file)}`);
378
+ }
379
+ if (report.structureDrift.addedFiles.length > 5) {
380
+ lines.push(` ${K0NTEXT_THEME.dim('... and more')}`);
381
+ }
382
+ lines.push('');
383
+ }
384
+
385
+ if (report.structureDrift.removedFiles.length > 0) {
386
+ lines.push(` ${K0NTEXT_THEME.error('-')} Removed files: ${report.structureDrift.removedFiles.length}`);
387
+ for (const file of report.structureDrift.removedFiles.slice(0, 5)) {
388
+ lines.push(` ${K0NTEXT_THEME.dim(file)}`);
389
+ }
390
+ if (report.structureDrift.removedFiles.length > 5) {
391
+ lines.push(` ${K0NTEXT_THEME.dim('... and more')}`);
392
+ }
393
+ lines.push('');
394
+ }
395
+ }
396
+
397
+ // Git drifts
398
+ if (report.gitDrift) {
399
+ lines.push(K0NTEXT_THEME.header('━━━ Git Changes ━──'));
400
+
401
+ if (report.gitDrift.uncommittedChanges.length > 0) {
402
+ lines.push(` Uncommitted changes: ${report.gitDrift.uncommittedChanges.length}`);
403
+
404
+ for (const file of report.gitDrift.uncommittedChanges.slice(0, 5)) {
405
+ const status = file.includes('(new file)') ? 'new' : 'modified';
406
+ const statusIcon = status === 'new' ? K0NTEXT_THEME.success('+') : K0NTEXT_THEME.warning('~');
407
+ lines.push(` ${statusIcon} ${K0NTEXT_THEME.dim(file)}`);
408
+ }
409
+ if (report.gitDrift.uncommittedChanges.length > 5) {
410
+ lines.push(` ${K0NTEXT_THEME.dim('... and more')}`);
411
+ }
412
+ lines.push('');
413
+ }
414
+ }
415
+
416
+ // Recommendations
417
+ lines.push(K0NTEXT_THEME.header('━━━ Recommendations ━━━'));
418
+
419
+ if (report.overallSeverity === 'critical' || report.overallSeverity === 'high') {
420
+ lines.push(` ${K0NTEXT_THEME.warning('⚠ Urgent action recommended:')}`);
421
+ lines.push(` ${K0NTEXT_THEME.cyan('•')} Run ${K0NTEXT_THEME.highlight('index')} to update your context`);
422
+ lines.push(` ${K0NTEXT_THEME.cyan('•')} Commit your changes to keep tracking in sync`);
423
+ } else if (report.overallSeverity === 'medium') {
424
+ lines.push(` ${K0NTEXT_THEME.info('ℹ Consider updating soon:')}`);
425
+ lines.push(` ${K0NTEXT_THEME.cyan('•')} Run ${K0NTEXT_THEME.highlight('index')} to refresh context`);
426
+ } else {
427
+ lines.push(` ${K0NTEXT_THEME.success('✓ Your context is up to date!')}`);
428
+ }
429
+
430
+ lines.push('');
431
+
432
+ return lines.join('\n');
433
+ }
434
+
435
+ /**
436
+ * Group drifts by severity
437
+ */
438
+ private groupBySeverity(drifts: FileDrift[]): Record<string, FileDrift[]> {
439
+ return {
440
+ critical: drifts.filter(d => d.severity === 'critical'),
441
+ high: drifts.filter(d => d.severity === 'high'),
442
+ medium: drifts.filter(d => d.severity === 'medium'),
443
+ low: drifts.filter(d => d.severity === 'low')
444
+ };
445
+ }
446
+
447
+ /**
448
+ * Get file type icon
449
+ */
450
+ private getFileTypeIcon(type: 'doc' | 'code' | 'config'): string {
451
+ const icons = {
452
+ doc: '📄',
453
+ code: '💻',
454
+ config: '⚙️'
455
+ };
456
+ return icons[type] || '📄';
457
+ }
458
+ }