@mneme-ai/core 0.10.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.
@@ -0,0 +1,209 @@
1
+ const REVERT_RE = /\b(revert|reverts|reverted|rollback)\b/i;
2
+ const HOTFIX_RE = /\b(hotfix|emergency|urgent|critical|p[01])\b/i;
3
+ const INCIDENT_RE = /\b(incident|outage|down|broken|crash|regression)\b/i;
4
+ const REWRITE_RE = /\b(rewrite|rewritten|redo|redid|second(?:\s|-)attempt)\b/i;
5
+ const STOPWORDS = new Set([
6
+ "the", "a", "an", "of", "to", "in", "on", "for", "and", "or", "with", "add", "added", "fix",
7
+ "remove", "update", "change", "make", "made", "new", "old", "this", "that", "is", "are", "be",
8
+ "was", "were", "my", "our", "your", "i", "we", "you", "it", "its", "into", "from", "by", "at",
9
+ ]);
10
+ /**
11
+ * Score similarity between an intent string and a commit, by token overlap
12
+ * + a small bonus for files-mentioned matching commit-touched files.
13
+ */
14
+ export function scoreSimilarity(intent, c) {
15
+ const intentTokens = tokenize(intent);
16
+ if (intentTokens.size === 0)
17
+ return 0;
18
+ const commitText = `${c.subject}\n${c.body || ""}`;
19
+ const commitTokens = tokenize(commitText);
20
+ let overlap = 0;
21
+ for (const t of intentTokens)
22
+ if (commitTokens.has(t))
23
+ overlap += 1;
24
+ const base = overlap / intentTokens.size;
25
+ // Path-level boost: if intent names a file/path, check commit touches it.
26
+ const pathHint = extractPathHint(intent);
27
+ let pathBoost = 0;
28
+ if (pathHint && c.files?.some((f) => f.includes(pathHint))) {
29
+ pathBoost = 0.2;
30
+ }
31
+ return Math.min(1, base + pathBoost);
32
+ }
33
+ function tokenize(s) {
34
+ const out = new Set();
35
+ for (const t of s
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9_/.-]+/g, " ")
38
+ .split(/\s+/)) {
39
+ if (!t)
40
+ continue;
41
+ if (t.length < 3)
42
+ continue;
43
+ if (STOPWORDS.has(t))
44
+ continue;
45
+ out.add(t);
46
+ }
47
+ return out;
48
+ }
49
+ function extractPathHint(intent) {
50
+ // crude: any token that looks like a path or file
51
+ const m = intent.match(/[A-Za-z0-9_./-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|md)/);
52
+ return m ? m[0] : null;
53
+ }
54
+ /**
55
+ * Build a premortem from an intent string and the commit history. Caller
56
+ * passes the full commit history (newest first or oldest first — we sort).
57
+ */
58
+ export function buildPremortem(intent, commits, opts = {}) {
59
+ const similarityFloor = opts.similarityFloor ?? 0.25;
60
+ const windowDays = opts.windowDays ?? 14;
61
+ const maxAttempts = opts.maxAttempts ?? 25;
62
+ const sorted = [...commits].sort((a, b) => a.authorDate.localeCompare(b.authorDate));
63
+ // Find similar past attempts. Exclude commits that are themselves regret
64
+ // signals (revert / hotfix / incident) — those aren't attempts, they're
65
+ // the consequence of one.
66
+ const scored = [];
67
+ for (const c of sorted) {
68
+ const subject = c.subject || "";
69
+ if (REVERT_RE.test(subject) || HOTFIX_RE.test(subject) || INCIDENT_RE.test(subject)) {
70
+ continue;
71
+ }
72
+ const s = scoreSimilarity(intent, c);
73
+ if (s >= similarityFloor)
74
+ scored.push({ commit: c, similarity: s });
75
+ }
76
+ scored.sort((a, b) => b.similarity - a.similarity);
77
+ const candidates = scored.slice(0, maxAttempts);
78
+ // For each candidate, walk forward window to find regret signal
79
+ const pastAttempts = [];
80
+ const windowMs = windowDays * 86_400_000;
81
+ for (const { commit: attempt, similarity } of candidates) {
82
+ const tStart = new Date(attempt.authorDate).getTime();
83
+ let regret;
84
+ let kind = "none";
85
+ let daysToRegret = 0;
86
+ for (const c of sorted) {
87
+ if (c.hash === attempt.hash)
88
+ continue;
89
+ const tC = new Date(c.authorDate).getTime();
90
+ if (tC <= tStart)
91
+ continue;
92
+ if (tC - tStart > windowMs)
93
+ break;
94
+ // Must share at least one file with the attempt
95
+ if (!shareFiles(attempt, c))
96
+ continue;
97
+ const text = `${c.subject}\n${c.body || ""}`;
98
+ if (REVERT_RE.test(text)) {
99
+ regret = c;
100
+ kind = "revert";
101
+ daysToRegret = Number(((tC - tStart) / 86_400_000).toFixed(1));
102
+ break;
103
+ }
104
+ if (HOTFIX_RE.test(text)) {
105
+ regret = c;
106
+ kind = "hotfix";
107
+ daysToRegret = Number(((tC - tStart) / 86_400_000).toFixed(1));
108
+ break;
109
+ }
110
+ if (INCIDENT_RE.test(text)) {
111
+ regret = c;
112
+ kind = "incident";
113
+ daysToRegret = Number(((tC - tStart) / 86_400_000).toFixed(1));
114
+ break;
115
+ }
116
+ if (REWRITE_RE.test(text) && shareFiles(attempt, c)) {
117
+ regret = c;
118
+ kind = "rewrite";
119
+ daysToRegret = Number(((tC - tStart) / 86_400_000).toFixed(1));
120
+ break;
121
+ }
122
+ }
123
+ pastAttempts.push({ attempt, regret, riskKind: kind, daysToRegret, similarity });
124
+ }
125
+ // Compute regret probability
126
+ const regretCount = pastAttempts.filter((p) => p.riskKind !== "none").length;
127
+ const regretProbability = pastAttempts.length === 0 ? 0 : regretCount / pastAttempts.length;
128
+ // Distill top risks: cluster regrets by riskKind, label them.
129
+ const byKind = new Map();
130
+ for (const p of pastAttempts) {
131
+ if (p.riskKind === "none")
132
+ continue;
133
+ const arr = byKind.get(p.riskKind) ?? [];
134
+ arr.push(p);
135
+ byKind.set(p.riskKind, arr);
136
+ }
137
+ const topRisks = [];
138
+ for (const [kind, arr] of byKind) {
139
+ topRisks.push({
140
+ label: riskLabel(kind, arr),
141
+ evidence: arr.map((a) => a.regret).filter(Boolean),
142
+ weight: Math.min(1, arr.length / Math.max(1, pastAttempts.length)),
143
+ });
144
+ }
145
+ topRisks.sort((a, b) => b.weight - a.weight);
146
+ // Verdict
147
+ let verdict = "low";
148
+ if (regretProbability >= 0.7)
149
+ verdict = "very_high";
150
+ else if (regretProbability >= 0.4)
151
+ verdict = "high";
152
+ else if (regretProbability >= 0.15)
153
+ verdict = "medium";
154
+ const summary = composeSummary(intent, pastAttempts.length, regretCount, verdict);
155
+ return {
156
+ intent,
157
+ pastAttempts,
158
+ regretProbability,
159
+ topRisks: topRisks.slice(0, 3),
160
+ verdict,
161
+ summary,
162
+ };
163
+ }
164
+ function shareFiles(a, b) {
165
+ if (!a.files?.length || !b.files?.length)
166
+ return false;
167
+ const setA = new Set(a.files);
168
+ for (const f of b.files)
169
+ if (setA.has(f))
170
+ return true;
171
+ return false;
172
+ }
173
+ function riskLabel(kind, arr) {
174
+ const n = arr.length;
175
+ switch (kind) {
176
+ case "revert":
177
+ return `change reverted within ${Math.round(median(arr.map((a) => a.daysToRegret)))}d (${n}× before)`;
178
+ case "hotfix":
179
+ return `hotfix follow-up needed (${n}× before)`;
180
+ case "incident":
181
+ return `linked to incident/regression (${n}× before)`;
182
+ case "rewrite":
183
+ return `partially rewritten soon after (${n}× before)`;
184
+ }
185
+ }
186
+ function median(xs) {
187
+ if (xs.length === 0)
188
+ return 0;
189
+ const s = [...xs].sort((a, b) => a - b);
190
+ const m = Math.floor(s.length / 2);
191
+ return s.length % 2 === 1 ? s[m] : (s[m - 1] + s[m]) / 2;
192
+ }
193
+ function composeSummary(intent, total, regret, verdict) {
194
+ if (total === 0) {
195
+ return `No similar past attempts found in this repo for "${intent}". Premortem cannot calibrate against history — proceed with normal review.`;
196
+ }
197
+ const pct = Math.round((regret / total) * 100);
198
+ switch (verdict) {
199
+ case "very_high":
200
+ return `${regret} of ${total} similar past attempts ended badly (${pct}%). This pattern has burned this repo before — slow down, write tests first, and review the cited commits.`;
201
+ case "high":
202
+ return `${regret} of ${total} similar past attempts hit problems (${pct}%). High caution warranted; review the cited risks before starting.`;
203
+ case "medium":
204
+ return `${regret} of ${total} similar past attempts had issues (${pct}%). Some historical risk — worth reading the cited commits.`;
205
+ case "low":
206
+ return `Only ${regret} of ${total} similar past attempts had problems (${pct}%). This kind of change has gone smoothly before — usual care is fine.`;
207
+ }
208
+ }
209
+ //# sourceMappingURL=premortem.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"premortem.js","sourceRoot":"","sources":["../../src/insights/premortem.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=premortem.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"premortem.test.d.ts","sourceRoot":"","sources":["../../src/insights/premortem.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildPremortem, scoreSimilarity } from "./premortem.js";
3
+ function mk(p) {
4
+ return {
5
+ shortHash: p.hash.slice(0, 7),
6
+ authorName: "Test",
7
+ authorEmail: "t@e.com",
8
+ committerDate: p.authorDate,
9
+ body: p.body ?? "",
10
+ files: p.files ?? ["src/cache.ts"],
11
+ parents: p.parents ?? [],
12
+ ...p,
13
+ };
14
+ }
15
+ describe("scoreSimilarity", () => {
16
+ it("scores 0 when no token overlap", () => {
17
+ const c = mk({ hash: "a1", authorDate: "2024-01-01", subject: "fix typo in readme" });
18
+ const s = scoreSimilarity("rebuild authentication layer", c);
19
+ expect(s).toBe(0);
20
+ });
21
+ it("scores higher when intent terms appear in commit", () => {
22
+ const c = mk({
23
+ hash: "a1",
24
+ authorDate: "2024-01-01",
25
+ subject: "add caching layer to api responses",
26
+ });
27
+ const s = scoreSimilarity("add caching layer", c);
28
+ expect(s).toBeGreaterThan(0.5);
29
+ });
30
+ it("ignores stopwords", () => {
31
+ const c = mk({ hash: "a1", authorDate: "2024-01-01", subject: "the a an of to in for" });
32
+ const s = scoreSimilarity("the a an of to", c);
33
+ expect(s).toBe(0);
34
+ });
35
+ it("boosts score when intent names a file the commit touched", () => {
36
+ const c = mk({
37
+ hash: "a1",
38
+ authorDate: "2024-01-01",
39
+ subject: "minor unrelated change",
40
+ files: ["src/auth.ts"],
41
+ });
42
+ const s = scoreSimilarity("rewrite src/auth.ts", c);
43
+ expect(s).toBeGreaterThan(0);
44
+ });
45
+ });
46
+ describe("buildPremortem", () => {
47
+ it("returns empty when no commits at all", () => {
48
+ const r = buildPremortem("add caching layer", []);
49
+ expect(r.pastAttempts).toHaveLength(0);
50
+ expect(r.regretProbability).toBe(0);
51
+ expect(r.verdict).toBe("low");
52
+ });
53
+ it("flags very_high verdict when most past attempts ended in revert", () => {
54
+ const commits = [];
55
+ // 4 attempts to add caching, 3 reverted
56
+ for (let i = 0; i < 4; i++) {
57
+ commits.push(mk({
58
+ hash: `att${i}`,
59
+ authorDate: `2024-0${i + 1}-01`,
60
+ subject: `add caching layer to api`,
61
+ files: ["src/cache.ts"],
62
+ }));
63
+ if (i < 3) {
64
+ commits.push(mk({
65
+ hash: `rev${i}`,
66
+ authorDate: `2024-0${i + 1}-05`,
67
+ subject: `revert "add caching layer to api"`,
68
+ files: ["src/cache.ts"],
69
+ }));
70
+ }
71
+ }
72
+ const r = buildPremortem("add caching layer", commits, { similarityFloor: 0.2 });
73
+ expect(r.regretProbability).toBeGreaterThanOrEqual(0.7);
74
+ expect(r.verdict).toBe("very_high");
75
+ });
76
+ it("flags low verdict when no past attempts had problems", () => {
77
+ const commits = [];
78
+ for (let i = 0; i < 3; i++) {
79
+ commits.push(mk({
80
+ hash: `att${i}`,
81
+ authorDate: `2024-0${i + 1}-01`,
82
+ subject: `add caching layer for api`,
83
+ files: ["src/cache.ts"],
84
+ }));
85
+ commits.push(mk({
86
+ hash: `success${i}`,
87
+ authorDate: `2024-0${i + 1}-05`,
88
+ subject: `extend caching to user objects`,
89
+ files: ["src/cache.ts"],
90
+ }));
91
+ }
92
+ const r = buildPremortem("add caching layer", commits, { similarityFloor: 0.2 });
93
+ expect(r.regretProbability).toBe(0);
94
+ expect(r.verdict).toBe("low");
95
+ });
96
+ it("clusters risks by kind", () => {
97
+ const commits = [
98
+ mk({ hash: "a1", authorDate: "2024-01-01", subject: "add cache" }),
99
+ mk({ hash: "a2", authorDate: "2024-01-03", subject: "revert add cache" }),
100
+ mk({ hash: "a3", authorDate: "2024-02-01", subject: "add cache for users" }),
101
+ mk({ hash: "a4", authorDate: "2024-02-05", subject: "hotfix cache invalidation" }),
102
+ mk({ hash: "a5", authorDate: "2024-03-01", subject: "add cache for orders" }),
103
+ mk({ hash: "a6", authorDate: "2024-03-04", subject: "incident: stale cache caused outage" }),
104
+ ];
105
+ const r = buildPremortem("add cache", commits, { similarityFloor: 0.2 });
106
+ const kinds = new Set(r.topRisks.map((x) => x.label.split(" ")[0]));
107
+ expect(kinds.size).toBeGreaterThan(0);
108
+ });
109
+ it("limits topRisks to at most 3", () => {
110
+ const commits = [];
111
+ const subjects = [
112
+ "add cache",
113
+ "revert add cache",
114
+ "add cache",
115
+ "hotfix add cache bug",
116
+ "add cache",
117
+ "incident outage cache",
118
+ "add cache",
119
+ "rewrite cache after issues",
120
+ ];
121
+ for (let i = 0; i < subjects.length; i++) {
122
+ commits.push(mk({ hash: `c${i}`, authorDate: `2024-${(i + 1).toString().padStart(2, "0")}-01`, subject: subjects[i] }));
123
+ }
124
+ const r = buildPremortem("add cache", commits, { similarityFloor: 0.2 });
125
+ expect(r.topRisks.length).toBeLessThanOrEqual(3);
126
+ });
127
+ it("does not count the attempt itself as its own regret", () => {
128
+ const commits = [
129
+ mk({ hash: "a1", authorDate: "2024-01-01", subject: "add caching layer" }),
130
+ ];
131
+ const r = buildPremortem("add caching layer", commits, { similarityFloor: 0.2 });
132
+ expect(r.pastAttempts[0].riskKind).toBe("none");
133
+ });
134
+ it("computes summary string for every verdict tier", () => {
135
+ const verdicts = [
136
+ "low",
137
+ "medium",
138
+ "high",
139
+ "very_high",
140
+ ];
141
+ const seen = new Set();
142
+ for (const v of verdicts) {
143
+ const commits = [];
144
+ const total = 10;
145
+ const regrets = v === "low" ? 0 : v === "medium" ? 2 : v === "high" ? 5 : 8;
146
+ for (let i = 0; i < total; i++) {
147
+ commits.push(mk({
148
+ hash: `a${i}`,
149
+ authorDate: `2024-${(i + 1).toString().padStart(2, "0")}-01`,
150
+ subject: `add caching feature`,
151
+ files: ["src/cache.ts"],
152
+ }));
153
+ if (i < regrets) {
154
+ commits.push(mk({
155
+ hash: `r${i}`,
156
+ authorDate: `2024-${(i + 1).toString().padStart(2, "0")}-05`,
157
+ subject: `revert add caching feature`,
158
+ files: ["src/cache.ts"],
159
+ }));
160
+ }
161
+ }
162
+ const r = buildPremortem("add caching feature", commits, { similarityFloor: 0.2 });
163
+ seen.add(r.verdict);
164
+ expect(r.summary.length).toBeGreaterThan(20);
165
+ }
166
+ expect(seen.size).toBeGreaterThanOrEqual(2);
167
+ });
168
+ });
169
+ //# sourceMappingURL=premortem.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"premortem.test.js","sourceRoot":"","sources":["../../src/insights/premortem.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `mneme time-machine <file>` — narrate a file's evolution as discrete
3
+ * epochs, not a flat diff log. Each epoch is a meaningful era in the
4
+ * file's life: when it was born, when it was rewritten, when it stabilized,
5
+ * when it broke things, when it was abandoned.
6
+ *
7
+ * What makes this novel:
8
+ * - Most tools show `git log file.ts` — a flat list. Time Machine
9
+ * groups commits into ERAS based on intent (rewrite, fix, polish,
10
+ * stable plateau).
11
+ * - Detects "rewrite events" (large simultaneous insertions+deletions)
12
+ * vs. "polish events" (small surgical changes) vs. "stable plateaus"
13
+ * (long quiet stretches).
14
+ * - Surfaces the WHY for each epoch by extracting the most informative
15
+ * commit message of that era.
16
+ * - Computes "narrative health" — how much of the history is rewrite
17
+ * vs. evolution vs. firefight.
18
+ *
19
+ * Pure data extraction. The CLI renders the timeline; an LLM (optional)
20
+ * can polish the prose.
21
+ */
22
+ import type { Commit, FileChange } from "../types.js";
23
+ export type EpochKind = "birth" | "rewrite" | "evolution" | "firefight" | "polish" | "plateau" | "twilight";
24
+ export interface FileEpoch {
25
+ kind: EpochKind;
26
+ /** Sequential number, 1-indexed. */
27
+ index: number;
28
+ /** Commits defining this epoch (chronological). */
29
+ commits: Commit[];
30
+ /** The most informative commit of the epoch — used for narration. */
31
+ defining: Commit;
32
+ fromDate: string;
33
+ toDate: string;
34
+ /** Total insertions across the epoch. */
35
+ insertions: number;
36
+ /** Total deletions across the epoch. */
37
+ deletions: number;
38
+ /** Days spanned by this epoch. */
39
+ spanDays: number;
40
+ /** A short prose label — the WHY of this epoch. */
41
+ label: string;
42
+ }
43
+ export interface TimeMachineResult {
44
+ filePath: string;
45
+ totalCommits: number;
46
+ totalSpanDays: number;
47
+ epochs: FileEpoch[];
48
+ /**
49
+ * Health balance across epochs:
50
+ * - rewriteRatio: fraction of commits in rewrite epochs (high = unstable)
51
+ * - firefightRatio: fraction in firefight epochs (high = pain)
52
+ * - polishRatio: fraction in polish/plateau (high = mature)
53
+ */
54
+ health: {
55
+ rewriteRatio: number;
56
+ firefightRatio: number;
57
+ polishRatio: number;
58
+ };
59
+ }
60
+ /**
61
+ * Compute file epochs from a chronological list of commits + per-commit
62
+ * file changes for the target file. Caller is responsible for filtering
63
+ * commits to ones that actually touched `filePath`.
64
+ */
65
+ export declare function buildTimeMachine(filePath: string, commits: Commit[], changes: Map<string, FileChange>, // commitHash -> file change for filePath
66
+ opts?: {
67
+ plateauDays?: number;
68
+ rewriteChurnLines?: number;
69
+ }): TimeMachineResult;
70
+ //# sourceMappingURL=time-machine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time-machine.d.ts","sourceRoot":"","sources":["../../src/insights/time-machine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,SAAS,GACT,WAAW,GACX,WAAW,GACX,QAAQ,GACR,SAAS,GACT,UAAU,CAAC;AAEf,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,SAAS,CAAC;IAChB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB;;;;;OAKG;IACH,MAAM,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAMD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,yCAAyC;AAC3E,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9D,iBAAiB,CAgJnB"}
@@ -0,0 +1,177 @@
1
+ const FIRE_RE = /\b(fix(?:es|ed)?|hotfix|revert|rollback|broken|crash|regression|emergency|urgent|critical|p[01])\b/i;
2
+ const REWRITE_RE = /\b(rewrite|refactor|overhaul|redesign|migrate|replace[ds]?|move(?:d)? to)\b/i;
3
+ const POLISH_RE = /\b(typo|format|lint|rename|comment|whitespace|cleanup|style|chore)\b/i;
4
+ /**
5
+ * Compute file epochs from a chronological list of commits + per-commit
6
+ * file changes for the target file. Caller is responsible for filtering
7
+ * commits to ones that actually touched `filePath`.
8
+ */
9
+ export function buildTimeMachine(filePath, commits, changes, // commitHash -> file change for filePath
10
+ opts = {}) {
11
+ const plateauDays = opts.plateauDays ?? 60;
12
+ const rewriteChurnLines = opts.rewriteChurnLines ?? 80;
13
+ const sorted = [...commits].sort((a, b) => a.authorDate.localeCompare(b.authorDate));
14
+ if (sorted.length === 0) {
15
+ return {
16
+ filePath,
17
+ totalCommits: 0,
18
+ totalSpanDays: 0,
19
+ epochs: [],
20
+ health: { rewriteRatio: 0, firefightRatio: 0, polishRatio: 0 },
21
+ };
22
+ }
23
+ const flavorOf = (c) => {
24
+ const text = `${c.subject}\n${c.body || ""}`;
25
+ const fc = changes.get(c.hash);
26
+ const churn = fc ? fc.insertions + fc.deletions : 0;
27
+ if (REWRITE_RE.test(text) || churn >= rewriteChurnLines)
28
+ return "rewrite";
29
+ if (FIRE_RE.test(text))
30
+ return "firefight";
31
+ if (POLISH_RE.test(text) || churn <= 5)
32
+ return "polish";
33
+ return "evolution";
34
+ };
35
+ const epochs = [];
36
+ let buf = [sorted[0]];
37
+ let bufFlavor = sorted[0].parents.length === 0 || epochs.length === 0 ? "birth" : flavorOf(sorted[0]);
38
+ // birth applies only to the very first commit
39
+ bufFlavor = "birth";
40
+ let prevDate = new Date(sorted[0].authorDate).getTime();
41
+ const flush = (kind) => {
42
+ const commitsInEpoch = buf;
43
+ if (commitsInEpoch.length === 0)
44
+ return;
45
+ let ins = 0;
46
+ let del = 0;
47
+ for (const c of commitsInEpoch) {
48
+ const fc = changes.get(c.hash);
49
+ if (fc) {
50
+ ins += fc.insertions;
51
+ del += fc.deletions;
52
+ }
53
+ }
54
+ const fromDate = commitsInEpoch[0].authorDate.slice(0, 10);
55
+ const toDate = commitsInEpoch[commitsInEpoch.length - 1].authorDate.slice(0, 10);
56
+ const spanDays = Math.max(0, Math.round((new Date(toDate).getTime() - new Date(fromDate).getTime()) / 86_400_000));
57
+ const defining = pickDefiningCommit(commitsInEpoch, kind);
58
+ epochs.push({
59
+ kind,
60
+ index: epochs.length + 1,
61
+ commits: commitsInEpoch,
62
+ defining,
63
+ fromDate,
64
+ toDate,
65
+ insertions: ins,
66
+ deletions: del,
67
+ spanDays,
68
+ label: labelEpoch(kind, defining, ins + del),
69
+ });
70
+ };
71
+ for (let i = 1; i < sorted.length; i++) {
72
+ const c = sorted[i];
73
+ const cTime = new Date(c.authorDate).getTime();
74
+ const gapDays = (cTime - prevDate) / 86_400_000;
75
+ const flavor = flavorOf(c);
76
+ // Long quiet gap → emit plateau epoch covering the gap
77
+ if (gapDays >= plateauDays) {
78
+ flush(bufFlavor);
79
+ epochs.push({
80
+ kind: "plateau",
81
+ index: epochs.length + 1,
82
+ commits: [],
83
+ defining: buf[buf.length - 1],
84
+ fromDate: buf[buf.length - 1].authorDate.slice(0, 10),
85
+ toDate: c.authorDate.slice(0, 10),
86
+ insertions: 0,
87
+ deletions: 0,
88
+ spanDays: Math.round(gapDays),
89
+ label: `quiet stretch — ${Math.round(gapDays)} days untouched`,
90
+ });
91
+ buf = [c];
92
+ bufFlavor = flavor;
93
+ }
94
+ else if (flavor !== bufFlavor) {
95
+ // flavor change → close current epoch, start new one
96
+ flush(bufFlavor);
97
+ buf = [c];
98
+ bufFlavor = flavor;
99
+ }
100
+ else {
101
+ buf.push(c);
102
+ }
103
+ prevDate = cTime;
104
+ }
105
+ flush(bufFlavor);
106
+ // Mark trailing plateau as "twilight" if file is quiet at end (>plateauDays
107
+ // since last touch).
108
+ const last = epochs[epochs.length - 1];
109
+ if (last && last.kind === "plateau" && last.spanDays >= plateauDays * 2) {
110
+ last.kind = "twilight";
111
+ last.label = `twilight — ${last.spanDays} days since last touch`;
112
+ }
113
+ // Compute health ratios
114
+ const totalCommits = sorted.length;
115
+ let rewrite = 0;
116
+ let firefight = 0;
117
+ let polish = 0;
118
+ for (const e of epochs) {
119
+ if (e.kind === "rewrite")
120
+ rewrite += e.commits.length;
121
+ else if (e.kind === "firefight")
122
+ firefight += e.commits.length;
123
+ else if (e.kind === "polish" || e.kind === "plateau")
124
+ polish += e.commits.length;
125
+ }
126
+ const totalSpanDays = Math.max(0, Math.round((new Date(sorted[sorted.length - 1].authorDate).getTime() -
127
+ new Date(sorted[0].authorDate).getTime()) /
128
+ 86_400_000));
129
+ return {
130
+ filePath,
131
+ totalCommits,
132
+ totalSpanDays,
133
+ epochs,
134
+ health: {
135
+ rewriteRatio: totalCommits === 0 ? 0 : rewrite / totalCommits,
136
+ firefightRatio: totalCommits === 0 ? 0 : firefight / totalCommits,
137
+ polishRatio: totalCommits === 0 ? 0 : polish / totalCommits,
138
+ },
139
+ };
140
+ }
141
+ function pickDefiningCommit(commits, kind) {
142
+ if (commits.length === 0)
143
+ throw new Error("empty epoch");
144
+ if (kind === "birth")
145
+ return commits[0];
146
+ if (kind === "rewrite") {
147
+ // longest body / most descriptive subject wins
148
+ return [...commits].sort((a, b) => (b.body?.length ?? 0) + b.subject.length - ((a.body?.length ?? 0) + a.subject.length))[0];
149
+ }
150
+ // For evolution/polish/firefight, pick the commit whose subject is longest
151
+ // (typically more informative than "fix typo")
152
+ return [...commits].sort((a, b) => b.subject.length - a.subject.length)[0];
153
+ }
154
+ function labelEpoch(kind, defining, churn) {
155
+ switch (kind) {
156
+ case "birth":
157
+ return `born — "${truncate(defining.subject, 60)}"`;
158
+ case "rewrite":
159
+ return `rewrite — "${truncate(defining.subject, 60)}" (${churn} lines)`;
160
+ case "evolution":
161
+ return `evolution — "${truncate(defining.subject, 60)}"`;
162
+ case "firefight":
163
+ return `firefight — "${truncate(defining.subject, 60)}"`;
164
+ case "polish":
165
+ return `polish — "${truncate(defining.subject, 60)}"`;
166
+ case "plateau":
167
+ return `plateau — quiet`;
168
+ case "twilight":
169
+ return `twilight — possibly abandoned`;
170
+ }
171
+ }
172
+ function truncate(s, n) {
173
+ if (s.length <= n)
174
+ return s;
175
+ return s.slice(0, n - 1).trimEnd() + "…";
176
+ }
177
+ //# sourceMappingURL=time-machine.js.map