@mneme-ai/core 0.9.0 → 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.
Files changed (153) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/insights/bus-factor.d.ts +58 -0
  6. package/dist/insights/bus-factor.d.ts.map +1 -0
  7. package/dist/insights/bus-factor.js +117 -0
  8. package/dist/insights/bus-factor.js.map +1 -0
  9. package/dist/insights/bus-factor.test.d.ts +2 -0
  10. package/dist/insights/bus-factor.test.d.ts.map +1 -0
  11. package/dist/insights/bus-factor.test.js +149 -0
  12. package/dist/insights/bus-factor.test.js.map +1 -0
  13. package/dist/insights/commit-coach.d.ts +80 -0
  14. package/dist/insights/commit-coach.d.ts.map +1 -0
  15. package/dist/insights/commit-coach.js +230 -0
  16. package/dist/insights/commit-coach.js.map +1 -0
  17. package/dist/insights/commit-coach.test.d.ts +2 -0
  18. package/dist/insights/commit-coach.test.d.ts.map +1 -0
  19. package/dist/insights/commit-coach.test.js +163 -0
  20. package/dist/insights/commit-coach.test.js.map +1 -0
  21. package/dist/insights/crystal-ball.d.ts +76 -0
  22. package/dist/insights/crystal-ball.d.ts.map +1 -0
  23. package/dist/insights/crystal-ball.js +219 -0
  24. package/dist/insights/crystal-ball.js.map +1 -0
  25. package/dist/insights/crystal-ball.test.d.ts +2 -0
  26. package/dist/insights/crystal-ball.test.d.ts.map +1 -0
  27. package/dist/insights/crystal-ball.test.js +157 -0
  28. package/dist/insights/crystal-ball.test.js.map +1 -0
  29. package/dist/insights/index.d.ts +5 -0
  30. package/dist/insights/index.d.ts.map +1 -1
  31. package/dist/insights/index.js +5 -0
  32. package/dist/insights/index.js.map +1 -1
  33. package/dist/insights/paradox.d.ts +36 -0
  34. package/dist/insights/paradox.d.ts.map +1 -0
  35. package/dist/insights/paradox.js +201 -0
  36. package/dist/insights/paradox.js.map +1 -0
  37. package/dist/insights/paradox.test.d.ts +2 -0
  38. package/dist/insights/paradox.test.d.ts.map +1 -0
  39. package/dist/insights/paradox.test.js +88 -0
  40. package/dist/insights/paradox.test.js.map +1 -0
  41. package/dist/insights/regret.d.ts +57 -0
  42. package/dist/insights/regret.d.ts.map +1 -0
  43. package/dist/insights/regret.js +137 -0
  44. package/dist/insights/regret.js.map +1 -0
  45. package/dist/insights/regret.test.d.ts +2 -0
  46. package/dist/insights/regret.test.d.ts.map +1 -0
  47. package/dist/insights/regret.test.js +153 -0
  48. package/dist/insights/regret.test.js.map +1 -0
  49. package/dist/insights/who-knows.d.ts +18 -0
  50. package/dist/insights/who-knows.d.ts.map +1 -1
  51. package/dist/insights/who-knows.js +29 -0
  52. package/dist/insights/who-knows.js.map +1 -1
  53. package/dist/insights/who-knows.test.js +63 -1
  54. package/dist/insights/who-knows.test.js.map +1 -1
  55. package/dist/quant/alpha.d.ts +87 -0
  56. package/dist/quant/alpha.d.ts.map +1 -0
  57. package/dist/quant/alpha.js +103 -0
  58. package/dist/quant/alpha.js.map +1 -0
  59. package/dist/quant/alpha.test.d.ts +2 -0
  60. package/dist/quant/alpha.test.d.ts.map +1 -0
  61. package/dist/quant/alpha.test.js +147 -0
  62. package/dist/quant/alpha.test.js.map +1 -0
  63. package/dist/quant/backtest.d.ts +57 -0
  64. package/dist/quant/backtest.d.ts.map +1 -0
  65. package/dist/quant/backtest.js +90 -0
  66. package/dist/quant/backtest.js.map +1 -0
  67. package/dist/quant/backtest.test.d.ts +2 -0
  68. package/dist/quant/backtest.test.d.ts.map +1 -0
  69. package/dist/quant/backtest.test.js +133 -0
  70. package/dist/quant/backtest.test.js.map +1 -0
  71. package/dist/quant/black-swan.d.ts +45 -0
  72. package/dist/quant/black-swan.d.ts.map +1 -0
  73. package/dist/quant/black-swan.js +112 -0
  74. package/dist/quant/black-swan.js.map +1 -0
  75. package/dist/quant/black-swan.test.d.ts +2 -0
  76. package/dist/quant/black-swan.test.d.ts.map +1 -0
  77. package/dist/quant/black-swan.test.js +131 -0
  78. package/dist/quant/black-swan.test.js.map +1 -0
  79. package/dist/quant/correlation-matrix.d.ts +54 -0
  80. package/dist/quant/correlation-matrix.d.ts.map +1 -0
  81. package/dist/quant/correlation-matrix.js +103 -0
  82. package/dist/quant/correlation-matrix.js.map +1 -0
  83. package/dist/quant/correlation-matrix.test.d.ts +2 -0
  84. package/dist/quant/correlation-matrix.test.d.ts.map +1 -0
  85. package/dist/quant/correlation-matrix.test.js +118 -0
  86. package/dist/quant/correlation-matrix.test.js.map +1 -0
  87. package/dist/quant/drawdown.d.ts +51 -0
  88. package/dist/quant/drawdown.d.ts.map +1 -0
  89. package/dist/quant/drawdown.js +96 -0
  90. package/dist/quant/drawdown.js.map +1 -0
  91. package/dist/quant/drawdown.test.d.ts +2 -0
  92. package/dist/quant/drawdown.test.d.ts.map +1 -0
  93. package/dist/quant/drawdown.test.js +166 -0
  94. package/dist/quant/drawdown.test.js.map +1 -0
  95. package/dist/quant/greek.d.ts +55 -0
  96. package/dist/quant/greek.d.ts.map +1 -0
  97. package/dist/quant/greek.js +157 -0
  98. package/dist/quant/greek.js.map +1 -0
  99. package/dist/quant/greek.test.d.ts +2 -0
  100. package/dist/quant/greek.test.d.ts.map +1 -0
  101. package/dist/quant/greek.test.js +138 -0
  102. package/dist/quant/greek.test.js.map +1 -0
  103. package/dist/quant/implied-volatility.d.ts +65 -0
  104. package/dist/quant/implied-volatility.d.ts.map +1 -0
  105. package/dist/quant/implied-volatility.js +149 -0
  106. package/dist/quant/implied-volatility.js.map +1 -0
  107. package/dist/quant/implied-volatility.test.d.ts +2 -0
  108. package/dist/quant/implied-volatility.test.d.ts.map +1 -0
  109. package/dist/quant/implied-volatility.test.js +127 -0
  110. package/dist/quant/implied-volatility.test.js.map +1 -0
  111. package/dist/quant/index.d.ts +28 -0
  112. package/dist/quant/index.d.ts.map +1 -0
  113. package/dist/quant/index.js +28 -0
  114. package/dist/quant/index.js.map +1 -0
  115. package/dist/quant/insider-trading.d.ts +56 -0
  116. package/dist/quant/insider-trading.d.ts.map +1 -0
  117. package/dist/quant/insider-trading.js +129 -0
  118. package/dist/quant/insider-trading.js.map +1 -0
  119. package/dist/quant/insider-trading.test.d.ts +2 -0
  120. package/dist/quant/insider-trading.test.d.ts.map +1 -0
  121. package/dist/quant/insider-trading.test.js +130 -0
  122. package/dist/quant/insider-trading.test.js.map +1 -0
  123. package/dist/quant/moneyball.d.ts +48 -0
  124. package/dist/quant/moneyball.d.ts.map +1 -0
  125. package/dist/quant/moneyball.js +110 -0
  126. package/dist/quant/moneyball.js.map +1 -0
  127. package/dist/quant/moneyball.test.d.ts +2 -0
  128. package/dist/quant/moneyball.test.d.ts.map +1 -0
  129. package/dist/quant/moneyball.test.js +137 -0
  130. package/dist/quant/moneyball.test.js.map +1 -0
  131. package/dist/quant/tax-loss-harvest.d.ts +59 -0
  132. package/dist/quant/tax-loss-harvest.d.ts.map +1 -0
  133. package/dist/quant/tax-loss-harvest.js +126 -0
  134. package/dist/quant/tax-loss-harvest.js.map +1 -0
  135. package/dist/quant/tax-loss-harvest.test.d.ts +2 -0
  136. package/dist/quant/tax-loss-harvest.test.d.ts.map +1 -0
  137. package/dist/quant/tax-loss-harvest.test.js +126 -0
  138. package/dist/quant/tax-loss-harvest.test.js.map +1 -0
  139. package/dist/retrieve/synthesize.d.ts.map +1 -1
  140. package/dist/retrieve/synthesize.js +56 -25
  141. package/dist/retrieve/synthesize.js.map +1 -1
  142. package/dist/retrieve/synthesize.test.js +26 -15
  143. package/dist/retrieve/synthesize.test.js.map +1 -1
  144. package/dist/store/schema.d.ts +2 -2
  145. package/dist/store/schema.d.ts.map +1 -1
  146. package/dist/store/schema.js +6 -2
  147. package/dist/store/schema.js.map +1 -1
  148. package/dist/store/sqlite.d.ts +2 -0
  149. package/dist/store/sqlite.d.ts.map +1 -1
  150. package/dist/store/sqlite.js +24 -0
  151. package/dist/store/sqlite.js.map +1 -1
  152. package/dist/store/sqlite.test.js +1 -1
  153. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=black-swan.test.d.ts.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlation-matrix.js","sourceRoot":"","sources":["../../src/quant/correlation-matrix.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=correlation-matrix.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlation-matrix.test.d.ts","sourceRoot":"","sources":["../../src/quant/correlation-matrix.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { correlationMatrix, classifyCouplingTier } from "./correlation-matrix.js";
