@mneme-ai/core 0.9.0 → 0.11.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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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/ghost.d.ts +80 -0
- package/dist/insights/ghost.d.ts.map +1 -0
- package/dist/insights/ghost.js +138 -0
- package/dist/insights/ghost.js.map +1 -0
- package/dist/insights/ghost.test.d.ts +2 -0
- package/dist/insights/ghost.test.d.ts.map +1 -0
- package/dist/insights/ghost.test.js +141 -0
- package/dist/insights/ghost.test.js.map +1 -0
- package/dist/insights/index.d.ts +8 -0
- package/dist/insights/index.d.ts.map +1 -1
- package/dist/insights/index.js +8 -0
- package/dist/insights/index.js.map +1 -1
- 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/premortem.d.ts +73 -0
- package/dist/insights/premortem.d.ts.map +1 -0
- package/dist/insights/premortem.js +209 -0
- package/dist/insights/premortem.js.map +1 -0
- package/dist/insights/premortem.test.d.ts +2 -0
- package/dist/insights/premortem.test.d.ts.map +1 -0
- package/dist/insights/premortem.test.js +169 -0
- package/dist/insights/premortem.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/time-machine.d.ts +70 -0
- package/dist/insights/time-machine.d.ts.map +1 -0
- package/dist/insights/time-machine.js +177 -0
- package/dist/insights/time-machine.js.map +1 -0
- package/dist/insights/time-machine.test.d.ts +2 -0
- package/dist/insights/time-machine.test.d.ts.map +1 -0
- package/dist/insights/time-machine.test.js +141 -0
- package/dist/insights/time-machine.test.js.map +1 -0
- package/dist/insights/who-knows.d.ts +18 -0
- package/dist/insights/who-knows.d.ts.map +1 -1
- package/dist/insights/who-knows.js +29 -0
- package/dist/insights/who-knows.js.map +1 -1
- package/dist/insights/who-knows.test.js +63 -1
- package/dist/insights/who-knows.test.js.map +1 -1
- 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/synthesize.d.ts.map +1 -1
- package/dist/retrieve/synthesize.js +56 -25
- package/dist/retrieve/synthesize.js.map +1 -1
- package/dist/retrieve/synthesize.test.js +26 -15
- package/dist/retrieve/synthesize.test.js.map +1 -1
- package/dist/store/schema.d.ts +2 -2
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +6 -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 +4 -0
- package/dist/util/index.d.ts.map +1 -1
- package/dist/util/index.js +26 -0
- package/dist/util/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mneme backtest` — validate insight commands retroactively.
|
|
3
|
+
*
|
|
4
|
+
* The killer property: every prediction Mneme makes ("this is risky") can
|
|
5
|
+
* be replayed against actual history to compute precision, recall, F1, and
|
|
6
|
+
* lift over a random baseline. This turns "we have an AI tool" into
|
|
7
|
+
* "we have an AI tool with measured edge against the past".
|
|
8
|
+
*
|
|
9
|
+
* Backtest works for any binary predictor: given a set of (commit,
|
|
10
|
+
* prediction) pairs and a window in which to count "trouble" outcomes,
|
|
11
|
+
* compute the standard classification metrics.
|
|
12
|
+
*
|
|
13
|
+
* Pure data analysis — no LLM. The actual replay (re-running a command at
|
|
14
|
+
* a frozen point in history) lives in the CLI command, but the metric
|
|
15
|
+
* math is here and unit-testable.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Compute classification metrics + verdict from a list of (predicted,
|
|
19
|
+
* actual) samples. Pure math — no I/O, deterministic.
|
|
20
|
+
*/
|
|
21
|
+
export function backtest(samples) {
|
|
22
|
+
const n = samples.length;
|
|
23
|
+
let tp = 0;
|
|
24
|
+
let fp = 0;
|
|
25
|
+
let tn = 0;
|
|
26
|
+
let fn = 0;
|
|
27
|
+
for (const s of samples) {
|
|
28
|
+
if (s.predicted && s.actual)
|
|
29
|
+
tp += 1;
|
|
30
|
+
else if (s.predicted && !s.actual)
|
|
31
|
+
fp += 1;
|
|
32
|
+
else if (!s.predicted && !s.actual)
|
|
33
|
+
tn += 1;
|
|
34
|
+
else
|
|
35
|
+
fn += 1;
|
|
36
|
+
}
|
|
37
|
+
const precision = tp + fp === 0 ? 0 : tp / (tp + fp);
|
|
38
|
+
const recall = tp + fn === 0 ? 0 : tp / (tp + fn);
|
|
39
|
+
const f1 = precision + recall === 0 ? 0 : (2 * precision * recall) / (precision + recall);
|
|
40
|
+
const baseRate = n === 0 ? 0 : (tp + fn) / n;
|
|
41
|
+
const lift = baseRate === 0 ? 0 : precision / baseRate;
|
|
42
|
+
const verdict = classifyVerdict(lift, precision, recall, n);
|
|
43
|
+
return {
|
|
44
|
+
n,
|
|
45
|
+
truePositives: tp,
|
|
46
|
+
falsePositives: fp,
|
|
47
|
+
trueNegatives: tn,
|
|
48
|
+
falseNegatives: fn,
|
|
49
|
+
precision,
|
|
50
|
+
recall,
|
|
51
|
+
f1,
|
|
52
|
+
baseRate,
|
|
53
|
+
lift,
|
|
54
|
+
verdict,
|
|
55
|
+
conclusion: buildConclusion(verdict, n, precision, recall, lift),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function classifyVerdict(lift, precision, recall, n) {
|
|
59
|
+
if (n < 5)
|
|
60
|
+
return "no-edge"; // sample too small
|
|
61
|
+
if (lift >= 2.5 && precision >= 0.6 && recall >= 0.5)
|
|
62
|
+
return "strong-edge";
|
|
63
|
+
if (lift >= 1.5 && precision >= 0.4)
|
|
64
|
+
return "real-edge";
|
|
65
|
+
if (lift >= 1.1)
|
|
66
|
+
return "weak";
|
|
67
|
+
return "no-edge";
|
|
68
|
+
}
|
|
69
|
+
function buildConclusion(verdict, n, precision, recall, lift) {
|
|
70
|
+
switch (verdict) {
|
|
71
|
+
case "no-edge":
|
|
72
|
+
if (n < 5)
|
|
73
|
+
return `Sample size too small (n=${n}) to draw conclusions.`;
|
|
74
|
+
return `No detectable edge over random — precision ${(precision * 100).toFixed(0)}%, lift ${lift.toFixed(2)}×.`;
|
|
75
|
+
case "weak":
|
|
76
|
+
return `Weak edge — beats random by ${((lift - 1) * 100).toFixed(0)}% but precision is still ${(precision * 100).toFixed(0)}%. Use as a soft prior.`;
|
|
77
|
+
case "real-edge":
|
|
78
|
+
return `Real predictive edge — precision ${(precision * 100).toFixed(0)}%, ${lift.toFixed(1)}× over random.`;
|
|
79
|
+
case "strong-edge":
|
|
80
|
+
return `Strong edge — precision ${(precision * 100).toFixed(0)}%, recall ${(recall * 100).toFixed(0)}%, ${lift.toFixed(1)}× over random. Trust this predictor.`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Aggregate a backtest result into a one-line markdown badge for the
|
|
85
|
+
* README / docs. Format: "F1 = 0.67 · 2.4× lift · n=14".
|
|
86
|
+
*/
|
|
87
|
+
export function badge(result) {
|
|
88
|
+
return `F1 = ${result.f1.toFixed(2)} · ${result.lift.toFixed(1)}× lift · n=${result.n}`;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=backtest.js.map
|
|
@@ -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"}
|