@mneme-ai/core 0.8.4 → 0.10.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/dist/entities/go-parser.d.ts +47 -0
- package/dist/entities/go-parser.d.ts.map +1 -0
- package/dist/entities/go-parser.js +315 -0
- package/dist/entities/go-parser.js.map +1 -0
- package/dist/entities/go-parser.test.d.ts +2 -0
- package/dist/entities/go-parser.test.d.ts.map +1 -0
- package/dist/entities/go-parser.test.js +147 -0
- package/dist/entities/go-parser.test.js.map +1 -0
- package/dist/entities/index.d.ts +1 -0
- package/dist/entities/index.d.ts.map +1 -1
- package/dist/entities/index.js +1 -0
- package/dist/entities/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/indexer/indexer.d.ts +12 -0
- package/dist/indexer/indexer.d.ts.map +1 -1
- package/dist/indexer/indexer.js +28 -1
- package/dist/indexer/indexer.js.map +1 -1
- package/dist/insights/bus-factor.d.ts +58 -0
- package/dist/insights/bus-factor.d.ts.map +1 -0
- package/dist/insights/bus-factor.js +117 -0
- package/dist/insights/bus-factor.js.map +1 -0
- package/dist/insights/bus-factor.test.d.ts +2 -0
- package/dist/insights/bus-factor.test.d.ts.map +1 -0
- package/dist/insights/bus-factor.test.js +149 -0
- package/dist/insights/bus-factor.test.js.map +1 -0
- package/dist/insights/commit-coach.d.ts +80 -0
- package/dist/insights/commit-coach.d.ts.map +1 -0
- package/dist/insights/commit-coach.js +230 -0
- package/dist/insights/commit-coach.js.map +1 -0
- package/dist/insights/commit-coach.test.d.ts +2 -0
- package/dist/insights/commit-coach.test.d.ts.map +1 -0
- package/dist/insights/commit-coach.test.js +163 -0
- package/dist/insights/commit-coach.test.js.map +1 -0
- package/dist/insights/crystal-ball.d.ts +76 -0
- package/dist/insights/crystal-ball.d.ts.map +1 -0
- package/dist/insights/crystal-ball.js +219 -0
- package/dist/insights/crystal-ball.js.map +1 -0
- package/dist/insights/crystal-ball.test.d.ts +2 -0
- package/dist/insights/crystal-ball.test.d.ts.map +1 -0
- package/dist/insights/crystal-ball.test.js +157 -0
- package/dist/insights/crystal-ball.test.js.map +1 -0
- package/dist/insights/decisions.d.ts +38 -0
- package/dist/insights/decisions.d.ts.map +1 -0
- package/dist/insights/decisions.js +125 -0
- package/dist/insights/decisions.js.map +1 -0
- package/dist/insights/decisions.test.d.ts +2 -0
- package/dist/insights/decisions.test.d.ts.map +1 -0
- package/dist/insights/decisions.test.js +141 -0
- package/dist/insights/decisions.test.js.map +1 -0
- package/dist/insights/dream.d.ts +71 -0
- package/dist/insights/dream.d.ts.map +1 -0
- package/dist/insights/dream.js +235 -0
- package/dist/insights/dream.js.map +1 -0
- package/dist/insights/dream.test.d.ts +2 -0
- package/dist/insights/dream.test.d.ts.map +1 -0
- package/dist/insights/dream.test.js +127 -0
- package/dist/insights/dream.test.js.map +1 -0
- package/dist/insights/index.d.ts +21 -0
- package/dist/insights/index.d.ts.map +1 -0
- package/dist/insights/index.js +21 -0
- package/dist/insights/index.js.map +1 -0
- package/dist/insights/obsidian.d.ts +42 -0
- package/dist/insights/obsidian.d.ts.map +1 -0
- package/dist/insights/obsidian.js +263 -0
- package/dist/insights/obsidian.js.map +1 -0
- package/dist/insights/obsidian.test.d.ts +2 -0
- package/dist/insights/obsidian.test.d.ts.map +1 -0
- package/dist/insights/obsidian.test.js +241 -0
- package/dist/insights/obsidian.test.js.map +1 -0
- package/dist/insights/paradox.d.ts +36 -0
- package/dist/insights/paradox.d.ts.map +1 -0
- package/dist/insights/paradox.js +201 -0
- package/dist/insights/paradox.js.map +1 -0
- package/dist/insights/paradox.test.d.ts +2 -0
- package/dist/insights/paradox.test.d.ts.map +1 -0
- package/dist/insights/paradox.test.js +88 -0
- package/dist/insights/paradox.test.js.map +1 -0
- package/dist/insights/regret.d.ts +57 -0
- package/dist/insights/regret.d.ts.map +1 -0
- package/dist/insights/regret.js +137 -0
- package/dist/insights/regret.js.map +1 -0
- package/dist/insights/regret.test.d.ts +2 -0
- package/dist/insights/regret.test.d.ts.map +1 -0
- package/dist/insights/regret.test.js +153 -0
- package/dist/insights/regret.test.js.map +1 -0
- package/dist/insights/stack-trace.d.ts +40 -0
- package/dist/insights/stack-trace.d.ts.map +1 -0
- package/dist/insights/stack-trace.js +127 -0
- package/dist/insights/stack-trace.js.map +1 -0
- package/dist/insights/stack-trace.test.d.ts +2 -0
- package/dist/insights/stack-trace.test.d.ts.map +1 -0
- package/dist/insights/stack-trace.test.js +103 -0
- package/dist/insights/stack-trace.test.js.map +1 -0
- package/dist/insights/story.d.ts +34 -0
- package/dist/insights/story.d.ts.map +1 -0
- package/dist/insights/story.js +100 -0
- package/dist/insights/story.js.map +1 -0
- package/dist/insights/story.test.d.ts +2 -0
- package/dist/insights/story.test.d.ts.map +1 -0
- package/dist/insights/story.test.js +99 -0
- package/dist/insights/story.test.js.map +1 -0
- package/dist/insights/suggest.d.ts +29 -0
- package/dist/insights/suggest.d.ts.map +1 -0
- package/dist/insights/suggest.js +93 -0
- package/dist/insights/suggest.js.map +1 -0
- package/dist/insights/suggest.test.d.ts +2 -0
- package/dist/insights/suggest.test.d.ts.map +1 -0
- package/dist/insights/suggest.test.js +71 -0
- package/dist/insights/suggest.test.js.map +1 -0
- package/dist/insights/who-knows.d.ts +66 -0
- package/dist/insights/who-knows.d.ts.map +1 -0
- package/dist/insights/who-knows.js +125 -0
- package/dist/insights/who-knows.js.map +1 -0
- package/dist/insights/who-knows.test.d.ts +2 -0
- package/dist/insights/who-knows.test.d.ts.map +1 -0
- package/dist/insights/who-knows.test.js +109 -0
- package/dist/insights/who-knows.test.js.map +1 -0
- package/dist/quant/alpha.d.ts +87 -0
- package/dist/quant/alpha.d.ts.map +1 -0
- package/dist/quant/alpha.js +103 -0
- package/dist/quant/alpha.js.map +1 -0
- package/dist/quant/alpha.test.d.ts +2 -0
- package/dist/quant/alpha.test.d.ts.map +1 -0
- package/dist/quant/alpha.test.js +147 -0
- package/dist/quant/alpha.test.js.map +1 -0
- package/dist/quant/backtest.d.ts +57 -0
- package/dist/quant/backtest.d.ts.map +1 -0
- package/dist/quant/backtest.js +90 -0
- package/dist/quant/backtest.js.map +1 -0
- package/dist/quant/backtest.test.d.ts +2 -0
- package/dist/quant/backtest.test.d.ts.map +1 -0
- package/dist/quant/backtest.test.js +133 -0
- package/dist/quant/backtest.test.js.map +1 -0
- package/dist/quant/black-swan.d.ts +45 -0
- package/dist/quant/black-swan.d.ts.map +1 -0
- package/dist/quant/black-swan.js +112 -0
- package/dist/quant/black-swan.js.map +1 -0
- package/dist/quant/black-swan.test.d.ts +2 -0
- package/dist/quant/black-swan.test.d.ts.map +1 -0
- package/dist/quant/black-swan.test.js +131 -0
- package/dist/quant/black-swan.test.js.map +1 -0
- package/dist/quant/correlation-matrix.d.ts +54 -0
- package/dist/quant/correlation-matrix.d.ts.map +1 -0
- package/dist/quant/correlation-matrix.js +103 -0
- package/dist/quant/correlation-matrix.js.map +1 -0
- package/dist/quant/correlation-matrix.test.d.ts +2 -0
- package/dist/quant/correlation-matrix.test.d.ts.map +1 -0
- package/dist/quant/correlation-matrix.test.js +118 -0
- package/dist/quant/correlation-matrix.test.js.map +1 -0
- package/dist/quant/drawdown.d.ts +51 -0
- package/dist/quant/drawdown.d.ts.map +1 -0
- package/dist/quant/drawdown.js +96 -0
- package/dist/quant/drawdown.js.map +1 -0
- package/dist/quant/drawdown.test.d.ts +2 -0
- package/dist/quant/drawdown.test.d.ts.map +1 -0
- package/dist/quant/drawdown.test.js +166 -0
- package/dist/quant/drawdown.test.js.map +1 -0
- package/dist/quant/greek.d.ts +55 -0
- package/dist/quant/greek.d.ts.map +1 -0
- package/dist/quant/greek.js +157 -0
- package/dist/quant/greek.js.map +1 -0
- package/dist/quant/greek.test.d.ts +2 -0
- package/dist/quant/greek.test.d.ts.map +1 -0
- package/dist/quant/greek.test.js +138 -0
- package/dist/quant/greek.test.js.map +1 -0
- package/dist/quant/implied-volatility.d.ts +65 -0
- package/dist/quant/implied-volatility.d.ts.map +1 -0
- package/dist/quant/implied-volatility.js +149 -0
- package/dist/quant/implied-volatility.js.map +1 -0
- package/dist/quant/implied-volatility.test.d.ts +2 -0
- package/dist/quant/implied-volatility.test.d.ts.map +1 -0
- package/dist/quant/implied-volatility.test.js +127 -0
- package/dist/quant/implied-volatility.test.js.map +1 -0
- package/dist/quant/index.d.ts +28 -0
- package/dist/quant/index.d.ts.map +1 -0
- package/dist/quant/index.js +28 -0
- package/dist/quant/index.js.map +1 -0
- package/dist/quant/insider-trading.d.ts +56 -0
- package/dist/quant/insider-trading.d.ts.map +1 -0
- package/dist/quant/insider-trading.js +129 -0
- package/dist/quant/insider-trading.js.map +1 -0
- package/dist/quant/insider-trading.test.d.ts +2 -0
- package/dist/quant/insider-trading.test.d.ts.map +1 -0
- package/dist/quant/insider-trading.test.js +130 -0
- package/dist/quant/insider-trading.test.js.map +1 -0
- package/dist/quant/moneyball.d.ts +48 -0
- package/dist/quant/moneyball.d.ts.map +1 -0
- package/dist/quant/moneyball.js +110 -0
- package/dist/quant/moneyball.js.map +1 -0
- package/dist/quant/moneyball.test.d.ts +2 -0
- package/dist/quant/moneyball.test.d.ts.map +1 -0
- package/dist/quant/moneyball.test.js +137 -0
- package/dist/quant/moneyball.test.js.map +1 -0
- package/dist/quant/tax-loss-harvest.d.ts +59 -0
- package/dist/quant/tax-loss-harvest.d.ts.map +1 -0
- package/dist/quant/tax-loss-harvest.js +126 -0
- package/dist/quant/tax-loss-harvest.js.map +1 -0
- package/dist/quant/tax-loss-harvest.test.d.ts +2 -0
- package/dist/quant/tax-loss-harvest.test.d.ts.map +1 -0
- package/dist/quant/tax-loss-harvest.test.js +126 -0
- package/dist/quant/tax-loss-harvest.test.js.map +1 -0
- package/dist/retrieve/index.d.ts +2 -0
- package/dist/retrieve/index.d.ts.map +1 -1
- package/dist/retrieve/index.js +2 -0
- package/dist/retrieve/index.js.map +1 -1
- package/dist/retrieve/intent.d.ts +32 -0
- package/dist/retrieve/intent.d.ts.map +1 -0
- package/dist/retrieve/intent.js +104 -0
- package/dist/retrieve/intent.js.map +1 -0
- package/dist/retrieve/intent.test.d.ts +2 -0
- package/dist/retrieve/intent.test.d.ts.map +1 -0
- package/dist/retrieve/intent.test.js +106 -0
- package/dist/retrieve/intent.test.js.map +1 -0
- package/dist/retrieve/search.d.ts +30 -0
- package/dist/retrieve/search.d.ts.map +1 -1
- package/dist/retrieve/search.js +48 -0
- package/dist/retrieve/search.js.map +1 -1
- package/dist/retrieve/search.test.js +84 -1
- package/dist/retrieve/search.test.js.map +1 -1
- package/dist/retrieve/synthesize.d.ts +57 -0
- package/dist/retrieve/synthesize.d.ts.map +1 -0
- package/dist/retrieve/synthesize.js +191 -0
- package/dist/retrieve/synthesize.js.map +1 -0
- package/dist/retrieve/synthesize.test.d.ts +2 -0
- package/dist/retrieve/synthesize.test.d.ts.map +1 -0
- package/dist/retrieve/synthesize.test.js +127 -0
- package/dist/retrieve/synthesize.test.js.map +1 -0
- package/dist/store/schema.d.ts +2 -2
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +60 -2
- package/dist/store/schema.js.map +1 -1
- package/dist/store/sqlite.d.ts +2 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +24 -0
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sqlite.test.js +1 -1
- package/dist/util/index.d.ts +2 -0
- package/dist/util/index.d.ts.map +1 -1
- package/dist/util/index.js +2 -1
- package/dist/util/index.js.map +1 -1
- package/dist/util/redact.d.ts +58 -0
- package/dist/util/redact.d.ts.map +1 -0
- package/dist/util/redact.js +129 -0
- package/dist/util/redact.js.map +1 -0
- package/dist/util/redact.test.d.ts +2 -0
- package/dist/util/redact.test.d.ts.map +1 -0
- package/dist/util/redact.test.js +148 -0
- package/dist/util/redact.test.js.map +1 -0
- package/dist/wisdom/calibrator.d.ts +43 -0
- package/dist/wisdom/calibrator.d.ts.map +1 -0
- package/dist/wisdom/calibrator.js +120 -0
- package/dist/wisdom/calibrator.js.map +1 -0
- package/dist/wisdom/feedback.d.ts +45 -0
- package/dist/wisdom/feedback.d.ts.map +1 -0
- package/dist/wisdom/feedback.js +116 -0
- package/dist/wisdom/feedback.js.map +1 -0
- package/dist/wisdom/index.d.ts +15 -0
- package/dist/wisdom/index.d.ts.map +1 -0
- package/dist/wisdom/index.js +15 -0
- package/dist/wisdom/index.js.map +1 -0
- package/dist/wisdom/types.d.ts +67 -0
- package/dist/wisdom/types.d.ts.map +1 -0
- package/dist/wisdom/types.js +20 -0
- package/dist/wisdom/types.js.map +1 -0
- package/dist/wisdom/wisdom.test.d.ts +2 -0
- package/dist/wisdom/wisdom.test.d.ts.map +1 -0
- package/dist/wisdom/wisdom.test.js +144 -0
- package/dist/wisdom/wisdom.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backtest.js","sourceRoot":"","sources":["../../src/quant/backtest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backtest.test.d.ts","sourceRoot":"","sources":["../../src/quant/backtest.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { backtest, classifyVerdict, badge } from "./backtest.js";
|
|
3
|
+
describe("backtest — confusion matrix counts", () => {
|
|
4
|
+
it("perfect predictor — all TP, no FP/FN", () => {
|
|
5
|
+
const r = backtest([
|
|
6
|
+
{ id: "a", predicted: true, actual: true },
|
|
7
|
+
{ id: "b", predicted: true, actual: true },
|
|
8
|
+
{ id: "c", predicted: false, actual: false },
|
|
9
|
+
{ id: "d", predicted: false, actual: false },
|
|
10
|
+
]);
|
|
11
|
+
expect(r.truePositives).toBe(2);
|
|
12
|
+
expect(r.falsePositives).toBe(0);
|
|
13
|
+
expect(r.trueNegatives).toBe(2);
|
|
14
|
+
expect(r.falseNegatives).toBe(0);
|
|
15
|
+
expect(r.precision).toBe(1);
|
|
16
|
+
expect(r.recall).toBe(1);
|
|
17
|
+
expect(r.f1).toBe(1);
|
|
18
|
+
});
|
|
19
|
+
it("worst predictor — all FP and FN", () => {
|
|
20
|
+
const r = backtest([
|
|
21
|
+
{ id: "a", predicted: true, actual: false },
|
|
22
|
+
{ id: "b", predicted: true, actual: false },
|
|
23
|
+
{ id: "c", predicted: false, actual: true },
|
|
24
|
+
{ id: "d", predicted: false, actual: true },
|
|
25
|
+
]);
|
|
26
|
+
expect(r.truePositives).toBe(0);
|
|
27
|
+
expect(r.precision).toBe(0);
|
|
28
|
+
expect(r.recall).toBe(0);
|
|
29
|
+
expect(r.f1).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
it("balanced realistic case", () => {
|
|
32
|
+
// 14 samples, 6 predicted high-risk, 4 of those actually had incidents
|
|
33
|
+
// 8 predicted clean, 2 actually had incidents
|
|
34
|
+
// Total positives: 6 (4 + 2)
|
|
35
|
+
const samples = [
|
|
36
|
+
{ id: "1", predicted: true, actual: true },
|
|
37
|
+
{ id: "2", predicted: true, actual: true },
|
|
38
|
+
{ id: "3", predicted: true, actual: true },
|
|
39
|
+
{ id: "4", predicted: true, actual: true },
|
|
40
|
+
{ id: "5", predicted: true, actual: false },
|
|
41
|
+
{ id: "6", predicted: true, actual: false },
|
|
42
|
+
{ id: "7", predicted: false, actual: true },
|
|
43
|
+
{ id: "8", predicted: false, actual: true },
|
|
44
|
+
{ id: "9", predicted: false, actual: false },
|
|
45
|
+
{ id: "10", predicted: false, actual: false },
|
|
46
|
+
{ id: "11", predicted: false, actual: false },
|
|
47
|
+
{ id: "12", predicted: false, actual: false },
|
|
48
|
+
{ id: "13", predicted: false, actual: false },
|
|
49
|
+
{ id: "14", predicted: false, actual: false },
|
|
50
|
+
];
|
|
51
|
+
const r = backtest(samples);
|
|
52
|
+
expect(r.precision).toBeCloseTo(4 / 6, 4);
|
|
53
|
+
expect(r.recall).toBeCloseTo(4 / 6, 4);
|
|
54
|
+
expect(r.f1).toBeCloseTo(4 / 6, 4);
|
|
55
|
+
expect(r.baseRate).toBeCloseTo(6 / 14, 4);
|
|
56
|
+
expect(r.lift).toBeCloseTo((4 / 6) / (6 / 14), 4);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("backtest — edge cases", () => {
|
|
60
|
+
it("empty input returns zeros + no-edge verdict", () => {
|
|
61
|
+
const r = backtest([]);
|
|
62
|
+
expect(r.n).toBe(0);
|
|
63
|
+
expect(r.precision).toBe(0);
|
|
64
|
+
expect(r.recall).toBe(0);
|
|
65
|
+
expect(r.f1).toBe(0);
|
|
66
|
+
expect(r.verdict).toBe("no-edge");
|
|
67
|
+
});
|
|
68
|
+
it("no positive predictions → precision = 0 (avoids div by zero)", () => {
|
|
69
|
+
const r = backtest([
|
|
70
|
+
{ id: "1", predicted: false, actual: false },
|
|
71
|
+
{ id: "2", predicted: false, actual: true },
|
|
72
|
+
]);
|
|
73
|
+
expect(r.precision).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
it("no actual positives → recall = 0", () => {
|
|
76
|
+
const r = backtest([
|
|
77
|
+
{ id: "1", predicted: true, actual: false },
|
|
78
|
+
{ id: "2", predicted: false, actual: false },
|
|
79
|
+
]);
|
|
80
|
+
expect(r.recall).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("classifyVerdict — tier from lift × precision × recall", () => {
|
|
84
|
+
it("strong-edge for high precision + recall + 2.5× lift", () => {
|
|
85
|
+
expect(classifyVerdict(3, 0.7, 0.6, 20)).toBe("strong-edge");
|
|
86
|
+
});
|
|
87
|
+
it("real-edge for moderate metrics", () => {
|
|
88
|
+
expect(classifyVerdict(1.8, 0.5, 0.4, 20)).toBe("real-edge");
|
|
89
|
+
});
|
|
90
|
+
it("weak for lift ≥ 1.1 only", () => {
|
|
91
|
+
expect(classifyVerdict(1.2, 0.3, 0.3, 20)).toBe("weak");
|
|
92
|
+
});
|
|
93
|
+
it("no-edge for too-small samples", () => {
|
|
94
|
+
expect(classifyVerdict(5, 0.9, 0.9, 4)).toBe("no-edge");
|
|
95
|
+
});
|
|
96
|
+
it("no-edge when no lift over random", () => {
|
|
97
|
+
expect(classifyVerdict(0.9, 0.3, 0.3, 20)).toBe("no-edge");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("badge — one-line summary string", () => {
|
|
101
|
+
it("includes F1, lift, and n", () => {
|
|
102
|
+
const r = backtest([
|
|
103
|
+
{ id: "1", predicted: true, actual: true },
|
|
104
|
+
{ id: "2", predicted: true, actual: false },
|
|
105
|
+
{ id: "3", predicted: false, actual: false },
|
|
106
|
+
{ id: "4", predicted: false, actual: false },
|
|
107
|
+
{ id: "5", predicted: false, actual: false },
|
|
108
|
+
]);
|
|
109
|
+
const b = badge(r);
|
|
110
|
+
expect(b).toMatch(/F1 = \d/);
|
|
111
|
+
expect(b).toMatch(/× lift/);
|
|
112
|
+
expect(b).toMatch(/n=5/);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe("backtest — conclusion text adapts to verdict", () => {
|
|
116
|
+
it("no-edge mentions sample size for small n", () => {
|
|
117
|
+
const r = backtest([{ id: "1", predicted: true, actual: true }]);
|
|
118
|
+
expect(r.conclusion.toLowerCase()).toContain("sample");
|
|
119
|
+
});
|
|
120
|
+
it("strong-edge says 'trust' or 'real predictive'", () => {
|
|
121
|
+
// build samples with strong edge: lift ≥ 2.5, precision ≥ 0.6, recall ≥ 0.5
|
|
122
|
+
const samples = [
|
|
123
|
+
...Array.from({ length: 8 }, (_, i) => ({ id: `tp${i}`, predicted: true, actual: true })),
|
|
124
|
+
...Array.from({ length: 2 }, (_, i) => ({ id: `fp${i}`, predicted: true, actual: false })),
|
|
125
|
+
...Array.from({ length: 4 }, (_, i) => ({ id: `fn${i}`, predicted: false, actual: true })),
|
|
126
|
+
...Array.from({ length: 36 }, (_, i) => ({ id: `tn${i}`, predicted: false, actual: false })),
|
|
127
|
+
];
|
|
128
|
+
const r = backtest(samples);
|
|
129
|
+
expect(r.verdict).toBe("strong-edge");
|
|
130
|
+
expect(r.conclusion.toLowerCase()).toMatch(/trust|strong/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
//# sourceMappingURL=backtest.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backtest.test.js","sourceRoot":"","sources":["../../src/quant/backtest.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mneme black-swan` — find rare-but-catastrophic file patterns.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Taleb: real risk lives in the tail. Files touched rarely
|
|
5
|
+
* but tied to high-severity incidents are far more dangerous than
|
|
6
|
+
* frequently-touched files with proportional bug counts.
|
|
7
|
+
*
|
|
8
|
+
* tail_risk = log(avg_severity + 1) × (1 / max(touch_frequency, 1))
|
|
9
|
+
*
|
|
10
|
+
* Files surface ranked by tail_risk — the silent assassins that look
|
|
11
|
+
* stable but explode when touched.
|
|
12
|
+
*
|
|
13
|
+
* Pure store-backed analysis. No LLM.
|
|
14
|
+
*/
|
|
15
|
+
import type { MnemeStore } from "../store/sqlite.js";
|
|
16
|
+
export interface BlackSwanCandidate {
|
|
17
|
+
filePath: string;
|
|
18
|
+
/** Total commits that ever touched this file. */
|
|
19
|
+
touchCount: number;
|
|
20
|
+
/** Days since the last touch. */
|
|
21
|
+
daysSinceTouch: number;
|
|
22
|
+
/** Number of incidents associated with this file. */
|
|
23
|
+
incidentCount: number;
|
|
24
|
+
/** Mean severity (1=low, 5=critical). */
|
|
25
|
+
avgSeverity: number;
|
|
26
|
+
/** Tail-risk score — see formula above. */
|
|
27
|
+
tailRisk: number;
|
|
28
|
+
/** Tier label for output. */
|
|
29
|
+
tier: "deceptive-calm" | "elevated" | "watch" | "background";
|
|
30
|
+
/** A 1-line operational recommendation. */
|
|
31
|
+
recommendation: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Walk every indexed file, compute touch frequency + linked-incident
|
|
35
|
+
* severity, and rank by tail risk. Conservative defaults: files with
|
|
36
|
+
* < incidents are skipped (no tail without trouble).
|
|
37
|
+
*/
|
|
38
|
+
export declare function findBlackSwans(store: MnemeStore, opts?: {
|
|
39
|
+
topN?: number;
|
|
40
|
+
minIncidents?: number;
|
|
41
|
+
maxTouches?: number;
|
|
42
|
+
now?: Date;
|
|
43
|
+
}): BlackSwanCandidate[];
|
|
44
|
+
export declare function classifyBlackSwanTier(tailRisk: number, touches: number, avgSeverity: number): BlackSwanCandidate["tier"];
|
|
45
|
+
//# sourceMappingURL=black-swan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"black-swan.d.ts","sourceRoot":"","sources":["../../src/quant/black-swan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,IAAI,EAAE,gBAAgB,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,CAAC;IAC7D,2CAA2C;IAC3C,cAAc,EAAE,MAAM,CAAC;CACxB;AAUD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EACjB,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAA;CAAO,GACnF,kBAAkB,EAAE,CAiEtB;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,kBAAkB,CAAC,MAAM,CAAC,CAK5B"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mneme black-swan` — find rare-but-catastrophic file patterns.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Taleb: real risk lives in the tail. Files touched rarely
|
|
5
|
+
* but tied to high-severity incidents are far more dangerous than
|
|
6
|
+
* frequently-touched files with proportional bug counts.
|
|
7
|
+
*
|
|
8
|
+
* tail_risk = log(avg_severity + 1) × (1 / max(touch_frequency, 1))
|
|
9
|
+
*
|
|
10
|
+
* Files surface ranked by tail_risk — the silent assassins that look
|
|
11
|
+
* stable but explode when touched.
|
|
12
|
+
*
|
|
13
|
+
* Pure store-backed analysis. No LLM.
|
|
14
|
+
*/
|
|
15
|
+
const SEVERITY_RANK = {
|
|
16
|
+
critical: 5,
|
|
17
|
+
high: 4,
|
|
18
|
+
medium: 3,
|
|
19
|
+
low: 2,
|
|
20
|
+
info: 1,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Walk every indexed file, compute touch frequency + linked-incident
|
|
24
|
+
* severity, and rank by tail risk. Conservative defaults: files with
|
|
25
|
+
* < incidents are skipped (no tail without trouble).
|
|
26
|
+
*/
|
|
27
|
+
export function findBlackSwans(store, opts = {}) {
|
|
28
|
+
const topN = opts.topN ?? 10;
|
|
29
|
+
const minIncidents = opts.minIncidents ?? 1;
|
|
30
|
+
const maxTouches = opts.maxTouches ?? 30;
|
|
31
|
+
const now = opts.now ?? new Date();
|
|
32
|
+
// Pull file → (touch count, last touch).
|
|
33
|
+
const fileRows = store.db
|
|
34
|
+
.prepare(`SELECT
|
|
35
|
+
fc.path AS path,
|
|
36
|
+
COUNT(*) AS touches,
|
|
37
|
+
MAX(c.author_date) AS last_touch
|
|
38
|
+
FROM file_changes fc
|
|
39
|
+
JOIN commits c ON c.hash = fc.commit_hash
|
|
40
|
+
GROUP BY fc.path
|
|
41
|
+
HAVING touches <= ?`)
|
|
42
|
+
.all(maxTouches);
|
|
43
|
+
// Pull file → incidents (parsed from incidents.affected_files).
|
|
44
|
+
const incidentRows = store.db
|
|
45
|
+
.prepare(`SELECT severity, affected_files FROM incidents`)
|
|
46
|
+
.all();
|
|
47
|
+
// Build file → { incidentCount, severities[] }
|
|
48
|
+
const incidentsByFile = new Map();
|
|
49
|
+
for (const r of incidentRows) {
|
|
50
|
+
if (!r.affected_files)
|
|
51
|
+
continue;
|
|
52
|
+
let files = [];
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(r.affected_files);
|
|
55
|
+
if (Array.isArray(parsed))
|
|
56
|
+
files = parsed.filter((x) => typeof x === "string");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Fallback: comma-separated paths
|
|
60
|
+
files = r.affected_files.split(",").map((s) => s.trim()).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
const sev = SEVERITY_RANK[r.severity?.toLowerCase()] ?? 3;
|
|
63
|
+
for (const f of files) {
|
|
64
|
+
if (!incidentsByFile.has(f))
|
|
65
|
+
incidentsByFile.set(f, []);
|
|
66
|
+
incidentsByFile.get(f).push(sev);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const candidates = [];
|
|
70
|
+
for (const r of fileRows) {
|
|
71
|
+
const sevs = incidentsByFile.get(r.path) ?? [];
|
|
72
|
+
if (sevs.length < minIncidents)
|
|
73
|
+
continue;
|
|
74
|
+
const avgSeverity = sevs.reduce((s, x) => s + x, 0) / sevs.length;
|
|
75
|
+
const daysSinceTouch = (now.getTime() - new Date(r.last_touch).getTime()) / 86_400_000;
|
|
76
|
+
const tailRisk = Math.log(avgSeverity + 1) * (1 / Math.max(r.touches, 1));
|
|
77
|
+
candidates.push({
|
|
78
|
+
filePath: r.path,
|
|
79
|
+
touchCount: r.touches,
|
|
80
|
+
daysSinceTouch: Math.round(daysSinceTouch),
|
|
81
|
+
incidentCount: sevs.length,
|
|
82
|
+
avgSeverity: Math.round(avgSeverity * 10) / 10,
|
|
83
|
+
tailRisk,
|
|
84
|
+
tier: classifyBlackSwanTier(tailRisk, r.touches, avgSeverity),
|
|
85
|
+
recommendation: buildBlackSwanRecommendation(r.touches, avgSeverity, daysSinceTouch),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
candidates.sort((a, b) => b.tailRisk - a.tailRisk);
|
|
89
|
+
return candidates.slice(0, topN);
|
|
90
|
+
}
|
|
91
|
+
export function classifyBlackSwanTier(tailRisk, touches, avgSeverity) {
|
|
92
|
+
if (tailRisk >= 0.8 && touches <= 3 && avgSeverity >= 4)
|
|
93
|
+
return "deceptive-calm";
|
|
94
|
+
if (tailRisk >= 0.5 && avgSeverity >= 3)
|
|
95
|
+
return "elevated";
|
|
96
|
+
if (tailRisk >= 0.2)
|
|
97
|
+
return "watch";
|
|
98
|
+
return "background";
|
|
99
|
+
}
|
|
100
|
+
function buildBlackSwanRecommendation(touches, avgSeverity, daysSinceTouch) {
|
|
101
|
+
if (touches <= 2 && avgSeverity >= 4) {
|
|
102
|
+
return "Mandatory pair-program + canary deploy. This file LOOKS stable but its track record is catastrophic.";
|
|
103
|
+
}
|
|
104
|
+
if (avgSeverity >= 4) {
|
|
105
|
+
return "Code-freeze without 2 reviewers + load test required. High tail risk on edits.";
|
|
106
|
+
}
|
|
107
|
+
if (daysSinceTouch > 365) {
|
|
108
|
+
return "Untouched for 1+ year. Schedule a review session before the next change.";
|
|
109
|
+
}
|
|
110
|
+
return "Monitor closely. Run mneme blast on any commit that touches this file.";
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=black-swan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"black-swan.js","sourceRoot":"","sources":["../../src/quant/black-swan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"black-swan.test.d.ts","sourceRoot":"","sources":["../../src/quant/black-swan.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { MnemeStore } from "../store/sqlite.js";
|
|
6
|
+
import { findBlackSwans, classifyBlackSwanTier } from "./black-swan.js";
|
|
7
|
+
let tmpDir;
|
|
8
|
+
let store;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mneme-bs-"));
|
|
11
|
+
store = new MnemeStore(join(tmpDir, "mneme.db"));
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
store.close();
|
|
15
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
const cmt = (hash, date, files) => ({
|
|
18
|
+
hash,
|
|
19
|
+
shortHash: hash.slice(0, 7),
|
|
20
|
+
authorName: "alice",
|
|
21
|
+
authorEmail: "a@x",
|
|
22
|
+
authorDate: `${date}T00:00:00Z`,
|
|
23
|
+
committerDate: `${date}T00:00:00Z`,
|
|
24
|
+
subject: "commit",
|
|
25
|
+
body: "",
|
|
26
|
+
parents: [],
|
|
27
|
+
files,
|
|
28
|
+
});
|
|
29
|
+
function seedCommits(commits) {
|
|
30
|
+
store.upsertCommits(commits);
|
|
31
|
+
for (const c of commits) {
|
|
32
|
+
store.upsertFileChanges(c.files.map((f) => ({
|
|
33
|
+
commitHash: c.hash,
|
|
34
|
+
path: f,
|
|
35
|
+
changeKind: "M",
|
|
36
|
+
insertions: 1,
|
|
37
|
+
deletions: 0,
|
|
38
|
+
})));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function seedIncident(severity, affectedFiles) {
|
|
42
|
+
store.db
|
|
43
|
+
.prepare(`INSERT INTO incidents (id, source, title, occurred_at, severity, affected_files)
|
|
44
|
+
VALUES (?, 'manual', ?, ?, ?, ?)`)
|
|
45
|
+
.run(`inc-${Math.random()}`, "test", "2024-12-01T00:00:00Z", severity, JSON.stringify(affectedFiles));
|
|
46
|
+
}
|
|
47
|
+
describe("classifyBlackSwanTier", () => {
|
|
48
|
+
it('"deceptive-calm" for high tail risk + few touches + critical severity', () => {
|
|
49
|
+
expect(classifyBlackSwanTier(1.5, 2, 4.5)).toBe("deceptive-calm");
|
|
50
|
+
});
|
|
51
|
+
it('"elevated" for moderate tail risk + medium severity', () => {
|
|
52
|
+
expect(classifyBlackSwanTier(0.6, 5, 3.0)).toBe("elevated");
|
|
53
|
+
});
|
|
54
|
+
it('"watch" for low tail risk', () => {
|
|
55
|
+
expect(classifyBlackSwanTier(0.3, 10, 2.0)).toBe("watch");
|
|
56
|
+
});
|
|
57
|
+
it('"background" for tiny tail risk', () => {
|
|
58
|
+
expect(classifyBlackSwanTier(0.05, 30, 1.5)).toBe("background");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe("findBlackSwans — basic detection", () => {
|
|
62
|
+
it("returns empty array when no incidents are linked to files", () => {
|
|
63
|
+
seedCommits([cmt("a1", "2024-01-01", ["src/x.ts"])]);
|
|
64
|
+
expect(findBlackSwans(store)).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
it("flags a low-touch + high-severity file as deceptive-calm", () => {
|
|
67
|
+
seedCommits([
|
|
68
|
+
cmt("a1", "2024-01-01", ["src/refund.ts"]),
|
|
69
|
+
cmt("a2", "2024-06-01", ["src/refund.ts"]),
|
|
70
|
+
]);
|
|
71
|
+
seedIncident("critical", ["src/refund.ts"]);
|
|
72
|
+
seedIncident("critical", ["src/refund.ts"]);
|
|
73
|
+
const candidates = findBlackSwans(store, { now: new Date("2024-12-01T00:00:00Z") });
|
|
74
|
+
const refund = candidates.find((c) => c.filePath === "src/refund.ts");
|
|
75
|
+
expect(refund).toBeDefined();
|
|
76
|
+
expect(refund.incidentCount).toBe(2);
|
|
77
|
+
expect(refund.avgSeverity).toBe(5);
|
|
78
|
+
expect(refund.tier).toBe("deceptive-calm");
|
|
79
|
+
});
|
|
80
|
+
it("respects maxTouches filter — high-traffic files are NOT black swans", () => {
|
|
81
|
+
const commits = Array.from({ length: 100 }, (_, i) => cmt(`a${i}`.padEnd(7, "x"), `2024-${String(((i % 12) + 1)).padStart(2, "0")}-01`, ["src/hot.ts"]));
|
|
82
|
+
seedCommits(commits);
|
|
83
|
+
seedIncident("critical", ["src/hot.ts"]);
|
|
84
|
+
expect(findBlackSwans(store, { maxTouches: 30 }).find((c) => c.filePath === "src/hot.ts")).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
it("respects minIncidents filter (default 1)", () => {
|
|
87
|
+
seedCommits([cmt("a1", "2024-01-01", ["src/never-broke.ts"])]);
|
|
88
|
+
// No incident seeded — should be excluded
|
|
89
|
+
expect(findBlackSwans(store).find((c) => c.filePath === "src/never-broke.ts")).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
it("sorts by tailRisk descending", () => {
|
|
92
|
+
seedCommits([
|
|
93
|
+
cmt("a1", "2024-01-01", ["src/calm.ts"]),
|
|
94
|
+
cmt("a2", "2024-02-01", ["src/calm.ts"]),
|
|
95
|
+
cmt("b1", "2024-01-01", ["src/medium.ts"]),
|
|
96
|
+
cmt("b2", "2024-02-01", ["src/medium.ts"]),
|
|
97
|
+
cmt("b3", "2024-03-01", ["src/medium.ts"]),
|
|
98
|
+
cmt("b4", "2024-04-01", ["src/medium.ts"]),
|
|
99
|
+
cmt("b5", "2024-05-01", ["src/medium.ts"]),
|
|
100
|
+
]);
|
|
101
|
+
seedIncident("critical", ["src/calm.ts"]); // severity 5, 2 touches → high tail
|
|
102
|
+
seedIncident("medium", ["src/medium.ts"]); // severity 3, 5 touches → low tail
|
|
103
|
+
const candidates = findBlackSwans(store);
|
|
104
|
+
expect(candidates[0].filePath).toBe("src/calm.ts");
|
|
105
|
+
});
|
|
106
|
+
it("respects topN cap", () => {
|
|
107
|
+
for (let i = 0; i < 30; i++) {
|
|
108
|
+
seedCommits([cmt(`x${i}`.padEnd(7, "x"), "2024-01-01", [`src/f${i}.ts`])]);
|
|
109
|
+
seedIncident("critical", [`src/f${i}.ts`]);
|
|
110
|
+
}
|
|
111
|
+
expect(findBlackSwans(store, { topN: 5 }).length).toBeLessThanOrEqual(5);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("findBlackSwans — recommendation text", () => {
|
|
115
|
+
it("recommends pair-program + canary for low-touch + critical files", () => {
|
|
116
|
+
seedCommits([cmt("a1", "2024-01-01", ["src/refund.ts"])]);
|
|
117
|
+
seedIncident("critical", ["src/refund.ts"]);
|
|
118
|
+
const c = findBlackSwans(store)[0];
|
|
119
|
+
expect(c.recommendation.toLowerCase()).toMatch(/pair-program|canary/);
|
|
120
|
+
});
|
|
121
|
+
it("recommends review session for files untouched > 1 year", () => {
|
|
122
|
+
seedCommits([
|
|
123
|
+
cmt("a1", "2022-01-01", ["src/legacy.ts"]),
|
|
124
|
+
cmt("a2", "2022-02-01", ["src/legacy.ts"]),
|
|
125
|
+
]);
|
|
126
|
+
seedIncident("medium", ["src/legacy.ts"]);
|
|
127
|
+
const c = findBlackSwans(store, { now: new Date("2024-12-01T00:00:00Z") }).find((x) => x.filePath === "src/legacy.ts");
|
|
128
|
+
expect(c.recommendation.toLowerCase()).toMatch(/review|untouched/);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
//# sourceMappingURL=black-swan.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"black-swan.test.js","sourceRoot":"","sources":["../../src/quant/black-swan.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mneme correlation-matrix` — find HIDDEN coupling between files.
|
|
3
|
+
*
|
|
4
|
+
* Static analysis catches imports. This catches *behavioral coupling*:
|
|
5
|
+
* "every time file X is touched, file Y has a bug fix within N days,
|
|
6
|
+
* even though X and Y don't import each other."
|
|
7
|
+
*
|
|
8
|
+
* Why this is novel: most tools see static dependency graphs (X imports
|
|
9
|
+
* Y). Behavioral graphs reveal architectural smells that imports don't:
|
|
10
|
+
* a config table that EVERY service silently depends on, an undocumented
|
|
11
|
+
* shared state, an order-of-operations contract.
|
|
12
|
+
*
|
|
13
|
+
* Output: ranked file-pair coupling with statistical significance score.
|
|
14
|
+
*
|
|
15
|
+
* Pure analysis. No LLM.
|
|
16
|
+
*/
|
|
17
|
+
import type { Commit } from "../types.js";
|
|
18
|
+
export interface CouplingPair {
|
|
19
|
+
fileA: string;
|
|
20
|
+
fileB: string;
|
|
21
|
+
/** Total commits that touched A. */
|
|
22
|
+
countA: number;
|
|
23
|
+
/** Total commits that touched B. */
|
|
24
|
+
countB: number;
|
|
25
|
+
/** Commits that touched A AND B together. */
|
|
26
|
+
coOccurrences: number;
|
|
27
|
+
/** Jaccard similarity = co_occurrences / (A + B - co_occurrences). */
|
|
28
|
+
jaccard: number;
|
|
29
|
+
/** Lift = P(B|A) / P(B) — how much MORE likely B is touched given A. */
|
|
30
|
+
lift: number;
|
|
31
|
+
/** Tier label. */
|
|
32
|
+
tier: "tight" | "strong" | "moderate" | "weak";
|
|
33
|
+
/** Plain-English interpretation. */
|
|
34
|
+
interpretation: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CorrelationOptions {
|
|
37
|
+
/** Minimum total touches per file to consider. */
|
|
38
|
+
minFileTouches?: number;
|
|
39
|
+
/** Minimum co-occurrences to surface a pair. */
|
|
40
|
+
minCoOccurrences?: number;
|
|
41
|
+
/** Top-N pairs to return. */
|
|
42
|
+
topN?: number;
|
|
43
|
+
/** Skip pairs where lift below this. */
|
|
44
|
+
minLift?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the file-pair coupling matrix from commit history.
|
|
48
|
+
*
|
|
49
|
+
* Algorithm: for each commit, mark every pair (file_i, file_j) as co-touched.
|
|
50
|
+
* Aggregate counts; compute Jaccard + lift per pair.
|
|
51
|
+
*/
|
|
52
|
+
export declare function correlationMatrix(commits: Commit[], opts?: CorrelationOptions): CouplingPair[];
|
|
53
|
+
export declare function classifyCouplingTier(jaccard: number, lift: number): CouplingPair["tier"];
|
|
54
|
+
//# sourceMappingURL=correlation-matrix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation-matrix.d.ts","sourceRoot":"","sources":["../../src/quant/correlation-matrix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC;IAC/C,oCAAoC;IACpC,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,kBAAuB,GAAG,YAAY,EAAE,CAwDlG;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAQxF"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mneme correlation-matrix` — find HIDDEN coupling between files.
|
|
3
|
+
*
|
|
4
|
+
* Static analysis catches imports. This catches *behavioral coupling*:
|
|
5
|
+
* "every time file X is touched, file Y has a bug fix within N days,
|
|
6
|
+
* even though X and Y don't import each other."
|
|
7
|
+
*
|
|
8
|
+
* Why this is novel: most tools see static dependency graphs (X imports
|
|
9
|
+
* Y). Behavioral graphs reveal architectural smells that imports don't:
|
|
10
|
+
* a config table that EVERY service silently depends on, an undocumented
|
|
11
|
+
* shared state, an order-of-operations contract.
|
|
12
|
+
*
|
|
13
|
+
* Output: ranked file-pair coupling with statistical significance score.
|
|
14
|
+
*
|
|
15
|
+
* Pure analysis. No LLM.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Build the file-pair coupling matrix from commit history.
|
|
19
|
+
*
|
|
20
|
+
* Algorithm: for each commit, mark every pair (file_i, file_j) as co-touched.
|
|
21
|
+
* Aggregate counts; compute Jaccard + lift per pair.
|
|
22
|
+
*/
|
|
23
|
+
export function correlationMatrix(commits, opts = {}) {
|
|
24
|
+
const minTouches = opts.minFileTouches ?? 3;
|
|
25
|
+
const minCo = opts.minCoOccurrences ?? 2;
|
|
26
|
+
const topN = opts.topN ?? 20;
|
|
27
|
+
const minLift = opts.minLift ?? 1.5;
|
|
28
|
+
// Step 1: per-file count.
|
|
29
|
+
const fileCount = new Map();
|
|
30
|
+
for (const c of commits) {
|
|
31
|
+
for (const f of c.files ?? [])
|
|
32
|
+
fileCount.set(f, (fileCount.get(f) ?? 0) + 1);
|
|
33
|
+
}
|
|
34
|
+
// Filter to files with enough activity.
|
|
35
|
+
const eligible = new Set();
|
|
36
|
+
for (const [f, n] of fileCount)
|
|
37
|
+
if (n >= minTouches)
|
|
38
|
+
eligible.add(f);
|
|
39
|
+
// Step 2: per-pair co-occurrence.
|
|
40
|
+
const pairKey = (a, b) => (a < b ? `${a}|${b}` : `${b}|${a}`);
|
|
41
|
+
const pairCount = new Map();
|
|
42
|
+
for (const c of commits) {
|
|
43
|
+
const files = (c.files ?? []).filter((f) => eligible.has(f));
|
|
44
|
+
for (let i = 0; i < files.length; i++) {
|
|
45
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
46
|
+
const k = pairKey(files[i], files[j]);
|
|
47
|
+
pairCount.set(k, (pairCount.get(k) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const totalCommits = commits.length;
|
|
52
|
+
const pairs = [];
|
|
53
|
+
for (const [k, n] of pairCount) {
|
|
54
|
+
if (n < minCo)
|
|
55
|
+
continue;
|
|
56
|
+
const [a, b] = k.split("|");
|
|
57
|
+
const A = fileCount.get(a);
|
|
58
|
+
const B = fileCount.get(b);
|
|
59
|
+
const jaccard = n / (A + B - n);
|
|
60
|
+
// P(B|A) = n / A; P(B) = B / totalCommits; lift = (n/A) / (B/totalCommits)
|
|
61
|
+
const lift = totalCommits === 0 || A === 0 || B === 0 ? 0 : (n / A) / (B / totalCommits);
|
|
62
|
+
if (lift < minLift)
|
|
63
|
+
continue;
|
|
64
|
+
pairs.push({
|
|
65
|
+
fileA: a,
|
|
66
|
+
fileB: b,
|
|
67
|
+
countA: A,
|
|
68
|
+
countB: B,
|
|
69
|
+
coOccurrences: n,
|
|
70
|
+
jaccard,
|
|
71
|
+
lift,
|
|
72
|
+
tier: classifyCouplingTier(jaccard, lift),
|
|
73
|
+
interpretation: buildCouplingInterpretation(jaccard, lift, n),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
pairs.sort((a, b) => b.lift - a.lift || b.jaccard - a.jaccard);
|
|
77
|
+
return pairs.slice(0, topN);
|
|
78
|
+
}
|
|
79
|
+
export function classifyCouplingTier(jaccard, lift) {
|
|
80
|
+
// 'tight' = perfect jaccard (≥ 0.9) OR (jaccard ≥ 0.6 AND lift ≥ 5).
|
|
81
|
+
// Real codebases rarely hit lift ≥ 5; jaccard 1.0 is a stronger signal
|
|
82
|
+
// than lift alone — fully co-touched files deserve the 'tight' label.
|
|
83
|
+
if (jaccard >= 0.9 || (jaccard >= 0.6 && lift >= 5))
|
|
84
|
+
return "tight";
|
|
85
|
+
if (jaccard >= 0.4 || lift >= 3)
|
|
86
|
+
return "strong";
|
|
87
|
+
if (jaccard >= 0.2 || lift >= 2)
|
|
88
|
+
return "moderate";
|
|
89
|
+
return "weak";
|
|
90
|
+
}
|
|
91
|
+
function buildCouplingInterpretation(jaccard, lift, co) {
|
|
92
|
+
if (jaccard >= 0.6) {
|
|
93
|
+
return `Tight behavioral coupling (Jaccard ${jaccard.toFixed(2)}). These files almost always change together. Likely candidates for a single module.`;
|
|
94
|
+
}
|
|
95
|
+
if (lift >= 5) {
|
|
96
|
+
return `${lift.toFixed(1)}× more likely than random to be touched together (${co} co-occurrences). Hidden dependency worth investigating.`;
|
|
97
|
+
}
|
|
98
|
+
if (jaccard >= 0.3) {
|
|
99
|
+
return `Moderate coupling (Jaccard ${jaccard.toFixed(2)}). Watch for accidental shared state.`;
|
|
100
|
+
}
|
|
101
|
+
return `Weak signal — ${co} shared touches with lift ${lift.toFixed(1)}×.`;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=correlation-matrix.js.map
|