@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,201 @@
1
+ /**
2
+ * `mneme paradox` — find architectural flip-flops in commit history.
3
+ *
4
+ * A "flip-flop" is a chain of decisions on the same topic where the
5
+ * direction reverses: A → B → A. This usually means the team forgot
6
+ * (or never wrote down) WHY they made the original decision.
7
+ *
8
+ * Implementation: walk decisions chronologically, group by topic
9
+ * (heuristically clustered by key terms), and detect ABA-style
10
+ * chains. Conservative — only emit when confidence is high that
11
+ * the decisions are actually about the same thing.
12
+ *
13
+ * Pure data analysis. No LLM. The CLI renders.
14
+ */
15
+ /**
16
+ * Stop-words that should NOT count as "topic" — too generic.
17
+ * Add to this set when noise topics appear in real-world repos.
18
+ */
19
+ const TOPIC_STOPWORDS = new Set([
20
+ "the", "a", "an", "to", "from", "for", "with", "of", "in", "on", "at",
21
+ "and", "or", "but", "is", "are", "was", "were",
22
+ "use", "used", "using", "make", "made",
23
+ "new", "old", "all", "more", "less",
24
+ "code", "thing", "stuff", "function", "method", "class", "module",
25
+ "version", "feature", "patch",
26
+ ]);
27
+ /**
28
+ * Extract topic keywords from a decision summary. Keeps only the
29
+ * meaningful nouns/identifiers (filtering verbs and stop-words).
30
+ */
31
+ function extractTopicKeywords(summary) {
32
+ const lower = summary.toLowerCase();
33
+ // Tokenize: split on non-alphanumeric, keep words ≥ 3 chars.
34
+ const tokens = lower
35
+ .replace(/[^\w\s./-]+/g, " ")
36
+ .split(/\s+/)
37
+ .filter((t) => t.length >= 3 && !TOPIC_STOPWORDS.has(t));
38
+ // Also exclude transition verbs that won't help cluster.
39
+ const VERB_BLOCKLIST = new Set([
40
+ "decided", "switch", "switched", "switching", "from",
41
+ "replace", "replaced", "with", "over", "instead",
42
+ "adopt", "adopted", "adopting",
43
+ "deprecate", "deprecated", "deprecating",
44
+ "migrate", "migrated", "migrating",
45
+ "reject", "rejected",
46
+ "choose", "chose", "chosen",
47
+ ]);
48
+ return tokens.filter((t) => !VERB_BLOCKLIST.has(t));
49
+ }
50
+ /**
51
+ * Group decisions by overlapping topic keywords. Two decisions belong
52
+ * to the same group if they share ≥ 1 meaningful keyword.
53
+ */
54
+ function groupByTopic(decisions) {
55
+ const decisionKeywords = decisions.map((d) => ({
56
+ decision: d,
57
+ keywords: new Set(extractTopicKeywords(d.summary)),
58
+ }));
59
+ // Union-find by shared keyword.
60
+ const groups = [];
61
+ for (const item of decisionKeywords) {
62
+ let placed = false;
63
+ for (const g of groups) {
64
+ // Share at least one keyword?
65
+ let shared = false;
66
+ for (const k of item.keywords) {
67
+ if (g.keywords.has(k)) {
68
+ shared = true;
69
+ break;
70
+ }
71
+ }
72
+ if (shared) {
73
+ g.decisions.push(item.decision);
74
+ for (const k of item.keywords)
75
+ g.keywords.add(k);
76
+ placed = true;
77
+ break;
78
+ }
79
+ }
80
+ if (!placed) {
81
+ groups.push({ keywords: new Set(item.keywords), decisions: [item.decision] });
82
+ }
83
+ }
84
+ // Pick a label per group — the keyword that appears in the most decisions
85
+ // of the group (the "common thread"). Ties broken by length.
86
+ const out = new Map();
87
+ for (const g of groups) {
88
+ if (g.decisions.length < 2)
89
+ continue; // need at least 2 decisions to flip
90
+ const label = pickLabel(g.decisions);
91
+ if (out.has(label)) {
92
+ // Merge into existing — multiple groups can hash to the same label.
93
+ out.get(label).push(...g.decisions);
94
+ }
95
+ else {
96
+ out.set(label, g.decisions);
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+ /** Label = the keyword that occurs in the most decisions of the chain. */
102
+ function pickLabel(decisions) {
103
+ const freq = new Map();
104
+ for (const d of decisions) {
105
+ const kws = new Set(extractTopicKeywords(d.summary));
106
+ for (const k of kws)
107
+ freq.set(k, (freq.get(k) ?? 0) + 1);
108
+ }
109
+ if (freq.size === 0)
110
+ return "(unnamed)";
111
+ return [...freq.entries()].sort((a, b) => b[1] - a[1] || b[0].length - a[0].length)[0][0];
112
+ }
113
+ /**
114
+ * Extract the *destination* keywords of a decision — the noun being
115
+ * switched/decided TO. This is the "side" that the decision is choosing.
116
+ *
117
+ * For "switched from X to Y" we want Y.
118
+ * For "decided to use Z" we want Z.
119
+ * For "adopted W" we want W.
120
+ */
121
+ function destinationKeywords(d) {
122
+ const summary = d.summary.toLowerCase();
123
+ // Match the LAST " to X" clause (closest to end of string).
124
+ // Pattern: "to" surrounded by spaces, then everything to end.
125
+ const toMatch = summary.match(/\bto\s+(.+)$/);
126
+ if (toMatch) {
127
+ return new Set(extractTopicKeywords(toMatch[1]));
128
+ }
129
+ // No "to" — fall back to keywords from the whole summary.
130
+ return new Set(extractTopicKeywords(summary));
131
+ }
132
+ /**
133
+ * Count flips in a chronologically-sorted decision chain.
134
+ *
135
+ * A "flip" is an ABA pattern — at index i, a destination keyword X appears,
136
+ * disappears in i+1, and reappears in i+2. The team chose X, switched away,
137
+ * then went back. That's exactly the "we keep flip-flopping" anti-pattern.
138
+ */
139
+ function countFlips(chain) {
140
+ if (chain.length < 3)
141
+ return 0;
142
+ const dests = chain.map(destinationKeywords);
143
+ let flips = 0;
144
+ for (let i = 0; i < dests.length - 2; i++) {
145
+ const a = dests[i];
146
+ const b = dests[i + 1];
147
+ const c = dests[i + 2];
148
+ let flipFound = false;
149
+ for (const k of a) {
150
+ if (!b.has(k) && c.has(k)) {
151
+ flipFound = true;
152
+ break;
153
+ }
154
+ }
155
+ if (flipFound)
156
+ flips += 1;
157
+ }
158
+ return flips;
159
+ }
160
+ /**
161
+ * Detect flip-flops across a list of decisions.
162
+ *
163
+ * Returns groups with ≥ 1 flip — the actual paradoxes worth investigating.
164
+ * Groups with 2+ decisions but no flips (just continuous progress) are
165
+ * suppressed.
166
+ */
167
+ export function detectParadoxes(decisions) {
168
+ if (decisions.length < 2)
169
+ return [];
170
+ const sorted = [...decisions].sort((a, b) => a.date.localeCompare(b.date));
171
+ const groups = groupByTopic(sorted);
172
+ const flipFlops = [];
173
+ for (const [topic, chain] of groups) {
174
+ if (chain.length < 3)
175
+ continue; // ABA needs 3 decisions minimum
176
+ const flips = countFlips(chain);
177
+ if (flips === 0)
178
+ continue;
179
+ const fromDate = new Date(chain[0].date);
180
+ const toDate = new Date(chain[chain.length - 1].date);
181
+ const spanMonths = (toDate.getTime() - fromDate.getTime()) / (30 * 86_400_000);
182
+ flipFlops.push({
183
+ topic,
184
+ chain,
185
+ flips,
186
+ spanMonths: Math.round(spanMonths * 10) / 10,
187
+ question: buildQuestion(topic, chain, flips),
188
+ });
189
+ }
190
+ flipFlops.sort((a, b) => b.flips - a.flips || b.spanMonths - a.spanMonths);
191
+ return flipFlops;
192
+ }
193
+ function buildQuestion(topic, chain, flips) {
194
+ const months = Math.round((new Date(chain[chain.length - 1].date).getTime() - new Date(chain[0].date).getTime()) /
195
+ (30 * 86_400_000));
196
+ if (flips >= 2) {
197
+ return `${flips} reversals in ${months} months — write an ADR before the next change.`;
198
+ }
199
+ return `One reversal in ${months} months — was the prior decision a mistake, or did context change?`;
200
+ }
201
+ //# sourceMappingURL=paradox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paradox.js","sourceRoot":"","sources":["../../src/insights/paradox.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=paradox.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paradox.test.d.ts","sourceRoot":"","sources":["../../src/insights/paradox.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { detectParadoxes } from "./paradox.js";
3
+ const dec = (date, summary, hash = `h${date.replace(/-/g, "")}`, kind = "switched") => ({
4
+ commitHash: hash,
5
+ shortHash: hash.slice(0, 7),
6
+ date,
7
+ author: "alice",
8
+ summary,
9
+ kind,
10
+ confidence: 0.9,
11
+ });
12
+ describe("detectParadoxes — basic flip detection", () => {
13
+ it("detects a clean ABA flip (Redis → in-memory → Redis)", () => {
14
+ const decisions = [
15
+ dec("2024-03-15", "decided to use Redis for caching"),
16
+ dec("2024-08-20", "switched from Redis to in-memory cache"),
17
+ dec("2025-02-10", "switched from in-memory back to Redis"),
18
+ ];
19
+ const paradoxes = detectParadoxes(decisions);
20
+ expect(paradoxes.length).toBeGreaterThanOrEqual(1);
21
+ const redis = paradoxes.find((p) => /redis|cache/i.test(p.topic));
22
+ expect(redis).toBeDefined();
23
+ expect(redis.flips).toBeGreaterThanOrEqual(1);
24
+ expect(redis.chain).toHaveLength(3);
25
+ });
26
+ it("does NOT flip on continuous progress (different targets each time)", () => {
27
+ const decisions = [
28
+ dec("2024-01-01", "adopted React"),
29
+ dec("2024-06-01", "switched from React to Solid"),
30
+ dec("2024-12-01", "switched from Solid to Svelte"),
31
+ ];
32
+ // Each decision picks a NEW target — no return to React. No flip.
33
+ const paradoxes = detectParadoxes(decisions);
34
+ const ui = paradoxes.find((p) => /react|solid|svelte/i.test(p.topic));
35
+ expect(ui?.flips ?? 0).toBe(0);
36
+ });
37
+ it("returns empty array when there are fewer than 2 decisions", () => {
38
+ expect(detectParadoxes([])).toEqual([]);
39
+ expect(detectParadoxes([dec("2024-01-01", "decided")])).toEqual([]);
40
+ });
41
+ it("requires at least 3 decisions to count as a flip-flop chain", () => {
42
+ const decisions = [
43
+ dec("2024-01-01", "use Redis cache"),
44
+ dec("2024-06-01", "switched from Redis to in-memory cache"),
45
+ ];
46
+ expect(detectParadoxes(decisions)).toEqual([]);
47
+ });
48
+ });
49
+ describe("detectParadoxes — span and metadata", () => {
50
+ it("computes spanMonths between first and last decision", () => {
51
+ const decisions = [
52
+ dec("2024-01-01", "use Redis cache"),
53
+ dec("2024-06-01", "switched from Redis to memcache"),
54
+ dec("2025-01-01", "back to Redis cache"),
55
+ ];
56
+ const p = detectParadoxes(decisions)[0];
57
+ expect(p.spanMonths).toBeGreaterThan(11);
58
+ expect(p.spanMonths).toBeLessThan(13);
59
+ });
60
+ it("attaches a question that prompts an ADR for repeated reversals", () => {
61
+ const decisions = [
62
+ dec("2024-01-01", "passport oauth"),
63
+ dec("2024-06-01", "switched from passport to custom auth"),
64
+ dec("2025-01-01", "switched back to passport"),
65
+ ];
66
+ const p = detectParadoxes(decisions)[0];
67
+ expect(p.question.toLowerCase()).toMatch(/reversal|adr|mistake|context/);
68
+ });
69
+ });
70
+ describe("detectParadoxes — sort order", () => {
71
+ it("sorts paradoxes by flip count desc, then by span", () => {
72
+ const decisions = [
73
+ // topic A: 2 flips (ABABA-ish)
74
+ dec("2024-01-01", "use Redis cache fast"),
75
+ dec("2024-04-01", "switched from Redis cache to memcached fast"),
76
+ dec("2024-08-01", "back to Redis cache"),
77
+ dec("2024-12-01", "memcached cache again"),
78
+ dec("2025-04-01", "Redis cache again"),
79
+ // topic B: 1 flip
80
+ dec("2024-02-01", "use webpack bundler"),
81
+ dec("2024-07-01", "switched from webpack bundler to vite bundler"),
82
+ dec("2024-12-01", "back to webpack bundler"),
83
+ ];
84
+ const paradoxes = detectParadoxes(decisions);
85
+ expect(paradoxes[0].flips).toBeGreaterThanOrEqual(paradoxes[paradoxes.length - 1].flips);
86
+ });
87
+ });
88
+ //# sourceMappingURL=paradox.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paradox.test.js","sourceRoot":"","sources":["../../src/insights/paradox.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `mneme regret` — surface commits that we shipped and then immediately
3
+ * fixed, hotfixed, or reverted. The "things we regretted" log.
4
+ *
5
+ * Reverse-mining git history for failure patterns. A commit is a "regret"
6
+ * if a follow-up commit (within `windowDays`) contains revert/hotfix/fix
7
+ * markers OR touches the same lines/files. The follow-up tells us this
8
+ * commit ALREADY caused pain.
9
+ *
10
+ * Why this is useful:
11
+ * - Postmortems: "what did we ship and regret?"
12
+ * - Retrospectives: pattern of regret reveals systemic issues
13
+ * - Pre-commit awareness: see what *kinds* of changes you tend to redo
14
+ *
15
+ * Pure data extraction — no LLM. The CLI command renders.
16
+ */
17
+ import type { Commit } from "../types.js";
18
+ export interface Regret {
19
+ shipped: Commit;
20
+ followup: Commit;
21
+ /** Days between ship and followup. */
22
+ daysToFix: number;
23
+ /**
24
+ * What kind of follow-up — drives the rendered label and lesson tone.
25
+ * 'revert' is strongest (full undo); 'hotfix' is urgent fix; 'fix' is
26
+ * any subsequent commit referencing the original; 'sameFiles' is the
27
+ * weakest signal (just touched the same files within window).
28
+ */
29
+ kind: "revert" | "hotfix" | "fix" | "sameFiles";
30
+ /** A 1-line lesson when one is extractable from the follow-up message. */
31
+ lesson?: string;
32
+ }
33
+ /**
34
+ * Detect regrets from a chronological commit list. Commits should be
35
+ * sorted oldest-first (caller's responsibility — we use index ordering
36
+ * to compute "follow-up").
37
+ */
38
+ export declare function detectRegrets(commits: Commit[], opts?: {
39
+ windowDays?: number;
40
+ minDaysToFix?: number;
41
+ }): Regret[];
42
+ /**
43
+ * Aggregate metrics — useful for repo-level trend reporting.
44
+ *
45
+ * `regretRate` is how many of the recent shipped commits later got fixed
46
+ * within the window. We exclude shipped commits whose own subject already
47
+ * looks like a fix (so we don't double-count repair work).
48
+ */
49
+ export interface RegretSummary {
50
+ totalShipped: number;
51
+ totalRegrets: number;
52
+ regretRate: number;
53
+ byKind: Record<Regret["kind"], number>;
54
+ averageDaysToFix: number;
55
+ }
56
+ export declare function summarizeRegrets(commits: Commit[], regrets: Regret[]): RegretSummary;
57
+ //# sourceMappingURL=regret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regret.d.ts","sourceRoot":"","sources":["../../src/insights/regret.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,WAAW,CAAC;IAChD,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAOD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO,GACxD,MAAM,EAAE,CAwDV;AAkCD;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IACvC,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,aAAa,CAgBpF"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * `mneme regret` — surface commits that we shipped and then immediately
3
+ * fixed, hotfixed, or reverted. The "things we regretted" log.
4
+ *
5
+ * Reverse-mining git history for failure patterns. A commit is a "regret"
6
+ * if a follow-up commit (within `windowDays`) contains revert/hotfix/fix
7
+ * markers OR touches the same lines/files. The follow-up tells us this
8
+ * commit ALREADY caused pain.
9
+ *
10
+ * Why this is useful:
11
+ * - Postmortems: "what did we ship and regret?"
12
+ * - Retrospectives: pattern of regret reveals systemic issues
13
+ * - Pre-commit awareness: see what *kinds* of changes you tend to redo
14
+ *
15
+ * Pure data extraction — no LLM. The CLI command renders.
16
+ */
17
+ const REVERT_RE = /\b(revert|reverts|reverted)\b/i;
18
+ const HOTFIX_RE = /\b(hotfix|emergency|urgent|critical)\b/i;
19
+ const FIX_RE = /\b(fix(?:es|ed)?|bug|broken|crash|regression)\b/i;
20
+ const REF_RE = /\b#?(\d{2,6})\b/;
21
+ /**
22
+ * Detect regrets from a chronological commit list. Commits should be
23
+ * sorted oldest-first (caller's responsibility — we use index ordering
24
+ * to compute "follow-up").
25
+ */
26
+ export function detectRegrets(commits, opts = {}) {
27
+ const windowMs = (opts.windowDays ?? 7) * 86_400_000;
28
+ const minDaysToFix = opts.minDaysToFix ?? 0;
29
+ const sorted = [...commits].sort((a, b) => a.authorDate.localeCompare(b.authorDate));
30
+ const out = [];
31
+ // Index commits by file for fast suffix matching.
32
+ for (let i = 0; i < sorted.length; i++) {
33
+ const shipped = sorted[i];
34
+ const shippedTime = new Date(shipped.authorDate).getTime();
35
+ const shippedHashShort = (shipped.shortHash || shipped.hash.slice(0, 7)).toLowerCase();
36
+ // Walk forward looking for a follow-up within the window.
37
+ for (let j = i + 1; j < sorted.length; j++) {
38
+ const followup = sorted[j];
39
+ const followupTime = new Date(followup.authorDate).getTime();
40
+ const dt = followupTime - shippedTime;
41
+ if (dt > windowMs)
42
+ break; // window exceeded; no later commit qualifies
43
+ const days = dt / 86_400_000;
44
+ if (days < minDaysToFix)
45
+ continue;
46
+ const fuText = `${followup.subject}\n${followup.body || ""}`;
47
+ const fuTextLower = fuText.toLowerCase();
48
+ let kind = null;
49
+ // Strongest signal: explicit revert that names the original commit.
50
+ if (REVERT_RE.test(fuText) && (fuTextLower.includes(shippedHashShort) || fuTextLower.includes(shipped.subject.toLowerCase()))) {
51
+ kind = "revert";
52
+ }
53
+ else if (REVERT_RE.test(fuText) && shareFiles(shipped, followup)) {
54
+ kind = "revert";
55
+ }
56
+ else if (HOTFIX_RE.test(fuText) && shareFiles(shipped, followup)) {
57
+ kind = "hotfix";
58
+ }
59
+ else if (FIX_RE.test(fuText) && shareFiles(shipped, followup)) {
60
+ // Don't double-count the shipped commit itself if it's also a fix.
61
+ if (!FIX_RE.test(shipped.subject))
62
+ kind = "fix";
63
+ }
64
+ else if (REF_RE.test(fuText) && shareFiles(shipped, followup)) {
65
+ // weak signal: same files within window — only count if not already a fix
66
+ kind = "sameFiles";
67
+ }
68
+ if (kind) {
69
+ out.push({
70
+ shipped,
71
+ followup,
72
+ daysToFix: Number(days.toFixed(1)),
73
+ kind,
74
+ lesson: extractLesson(fuText, kind),
75
+ });
76
+ break; // one regret per shipped commit (the first follow-up)
77
+ }
78
+ }
79
+ }
80
+ // Sort by recency of ship date (newest first — what users want to see).
81
+ return out.sort((a, b) => b.shipped.authorDate.localeCompare(a.shipped.authorDate));
82
+ }
83
+ function shareFiles(a, b) {
84
+ if (!a.files?.length || !b.files?.length)
85
+ return false;
86
+ const setA = new Set(a.files);
87
+ for (const f of b.files)
88
+ if (setA.has(f))
89
+ return true;
90
+ return false;
91
+ }
92
+ function extractLesson(text, kind) {
93
+ // Simple heuristic — find the first sentence that explains WHY.
94
+ const lines = text.split(/\n+/);
95
+ for (const line of lines) {
96
+ const t = line.trim();
97
+ if (!t)
98
+ continue;
99
+ if (t.length < 20 || t.length > 200)
100
+ continue;
101
+ // Skip the subject line (usually the first non-empty)
102
+ if (t.toLowerCase().startsWith("revert "))
103
+ continue;
104
+ if (t.toLowerCase().startsWith("hotfix"))
105
+ continue;
106
+ return t;
107
+ }
108
+ // Fallback — generic lesson by kind.
109
+ switch (kind) {
110
+ case "revert":
111
+ return "this change was fully reverted within the window";
112
+ case "hotfix":
113
+ return "needed an urgent follow-up fix";
114
+ case "fix":
115
+ return "a follow-up commit fixed it";
116
+ case "sameFiles":
117
+ return "the same files were touched again very soon after";
118
+ }
119
+ }
120
+ export function summarizeRegrets(commits, regrets) {
121
+ const shippedCandidates = commits.filter((c) => !FIX_RE.test(c.subject));
122
+ const total = shippedCandidates.length;
123
+ const byKind = { revert: 0, hotfix: 0, fix: 0, sameFiles: 0 };
124
+ let sumDays = 0;
125
+ for (const r of regrets) {
126
+ byKind[r.kind] += 1;
127
+ sumDays += r.daysToFix;
128
+ }
129
+ return {
130
+ totalShipped: total,
131
+ totalRegrets: regrets.length,
132
+ regretRate: total === 0 ? 0 : regrets.length / total,
133
+ byKind,
134
+ averageDaysToFix: regrets.length === 0 ? 0 : sumDays / regrets.length,
135
+ };
136
+ }
137
+ //# sourceMappingURL=regret.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regret.js","sourceRoot":"","sources":["../../src/insights/regret.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=regret.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regret.test.d.ts","sourceRoot":"","sources":["../../src/insights/regret.test.ts"],"names":[],"mappings":""}