composto-ai 0.4.0 → 0.6.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/README.md +79 -6
- package/dist/index.js +883 -84
- package/dist/mcp/server.js +54 -37
- package/dist/memory/api.js +53 -200
- package/dist/memory/worker.js +94 -59
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -1558,7 +1558,25 @@ function openDatabase(path) {
|
|
|
1558
1558
|
}
|
|
1559
1559
|
|
|
1560
1560
|
// src/memory/schema.ts
|
|
1561
|
-
var CURRENT_VERSION =
|
|
1561
|
+
var CURRENT_VERSION = 3;
|
|
1562
|
+
var V2_SQL = `
|
|
1563
|
+
CREATE INDEX IF NOT EXISTS idx_ft_file_commit ON file_touches(file_path, commit_sha);
|
|
1564
|
+
`;
|
|
1565
|
+
var V3_SQL = `
|
|
1566
|
+
CREATE TABLE IF NOT EXISTS hook_invocations (
|
|
1567
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1568
|
+
timestamp INTEGER NOT NULL,
|
|
1569
|
+
platform TEXT NOT NULL,
|
|
1570
|
+
event TEXT NOT NULL,
|
|
1571
|
+
file_path TEXT,
|
|
1572
|
+
verdict TEXT,
|
|
1573
|
+
score REAL,
|
|
1574
|
+
confidence REAL,
|
|
1575
|
+
latency_ms INTEGER NOT NULL,
|
|
1576
|
+
cache_hit INTEGER NOT NULL
|
|
1577
|
+
);
|
|
1578
|
+
CREATE INDEX IF NOT EXISTS idx_hi_timestamp ON hook_invocations(timestamp);
|
|
1579
|
+
`;
|
|
1562
1580
|
var V1_SQL = `
|
|
1563
1581
|
CREATE TABLE IF NOT EXISTS index_state (
|
|
1564
1582
|
key TEXT PRIMARY KEY,
|
|
@@ -1646,7 +1664,9 @@ function runMigrations(db) {
|
|
|
1646
1664
|
if (current >= CURRENT_VERSION) return;
|
|
1647
1665
|
db.exec("BEGIN");
|
|
1648
1666
|
try {
|
|
1649
|
-
db.exec(V1_SQL);
|
|
1667
|
+
if (current < 1) db.exec(V1_SQL);
|
|
1668
|
+
if (current < 2) db.exec(V2_SQL);
|
|
1669
|
+
if (current < 3) db.exec(V3_SQL);
|
|
1650
1670
|
db.pragma(`user_version = ${CURRENT_VERSION}`);
|
|
1651
1671
|
db.exec("COMMIT");
|
|
1652
1672
|
} catch (err) {
|
|
@@ -1788,13 +1808,19 @@ function computeRevertMatch(db, filePath) {
|
|
|
1788
1808
|
};
|
|
1789
1809
|
}
|
|
1790
1810
|
|
|
1811
|
+
// src/memory/signals/db-clock.ts
|
|
1812
|
+
function getDbMaxTimestamp(db) {
|
|
1813
|
+
const row = db.prepare("SELECT MAX(timestamp) AS ts FROM commits").get();
|
|
1814
|
+
return row?.ts ?? null;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1791
1817
|
// src/memory/signals/hotspot.ts
|
|
1792
1818
|
var WINDOW_SECONDS = 90 * 86400;
|
|
1793
1819
|
var SATURATION_TOUCHES = 30;
|
|
1794
1820
|
var FALLBACK_PRECISION2 = 0.3;
|
|
1795
1821
|
function computeHotspot(db, filePath) {
|
|
1796
|
-
const
|
|
1797
|
-
const lowerBound =
|
|
1822
|
+
const anchor = getDbMaxTimestamp(db) ?? Math.floor(Date.now() / 1e3);
|
|
1823
|
+
const lowerBound = anchor - WINDOW_SECONDS;
|
|
1798
1824
|
const row = db.prepare(`
|
|
1799
1825
|
SELECT COUNT(*) AS n
|
|
1800
1826
|
FROM file_touches ft
|
|
@@ -1852,38 +1878,12 @@ function computeFixRatio(db, filePath) {
|
|
|
1852
1878
|
};
|
|
1853
1879
|
}
|
|
1854
1880
|
|
|
1855
|
-
// src/memory/signals/coverage-decline.ts
|
|
1856
|
-
var FALLBACK_PRECISION4 = 0.3;
|
|
1857
|
-
function computeCoverageDecline(db, repoPath, filePath) {
|
|
1858
|
-
const cal = getCalibration(db, "coverage_decline", FALLBACK_PRECISION4);
|
|
1859
|
-
let strength = 0;
|
|
1860
|
-
try {
|
|
1861
|
-
const entries = getGitLog(repoPath, 200);
|
|
1862
|
-
const trends = {
|
|
1863
|
-
hotspots: detectHotspots(entries, { threshold: 10, fixRatioThreshold: 0.5 }),
|
|
1864
|
-
decaySignals: detectDecay(entries),
|
|
1865
|
-
inconsistencies: detectInconsistencies(entries)
|
|
1866
|
-
};
|
|
1867
|
-
const health = computeHealthFromTrends(filePath, trends);
|
|
1868
|
-
if (health.coverageTrend === "down") strength = 1;
|
|
1869
|
-
} catch {
|
|
1870
|
-
strength = 0;
|
|
1871
|
-
}
|
|
1872
|
-
return {
|
|
1873
|
-
type: "coverage_decline",
|
|
1874
|
-
strength,
|
|
1875
|
-
precision: cal.precision,
|
|
1876
|
-
sample_size: cal.sampleSize,
|
|
1877
|
-
evidence: []
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
1881
|
// src/memory/signals/author-churn.ts
|
|
1882
1882
|
var WINDOW_SECONDS2 = 90 * 86400;
|
|
1883
1883
|
var INACTIVE_THRESHOLD = 5;
|
|
1884
|
-
var
|
|
1884
|
+
var FALLBACK_PRECISION4 = 0.3;
|
|
1885
1885
|
function computeAuthorChurn(db, filePath) {
|
|
1886
|
-
const cal = getCalibration(db, "author_churn",
|
|
1886
|
+
const cal = getCalibration(db, "author_churn", FALLBACK_PRECISION4);
|
|
1887
1887
|
const base = {
|
|
1888
1888
|
type: "author_churn",
|
|
1889
1889
|
precision: cal.precision,
|
|
@@ -1899,8 +1899,8 @@ function computeAuthorChurn(db, filePath) {
|
|
|
1899
1899
|
LIMIT 1
|
|
1900
1900
|
`).get(filePath);
|
|
1901
1901
|
if (!lastTouch) return { ...base, strength: 0 };
|
|
1902
|
-
const
|
|
1903
|
-
const lowerBound =
|
|
1902
|
+
const anchor = getDbMaxTimestamp(db) ?? Math.floor(Date.now() / 1e3);
|
|
1903
|
+
const lowerBound = anchor - WINDOW_SECONDS2;
|
|
1904
1904
|
const activity = db.prepare(`SELECT COUNT(*) AS n FROM commits WHERE author = ? AND timestamp >= ?`).get(lastTouch.author, lowerBound);
|
|
1905
1905
|
let strength = 0;
|
|
1906
1906
|
if (activity.n === 0) strength = 1;
|
|
@@ -1909,12 +1909,11 @@ function computeAuthorChurn(db, filePath) {
|
|
|
1909
1909
|
}
|
|
1910
1910
|
|
|
1911
1911
|
// src/memory/signals/index.ts
|
|
1912
|
-
function collectSignals(db,
|
|
1912
|
+
function collectSignals(db, _repoPath, filePath) {
|
|
1913
1913
|
return [
|
|
1914
1914
|
computeRevertMatch(db, filePath),
|
|
1915
1915
|
computeHotspot(db, filePath),
|
|
1916
1916
|
computeFixRatio(db, filePath),
|
|
1917
|
-
computeCoverageDecline(db, repoPath, filePath),
|
|
1918
1917
|
computeAuthorChurn(db, filePath)
|
|
1919
1918
|
];
|
|
1920
1919
|
}
|
|
@@ -2245,6 +2244,24 @@ var MemoryAPI = class {
|
|
|
2245
2244
|
});
|
|
2246
2245
|
return this.bootstrapPromise;
|
|
2247
2246
|
}
|
|
2247
|
+
// bootstrapFromBoundary indexes only commits between fromSha and HEAD.
|
|
2248
|
+
// Used by `composto index --since=YYYY-MM-DD` to bound work on huge repos.
|
|
2249
|
+
// Pass fromSha=null to index the full history (same as bootstrapIfNeeded).
|
|
2250
|
+
async bootstrapFromBoundary(fromSha) {
|
|
2251
|
+
if (this.bootstrapPromise) return this.bootstrapPromise;
|
|
2252
|
+
const head = revParseHead(this.repoPath);
|
|
2253
|
+
const range = { from: fromSha, to: head };
|
|
2254
|
+
this.bootstrapPromise = this.pool.runIngest({ dbPath: this.dbPath, repoPath: this.repoPath, range }).then(() => {
|
|
2255
|
+
this.log.info("bootstrap_done", { through: range.to, from: range.from });
|
|
2256
|
+
}).catch((err) => {
|
|
2257
|
+
this.log.error("bootstrap_failed", { message: err.message });
|
|
2258
|
+
this.failures.recordFailure("ingest_failure");
|
|
2259
|
+
throw err;
|
|
2260
|
+
}).finally(() => {
|
|
2261
|
+
this.bootstrapPromise = null;
|
|
2262
|
+
});
|
|
2263
|
+
return this.bootstrapPromise;
|
|
2264
|
+
}
|
|
2248
2265
|
async blastradius(input) {
|
|
2249
2266
|
const start = Date.now();
|
|
2250
2267
|
if (this.failures.isDisabled()) {
|
|
@@ -2366,7 +2383,7 @@ var MemoryAPI = class {
|
|
|
2366
2383
|
var ALL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs"];
|
|
2367
2384
|
var server = new McpServer({
|
|
2368
2385
|
name: "composto",
|
|
2369
|
-
version: "0.4.
|
|
2386
|
+
version: "0.4.2"
|
|
2370
2387
|
});
|
|
2371
2388
|
server.tool(
|
|
2372
2389
|
"composto_ir",
|
package/dist/memory/api.js
CHANGED
|
@@ -14,7 +14,25 @@ function openDatabase(path) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// src/memory/schema.ts
|
|
17
|
-
var CURRENT_VERSION =
|
|
17
|
+
var CURRENT_VERSION = 3;
|
|
18
|
+
var V2_SQL = `
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_ft_file_commit ON file_touches(file_path, commit_sha);
|
|
20
|
+
`;
|
|
21
|
+
var V3_SQL = `
|
|
22
|
+
CREATE TABLE IF NOT EXISTS hook_invocations (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
timestamp INTEGER NOT NULL,
|
|
25
|
+
platform TEXT NOT NULL,
|
|
26
|
+
event TEXT NOT NULL,
|
|
27
|
+
file_path TEXT,
|
|
28
|
+
verdict TEXT,
|
|
29
|
+
score REAL,
|
|
30
|
+
confidence REAL,
|
|
31
|
+
latency_ms INTEGER NOT NULL,
|
|
32
|
+
cache_hit INTEGER NOT NULL
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_hi_timestamp ON hook_invocations(timestamp);
|
|
35
|
+
`;
|
|
18
36
|
var V1_SQL = `
|
|
19
37
|
CREATE TABLE IF NOT EXISTS index_state (
|
|
20
38
|
key TEXT PRIMARY KEY,
|
|
@@ -102,7 +120,9 @@ function runMigrations(db) {
|
|
|
102
120
|
if (current >= CURRENT_VERSION) return;
|
|
103
121
|
db.exec("BEGIN");
|
|
104
122
|
try {
|
|
105
|
-
db.exec(V1_SQL);
|
|
123
|
+
if (current < 1) db.exec(V1_SQL);
|
|
124
|
+
if (current < 2) db.exec(V2_SQL);
|
|
125
|
+
if (current < 3) db.exec(V3_SQL);
|
|
106
126
|
db.pragma(`user_version = ${CURRENT_VERSION}`);
|
|
107
127
|
db.exec("COMMIT");
|
|
108
128
|
} catch (err) {
|
|
@@ -244,13 +264,19 @@ function computeRevertMatch(db, filePath) {
|
|
|
244
264
|
};
|
|
245
265
|
}
|
|
246
266
|
|
|
267
|
+
// src/memory/signals/db-clock.ts
|
|
268
|
+
function getDbMaxTimestamp(db) {
|
|
269
|
+
const row = db.prepare("SELECT MAX(timestamp) AS ts FROM commits").get();
|
|
270
|
+
return row?.ts ?? null;
|
|
271
|
+
}
|
|
272
|
+
|
|
247
273
|
// src/memory/signals/hotspot.ts
|
|
248
274
|
var WINDOW_SECONDS = 90 * 86400;
|
|
249
275
|
var SATURATION_TOUCHES = 30;
|
|
250
276
|
var FALLBACK_PRECISION2 = 0.3;
|
|
251
277
|
function computeHotspot(db, filePath) {
|
|
252
|
-
const
|
|
253
|
-
const lowerBound =
|
|
278
|
+
const anchor = getDbMaxTimestamp(db) ?? Math.floor(Date.now() / 1e3);
|
|
279
|
+
const lowerBound = anchor - WINDOW_SECONDS;
|
|
254
280
|
const row = db.prepare(`
|
|
255
281
|
SELECT COUNT(*) AS n
|
|
256
282
|
FROM file_touches ft
|
|
@@ -308,202 +334,12 @@ function computeFixRatio(db, filePath) {
|
|
|
308
334
|
};
|
|
309
335
|
}
|
|
310
336
|
|
|
311
|
-
// src/trends/git-log-parser.ts
|
|
312
|
-
import { execSync as execSync2 } from "child_process";
|
|
313
|
-
var BUG_FIX_PATTERNS = [
|
|
314
|
-
/\bfix\b/i,
|
|
315
|
-
/\bbugfix\b/i,
|
|
316
|
-
/\bhotfix\b/i,
|
|
317
|
-
/\bpatch\b/i,
|
|
318
|
-
/\bresolve\b/i,
|
|
319
|
-
/\bbug\b/i
|
|
320
|
-
];
|
|
321
|
-
function isBugFixCommit(message) {
|
|
322
|
-
return BUG_FIX_PATTERNS.some((p) => p.test(message));
|
|
323
|
-
}
|
|
324
|
-
function parseGitLogOutput(output) {
|
|
325
|
-
const entries = [];
|
|
326
|
-
const lines = output.split("\n");
|
|
327
|
-
let i = 0;
|
|
328
|
-
while (i < lines.length) {
|
|
329
|
-
const line = lines[i].trim();
|
|
330
|
-
if (!line || !line.includes("|")) {
|
|
331
|
-
i++;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
const [hash, author, date, ...messageParts] = line.split("|");
|
|
335
|
-
const message = messageParts.join("|");
|
|
336
|
-
const files = [];
|
|
337
|
-
i++;
|
|
338
|
-
while (i < lines.length && lines[i].trim() !== "" && !lines[i].includes("|")) {
|
|
339
|
-
const fileLine = lines[i].trim();
|
|
340
|
-
if (fileLine) files.push(fileLine);
|
|
341
|
-
i++;
|
|
342
|
-
}
|
|
343
|
-
entries.push({ hash, author, date, message, files });
|
|
344
|
-
}
|
|
345
|
-
return entries;
|
|
346
|
-
}
|
|
347
|
-
function getGitLog(repoPath, count = 100) {
|
|
348
|
-
try {
|
|
349
|
-
const output = execSync2(
|
|
350
|
-
`git log --format="%h|%an|%as|%s" --name-only -n ${count}`,
|
|
351
|
-
{ cwd: repoPath, encoding: "utf-8", timeout: 1e4 }
|
|
352
|
-
);
|
|
353
|
-
return parseGitLogOutput(output);
|
|
354
|
-
} catch {
|
|
355
|
-
return [];
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/trends/hotspot.ts
|
|
360
|
-
function detectHotspots(entries, options) {
|
|
361
|
-
const fileStats = /* @__PURE__ */ new Map();
|
|
362
|
-
for (const entry of entries) {
|
|
363
|
-
const isFix = isBugFixCommit(entry.message);
|
|
364
|
-
for (const file of entry.files) {
|
|
365
|
-
const stats = fileStats.get(file) ?? { changes: 0, fixes: 0, authors: /* @__PURE__ */ new Set() };
|
|
366
|
-
stats.changes++;
|
|
367
|
-
if (isFix) stats.fixes++;
|
|
368
|
-
stats.authors.add(entry.author);
|
|
369
|
-
fileStats.set(file, stats);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
const hotspots = [];
|
|
373
|
-
for (const [file, stats] of fileStats) {
|
|
374
|
-
const fixRatio = stats.changes > 0 ? stats.fixes / stats.changes : 0;
|
|
375
|
-
if (stats.changes >= options.threshold && fixRatio >= options.fixRatioThreshold) {
|
|
376
|
-
hotspots.push({
|
|
377
|
-
file,
|
|
378
|
-
changesInLast30Commits: stats.changes,
|
|
379
|
-
bugFixRatio: fixRatio,
|
|
380
|
-
authorCount: stats.authors.size
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return hotspots.sort((a, b) => b.changesInLast30Commits - a.changesInLast30Commits);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// src/trends/decay.ts
|
|
388
|
-
function detectDecay(entries) {
|
|
389
|
-
const fileChanges = /* @__PURE__ */ new Map();
|
|
390
|
-
for (const entry of entries) {
|
|
391
|
-
for (const file of entry.files) {
|
|
392
|
-
const changes = fileChanges.get(file) ?? [];
|
|
393
|
-
changes.push({ date: entry.date });
|
|
394
|
-
fileChanges.set(file, changes);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
const signals = [];
|
|
398
|
-
for (const [file, changes] of fileChanges) {
|
|
399
|
-
if (changes.length < 4) continue;
|
|
400
|
-
const sorted = [...changes].sort((a, b) => a.date.localeCompare(b.date));
|
|
401
|
-
const firstDate = new Date(sorted[0].date).getTime();
|
|
402
|
-
const lastDate = new Date(sorted[sorted.length - 1].date).getTime();
|
|
403
|
-
const midDate = firstDate + (lastDate - firstDate) / 2;
|
|
404
|
-
const firstHalfCount = sorted.filter((c) => new Date(c.date).getTime() <= midDate).length;
|
|
405
|
-
const secondHalfCount = sorted.length - firstHalfCount;
|
|
406
|
-
if (secondHalfCount > firstHalfCount) {
|
|
407
|
-
signals.push({
|
|
408
|
-
file,
|
|
409
|
-
metric: "churn",
|
|
410
|
-
trend: "declining",
|
|
411
|
-
dataPoints: sorted.map((c, i) => ({ date: c.date, value: i + 1 }))
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return signals;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// src/trends/inconsistency.ts
|
|
419
|
-
function detectInconsistencies(entries, minAuthors = 3) {
|
|
420
|
-
const fileAuthors = /* @__PURE__ */ new Map();
|
|
421
|
-
for (const entry of entries) {
|
|
422
|
-
for (const file of entry.files) {
|
|
423
|
-
const authors = fileAuthors.get(file) ?? /* @__PURE__ */ new Map();
|
|
424
|
-
const commits = authors.get(entry.author) ?? [];
|
|
425
|
-
commits.push(entry.message);
|
|
426
|
-
authors.set(entry.author, commits);
|
|
427
|
-
fileAuthors.set(file, authors);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
const inconsistencies = [];
|
|
431
|
-
for (const [file, authors] of fileAuthors) {
|
|
432
|
-
if (authors.size >= minAuthors) {
|
|
433
|
-
const patterns = Array.from(authors.entries()).map(([author, commits]) => ({
|
|
434
|
-
author,
|
|
435
|
-
style: categorizeStyle(commits)
|
|
436
|
-
}));
|
|
437
|
-
inconsistencies.push({ file, patterns });
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return inconsistencies;
|
|
441
|
-
}
|
|
442
|
-
function categorizeStyle(commits) {
|
|
443
|
-
const types = commits.map((m) => {
|
|
444
|
-
if (m.match(/^fix/i)) return "fix";
|
|
445
|
-
if (m.match(/^feat/i)) return "feature";
|
|
446
|
-
if (m.match(/^refactor/i)) return "refactor";
|
|
447
|
-
return "other";
|
|
448
|
-
});
|
|
449
|
-
const primary = mode(types);
|
|
450
|
-
return `primarily ${primary} (${commits.length} commits)`;
|
|
451
|
-
}
|
|
452
|
-
function mode(arr) {
|
|
453
|
-
const counts = /* @__PURE__ */ new Map();
|
|
454
|
-
for (const item of arr) {
|
|
455
|
-
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
456
|
-
}
|
|
457
|
-
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "unknown";
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// src/ir/health.ts
|
|
461
|
-
function computeHealthFromTrends(file, trends) {
|
|
462
|
-
const hotspot = trends.hotspots.find((h) => h.file === file);
|
|
463
|
-
const decay = trends.decaySignals.find((d) => d.file === file);
|
|
464
|
-
const inconsistency = trends.inconsistencies.find((i) => i.file === file);
|
|
465
|
-
return {
|
|
466
|
-
churn: hotspot?.changesInLast30Commits ?? 0,
|
|
467
|
-
fixRatio: hotspot?.bugFixRatio ?? 0,
|
|
468
|
-
coverageTrend: decay?.trend === "declining" ? "down" : decay?.trend === "improving" ? "up" : "stable",
|
|
469
|
-
staleness: "",
|
|
470
|
-
authorCount: hotspot?.authorCount ?? 0,
|
|
471
|
-
consistency: inconsistency ? "low" : "high"
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// src/memory/signals/coverage-decline.ts
|
|
476
|
-
var FALLBACK_PRECISION4 = 0.3;
|
|
477
|
-
function computeCoverageDecline(db, repoPath, filePath) {
|
|
478
|
-
const cal = getCalibration(db, "coverage_decline", FALLBACK_PRECISION4);
|
|
479
|
-
let strength = 0;
|
|
480
|
-
try {
|
|
481
|
-
const entries = getGitLog(repoPath, 200);
|
|
482
|
-
const trends = {
|
|
483
|
-
hotspots: detectHotspots(entries, { threshold: 10, fixRatioThreshold: 0.5 }),
|
|
484
|
-
decaySignals: detectDecay(entries),
|
|
485
|
-
inconsistencies: detectInconsistencies(entries)
|
|
486
|
-
};
|
|
487
|
-
const health = computeHealthFromTrends(filePath, trends);
|
|
488
|
-
if (health.coverageTrend === "down") strength = 1;
|
|
489
|
-
} catch {
|
|
490
|
-
strength = 0;
|
|
491
|
-
}
|
|
492
|
-
return {
|
|
493
|
-
type: "coverage_decline",
|
|
494
|
-
strength,
|
|
495
|
-
precision: cal.precision,
|
|
496
|
-
sample_size: cal.sampleSize,
|
|
497
|
-
evidence: []
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
337
|
// src/memory/signals/author-churn.ts
|
|
502
338
|
var WINDOW_SECONDS2 = 90 * 86400;
|
|
503
339
|
var INACTIVE_THRESHOLD = 5;
|
|
504
|
-
var
|
|
340
|
+
var FALLBACK_PRECISION4 = 0.3;
|
|
505
341
|
function computeAuthorChurn(db, filePath) {
|
|
506
|
-
const cal = getCalibration(db, "author_churn",
|
|
342
|
+
const cal = getCalibration(db, "author_churn", FALLBACK_PRECISION4);
|
|
507
343
|
const base = {
|
|
508
344
|
type: "author_churn",
|
|
509
345
|
precision: cal.precision,
|
|
@@ -519,8 +355,8 @@ function computeAuthorChurn(db, filePath) {
|
|
|
519
355
|
LIMIT 1
|
|
520
356
|
`).get(filePath);
|
|
521
357
|
if (!lastTouch) return { ...base, strength: 0 };
|
|
522
|
-
const
|
|
523
|
-
const lowerBound =
|
|
358
|
+
const anchor = getDbMaxTimestamp(db) ?? Math.floor(Date.now() / 1e3);
|
|
359
|
+
const lowerBound = anchor - WINDOW_SECONDS2;
|
|
524
360
|
const activity = db.prepare(`SELECT COUNT(*) AS n FROM commits WHERE author = ? AND timestamp >= ?`).get(lastTouch.author, lowerBound);
|
|
525
361
|
let strength = 0;
|
|
526
362
|
if (activity.n === 0) strength = 1;
|
|
@@ -529,12 +365,11 @@ function computeAuthorChurn(db, filePath) {
|
|
|
529
365
|
}
|
|
530
366
|
|
|
531
367
|
// src/memory/signals/index.ts
|
|
532
|
-
function collectSignals(db,
|
|
368
|
+
function collectSignals(db, _repoPath, filePath) {
|
|
533
369
|
return [
|
|
534
370
|
computeRevertMatch(db, filePath),
|
|
535
371
|
computeHotspot(db, filePath),
|
|
536
372
|
computeFixRatio(db, filePath),
|
|
537
|
-
computeCoverageDecline(db, repoPath, filePath),
|
|
538
373
|
computeAuthorChurn(db, filePath)
|
|
539
374
|
];
|
|
540
375
|
}
|
|
@@ -865,6 +700,24 @@ var MemoryAPI = class {
|
|
|
865
700
|
});
|
|
866
701
|
return this.bootstrapPromise;
|
|
867
702
|
}
|
|
703
|
+
// bootstrapFromBoundary indexes only commits between fromSha and HEAD.
|
|
704
|
+
// Used by `composto index --since=YYYY-MM-DD` to bound work on huge repos.
|
|
705
|
+
// Pass fromSha=null to index the full history (same as bootstrapIfNeeded).
|
|
706
|
+
async bootstrapFromBoundary(fromSha) {
|
|
707
|
+
if (this.bootstrapPromise) return this.bootstrapPromise;
|
|
708
|
+
const head = revParseHead(this.repoPath);
|
|
709
|
+
const range = { from: fromSha, to: head };
|
|
710
|
+
this.bootstrapPromise = this.pool.runIngest({ dbPath: this.dbPath, repoPath: this.repoPath, range }).then(() => {
|
|
711
|
+
this.log.info("bootstrap_done", { through: range.to, from: range.from });
|
|
712
|
+
}).catch((err) => {
|
|
713
|
+
this.log.error("bootstrap_failed", { message: err.message });
|
|
714
|
+
this.failures.recordFailure("ingest_failure");
|
|
715
|
+
throw err;
|
|
716
|
+
}).finally(() => {
|
|
717
|
+
this.bootstrapPromise = null;
|
|
718
|
+
});
|
|
719
|
+
return this.bootstrapPromise;
|
|
720
|
+
}
|
|
868
721
|
async blastradius(input) {
|
|
869
722
|
const start = Date.now();
|
|
870
723
|
if (this.failures.isDisabled()) {
|
package/dist/memory/worker.js
CHANGED
|
@@ -17,7 +17,25 @@ function openDatabase(path) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// src/memory/schema.ts
|
|
20
|
-
var CURRENT_VERSION =
|
|
20
|
+
var CURRENT_VERSION = 3;
|
|
21
|
+
var V2_SQL = `
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_ft_file_commit ON file_touches(file_path, commit_sha);
|
|
23
|
+
`;
|
|
24
|
+
var V3_SQL = `
|
|
25
|
+
CREATE TABLE IF NOT EXISTS hook_invocations (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
timestamp INTEGER NOT NULL,
|
|
28
|
+
platform TEXT NOT NULL,
|
|
29
|
+
event TEXT NOT NULL,
|
|
30
|
+
file_path TEXT,
|
|
31
|
+
verdict TEXT,
|
|
32
|
+
score REAL,
|
|
33
|
+
confidence REAL,
|
|
34
|
+
latency_ms INTEGER NOT NULL,
|
|
35
|
+
cache_hit INTEGER NOT NULL
|
|
36
|
+
);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_hi_timestamp ON hook_invocations(timestamp);
|
|
38
|
+
`;
|
|
21
39
|
var V1_SQL = `
|
|
22
40
|
CREATE TABLE IF NOT EXISTS index_state (
|
|
23
41
|
key TEXT PRIMARY KEY,
|
|
@@ -105,7 +123,9 @@ function runMigrations(db) {
|
|
|
105
123
|
if (current >= CURRENT_VERSION) return;
|
|
106
124
|
db.exec("BEGIN");
|
|
107
125
|
try {
|
|
108
|
-
db.exec(V1_SQL);
|
|
126
|
+
if (current < 1) db.exec(V1_SQL);
|
|
127
|
+
if (current < 2) db.exec(V2_SQL);
|
|
128
|
+
if (current < 3) db.exec(V3_SQL);
|
|
109
129
|
db.pragma(`user_version = ${CURRENT_VERSION}`);
|
|
110
130
|
db.exec("COMMIT");
|
|
111
131
|
} catch (err) {
|
|
@@ -153,50 +173,40 @@ function deriveFixLinks(db) {
|
|
|
153
173
|
(fix_commit_sha, suspected_break_sha, evidence_type, confidence, window_hours)
|
|
154
174
|
VALUES (?, ?, ?, ?, ?)
|
|
155
175
|
`);
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
SELECT sha, timestamp FROM commits WHERE is_fix = 1
|
|
166
|
-
`).all();
|
|
167
|
-
const priorByFile = db.prepare(`
|
|
168
|
-
SELECT c.sha AS prior_sha
|
|
176
|
+
const bulkFollowup = db.prepare(`
|
|
177
|
+
INSERT OR IGNORE INTO fix_links
|
|
178
|
+
(fix_commit_sha, suspected_break_sha, evidence_type, confidence, window_hours)
|
|
179
|
+
SELECT DISTINCT
|
|
180
|
+
ft_fix.commit_sha,
|
|
181
|
+
c_prior.sha,
|
|
182
|
+
'short_followup_fix',
|
|
183
|
+
0.7,
|
|
184
|
+
?
|
|
169
185
|
FROM file_touches ft_fix
|
|
186
|
+
JOIN commits c_fix ON c_fix.sha = ft_fix.commit_sha AND c_fix.is_fix = 1
|
|
170
187
|
JOIN file_touches ft_prior ON ft_prior.file_path = ft_fix.file_path
|
|
171
|
-
JOIN commits
|
|
172
|
-
WHERE
|
|
173
|
-
AND
|
|
174
|
-
AND
|
|
175
|
-
AND
|
|
176
|
-
AND
|
|
177
|
-
AND c.is_revert = 0
|
|
188
|
+
JOIN commits c_prior ON c_prior.sha = ft_prior.commit_sha
|
|
189
|
+
WHERE c_prior.timestamp < c_fix.timestamp
|
|
190
|
+
AND c_prior.timestamp >= c_fix.timestamp - ?
|
|
191
|
+
AND c_prior.sha != c_fix.sha
|
|
192
|
+
AND c_prior.is_fix = 0
|
|
193
|
+
AND c_prior.is_revert = 0
|
|
178
194
|
`);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
const bulkRevert = db.prepare(`
|
|
196
|
+
INSERT OR IGNORE INTO fix_links
|
|
197
|
+
(fix_commit_sha, suspected_break_sha, evidence_type, confidence, window_hours)
|
|
198
|
+
SELECT c.sha, c.reverts_sha, 'revert_marker', 1.0, NULL
|
|
195
199
|
FROM commits c
|
|
196
|
-
JOIN
|
|
197
|
-
WHERE
|
|
198
|
-
ORDER BY c.timestamp ASC
|
|
200
|
+
JOIN commits target ON target.sha = c.reverts_sha
|
|
201
|
+
WHERE c.is_revert = 1 AND c.reverts_sha IS NOT NULL
|
|
199
202
|
`);
|
|
203
|
+
const allFixesByFile = db.prepare(`
|
|
204
|
+
SELECT ft.file_path, c.sha, c.timestamp
|
|
205
|
+
FROM commits c
|
|
206
|
+
JOIN file_touches ft ON ft.commit_sha = c.sha
|
|
207
|
+
WHERE c.is_fix = 1
|
|
208
|
+
ORDER BY ft.file_path, c.timestamp ASC
|
|
209
|
+
`).all();
|
|
200
210
|
const priorNonFixOnFile = db.prepare(`
|
|
201
211
|
SELECT c.sha
|
|
202
212
|
FROM commits c
|
|
@@ -207,19 +217,29 @@ function deriveFixLinks(db) {
|
|
|
207
217
|
ORDER BY c.timestamp DESC
|
|
208
218
|
LIMIT 1
|
|
209
219
|
`);
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
const derive = db.transaction(() => {
|
|
221
|
+
bulkRevert.run();
|
|
222
|
+
bulkFollowup.run(FOLLOWUP_WINDOW_HOURS, FOLLOWUP_WINDOW_HOURS * 3600);
|
|
212
223
|
const windowSec = CHAIN_WINDOW_DAYS * 86400;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
let i = 0;
|
|
225
|
+
while (i < allFixesByFile.length) {
|
|
226
|
+
let j = i;
|
|
227
|
+
while (j < allFixesByFile.length && allFixesByFile[j].file_path === allFixesByFile[i].file_path) j++;
|
|
228
|
+
const rows = allFixesByFile.slice(i, j);
|
|
229
|
+
const file_path = rows[0].file_path;
|
|
230
|
+
i = j;
|
|
231
|
+
for (let k = 0; k + CHAIN_MIN - 1 < rows.length; k++) {
|
|
232
|
+
const windowEnd = rows[k + CHAIN_MIN - 1];
|
|
233
|
+
if (windowEnd.timestamp - rows[k].timestamp > windowSec) continue;
|
|
234
|
+
const prior = priorNonFixOnFile.get(file_path, rows[k].timestamp);
|
|
235
|
+
if (!prior) continue;
|
|
236
|
+
for (let m = k; m < rows.length && rows[m].timestamp - rows[k].timestamp <= windowSec; m++) {
|
|
237
|
+
insert.run(rows[m].sha, prior.sha, "same_region_fix_chain", 0.4, CHAIN_WINDOW_DAYS * 24);
|
|
238
|
+
}
|
|
220
239
|
}
|
|
221
240
|
}
|
|
222
|
-
}
|
|
241
|
+
});
|
|
242
|
+
derive();
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
// src/memory/calibration.ts
|
|
@@ -258,9 +278,6 @@ function validateFixRatio(db) {
|
|
|
258
278
|
`).get().n;
|
|
259
279
|
return { total, hits };
|
|
260
280
|
}
|
|
261
|
-
function validateCoverageDecline(_db) {
|
|
262
|
-
return { total: 0, hits: 0 };
|
|
263
|
-
}
|
|
264
281
|
function validateAuthorChurn(db) {
|
|
265
282
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM file_touches`).get().n;
|
|
266
283
|
const hits = db.prepare(`SELECT COUNT(*) AS n FROM fix_links`).get().n;
|
|
@@ -270,7 +287,6 @@ var VALIDATORS = {
|
|
|
270
287
|
revert_match: validateRevertMatch,
|
|
271
288
|
hotspot: validateHotspot,
|
|
272
289
|
fix_ratio: validateFixRatio,
|
|
273
|
-
coverage_decline: validateCoverageDecline,
|
|
274
290
|
author_churn: validateAuthorChurn
|
|
275
291
|
};
|
|
276
292
|
function refreshCalibration(db, headSha) {
|
|
@@ -354,10 +370,24 @@ function parseLogOutput(output) {
|
|
|
354
370
|
}
|
|
355
371
|
return commits;
|
|
356
372
|
}
|
|
373
|
+
function resolveRevertsSha(raw, knownShas) {
|
|
374
|
+
if (!raw) return null;
|
|
375
|
+
if (knownShas.has(raw)) return raw;
|
|
376
|
+
if (raw.length < 40) {
|
|
377
|
+
for (const sha of knownShas) {
|
|
378
|
+
if (sha.startsWith(raw)) return sha;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
357
383
|
function ingestRange(db, repoPath, range) {
|
|
358
384
|
const raw = logRange(repoPath, range.from, range.to);
|
|
359
385
|
const commits = parseLogOutput(raw);
|
|
360
386
|
commits.sort((a, b) => a.timestamp - b.timestamp);
|
|
387
|
+
const knownShas = new Set(commits.map((c) => c.sha));
|
|
388
|
+
for (const existing of db.prepare(`SELECT sha FROM commits`).all()) {
|
|
389
|
+
knownShas.add(existing.sha);
|
|
390
|
+
}
|
|
361
391
|
const insertCommit = db.prepare(`
|
|
362
392
|
INSERT OR IGNORE INTO commits
|
|
363
393
|
(sha, parent_sha, author, timestamp, subject, is_fix, is_revert, reverts_sha)
|
|
@@ -383,16 +413,21 @@ function ingestRange(db, repoPath, range) {
|
|
|
383
413
|
c.subject,
|
|
384
414
|
parsed.is_fix ? 1 : 0,
|
|
385
415
|
parsed.is_revert ? 1 : 0,
|
|
386
|
-
parsed.reverts_sha
|
|
416
|
+
resolveRevertsSha(parsed.reverts_sha, knownShas)
|
|
387
417
|
);
|
|
388
418
|
for (const t of c.touches) {
|
|
389
419
|
insertTouch.run(c.sha, t.file_path, t.adds, t.dels, t.change_type);
|
|
390
420
|
}
|
|
391
421
|
}
|
|
392
422
|
});
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
423
|
+
db.pragma("foreign_keys = OFF");
|
|
424
|
+
try {
|
|
425
|
+
const BATCH = 1e3;
|
|
426
|
+
for (let i = 0; i < commits.length; i += BATCH) {
|
|
427
|
+
tx(commits.slice(i, i + BATCH));
|
|
428
|
+
}
|
|
429
|
+
} finally {
|
|
430
|
+
db.pragma("foreign_keys = ON");
|
|
396
431
|
}
|
|
397
432
|
deriveFixLinks(db);
|
|
398
433
|
if (shouldRefresh(db, range.to)) {
|