3
+ const cmt = (hash, date, files) => ({
4
+ hash,
5
+ shortHash: hash.slice(0, 7),
6
+ authorName: "alice",
7
+ authorEmail: "a@x",
8
+ authorDate: `${date}T00:00:00Z`,
9
+ committerDate: `${date}T00:00:00Z`,
10
+ subject: "x",
11
+ body: "",
12
+ parents: [],
13
+ files,
14
+ });
15
+ describe("classifyCouplingTier", () => {
16
+ it("'tight' when both jaccard ≥ 0.6 + lift ≥ 5", () => {
17
+ expect(classifyCouplingTier(0.7, 6)).toBe("tight");
18
+ });
19
+ it("'strong' when one of jaccard ≥ 0.4 or lift ≥ 3", () => {
20
+ expect(classifyCouplingTier(0.5, 1)).toBe("strong");
21
+ expect(classifyCouplingTier(0.1, 4)).toBe("strong");
22
+ });
23
+ it("'moderate' for middling values", () => {
24
+ expect(classifyCouplingTier(0.25, 2)).toBe("moderate");
25
+ });
26
+ it("'weak' otherwise", () => {
27
+ expect(classifyCouplingTier(0.05, 1.2)).toBe("weak");
28
+ });
29
+ });
30
+ describe("correlationMatrix — basic detection", () => {
31
+ it("returns empty when there are no co-occurring file pairs", () => {
32
+ const commits = [
33
+ cmt("a1", "2024-01-01", ["src/a.ts"]),
34
+ cmt("a2", "2024-01-02", ["src/b.ts"]),
35
+ cmt("a3", "2024-01-03", ["src/c.ts"]),
36
+ ];
37
+ expect(correlationMatrix(commits)).toEqual([]);
38
+ });
39
+ it("detects a tightly-coupled pair when both files always change together", () => {
40
+ // 10 co-touched commits + 20 background commits on unrelated files.
41
+ // Background commits lower the baseline rate of x and y so that
42
+ // co-occurrence has lift > 1.5 (otherwise lift = 1 and is filtered out).
43
+ const co = Array.from({ length: 10 }, (_, i) => cmt(`c${i}`.padEnd(7, "x"), `2024-01-${String(i + 1).padStart(2, "0")}`, ["src/x.ts", "src/y.ts"]));
44
+ const bg = Array.from({ length: 20 }, (_, i) => cmt(`bg${i}`.padEnd(7, "x"), `2024-02-${String((i % 28) + 1).padStart(2, "0")}`, [`src/other${i}.ts`]));
45
+ const pairs = correlationMatrix([...co, ...bg], { minFileTouches: 3, minCoOccurrences: 2 });
46
+ const xy = pairs.find((p) => (p.fileA === "src/x.ts" && p.fileB === "src/y.ts") ||
47
+ (p.fileA === "src/y.ts" && p.fileB === "src/x.ts"));
48
+ expect(xy).toBeDefined();
49
+ expect(xy.jaccard).toBeCloseTo(1, 3);
50
+ expect(xy.tier).toBe("tight");
51
+ });
52
+ it("respects minFileTouches threshold", () => {
53
+ const commits = [
54
+ cmt("a1", "2024-01-01", ["src/x.ts", "src/y.ts"]),
55
+ cmt("a2", "2024-01-02", ["src/x.ts", "src/y.ts"]),
56
+ ];
57
+ expect(correlationMatrix(commits, { minFileTouches: 5 })).toEqual([]);
58
+ expect(correlationMatrix(commits, { minFileTouches: 1, minCoOccurrences: 1, minLift: 0 })).toHaveLength(1);
59
+ });
60
+ it("respects minCoOccurrences threshold", () => {
61
+ const commits = [
62
+ cmt("a1", "2024-01-01", ["src/x.ts", "src/y.ts"]),
63
+ cmt("a2", "2024-01-02", ["src/x.ts"]),
64
+ cmt("a3", "2024-01-03", ["src/y.ts"]),
65
+ cmt("a4", "2024-01-04", ["src/x.ts"]),
66
+ ];
67
+ expect(correlationMatrix(commits, { minFileTouches: 1, minCoOccurrences: 3 })).toEqual([]);
68
+ });
69
+ it("respects minLift threshold (filters weak pairs)", () => {
70
+ // Build a noisy history where x and y co-occur but at random level
71
+ const commits = [];
72
+ for (let i = 0; i < 20; i++)
73
+ commits.push(cmt(`x${i}`.padEnd(7, "y"), `2024-01-${String(i + 1).padStart(2, "0")}`, ["src/x.ts"]));
74
+ for (let i = 0; i < 20; i++)
75
+ commits.push(cmt(`y${i}`.padEnd(7, "x"), `2024-02-${String(i + 1).padStart(2, "0")}`, ["src/y.ts"]));
76
+ commits.push(cmt("co1", "2024-03-01", ["src/x.ts", "src/y.ts"]));
77
+ commits.push(cmt("co2", "2024-03-02", ["src/x.ts", "src/y.ts"]));
78
+ // 2 co-occurrences but x has 22 total, y has 22 total, so jaccard tiny + lift low
79
+ expect(correlationMatrix(commits, { minLift: 5 })).toEqual([]);
80
+ });
81
+ });
82
+ describe("correlationMatrix — sort + tiers", () => {
83
+ it("sorts by lift desc", () => {
84
+ const commits = [
85
+ // Pair 1: TIGHT (5 co-occurrences, files appear in nothing else).
86
+ cmt("a1", "2024-01-01", ["src/tight-a.ts", "src/tight-b.ts"]),
87
+ cmt("a2", "2024-01-02", ["src/tight-a.ts", "src/tight-b.ts"]),
88
+ cmt("a3", "2024-01-03", ["src/tight-a.ts", "src/tight-b.ts"]),
89
+ cmt("a4", "2024-01-04", ["src/tight-a.ts", "src/tight-b.ts"]),
90
+ cmt("a5", "2024-01-05", ["src/tight-a.ts", "src/tight-b.ts"]),
91
+ // Pair 2: LOOSER (3 co, but both files also touched alone — diluted lift).
92
+ cmt("b1", "2024-02-01", ["src/loose-a.ts", "src/loose-b.ts"]),
93
+ cmt("b2", "2024-02-02", ["src/loose-a.ts", "src/loose-b.ts"]),
94
+ cmt("b3", "2024-02-03", ["src/loose-a.ts", "src/loose-b.ts"]),
95
+ cmt("b4", "2024-02-04", ["src/loose-a.ts"]),
96
+ cmt("b5", "2024-02-05", ["src/loose-a.ts"]),
97
+ cmt("b6", "2024-02-06", ["src/loose-b.ts"]),
98
+ cmt("b7", "2024-02-07", ["src/loose-b.ts"]),
99
+ // Background — 15 commits on unrelated files lower the global baseline,
100
+ // so even the tight pair has lift > 1.5 (otherwise lift = 1 and filtered).
101
+ ...Array.from({ length: 15 }, (_, i) => cmt(`bg${i}`.padEnd(7, "x"), `2024-03-${String(i + 1).padStart(2, "0")}`, [`src/other${i}.ts`])),
102
+ ];
103
+ const pairs = correlationMatrix(commits, { minFileTouches: 3, minCoOccurrences: 2 });
104
+ expect(pairs[0].fileA).toMatch(/tight-/);
105
+ expect(pairs[0].lift).toBeGreaterThan(pairs[pairs.length - 1].lift);
106
+ });
107
+ it("respects topN", () => {
108
+ const commits = [];
109
+ // 10 file pairs, each with 4 co-occurrences
110
+ for (let p = 0; p < 10; p++) {
111
+ for (let i = 0; i < 4; i++) {
112
+ commits.push(cmt(`p${p}c${i}`.padEnd(7, "x"), `2024-${String(p + 1).padStart(2, "0")}-0${i + 1}`, [`src/p${p}-a.ts`, `src/p${p}-b.ts`]));
113
+ }
114
+ }
115
+ expect(correlationMatrix(commits, { minFileTouches: 1, minCoOccurrences: 2, topN: 5, minLift: 0 }).length).toBeLessThanOrEqual(5);
116
+ });
117
+ });
118
+ //# sourceMappingURL=correlation-matrix.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correlation-matrix.test.js","sourceRoot":"","sources":["../../src/quant/correlation-matrix.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `mneme drawdown` — find the worst "losing streaks" in repo history.
3
+ *
4
+ * In finance, a drawdown is a peak-to-trough decline. In a codebase, it's
5
+ * a stretch of bug-fix-only commits with no feature progress — pure
6
+ * firefighting. Surfacing these is gold for retrospectives:
7
+ *
8
+ * "We spent 3 weeks last September shipping nothing but fixes."
9
+ *
10
+ * Pure data analysis. No LLM. Tested via TDD against synthetic histories.
11
+ */
12
+ import type { Commit } from "../types.js";
13
+ export interface Drawdown {
14
+ /** Start commit (peak — last feat before the drawdown). */
15
+ startHash: string;
16
+ startDate: string;
17
+ /** End commit (trough — last fix in the streak). */
18
+ endHash: string;
19
+ endDate: string;
20
+ /** Number of consecutive non-feat commits in the streak. */
21
+ length: number;
22
+ /** Days between start and end. */
23
+ durationDays: number;
24
+ /** Sample fix subjects from inside the streak (up to 3). */
25
+ sampleFixes: string[];
26
+ /** Severity tier from length × duration. */
27
+ tier: "minor" | "moderate" | "severe" | "critical";
28
+ }
29
+ /** Decide whether a commit subject is a "fix" (counts toward drawdown). */
30
+ export declare function isFixCommit(c: Commit): boolean;
31
+ /** Decide whether a commit subject is a "feature" (resets the streak). */
32
+ export declare function isFeatCommit(c: Commit): boolean;
33
+ /**
34
+ * Detect drawdowns — runs of ≥ minLength consecutive non-feat commits
35
+ * where ≥ fixRatio of those commits are explicit fixes.
36
+ */
37
+ export declare function detectDrawdowns(commits: Commit[], opts?: {
38
+ minLength?: number;
39
+ fixRatio?: number;
40
+ topN?: number;
41
+ }): Drawdown[];
42
+ export declare function classifyDrawdown(length: number, durationDays: number): Drawdown["tier"];
43
+ export interface DrawdownSummary {
44
+ total: number;
45
+ longestStreak: number;
46
+ totalFixingDays: number;
47
+ /** Fraction of the repo's lifespan spent in drawdown. */
48
+ drawdownFraction: number;
49
+ }
50
+ export declare function summarizeDrawdowns(commits: Commit[], drawdowns: Drawdown[]): DrawdownSummary;
51
+ //# sourceMappingURL=drawdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawdown.d.ts","sourceRoot":"","sources":["../../src/quant/drawdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,4CAA4C;IAC5C,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,CAAC;CACpD;AAKD,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAClE,QAAQ,EAAE,CA4CZ;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAKvF;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,eAAe,CAiB5F"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `mneme drawdown` — find the worst "losing streaks" in repo history.
3
+ *
4
+ * In finance, a drawdown is a peak-to-trough decline. In a codebase, it's
5
+ * a stretch of bug-fix-only commits with no feature progress — pure
6
+ * firefighting. Surfacing these is gold for retrospectives:
7
+ *
8
+ * "We spent 3 weeks last September shipping nothing but fixes."
9
+ *
10
+ * Pure data analysis. No LLM. Tested via TDD against synthetic histories.
11
+ */
12
+ const FEAT_RE = /^feat[(:]/i;
13
+ const FIX_RE = /^(fix|hotfix|bug|revert|patch)[(:]/i;
14
+ /** Decide whether a commit subject is a "fix" (counts toward drawdown). */
15
+ export function isFixCommit(c) {
16
+ return FIX_RE.test(c.subject);
17
+ }
18
+ /** Decide whether a commit subject is a "feature" (resets the streak). */
19
+ export function isFeatCommit(c) {
20
+ return FEAT_RE.test(c.subject);
21
+ }
22
+ /**
23
+ * Detect drawdowns — runs of ≥ minLength consecutive non-feat commits
24
+ * where ≥ fixRatio of those commits are explicit fixes.
25
+ */
26
+ export function detectDrawdowns(commits, opts = {}) {
27
+ const minLength = opts.minLength ?? 3;
28
+ const fixRatio = opts.fixRatio ?? 0.5;
29
+ const topN = opts.topN ?? 10;
30
+ const sorted = [...commits].sort((a, b) => a.authorDate.localeCompare(b.authorDate));
31
+ const out = [];
32
+ let i = 0;
33
+ while (i < sorted.length) {
34
+ if (isFeatCommit(sorted[i])) {
35
+ i += 1;
36
+ continue;
37
+ }
38
+ // Walk forward while non-feat.
39
+ let j = i;
40
+ let fixCount = 0;
41
+ while (j < sorted.length && !isFeatCommit(sorted[j])) {
42
+ if (isFixCommit(sorted[j]))
43
+ fixCount += 1;
44
+ j += 1;
45
+ }
46
+ const length = j - i;
47
+ const ratio = length === 0 ? 0 : fixCount / length;
48
+ if (length >= minLength && ratio >= fixRatio) {
49
+ const start = sorted[i];
50
+ const end = sorted[j - 1];
51
+ const dur = (new Date(end.authorDate).getTime() - new Date(start.authorDate).getTime()) / 86_400_000;
52
+ const samples = sorted.slice(i, j).filter(isFixCommit).slice(0, 3).map((c) => c.subject);
53
+ out.push({
54
+ startHash: start.shortHash || start.hash.slice(0, 7),
55
+ startDate: start.authorDate.slice(0, 10),
56
+ endHash: end.shortHash || end.hash.slice(0, 7),
57
+ endDate: end.authorDate.slice(0, 10),
58
+ length,
59
+ durationDays: Math.round(dur * 10) / 10,
60
+ sampleFixes: samples,
61
+ tier: classifyDrawdown(length, dur),
62
+ });
63
+ }
64
+ i = j === i ? i + 1 : j;
65
+ }
66
+ // Sort by severity (length × duration), desc.
67
+ out.sort((a, b) => b.length * Math.max(b.durationDays, 1) - a.length * Math.max(a.durationDays, 1));
68
+ return out.slice(0, topN);
69
+ }
70
+ export function classifyDrawdown(length, durationDays) {
71
+ if (length >= 15 || durationDays >= 30)
72
+ return "critical";
73
+ if (length >= 8 || durationDays >= 14)
74
+ return "severe";
75
+ if (length >= 5 || durationDays >= 7)
76
+ return "moderate";
77
+ return "minor";
78
+ }
79
+ export function summarizeDrawdowns(commits, drawdowns) {
80
+ if (commits.length === 0) {
81
+ return { total: 0, longestStreak: 0, totalFixingDays: 0, drawdownFraction: 0 };
82
+ }
83
+ const sorted = [...commits].sort((a, b) => a.authorDate.localeCompare(b.authorDate));
84
+ const repoSpan = (new Date(sorted[sorted.length - 1].authorDate).getTime() -
85
+ new Date(sorted[0].authorDate).getTime()) /
86
+ 86_400_000;
87
+ const totalFixingDays = drawdowns.reduce((s, d) => s + d.durationDays, 0);
88
+ const longest = drawdowns.reduce((m, d) => Math.max(m, d.length), 0);
89
+ return {
90
+ total: drawdowns.length,
91
+ longestStreak: longest,
92
+ totalFixingDays: Math.round(totalFixingDays * 10) / 10,
93
+ drawdownFraction: repoSpan === 0 ? 0 : totalFixingDays / repoSpan,
94
+ };
95
+ }
96
+ //# sourceMappingURL=drawdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawdown.js","sourceRoot":"","sources":["../../src/quant/drawdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=drawdown.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawdown.test.d.ts","sourceRoot":"","sources":["../../src/quant/drawdown.test.ts"],"names":[],"mappings":""}