ai-mind-map 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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/change-tracker/change-log.d.ts +160 -0
  4. package/dist/change-tracker/change-log.d.ts.map +1 -0
  5. package/dist/change-tracker/change-log.js +507 -0
  6. package/dist/change-tracker/change-log.js.map +1 -0
  7. package/dist/change-tracker/diff-engine.d.ts +149 -0
  8. package/dist/change-tracker/diff-engine.d.ts.map +1 -0
  9. package/dist/change-tracker/diff-engine.js +530 -0
  10. package/dist/change-tracker/diff-engine.js.map +1 -0
  11. package/dist/change-tracker/watcher.d.ts +137 -0
  12. package/dist/change-tracker/watcher.d.ts.map +1 -0
  13. package/dist/change-tracker/watcher.js +300 -0
  14. package/dist/change-tracker/watcher.js.map +1 -0
  15. package/dist/cli.d.ts +20 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +937 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/config.d.ts +38 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +222 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/context/compressor.d.ts +49 -0
  24. package/dist/context/compressor.d.ts.map +1 -0
  25. package/dist/context/compressor.js +769 -0
  26. package/dist/context/compressor.js.map +1 -0
  27. package/dist/context/progressive-disclosure.d.ts +71 -0
  28. package/dist/context/progressive-disclosure.d.ts.map +1 -0
  29. package/dist/context/progressive-disclosure.js +470 -0
  30. package/dist/context/progressive-disclosure.js.map +1 -0
  31. package/dist/context/token-budget.d.ts +121 -0
  32. package/dist/context/token-budget.d.ts.map +1 -0
  33. package/dist/context/token-budget.js +282 -0
  34. package/dist/context/token-budget.js.map +1 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +944 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/install.d.ts +66 -0
  40. package/dist/install.d.ts.map +1 -0
  41. package/dist/install.js +946 -0
  42. package/dist/install.js.map +1 -0
  43. package/dist/knowledge-graph/architecture.d.ts +213 -0
  44. package/dist/knowledge-graph/architecture.d.ts.map +1 -0
  45. package/dist/knowledge-graph/architecture.js +585 -0
  46. package/dist/knowledge-graph/architecture.js.map +1 -0
  47. package/dist/knowledge-graph/cypher.d.ts +113 -0
  48. package/dist/knowledge-graph/cypher.d.ts.map +1 -0
  49. package/dist/knowledge-graph/cypher.js +1051 -0
  50. package/dist/knowledge-graph/cypher.js.map +1 -0
  51. package/dist/knowledge-graph/dead-code.d.ts +121 -0
  52. package/dist/knowledge-graph/dead-code.d.ts.map +1 -0
  53. package/dist/knowledge-graph/dead-code.js +331 -0
  54. package/dist/knowledge-graph/dead-code.js.map +1 -0
  55. package/dist/knowledge-graph/flow-analyzer.d.ts +167 -0
  56. package/dist/knowledge-graph/flow-analyzer.d.ts.map +1 -0
  57. package/dist/knowledge-graph/flow-analyzer.js +739 -0
  58. package/dist/knowledge-graph/flow-analyzer.js.map +1 -0
  59. package/dist/knowledge-graph/graph.d.ts +291 -0
  60. package/dist/knowledge-graph/graph.d.ts.map +1 -0
  61. package/dist/knowledge-graph/graph.js +978 -0
  62. package/dist/knowledge-graph/graph.js.map +1 -0
  63. package/dist/knowledge-graph/index.d.ts +17 -0
  64. package/dist/knowledge-graph/index.d.ts.map +1 -0
  65. package/dist/knowledge-graph/index.js +14 -0
  66. package/dist/knowledge-graph/index.js.map +1 -0
  67. package/dist/knowledge-graph/indexer.d.ts +112 -0
  68. package/dist/knowledge-graph/indexer.d.ts.map +1 -0
  69. package/dist/knowledge-graph/indexer.js +506 -0
  70. package/dist/knowledge-graph/indexer.js.map +1 -0
  71. package/dist/knowledge-graph/pagerank.d.ts +141 -0
  72. package/dist/knowledge-graph/pagerank.d.ts.map +1 -0
  73. package/dist/knowledge-graph/pagerank.js +493 -0
  74. package/dist/knowledge-graph/pagerank.js.map +1 -0
  75. package/dist/knowledge-graph/parser.d.ts +55 -0
  76. package/dist/knowledge-graph/parser.d.ts.map +1 -0
  77. package/dist/knowledge-graph/parser.js +1090 -0
  78. package/dist/knowledge-graph/parser.js.map +1 -0
  79. package/dist/knowledge-graph/snapshot.d.ts +107 -0
  80. package/dist/knowledge-graph/snapshot.d.ts.map +1 -0
  81. package/dist/knowledge-graph/snapshot.js +435 -0
  82. package/dist/knowledge-graph/snapshot.js.map +1 -0
  83. package/dist/memory/decision-log.d.ts +151 -0
  84. package/dist/memory/decision-log.d.ts.map +1 -0
  85. package/dist/memory/decision-log.js +482 -0
  86. package/dist/memory/decision-log.js.map +1 -0
  87. package/dist/memory/persistent-memory.d.ts +182 -0
  88. package/dist/memory/persistent-memory.d.ts.map +1 -0
  89. package/dist/memory/persistent-memory.js +579 -0
  90. package/dist/memory/persistent-memory.js.map +1 -0
  91. package/dist/memory/session-memory.d.ts +165 -0
  92. package/dist/memory/session-memory.d.ts.map +1 -0
  93. package/dist/memory/session-memory.js +382 -0
  94. package/dist/memory/session-memory.js.map +1 -0
  95. package/dist/stress-test.d.ts +10 -0
  96. package/dist/stress-test.d.ts.map +1 -0
  97. package/dist/stress-test.js +258 -0
  98. package/dist/stress-test.js.map +1 -0
  99. package/dist/tools/advanced-tools.d.ts +32 -0
  100. package/dist/tools/advanced-tools.d.ts.map +1 -0
  101. package/dist/tools/advanced-tools.js +480 -0
  102. package/dist/tools/advanced-tools.js.map +1 -0
  103. package/dist/tools/change-tools.d.ts +76 -0
  104. package/dist/tools/change-tools.d.ts.map +1 -0
  105. package/dist/tools/change-tools.js +93 -0
  106. package/dist/tools/change-tools.js.map +1 -0
  107. package/dist/tools/context-tools.d.ts +68 -0
  108. package/dist/tools/context-tools.d.ts.map +1 -0
  109. package/dist/tools/context-tools.js +141 -0
  110. package/dist/tools/context-tools.js.map +1 -0
  111. package/dist/tools/debug-tools.d.ts +25 -0
  112. package/dist/tools/debug-tools.d.ts.map +1 -0
  113. package/dist/tools/debug-tools.js +286 -0
  114. package/dist/tools/debug-tools.js.map +1 -0
  115. package/dist/tools/evolving-tools.d.ts +23 -0
  116. package/dist/tools/evolving-tools.d.ts.map +1 -0
  117. package/dist/tools/evolving-tools.js +207 -0
  118. package/dist/tools/evolving-tools.js.map +1 -0
  119. package/dist/tools/flow-tools.d.ts +24 -0
  120. package/dist/tools/flow-tools.d.ts.map +1 -0
  121. package/dist/tools/flow-tools.js +265 -0
  122. package/dist/tools/flow-tools.js.map +1 -0
  123. package/dist/tools/graph-tools.d.ts +71 -0
  124. package/dist/tools/graph-tools.d.ts.map +1 -0
  125. package/dist/tools/graph-tools.js +165 -0
  126. package/dist/tools/graph-tools.js.map +1 -0
  127. package/dist/tools/memory-tools.d.ts +62 -0
  128. package/dist/tools/memory-tools.d.ts.map +1 -0
  129. package/dist/tools/memory-tools.js +195 -0
  130. package/dist/tools/memory-tools.js.map +1 -0
  131. package/dist/tools/smart-tools.d.ts +23 -0
  132. package/dist/tools/smart-tools.d.ts.map +1 -0
  133. package/dist/tools/smart-tools.js +482 -0
  134. package/dist/tools/smart-tools.js.map +1 -0
  135. package/dist/tools/snapshot-tools.d.ts +19 -0
  136. package/dist/tools/snapshot-tools.d.ts.map +1 -0
  137. package/dist/tools/snapshot-tools.js +149 -0
  138. package/dist/tools/snapshot-tools.js.map +1 -0
  139. package/dist/types.d.ts +181 -0
  140. package/dist/types.d.ts.map +1 -0
  141. package/dist/types.js +45 -0
  142. package/dist/types.js.map +1 -0
  143. package/dist/utils/logger.d.ts +59 -0
  144. package/dist/utils/logger.d.ts.map +1 -0
  145. package/dist/utils/logger.js +142 -0
  146. package/dist/utils/logger.js.map +1 -0
  147. package/dist/utils/token-counter.d.ts +51 -0
  148. package/dist/utils/token-counter.d.ts.map +1 -0
  149. package/dist/utils/token-counter.js +181 -0
  150. package/dist/utils/token-counter.js.map +1 -0
  151. package/install.ps1 +321 -0
  152. package/install.sh +345 -0
  153. package/package.json +94 -0
  154. package/setup.bat +62 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * AI Mind Map — Change Log (Persistent Change History)
