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.
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/dist/change-tracker/change-log.d.ts +160 -0
- package/dist/change-tracker/change-log.d.ts.map +1 -0
- package/dist/change-tracker/change-log.js +507 -0
- package/dist/change-tracker/change-log.js.map +1 -0
- package/dist/change-tracker/diff-engine.d.ts +149 -0
- package/dist/change-tracker/diff-engine.d.ts.map +1 -0
- package/dist/change-tracker/diff-engine.js +530 -0
- package/dist/change-tracker/diff-engine.js.map +1 -0
- package/dist/change-tracker/watcher.d.ts +137 -0
- package/dist/change-tracker/watcher.d.ts.map +1 -0
- package/dist/change-tracker/watcher.js +300 -0
- package/dist/change-tracker/watcher.js.map +1 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +937 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +222 -0
- package/dist/config.js.map +1 -0
- package/dist/context/compressor.d.ts +49 -0
- package/dist/context/compressor.d.ts.map +1 -0
- package/dist/context/compressor.js +769 -0
- package/dist/context/compressor.js.map +1 -0
- package/dist/context/progressive-disclosure.d.ts +71 -0
- package/dist/context/progressive-disclosure.d.ts.map +1 -0
- package/dist/context/progressive-disclosure.js +470 -0
- package/dist/context/progressive-disclosure.js.map +1 -0
- package/dist/context/token-budget.d.ts +121 -0
- package/dist/context/token-budget.d.ts.map +1 -0
- package/dist/context/token-budget.js +282 -0
- package/dist/context/token-budget.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +944 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +66 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +946 -0
- package/dist/install.js.map +1 -0
- package/dist/knowledge-graph/architecture.d.ts +213 -0
- package/dist/knowledge-graph/architecture.d.ts.map +1 -0
- package/dist/knowledge-graph/architecture.js +585 -0
- package/dist/knowledge-graph/architecture.js.map +1 -0
- package/dist/knowledge-graph/cypher.d.ts +113 -0
- package/dist/knowledge-graph/cypher.d.ts.map +1 -0
- package/dist/knowledge-graph/cypher.js +1051 -0
- package/dist/knowledge-graph/cypher.js.map +1 -0
- package/dist/knowledge-graph/dead-code.d.ts +121 -0
- package/dist/knowledge-graph/dead-code.d.ts.map +1 -0
- package/dist/knowledge-graph/dead-code.js +331 -0
- package/dist/knowledge-graph/dead-code.js.map +1 -0
- package/dist/knowledge-graph/flow-analyzer.d.ts +167 -0
- package/dist/knowledge-graph/flow-analyzer.d.ts.map +1 -0
- package/dist/knowledge-graph/flow-analyzer.js +739 -0
- package/dist/knowledge-graph/flow-analyzer.js.map +1 -0
- package/dist/knowledge-graph/graph.d.ts +291 -0
- package/dist/knowledge-graph/graph.d.ts.map +1 -0
- package/dist/knowledge-graph/graph.js +978 -0
- package/dist/knowledge-graph/graph.js.map +1 -0
- package/dist/knowledge-graph/index.d.ts +17 -0
- package/dist/knowledge-graph/index.d.ts.map +1 -0
- package/dist/knowledge-graph/index.js +14 -0
- package/dist/knowledge-graph/index.js.map +1 -0
- package/dist/knowledge-graph/indexer.d.ts +112 -0
- package/dist/knowledge-graph/indexer.d.ts.map +1 -0
- package/dist/knowledge-graph/indexer.js +506 -0
- package/dist/knowledge-graph/indexer.js.map +1 -0
- package/dist/knowledge-graph/pagerank.d.ts +141 -0
- package/dist/knowledge-graph/pagerank.d.ts.map +1 -0
- package/dist/knowledge-graph/pagerank.js +493 -0
- package/dist/knowledge-graph/pagerank.js.map +1 -0
- package/dist/knowledge-graph/parser.d.ts +55 -0
- package/dist/knowledge-graph/parser.d.ts.map +1 -0
- package/dist/knowledge-graph/parser.js +1090 -0
- package/dist/knowledge-graph/parser.js.map +1 -0
- package/dist/knowledge-graph/snapshot.d.ts +107 -0
- package/dist/knowledge-graph/snapshot.d.ts.map +1 -0
- package/dist/knowledge-graph/snapshot.js +435 -0
- package/dist/knowledge-graph/snapshot.js.map +1 -0
- package/dist/memory/decision-log.d.ts +151 -0
- package/dist/memory/decision-log.d.ts.map +1 -0
- package/dist/memory/decision-log.js +482 -0
- package/dist/memory/decision-log.js.map +1 -0
- package/dist/memory/persistent-memory.d.ts +182 -0
- package/dist/memory/persistent-memory.d.ts.map +1 -0
- package/dist/memory/persistent-memory.js +579 -0
- package/dist/memory/persistent-memory.js.map +1 -0
- package/dist/memory/session-memory.d.ts +165 -0
- package/dist/memory/session-memory.d.ts.map +1 -0
- package/dist/memory/session-memory.js +382 -0
- package/dist/memory/session-memory.js.map +1 -0
- package/dist/stress-test.d.ts +10 -0
- package/dist/stress-test.d.ts.map +1 -0
- package/dist/stress-test.js +258 -0
- package/dist/stress-test.js.map +1 -0
- package/dist/tools/advanced-tools.d.ts +32 -0
- package/dist/tools/advanced-tools.d.ts.map +1 -0
- package/dist/tools/advanced-tools.js +480 -0
- package/dist/tools/advanced-tools.js.map +1 -0
- package/dist/tools/change-tools.d.ts +76 -0
- package/dist/tools/change-tools.d.ts.map +1 -0
- package/dist/tools/change-tools.js +93 -0
- package/dist/tools/change-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +68 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +141 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/debug-tools.d.ts +25 -0
- package/dist/tools/debug-tools.d.ts.map +1 -0
- package/dist/tools/debug-tools.js +286 -0
- package/dist/tools/debug-tools.js.map +1 -0
- package/dist/tools/evolving-tools.d.ts +23 -0
- package/dist/tools/evolving-tools.d.ts.map +1 -0
- package/dist/tools/evolving-tools.js +207 -0
- package/dist/tools/evolving-tools.js.map +1 -0
- package/dist/tools/flow-tools.d.ts +24 -0
- package/dist/tools/flow-tools.d.ts.map +1 -0
- package/dist/tools/flow-tools.js +265 -0
- package/dist/tools/flow-tools.js.map +1 -0
- package/dist/tools/graph-tools.d.ts +71 -0
- package/dist/tools/graph-tools.d.ts.map +1 -0
- package/dist/tools/graph-tools.js +165 -0
- package/dist/tools/graph-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +62 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +195 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/smart-tools.d.ts +23 -0
- package/dist/tools/smart-tools.d.ts.map +1 -0
- package/dist/tools/smart-tools.js +482 -0
- package/dist/tools/smart-tools.js.map +1 -0
- package/dist/tools/snapshot-tools.d.ts +19 -0
- package/dist/tools/snapshot-tools.d.ts.map +1 -0
- package/dist/tools/snapshot-tools.js +149 -0
- package/dist/tools/snapshot-tools.js.map +1 -0
- package/dist/types.d.ts +181 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +45 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +142 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/token-counter.d.ts +51 -0
- package/dist/utils/token-counter.d.ts.map +1 -0
- package/dist/utils/token-counter.js +181 -0
- package/dist/utils/token-counter.js.map +1 -0
- package/install.ps1 +321 -0
- package/install.sh +345 -0
- package/package.json +94 -0
- 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
|