composto-ai 0.3.0 → 0.4.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.
- package/README.md +46 -1
- package/dist/index.js +2514 -54
- package/dist/mcp/server.js +2396 -40
- package/dist/memory/api.js +986 -0
- package/dist/memory/pool.js +57 -0
- package/dist/memory/worker.js +448 -0
- package/package.json +4 -1
- package/dist/chunk-AARGW2GV.js +0 -1531
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/memory/pool.ts
|
|
4
|
+
import { Worker } from "worker_threads";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
function resolveWorkerPath() {
|
|
8
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
if (here.endsWith("/memory") || here.endsWith("\\memory")) {
|
|
10
|
+
return join(here, "worker.js");
|
|
11
|
+
}
|
|
12
|
+
return join(here, "memory", "worker.js");
|
|
13
|
+
}
|
|
14
|
+
var WorkerPool = class {
|
|
15
|
+
workers = [];
|
|
16
|
+
nextJobId = 1;
|
|
17
|
+
pending = /* @__PURE__ */ new Map();
|
|
18
|
+
constructor(opts = {}) {
|
|
19
|
+
const size = Math.max(1, opts.size ?? 1);
|
|
20
|
+
for (let i = 0; i < size; i++) this.spawn();
|
|
21
|
+
}
|
|
22
|
+
spawn() {
|
|
23
|
+
const worker = new Worker(resolveWorkerPath());
|
|
24
|
+
worker.on("message", (msg) => {
|
|
25
|
+
const job = this.pending.get(msg.jobId);
|
|
26
|
+
if (!job) return;
|
|
27
|
+
this.pending.delete(msg.jobId);
|
|
28
|
+
if (msg.type === "ingest_done") {
|
|
29
|
+
job.resolve({ status: "done", commits: msg.commits });
|
|
30
|
+
} else if (msg.type === "ingest_error") {
|
|
31
|
+
job.reject(new Error(msg.message));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
worker.on("error", (err) => {
|
|
35
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
36
|
+
for (const job of this.pending.values()) job.reject(error);
|
|
37
|
+
this.pending.clear();
|
|
38
|
+
});
|
|
39
|
+
this.workers.push(worker);
|
|
40
|
+
}
|
|
41
|
+
runIngest(args) {
|
|
42
|
+
const jobId = this.nextJobId++;
|
|
43
|
+
const worker = this.workers[jobId % this.workers.length];
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
this.pending.set(jobId, { resolve, reject });
|
|
46
|
+
worker.postMessage({ type: "ingest", jobId, ...args });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async close() {
|
|
50
|
+
await Promise.all(this.workers.map((w) => w.terminate()));
|
|
51
|
+
this.workers = [];
|
|
52
|
+
this.pending.clear();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
export {
|
|
56
|
+
WorkerPool
|
|
57
|
+
};
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/memory/worker.ts
|
|
4
|
+
import { parentPort } from "worker_threads";
|
|
5
|
+
|
|
6
|
+
// src/memory/db.ts
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import { mkdirSync } from "fs";
|
|
9
|
+
import { dirname } from "path";
|
|
10
|
+
function openDatabase(path) {
|
|
11
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
12
|
+
const db = new Database(path);
|
|
13
|
+
db.pragma("journal_mode = WAL");
|
|
14
|
+
db.pragma("synchronous = NORMAL");
|
|
15
|
+
db.pragma("foreign_keys = ON");
|
|
16
|
+
return db;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/memory/schema.ts
|
|
20
|
+
var CURRENT_VERSION = 1;
|
|
21
|
+
var V1_SQL = `
|
|
22
|
+
CREATE TABLE IF NOT EXISTS index_state (
|
|
23
|
+
key TEXT PRIMARY KEY,
|
|
24
|
+
value TEXT NOT NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS commits (
|
|
28
|
+
sha TEXT PRIMARY KEY,
|
|
29
|
+
parent_sha TEXT,
|
|
30
|
+
author TEXT NOT NULL,
|
|
31
|
+
timestamp INTEGER NOT NULL,
|
|
32
|
+
subject TEXT NOT NULL,
|
|
33
|
+
is_fix INTEGER NOT NULL,
|
|
34
|
+
is_revert INTEGER NOT NULL,
|
|
35
|
+
reverts_sha TEXT,
|
|
36
|
+
FOREIGN KEY (reverts_sha) REFERENCES commits(sha)
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_commits_timestamp ON commits(timestamp);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_commits_is_fix ON commits(is_fix) WHERE is_fix = 1;
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS file_touches (
|
|
42
|
+
commit_sha TEXT NOT NULL,
|
|
43
|
+
file_path TEXT NOT NULL,
|
|
44
|
+
adds INTEGER NOT NULL,
|
|
45
|
+
dels INTEGER NOT NULL,
|
|
46
|
+
change_type TEXT NOT NULL,
|
|
47
|
+
renamed_from TEXT,
|
|
48
|
+
PRIMARY KEY (commit_sha, file_path),
|
|
49
|
+
FOREIGN KEY (commit_sha) REFERENCES commits(sha)
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_ft_file ON file_touches(file_path);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
54
|
+
id INTEGER PRIMARY KEY,
|
|
55
|
+
file_path TEXT NOT NULL,
|
|
56
|
+
kind TEXT NOT NULL,
|
|
57
|
+
qualified_name TEXT NOT NULL,
|
|
58
|
+
first_seen_sha TEXT NOT NULL,
|
|
59
|
+
last_seen_sha TEXT,
|
|
60
|
+
UNIQUE (file_path, kind, qualified_name)
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS symbol_touches (
|
|
65
|
+
commit_sha TEXT NOT NULL,
|
|
66
|
+
symbol_id INTEGER NOT NULL,
|
|
67
|
+
change_type TEXT NOT NULL,
|
|
68
|
+
PRIMARY KEY (commit_sha, symbol_id),
|
|
69
|
+
FOREIGN KEY (commit_sha) REFERENCES commits(sha),
|
|
70
|
+
FOREIGN KEY (symbol_id) REFERENCES symbols(id)
|
|
71
|
+
);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_st_symbol ON symbol_touches(symbol_id);
|
|
73
|
+
|
|
74
|
+
CREATE TABLE IF NOT EXISTS fix_links (
|
|
75
|
+
fix_commit_sha TEXT NOT NULL,
|
|
76
|
+
suspected_break_sha TEXT NOT NULL,
|
|
77
|
+
evidence_type TEXT NOT NULL,
|
|
78
|
+
confidence REAL NOT NULL,
|
|
79
|
+
window_hours INTEGER,
|
|
80
|
+
PRIMARY KEY (fix_commit_sha, suspected_break_sha, evidence_type),
|
|
81
|
+
FOREIGN KEY (fix_commit_sha) REFERENCES commits(sha),
|
|
82
|
+
FOREIGN KEY (suspected_break_sha) REFERENCES commits(sha)
|
|
83
|
+
);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_fl_break ON fix_links(suspected_break_sha);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE IF NOT EXISTS signal_calibration (
|
|
87
|
+
signal_type TEXT PRIMARY KEY,
|
|
88
|
+
precision REAL NOT NULL,
|
|
89
|
+
sample_size INTEGER NOT NULL,
|
|
90
|
+
last_computed_sha TEXT NOT NULL,
|
|
91
|
+
computed_at INTEGER NOT NULL
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS file_index_state (
|
|
95
|
+
file_path TEXT PRIMARY KEY,
|
|
96
|
+
last_commit_indexed TEXT NOT NULL,
|
|
97
|
+
last_blob_indexed TEXT,
|
|
98
|
+
indexed_at INTEGER NOT NULL,
|
|
99
|
+
parse_failed INTEGER NOT NULL DEFAULT 0,
|
|
100
|
+
FOREIGN KEY (last_commit_indexed) REFERENCES commits(sha)
|
|
101
|
+
);
|
|
102
|
+
`;
|
|
103
|
+
function runMigrations(db) {
|
|
104
|
+
const current = db.pragma("user_version", { simple: true });
|
|
105
|
+
if (current >= CURRENT_VERSION) return;
|
|
106
|
+
db.exec("BEGIN");
|
|
107
|
+
try {
|
|
108
|
+
db.exec(V1_SQL);
|
|
109
|
+
db.pragma(`user_version = ${CURRENT_VERSION}`);
|
|
110
|
+
db.exec("COMMIT");
|
|
111
|
+
} catch (err) {
|
|
112
|
+
db.exec("ROLLBACK");
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/memory/git.ts
|
|
118
|
+
import { execSync } from "child_process";
|
|
119
|
+
function logRange(cwd, from, to, timeoutMs = 6e4) {
|
|
120
|
+
const range = from ? `${from}..${to}` : to;
|
|
121
|
+
const fmt = "--format=%x1e%H%x00%P%x00%an%x00%at%x00%s%x00%b%x1f";
|
|
122
|
+
const cmd = `git log ${fmt} --numstat --no-renames ${range}`;
|
|
123
|
+
return execSync(cmd, { cwd, encoding: "utf-8", timeout: timeoutMs, maxBuffer: 256 * 1024 * 1024 });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/memory/commit-parser.ts
|
|
127
|
+
var FIX_PATTERNS = [
|
|
128
|
+
/\bfix(es|ed|ing)?\b/i,
|
|
129
|
+
/\bbugfix\b/i,
|
|
130
|
+
/\bhotfix\b/i,
|
|
131
|
+
/\bpatch\b/i,
|
|
132
|
+
/\bbug\b/i,
|
|
133
|
+
/closes?\s+#\d+/i,
|
|
134
|
+
/resolves?\s+#\d+/i
|
|
135
|
+
];
|
|
136
|
+
var REVERT_SUBJECT = /^\s*revert\b/i;
|
|
137
|
+
var REVERT_BODY_SHA = /This reverts commit ([0-9a-f]{7,40})/i;
|
|
138
|
+
function parseCommit(subject, body) {
|
|
139
|
+
const is_revert = REVERT_SUBJECT.test(subject);
|
|
140
|
+
const match = is_revert ? body.match(REVERT_BODY_SHA) : null;
|
|
141
|
+
const reverts_sha = match ? match[1] : null;
|
|
142
|
+
const is_fix = !is_revert && FIX_PATTERNS.some((re) => re.test(subject));
|
|
143
|
+
return { is_fix, is_revert, reverts_sha };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/memory/ingest/fix-links.ts
|
|
147
|
+
var FOLLOWUP_WINDOW_HOURS = 72;
|
|
148
|
+
var CHAIN_WINDOW_DAYS = 14;
|
|
149
|
+
var CHAIN_MIN = 3;
|
|
150
|
+
function deriveFixLinks(db) {
|
|
151
|
+
const insert = db.prepare(`
|
|
152
|
+
INSERT OR IGNORE INTO fix_links
|
|
153
|
+
(fix_commit_sha, suspected_break_sha, evidence_type, confidence, window_hours)
|
|
154
|
+
VALUES (?, ?, ?, ?, ?)
|
|
155
|
+
`);
|
|
156
|
+
const reverts = db.prepare(`
|
|
157
|
+
SELECT sha, reverts_sha FROM commits
|
|
158
|
+
WHERE is_revert = 1 AND reverts_sha IS NOT NULL
|
|
159
|
+
`).all();
|
|
160
|
+
for (const r of reverts) {
|
|
161
|
+
const exists = db.prepare("SELECT 1 FROM commits WHERE sha = ?").get(r.reverts_sha);
|
|
162
|
+
if (exists) insert.run(r.sha, r.reverts_sha, "revert_marker", 1, null);
|
|
163
|
+
}
|
|
164
|
+
const fixes = db.prepare(`
|
|
165
|
+
SELECT sha, timestamp FROM commits WHERE is_fix = 1
|
|
166
|
+
`).all();
|
|
167
|
+
const priorByFile = db.prepare(`
|
|
168
|
+
SELECT c.sha AS prior_sha
|
|
169
|
+
FROM file_touches ft_fix
|
|
170
|
+
JOIN file_touches ft_prior ON ft_prior.file_path = ft_fix.file_path
|
|
171
|
+
JOIN commits c ON c.sha = ft_prior.commit_sha
|
|
172
|
+
WHERE ft_fix.commit_sha = ?
|
|
173
|
+
AND c.timestamp < ?
|
|
174
|
+
AND c.timestamp >= ?
|
|
175
|
+
AND c.sha != ?
|
|
176
|
+
AND c.is_fix = 0
|
|
177
|
+
AND c.is_revert = 0
|
|
178
|
+
`);
|
|
179
|
+
for (const f of fixes) {
|
|
180
|
+
const lowerBound = f.timestamp - FOLLOWUP_WINDOW_HOURS * 3600;
|
|
181
|
+
const priors = priorByFile.all(f.sha, f.timestamp, lowerBound, f.sha);
|
|
182
|
+
const unique = new Set(priors.map((p) => p.prior_sha));
|
|
183
|
+
for (const prior of unique) {
|
|
184
|
+
insert.run(f.sha, prior, "short_followup_fix", 0.7, FOLLOWUP_WINDOW_HOURS);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const filesWithFixes = db.prepare(`
|
|
188
|
+
SELECT DISTINCT ft.file_path
|
|
189
|
+
FROM file_touches ft
|
|
190
|
+
JOIN commits c ON c.sha = ft.commit_sha
|
|
191
|
+
WHERE c.is_fix = 1
|
|
192
|
+
`).all();
|
|
193
|
+
const fixesOnFile = db.prepare(`
|
|
194
|
+
SELECT c.sha, c.timestamp
|
|
195
|
+
FROM commits c
|
|
196
|
+
JOIN file_touches ft ON ft.commit_sha = c.sha
|
|
197
|
+
WHERE ft.file_path = ? AND c.is_fix = 1
|
|
198
|
+
ORDER BY c.timestamp ASC
|
|
199
|
+
`);
|
|
200
|
+
const priorNonFixOnFile = db.prepare(`
|
|
201
|
+
SELECT c.sha
|
|
202
|
+
FROM commits c
|
|
203
|
+
JOIN file_touches ft ON ft.commit_sha = c.sha
|
|
204
|
+
WHERE ft.file_path = ?
|
|
205
|
+
AND c.timestamp < ?
|
|
206
|
+
AND c.is_fix = 0 AND c.is_revert = 0
|
|
207
|
+
ORDER BY c.timestamp DESC
|
|
208
|
+
LIMIT 1
|
|
209
|
+
`);
|
|
210
|
+
for (const { file_path } of filesWithFixes) {
|
|
211
|
+
const rows = fixesOnFile.all(file_path);
|
|
212
|
+
const windowSec = CHAIN_WINDOW_DAYS * 86400;
|
|
213
|
+
for (let i = 0; i + CHAIN_MIN - 1 < rows.length; i++) {
|
|
214
|
+
const windowEnd = rows[i + CHAIN_MIN - 1];
|
|
215
|
+
if (windowEnd.timestamp - rows[i].timestamp > windowSec) continue;
|
|
216
|
+
const prior = priorNonFixOnFile.get(file_path, rows[i].timestamp);
|
|
217
|
+
if (!prior) continue;
|
|
218
|
+
for (let j = i; j < rows.length && rows[j].timestamp - rows[i].timestamp <= windowSec; j++) {
|
|
219
|
+
insert.run(rows[j].sha, prior.sha, "same_region_fix_chain", 0.4, CHAIN_WINDOW_DAYS * 24);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/memory/calibration.ts
|
|
226
|
+
var LOOKAHEAD_SECONDS = 14 * 86400;
|
|
227
|
+
var REFRESH_AFTER_SECONDS = 7 * 86400;
|
|
228
|
+
function validateRevertMatch(db) {
|
|
229
|
+
const total = db.prepare(`SELECT COUNT(*) AS n FROM fix_links`).get().n;
|
|
230
|
+
const hits = db.prepare(`
|
|
231
|
+
SELECT COUNT(*) AS n
|
|
232
|
+
FROM fix_links fl
|
|
233
|
+
JOIN commits fix_c ON fix_c.sha = fl.fix_commit_sha
|
|
234
|
+
JOIN commits break_c ON break_c.sha = fl.suspected_break_sha
|
|
235
|
+
WHERE fix_c.timestamp - break_c.timestamp <= ?
|
|
236
|
+
`).get(LOOKAHEAD_SECONDS).n;
|
|
237
|
+
return { total, hits };
|
|
238
|
+
}
|
|
239
|
+
function validateHotspot(db) {
|
|
240
|
+
const total = db.prepare(`
|
|
241
|
+
SELECT COUNT(DISTINCT file_path) AS n FROM file_touches
|
|
242
|
+
`).get().n;
|
|
243
|
+
const hits = db.prepare(`
|
|
244
|
+
SELECT COUNT(DISTINCT ft.file_path) AS n
|
|
245
|
+
FROM file_touches ft
|
|
246
|
+
JOIN fix_links fl ON fl.suspected_break_sha = ft.commit_sha
|
|
247
|
+
`).get().n;
|
|
248
|
+
return { total, hits };
|
|
249
|
+
}
|
|
250
|
+
function validateFixRatio(db) {
|
|
251
|
+
const total = db.prepare(`
|
|
252
|
+
SELECT COUNT(DISTINCT file_path) AS n FROM file_touches
|
|
253
|
+
`).get().n;
|
|
254
|
+
const hits = db.prepare(`
|
|
255
|
+
SELECT COUNT(DISTINCT ft.file_path) AS n
|
|
256
|
+
FROM file_touches ft
|
|
257
|
+
JOIN fix_links fl ON fl.suspected_break_sha = ft.commit_sha
|
|
258
|
+
`).get().n;
|
|
259
|
+
return { total, hits };
|
|
260
|
+
}
|
|
261
|
+
function validateCoverageDecline(_db) {
|
|
262
|
+
return { total: 0, hits: 0 };
|
|
263
|
+
}
|
|
264
|
+
function validateAuthorChurn(db) {
|
|
265
|
+
const total = db.prepare(`SELECT COUNT(*) AS n FROM file_touches`).get().n;
|
|
266
|
+
const hits = db.prepare(`SELECT COUNT(*) AS n FROM fix_links`).get().n;
|
|
267
|
+
return { total, hits };
|
|
268
|
+
}
|
|
269
|
+
var VALIDATORS = {
|
|
270
|
+
revert_match: validateRevertMatch,
|
|
271
|
+
hotspot: validateHotspot,
|
|
272
|
+
fix_ratio: validateFixRatio,
|
|
273
|
+
coverage_decline: validateCoverageDecline,
|
|
274
|
+
author_churn: validateAuthorChurn
|
|
275
|
+
};
|
|
276
|
+
function refreshCalibration(db, headSha) {
|
|
277
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
278
|
+
const upsert = db.prepare(`
|
|
279
|
+
INSERT INTO signal_calibration (signal_type, precision, sample_size, last_computed_sha, computed_at)
|
|
280
|
+
VALUES (?, ?, ?, ?, ?)
|
|
281
|
+
ON CONFLICT(signal_type) DO UPDATE SET
|
|
282
|
+
precision = excluded.precision,
|
|
283
|
+
sample_size = excluded.sample_size,
|
|
284
|
+
last_computed_sha = excluded.last_computed_sha,
|
|
285
|
+
computed_at = excluded.computed_at
|
|
286
|
+
`);
|
|
287
|
+
for (const [type, validator] of Object.entries(VALIDATORS)) {
|
|
288
|
+
const v = validator(db);
|
|
289
|
+
const precision = v.total === 0 ? 0 : v.hits / v.total;
|
|
290
|
+
upsert.run(type, precision, v.total, headSha, now);
|
|
291
|
+
}
|
|
292
|
+
db.prepare(`
|
|
293
|
+
INSERT INTO index_state (key, value) VALUES ('calibration_last_refreshed_at', ?)
|
|
294
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
295
|
+
`).run(String(now));
|
|
296
|
+
}
|
|
297
|
+
function shouldRefresh(db, currentHeadSha) {
|
|
298
|
+
const lastTimeRow = db.prepare(`SELECT value FROM index_state WHERE key = 'calibration_last_refreshed_at'`).get();
|
|
299
|
+
if (!lastTimeRow) return true;
|
|
300
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
301
|
+
const lastTime = parseInt(lastTimeRow.value, 10);
|
|
302
|
+
if (now - lastTime >= REFRESH_AFTER_SECONDS) return true;
|
|
303
|
+
const anyCal = db.prepare(`SELECT last_computed_sha FROM signal_calibration LIMIT 1`).get();
|
|
304
|
+
if (!anyCal) return true;
|
|
305
|
+
return anyCal.last_computed_sha !== currentHeadSha;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/memory/ingest/tier1.ts
|
|
309
|
+
var RECORD_SEP = "";
|
|
310
|
+
var FIELD_SEP = "\0";
|
|
311
|
+
var RECORD_END = "";
|
|
312
|
+
function parseNumstatLine(line) {
|
|
313
|
+
const parts = line.split(" ");
|
|
314
|
+
if (parts.length < 3) return null;
|
|
315
|
+
const adds = parts[0] === "-" ? 0 : parseInt(parts[0], 10);
|
|
316
|
+
const dels = parts[1] === "-" ? 0 : parseInt(parts[1], 10);
|
|
317
|
+
if (Number.isNaN(adds) || Number.isNaN(dels)) return null;
|
|
318
|
+
return { adds, dels, path: parts.slice(2).join(" ").trim() };
|
|
319
|
+
}
|
|
320
|
+
function parseLogOutput(output) {
|
|
321
|
+
const commits = [];
|
|
322
|
+
const records = output.split(RECORD_SEP).slice(1);
|
|
323
|
+
for (const rec of records) {
|
|
324
|
+
const endIdx = rec.indexOf(RECORD_END);
|
|
325
|
+
if (endIdx === -1) continue;
|
|
326
|
+
const content = rec.slice(0, endIdx);
|
|
327
|
+
const tail = rec.slice(endIdx + 1);
|
|
328
|
+
const fields = content.split(FIELD_SEP);
|
|
329
|
+
if (fields.length < 6) continue;
|
|
330
|
+
const [sha, parent, author, tsStr, subject, ...rest] = fields;
|
|
331
|
+
const body = rest.join(FIELD_SEP);
|
|
332
|
+
const touches = [];
|
|
333
|
+
for (const line of tail.split("\n")) {
|
|
334
|
+
const trimmed = line.trim();
|
|
335
|
+
if (!trimmed) continue;
|
|
336
|
+
const parsed = parseNumstatLine(trimmed);
|
|
337
|
+
if (!parsed) continue;
|
|
338
|
+
touches.push({
|
|
339
|
+
file_path: parsed.path,
|
|
340
|
+
adds: parsed.adds,
|
|
341
|
+
dels: parsed.dels,
|
|
342
|
+
change_type: parsed.adds > 0 && parsed.dels === 0 ? "A" : parsed.dels > 0 && parsed.adds === 0 ? "D" : "M"
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
commits.push({
|
|
346
|
+
sha,
|
|
347
|
+
parent_sha: parent ? parent.split(" ")[0] : null,
|
|
348
|
+
author,
|
|
349
|
+
timestamp: parseInt(tsStr, 10),
|
|
350
|
+
subject,
|
|
351
|
+
body,
|
|
352
|
+
touches
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return commits;
|
|
356
|
+
}
|
|
357
|
+
function resolveRevertsSha(raw, knownShas) {
|
|
358
|
+
if (!raw) return null;
|
|
359
|
+
if (knownShas.has(raw)) return raw;
|
|
360
|
+
if (raw.length < 40) {
|
|
361
|
+
for (const sha of knownShas) {
|
|
362
|
+
if (sha.startsWith(raw)) return sha;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
function ingestRange(db, repoPath, range) {
|
|
368
|
+
const raw = logRange(repoPath, range.from, range.to);
|
|
369
|
+
const commits = parseLogOutput(raw);
|
|
370
|
+
commits.sort((a, b) => a.timestamp - b.timestamp);
|
|
371
|
+
const knownShas = new Set(commits.map((c) => c.sha));
|
|
372
|
+
for (const existing of db.prepare(`SELECT sha FROM commits`).all()) {
|
|
373
|
+
knownShas.add(existing.sha);
|
|
374
|
+
}
|
|
375
|
+
const insertCommit = db.prepare(`
|
|
376
|
+
INSERT OR IGNORE INTO commits
|
|
377
|
+
(sha, parent_sha, author, timestamp, subject, is_fix, is_revert, reverts_sha)
|
|
378
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
379
|
+
`);
|
|
380
|
+
const insertTouch = db.prepare(`
|
|
381
|
+
INSERT OR IGNORE INTO file_touches
|
|
382
|
+
(commit_sha, file_path, adds, dels, change_type, renamed_from)
|
|
383
|
+
VALUES (?, ?, ?, ?, ?, NULL)
|
|
384
|
+
`);
|
|
385
|
+
const upsertState = db.prepare(`
|
|
386
|
+
INSERT INTO index_state (key, value) VALUES (?, ?)
|
|
387
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
388
|
+
`);
|
|
389
|
+
const tx = db.transaction((batch) => {
|
|
390
|
+
for (const c of batch) {
|
|
391
|
+
const parsed = parseCommit(c.subject, c.body);
|
|
392
|
+
insertCommit.run(
|
|
393
|
+
c.sha,
|
|
394
|
+
c.parent_sha,
|
|
395
|
+
c.author,
|
|
396
|
+
c.timestamp,
|
|
397
|
+
c.subject,
|
|
398
|
+
parsed.is_fix ? 1 : 0,
|
|
399
|
+
parsed.is_revert ? 1 : 0,
|
|
400
|
+
resolveRevertsSha(parsed.reverts_sha, knownShas)
|
|
401
|
+
);
|
|
402
|
+
for (const t of c.touches) {
|
|
403
|
+
insertTouch.run(c.sha, t.file_path, t.adds, t.dels, t.change_type);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
db.pragma("foreign_keys = OFF");
|
|
408
|
+
try {
|
|
409
|
+
const BATCH = 1e3;
|
|
410
|
+
for (let i = 0; i < commits.length; i += BATCH) {
|
|
411
|
+
tx(commits.slice(i, i + BATCH));
|
|
412
|
+
}
|
|
413
|
+
} finally {
|
|
414
|
+
db.pragma("foreign_keys = ON");
|
|
415
|
+
}
|
|
416
|
+
deriveFixLinks(db);
|
|
417
|
+
if (shouldRefresh(db, range.to)) {
|
|
418
|
+
refreshCalibration(db, range.to);
|
|
419
|
+
}
|
|
420
|
+
upsertState.run("last_indexed_sha", range.to);
|
|
421
|
+
const totalRow = db.prepare("SELECT COUNT(*) AS n FROM commits").get();
|
|
422
|
+
upsertState.run("indexed_commits_total", String(totalRow.n));
|
|
423
|
+
return commits.length;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/memory/worker.ts
|
|
427
|
+
if (!parentPort) {
|
|
428
|
+
throw new Error("memory/worker.ts must run inside a Worker");
|
|
429
|
+
}
|
|
430
|
+
parentPort.on("message", (msg) => {
|
|
431
|
+
if (msg.type === "ingest") {
|
|
432
|
+
try {
|
|
433
|
+
const db = openDatabase(msg.dbPath);
|
|
434
|
+
runMigrations(db);
|
|
435
|
+
const n = ingestRange(db, msg.repoPath, msg.range);
|
|
436
|
+
db.close();
|
|
437
|
+
const out = { type: "ingest_done", jobId: msg.jobId, commits: n };
|
|
438
|
+
parentPort.postMessage(out);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
const out = {
|
|
441
|
+
type: "ingest_error",
|
|
442
|
+
jobId: msg.jobId,
|
|
443
|
+
message: err instanceof Error ? err.message : String(err)
|
|
444
|
+
};
|
|
445
|
+
parentPort.postMessage(out);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "composto-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Proactive AI team companion — less tokens, more insight",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"packageManager": "pnpm@10.30.1",
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
35
36
|
"@types/node": "^25.5.2",
|
|
36
37
|
"@types/picomatch": "^4.0.3",
|
|
37
38
|
"tsup": "^8.5.1",
|
|
@@ -42,6 +43,8 @@
|
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@anthropic-ai/sdk": "^0.87.0",
|
|
44
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
+
"better-sqlite3": "^11.10.0",
|
|
47
|
+
"ignore": "^7.0.5",
|
|
45
48
|
"picomatch": "^4.0.4",
|
|
46
49
|
"web-tree-sitter": "^0.26.8",
|
|
47
50
|
"yaml": "^2.8.3",
|