3
+ *
4
+ * SQLite-backed change-history store with FTS5 full-text search and
5
+ * BM25-ranked retrieval. Provides session management, time-range queries,
6
+ * pruning, and aggregate statistics.
7
+ *
8
+ * Inspired by context-mode's BM25-ranked session history and Mem0's
9
+ * persistent memory layer.
10
+ */
11
+ import type { FileChange, ChangeSession, ChangeType } from '../types.js';
12
+ /** Query options for retrieving change records. */
13
+ export interface ChangeQueryOptions {
14
+ sessionId?: string;
15
+ filePath?: string;
16
+ /** Return only changes that affected this symbol name. */
17
+ symbol?: string;
18
+ /** Only changes of this type. */
19
+ changeType?: ChangeType;
20
+ /** Only changes after this epoch (ms). */
21
+ since?: number;
22
+ /** Only changes before this epoch (ms). */
23
+ until?: number;
24
+ /** Max records to return (default 100). */
25
+ limit?: number;
26
+ /** Offset for pagination. */
27
+ offset?: number;
28
+ }
29
+ /** A single result from a BM25-ranked search. */
30
+ export interface RankedChangeResult {
31
+ change: FileChange;
32
+ /** BM25 relevance score (lower = more relevant in SQLite FTS5). */
33
+ rank: number;
34
+ }
35
+ /** Aggregate statistics for the change log. */
36
+ export interface ChangeLogStats {
37
+ totalChanges: number;
38
+ totalSessions: number;
39
+ activeSessions: number;
40
+ mostChangedFiles: {
41
+ filePath: string;
42
+ changeCount: number;
43
+ }[];
44
+ mostActiveSessions: {
45
+ sessionId: string;
46
+ changeCount: number;
47
+ startedAt: number;
48
+ }[];
49
+ linesAddedAllTime: number;
50
+ linesRemovedAllTime: number;
51
+ }
52
+ /** Configuration for change-log behavior. */
53
+ export interface ChangeLogConfig {
54
+ /** Path to the SQLite database file. */
55
+ dbPath: string;
56
+ /** Number of days to retain change history (default 30). */
57
+ retentionDays?: number;
58
+ /** Max results for ranked retrieval (default 20). */
59
+ defaultSearchLimit?: number;
60
+ }
61
+ /**
62
+ * Persistent change log backed by SQLite with FTS5.
63
+ *
64
+ * ```ts
65
+ * const log = new ChangeLog({ dbPath: '.mindmap/mindmap.db' });
66
+ * const session = log.startSession();
67
+ * log.recordChange({ … });
68
+ * log.endSession(session.sessionId, 'Implemented auth flow');
69
+ * ```
70
+ */
71
+ export declare class ChangeLog {
72
+ private readonly db;
73
+ private readonly retentionDays;
74
+ private readonly defaultSearchLimit;
75
+ private stmtInsertChange;
76
+ private stmtInsertSession;
77
+ private stmtEndSession;
78
+ private stmtGetSession;
79
+ private stmtDeleteOldChanges;
80
+ private stmtDeleteOldSessions;
81
+ constructor(config: ChangeLogConfig);
82
+ private initSchema;
83
+ private prepareStatements;
84
+ /**
85
+ * Start a new change-tracking session.
86
+ *
87
+ * @returns The newly created session record.
88
+ */
89
+ startSession(sessionId?: string): ChangeSession;
90
+ /**
91
+ * End an existing session, computing aggregate metadata.
92
+ *
93
+ * @param sessionId The session to end.
94
+ * @param summary Optional human-readable summary of the session.
95
+ */
96
+ endSession(sessionId: string, summary?: string): ChangeSession | null;
97
+ /**
98
+ * Retrieve a session by ID.
99
+ */
100
+ getSession(sessionId: string): ChangeSession | null;
101
+ /**
102
+ * Get the most recently started session (active or ended).
103
+ */
104
+ getLatestSession(): ChangeSession | null;
105
+ /**
106
+ * Get all active (un-ended) sessions.
107
+ */
108
+ getActiveSessions(): ChangeSession[];
109
+ /**
110
+ * Persist one or more {@link FileChange} records.
111
+ */
112
+ recordChange(change: FileChange): void;
113
+ /**
114
+ * Record multiple changes in a single transaction.
115
+ */
116
+ recordChanges(changes: FileChange[]): void;
117
+ /**
118
+ * Query change records with flexible filtering.
119
+ */
120
+ queryChanges(options?: ChangeQueryOptions): FileChange[];
121
+ /**
122
+ * Get all changes for a specific file, across all sessions.
123
+ */
124
+ getFileHistory(filePath: string, limit?: number): FileChange[];
125
+ /**
126
+ * Get all changes in a specific session.
127
+ */
128
+ getSessionChanges(sessionId: string): FileChange[];
129
+ /**
130
+ * Full-text search across change summaries, file paths, and affected
131
+ * symbols, ranked by BM25 relevance.
132
+ *
133
+ * @param query FTS5 query string (supports AND, OR, NOT, phrase matching).
134
+ * @param limit Maximum results to return.
135
+ */
136
+ searchChanges(query: string, limit?: number): RankedChangeResult[];
137
+ /**
138
+ * Generate an aggregate summary for a session, combining all its change
139
+ * records.
140
+ */
141
+ generateSessionSummary(sessionId: string): string;
142
+ /**
143
+ * Delete change records older than the configured retention period.
144
+ *
145
+ * @returns Number of records deleted.
146
+ */
147
+ pruneOldChanges(): {
148
+ changesDeleted: number;
149
+ sessionsDeleted: number;
150
+ };
151
+ /**
152
+ * Compute aggregate statistics across the entire change log.
153
+ */
154
+ getStats(topN?: number): ChangeLogStats;
155
+ /**
156
+ * Close the database connection. Call this during graceful shutdown.
157
+ */
158
+ close(): void;
159
+ }
160
+ //# sourceMappingURL=change-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"change-log.d.ts","sourceRoot":"","sources":["../../src/change-tracker/change-log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,507 @@
1
+ /**
2
+ * AI Mind Map — Change Log (Persistent Change History)
3
+ *
4
+ * SQLite-backed change-history store with FTS5 full-text search and
5
+ * BM25-ranked retrieval. Provides session management, time-range queries,
6
+ * pruning, and aggregate statistics.
7
+ *
8
+ * Inspired by context-mode's BM25-ranked session history and Mem0's
9
+ * persistent memory layer.
10
+ */
11
+ import Database from 'better-sqlite3';
12
+ import path from 'node:path';
13
+ import { randomUUID } from 'node:crypto';
14
+ // ------------------------------------------------------------------- db --
15
+ /**
16
+ * Persistent change log backed by SQLite with FTS5.
17
+ *
18
+ * ```ts
19
+ * const log = new ChangeLog({ dbPath: '.mindmap/mindmap.db' });
20
+ * const session = log.startSession();
21
+ * log.recordChange({ … });
22
+ * log.endSession(session.sessionId, 'Implemented auth flow');
23
+ * ```
24
+ */
25
+ export class ChangeLog {
26
+ db;
27
+ retentionDays;
28
+ defaultSearchLimit;
29
+ // ----- prepared statements (lazy) ------------------------------------
30
+ stmtInsertChange;
31
+ stmtInsertSession;
32
+ stmtEndSession;
33
+ stmtGetSession;
34
+ stmtDeleteOldChanges;
35
+ stmtDeleteOldSessions;
36
+ constructor(config) {
37
+ const dbPath = path.resolve(config.dbPath);
38
+ this.retentionDays = config.retentionDays ?? 30;
39
+ this.defaultSearchLimit = config.defaultSearchLimit ?? 20;
40
+ this.db = new Database(dbPath);
41
+ this.db.pragma('journal_mode = WAL');
42
+ this.db.pragma('foreign_keys = ON');
43
+ this.initSchema();
44
+ this.prepareStatements();
45
+ }
46
+ // ----------------------------------------------------------- schema ---
47
+ initSchema() {
48
+ this.db.exec(`
49
+ -- Sessions table
50
+ CREATE TABLE IF NOT EXISTS change_sessions (
51
+ session_id TEXT PRIMARY KEY,
52
+ started_at INTEGER NOT NULL,
53
+ ended_at INTEGER,
54
+ total_changes INTEGER NOT NULL DEFAULT 0,
55
+ files_modified TEXT NOT NULL DEFAULT '[]',
56
+ summary TEXT NOT NULL DEFAULT ''
57
+ );
58
+
59
+ -- Change records table
60
+ CREATE TABLE IF NOT EXISTS change_records (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ file_path TEXT NOT NULL,
63
+ change_type TEXT NOT NULL,
64
+ old_path TEXT,
65
+ summary TEXT NOT NULL DEFAULT '',
66
+ symbols_affected TEXT NOT NULL DEFAULT '[]',
67
+ lines_added INTEGER NOT NULL DEFAULT 0,
68
+ lines_removed INTEGER NOT NULL DEFAULT 0,
69
+ timestamp INTEGER NOT NULL,
70
+ session_id TEXT NOT NULL,
71
+ FOREIGN KEY (session_id) REFERENCES change_sessions(session_id)
72
+ ON DELETE CASCADE
73
+ );
74
+
75
+ -- Indexes for common query patterns
76
+ CREATE INDEX IF NOT EXISTS idx_change_records_session
77
+ ON change_records(session_id);
78
+ CREATE INDEX IF NOT EXISTS idx_change_records_file
79
+ ON change_records(file_path);
80
+ CREATE INDEX IF NOT EXISTS idx_change_records_timestamp
81
+ ON change_records(timestamp);
82
+ CREATE INDEX IF NOT EXISTS idx_change_records_change_type
83
+ ON change_records(change_type);
84
+
85
+ -- FTS5 virtual table for full-text search across summaries and symbols
86
+ CREATE VIRTUAL TABLE IF NOT EXISTS change_records_fts USING fts5(
87
+ file_path,
88
+ summary,
89
+ symbols_affected,
90
+ content = 'change_records',
91
+ content_rowid = 'id',
92
+ tokenize = 'porter unicode61'
93
+ );
94
+
95
+ -- Triggers to keep FTS index in sync
96
+ CREATE TRIGGER IF NOT EXISTS trg_change_records_ai
97
+ AFTER INSERT ON change_records BEGIN
98
+ INSERT INTO change_records_fts(rowid, file_path, summary, symbols_affected)
99
+ VALUES (new.id, new.file_path, new.summary, new.symbols_affected);
100
+ END;
101
+
102
+ CREATE TRIGGER IF NOT EXISTS trg_change_records_ad
103
+ AFTER DELETE ON change_records BEGIN
104
+ INSERT INTO change_records_fts(change_records_fts, rowid, file_path, summary, symbols_affected)
105
+ VALUES ('delete', old.id, old.file_path, old.summary, old.symbols_affected);
106
+ END;
107
+
108
+ CREATE TRIGGER IF NOT EXISTS trg_change_records_au
109
+ AFTER UPDATE ON change_records BEGIN
110
+ INSERT INTO change_records_fts(change_records_fts, rowid, file_path, summary, symbols_affected)
111
+ VALUES ('delete', old.id, old.file_path, old.summary, old.symbols_affected);
112
+ INSERT INTO change_records_fts(rowid, file_path, summary, symbols_affected)
113
+ VALUES (new.id, new.file_path, new.summary, new.symbols_affected);
114
+ END;
115
+ `);
116
+ }
117
+ prepareStatements() {
118
+ this.stmtInsertChange = this.db.prepare(`
119
+ INSERT INTO change_records
120
+ (file_path, change_type, old_path, summary, symbols_affected,
121
+ lines_added, lines_removed, timestamp, session_id)
122
+ VALUES
123
+ (@filePath, @changeType, @oldPath, @summary, @symbolsAffected,
124
+ @linesAdded, @linesRemoved, @timestamp, @sessionId)
125
+ `);
126
+ this.stmtInsertSession = this.db.prepare(`
127
+ INSERT INTO change_sessions (session_id, started_at)
128
+ VALUES (@sessionId, @startedAt)
129
+ `);
130
+ this.stmtEndSession = this.db.prepare(`
131
+ UPDATE change_sessions
132
+ SET ended_at = @endedAt,
133
+ total_changes = (
134
+ SELECT COUNT(*) FROM change_records WHERE session_id = @sessionId
135
+ ),
136
+ files_modified = (
137
+ SELECT json_group_array(DISTINCT file_path)
138
+ FROM change_records WHERE session_id = @sessionId
139
+ ),
140
+ summary = @summary
141
+ WHERE session_id = @sessionId
142
+ `);
143
+ this.stmtGetSession = this.db.prepare(`
144
+ SELECT * FROM change_sessions WHERE session_id = ?
145
+ `);
146
+ this.stmtDeleteOldChanges = this.db.prepare(`
147
+ DELETE FROM change_records WHERE timestamp < ?
148
+ `);
149
+ this.stmtDeleteOldSessions = this.db.prepare(`
150
+ DELETE FROM change_sessions WHERE ended_at IS NOT NULL AND ended_at < ?
151
+ `);
152
+ }
153
+ // ----------------------------------------------------- sessions ------
154
+ /**
155
+ * Start a new change-tracking session.
156
+ *
157
+ * @returns The newly created session record.
158
+ */
159
+ startSession(sessionId) {
160
+ const id = sessionId ?? randomUUID();
161
+ const now = Date.now();
162
+ this.stmtInsertSession.run({ sessionId: id, startedAt: now });
163
+ return {
164
+ sessionId: id,
165
+ startedAt: now,
166
+ endedAt: null,
167
+ totalChanges: 0,
168
+ filesModified: [],
169
+ summary: '',
170
+ };
171
+ }
172
+ /**
173
+ * End an existing session, computing aggregate metadata.
174
+ *
175
+ * @param sessionId The session to end.
176
+ * @param summary Optional human-readable summary of the session.
177
+ */
178
+ endSession(sessionId, summary = '') {
179
+ const now = Date.now();
180
+ this.stmtEndSession.run({ sessionId, endedAt: now, summary });
181
+ return this.getSession(sessionId);
182
+ }
183
+ /**
184
+ * Retrieve a session by ID.
185
+ */
186
+ getSession(sessionId) {
187
+ const row = this.stmtGetSession.get(sessionId);
188
+ if (!row)
189
+ return null;
190
+ return rowToSession(row);
191
+ }
192
+ /**
193
+ * Get the most recently started session (active or ended).
194
+ */
195
+ getLatestSession() {
196
+ const row = this.db
197
+ .prepare('SELECT * FROM change_sessions ORDER BY started_at DESC LIMIT 1')
198
+ .get();
199
+ return row ? rowToSession(row) : null;
200
+ }
201
+ /**
202
+ * Get all active (un-ended) sessions.
203
+ */
204
+ getActiveSessions() {
205
+ const rows = this.db
206
+ .prepare('SELECT * FROM change_sessions WHERE ended_at IS NULL ORDER BY started_at DESC')
207
+ .all();
208
+ return rows.map(rowToSession);
209
+ }
210
+ // -------------------------------------------------- record changes ----
211
+ /**
212
+ * Persist one or more {@link FileChange} records.
213
+ */
214
+ recordChange(change) {
215
+ this.stmtInsertChange.run({
216
+ filePath: change.filePath,
217
+ changeType: change.changeType,
218
+ oldPath: change.oldPath ?? null,
219
+ summary: change.summary,
220
+ symbolsAffected: JSON.stringify(change.symbolsAffected),
221
+ linesAdded: change.linesAdded,
222
+ linesRemoved: change.linesRemoved,
223
+ timestamp: change.timestamp,
224
+ sessionId: change.sessionId,
225
+ });
226
+ }
227
+ /**
228
+ * Record multiple changes in a single transaction.
229
+ */
230
+ recordChanges(changes) {
231
+ const txn = this.db.transaction((items) => {
232
+ for (const c of items) {
233
+ this.recordChange(c);
234
+ }
235
+ });
236
+ txn(changes);
237
+ }
238
+ // --------------------------------------------------------- queries ----
239
+ /**
240
+ * Query change records with flexible filtering.
241
+ */
242
+ queryChanges(options = {}) {
243
+ const clauses = [];
244
+ const params = {};
245
+ if (options.sessionId) {
246
+ clauses.push('session_id = @sessionId');
247
+ params.sessionId = options.sessionId;
248
+ }
249
+ if (options.filePath) {
250
+ clauses.push('file_path = @filePath');
251
+ params.filePath = options.filePath;
252
+ }
253
+ if (options.changeType) {
254
+ clauses.push('change_type = @changeType');
255
+ params.changeType = options.changeType;
256
+ }
257
+ if (options.since !== undefined) {
258
+ clauses.push('timestamp >= @since');
259
+ params.since = options.since;
260
+ }
261
+ if (options.until !== undefined) {
262
+ clauses.push('timestamp <= @until');
263
+ params.until = options.until;
264
+ }
265
+ if (options.symbol) {
266
+ // JSON array search: symbols_affected LIKE '%"symbolName"%'
267
+ clauses.push("symbols_affected LIKE @symbolPattern");
268
+ params.symbolPattern = `%"${options.symbol}"%`;
269
+ }
270
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
271
+ const limit = options.limit ?? 100;
272
+ const offset = options.offset ?? 0;
273
+ const sql = `
274
+ SELECT * FROM change_records
275
+ ${where}
276
+ ORDER BY timestamp DESC
277
+ LIMIT @limit OFFSET @offset
278
+ `;
279
+ const rows = this.db.prepare(sql).all({ ...params, limit, offset });
280
+ return rows.map(rowToFileChange);
281
+ }
282
+ /**
283
+ * Get all changes for a specific file, across all sessions.
284
+ */
285
+ getFileHistory(filePath, limit = 50) {
286
+ return this.queryChanges({ filePath, limit });
287
+ }
288
+ /**
289
+ * Get all changes in a specific session.
290
+ */
291
+ getSessionChanges(sessionId) {
292
+ return this.queryChanges({ sessionId, limit: 10000 });
293
+ }
294
+ // --------------------------------------------------- FTS5 / BM25 -----
295
+ /**
296
+ * Full-text search across change summaries, file paths, and affected
297
+ * symbols, ranked by BM25 relevance.
298
+ *
299
+ * @param query FTS5 query string (supports AND, OR, NOT, phrase matching).
300
+ * @param limit Maximum results to return.
301
+ */
302
+ searchChanges(query, limit) {
303
+ const maxResults = limit ?? this.defaultSearchLimit;
304
+ // Sanitise: escape double-quotes, wrap terms for prefix matching
305
+ const sanitised = sanitiseFtsQuery(query);
306
+ if (!sanitised)
307
+ return [];
308
+ try {
309
+ const sql = `
310
+ SELECT
311
+ cr.*,
312
+ bm25(change_records_fts, 1.0, 2.0, 1.5) AS rank
313
+ FROM change_records_fts fts
314
+ JOIN change_records cr ON cr.id = fts.rowid
315
+ WHERE change_records_fts MATCH @query
316
+ ORDER BY rank
317
+ LIMIT @limit
318
+ `;
319
+ const rows = this.db.prepare(sql).all({ query: sanitised, limit: maxResults });
320
+ return rows.map((r) => ({
321
+ change: rowToFileChange(r),
322
+ rank: r.rank,
323
+ }));
324
+ }
325
+ catch {
326
+ // FTS query syntax errors — return empty rather than crashing.
327
+ return [];
328
+ }
329
+ }
330
+ // ------------------------------------------------ session summaries ---
331
+ /**
332
+ * Generate an aggregate summary for a session, combining all its change
333
+ * records.
334
+ */
335
+ generateSessionSummary(sessionId) {
336
+ const session = this.getSession(sessionId);
337
+ if (!session)
338
+ return `Session ${sessionId} not found.`;
339
+ const changes = this.getSessionChanges(sessionId);
340
+ if (changes.length === 0)
341
+ return `Session ${sessionId}: no changes recorded.`;
342
+ const created = changes.filter((c) => c.changeType === 'created');
343
+ const modified = changes.filter((c) => c.changeType === 'modified');
344
+ const deleted = changes.filter((c) => c.changeType === 'deleted');
345
+ const renamed = changes.filter((c) => c.changeType === 'renamed');
346
+ const totalAdded = changes.reduce((s, c) => s + c.linesAdded, 0);
347
+ const totalRemoved = changes.reduce((s, c) => s + c.linesRemoved, 0);
348
+ const lines = [];
349
+ lines.push(`Session ${sessionId}`);
350
+ lines.push(` Started: ${new Date(session.startedAt).toISOString()}`);
351
+ if (session.endedAt) {
352
+ lines.push(` Ended: ${new Date(session.endedAt).toISOString()}`);
353
+ }
354
+ lines.push(` Total: ${changes.length} changes (+${totalAdded}, -${totalRemoved} lines)`);
355
+ if (created.length > 0) {
356
+ lines.push(` Created (${created.length}):`);
357
+ for (const c of created.slice(0, 10))
358
+ lines.push(` + ${c.filePath}`);
359
+ if (created.length > 10)
360
+ lines.push(` … and ${created.length - 10} more`);
361
+ }
362
+ if (modified.length > 0) {
363
+ lines.push(` Modified (${modified.length}):`);
364
+ for (const c of modified.slice(0, 10))
365
+ lines.push(` ~ ${c.filePath}: ${c.summary}`);
366
+ if (modified.length > 10)
367
+ lines.push(` … and ${modified.length - 10} more`);
368
+ }
369
+ if (deleted.length > 0) {
370
+ lines.push(` Deleted (${deleted.length}):`);
371
+ for (const c of deleted.slice(0, 10))
372
+ lines.push(` - ${c.filePath}`);
373
+ if (deleted.length > 10)
374
+ lines.push(` … and ${deleted.length - 10} more`);
375
+ }
376
+ if (renamed.length > 0) {
377
+ lines.push(` Renamed (${renamed.length}):`);
378
+ for (const c of renamed.slice(0, 10)) {
379
+ lines.push(` → ${c.oldPath ?? '?'} → ${c.filePath}`);
380
+ }
381
+ if (renamed.length > 10)
382
+ lines.push(` … and ${renamed.length - 10} more`);
383
+ }
384
+ return lines.join('\n');
385
+ }
386
+ // --------------------------------------------------------- pruning ----
387
+ /**
388
+ * Delete change records older than the configured retention period.
389
+ *
390
+ * @returns Number of records deleted.
391
+ */
392
+ pruneOldChanges() {
393
+ const cutoff = Date.now() - this.retentionDays * 24 * 60 * 60 * 1000;
394
+ const changesResult = this.stmtDeleteOldChanges.run(cutoff);
395
+ const sessionsResult = this.stmtDeleteOldSessions.run(cutoff);
396
+ return {
397
+ changesDeleted: changesResult.changes,
398
+ sessionsDeleted: sessionsResult.changes,
399
+ };
400
+ }
401
+ // ------------------------------------------------------- statistics ---
402
+ /**
403
+ * Compute aggregate statistics across the entire change log.
404
+ */
405
+ getStats(topN = 10) {
406
+ const totalChanges = this.db.prepare('SELECT COUNT(*) AS cnt FROM change_records').get().cnt;
407
+ const totalSessions = this.db.prepare('SELECT COUNT(*) AS cnt FROM change_sessions').get().cnt;
408
+ const activeSessions = this.db.prepare('SELECT COUNT(*) AS cnt FROM change_sessions WHERE ended_at IS NULL').get().cnt;
409
+ const mostChangedFiles = this.db
410
+ .prepare(`SELECT file_path AS filePath, COUNT(*) AS changeCount
411
+ FROM change_records
412
+ GROUP BY file_path
413
+ ORDER BY changeCount DESC
414
+ LIMIT ?`)
415
+ .all(topN);
416
+ const mostActiveSessions = this.db
417
+ .prepare(`SELECT cs.session_id AS sessionId,
418
+ COUNT(cr.id) AS changeCount,
419
+ cs.started_at AS startedAt
420
+ FROM change_sessions cs
421
+ LEFT JOIN change_records cr ON cr.session_id = cs.session_id
422
+ GROUP BY cs.session_id
423
+ ORDER BY changeCount DESC
424
+ LIMIT ?`)
425
+ .all(topN);
426
+ const lineStats = this.db
427
+ .prepare(`SELECT
428
+ COALESCE(SUM(lines_added), 0) AS linesAdded,
429
+ COALESCE(SUM(lines_removed), 0) AS linesRemoved
430
+ FROM change_records`)
431
+ .get();
432
+ return {
433
+ totalChanges,
434
+ totalSessions,
435
+ activeSessions,
436
+ mostChangedFiles,
437
+ mostActiveSessions,
438
+ linesAddedAllTime: lineStats.linesAdded,
439
+ linesRemovedAllTime: lineStats.linesRemoved,
440
+ };
441
+ }
442
+ // ---------------------------------------------------------- cleanup ---
443
+ /**
444
+ * Close the database connection. Call this during graceful shutdown.
445
+ */
446
+ close() {
447
+ this.db.close();
448
+ }
449
+ }
450
+ /** Convert a raw DB row into a {@link FileChange}. */
451
+ function rowToFileChange(row) {
452
+ return {
453
+ filePath: row.file_path,
454
+ changeType: row.change_type,
455
+ oldPath: row.old_path ?? undefined,
456
+ summary: row.summary,
457
+ symbolsAffected: safeJsonParse(row.symbols_affected, []),
458
+ linesAdded: row.lines_added,
459
+ linesRemoved: row.lines_removed,
460
+ timestamp: row.timestamp,
461
+ sessionId: row.session_id,
462
+ };
463
+ }
464
+ /** Convert a raw DB row into a {@link ChangeSession}. */
465
+ function rowToSession(row) {
466
+ return {
467
+ sessionId: row.session_id,
468
+ startedAt: row.started_at,
469
+ endedAt: row.ended_at,
470
+ totalChanges: row.total_changes,
471
+ filesModified: safeJsonParse(row.files_modified, []),
472
+ summary: row.summary,
473
+ };
474
+ }
475
+ /** Safely parse a JSON string with a fallback. */
476
+ function safeJsonParse(json, fallback) {
477
+ try {
478
+ return JSON.parse(json);
479
+ }
480
+ catch {
481
+ return fallback;
482
+ }
483
+ }
484
+ /**
485
+ * Sanitise a user-provided query string for FTS5 MATCH.
486
+ *
487
+ * - Removes characters that break FTS5 syntax.
488
+ * - Wraps each term in double-quotes with a `*` suffix for prefix matching.
489
+ *
490
+ * Returns `null` if the query is effectively empty after sanitisation.
491
+ */
492
+ function sanitiseFtsQuery(raw) {
493
+ // Strip special FTS5 operators that could break syntax
494
+ const cleaned = raw
495
+ .replace(/["""]/g, '') // remove quotes
496
+ .replace(/[{}()\[\]^~\\:]/g, '') // remove other specials
497
+ .trim();
498
+ if (cleaned.length === 0)
499
+ return null;
500
+ // Split into tokens and wrap each for prefix matching
501
+ const terms = cleaned.split(/\s+/).filter(Boolean);
502
+ if (terms.length === 0)
503
+ return null;
504
+ // Use OR between terms for broader matching
505
+ return terms.map((t) => `"${t}"*`).join(' OR ');
506
+ }
507
+ //# sourceMappingURL=change-log.js.map