@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.
- 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 +3 -0
- package/dist/insights/index.d.ts.map +1 -1
- package/dist/insights/index.js +3 -0
- package/dist/insights/index.js.map +1 -1
- 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/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/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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mneme ghost` — surface "ghost code": files that haunt the repo without
|
|
3
|
+
* doing anything. Started but never finished, declared but never imported,
|
|
4
|
+
* touched once a year ago and ignored ever since.
|
|
5
|
+
*
|
|
6
|
+
* Ghosts are dangerous because they:
|
|
7
|
+
* - Add maintenance surface that nobody knows is unused
|
|
8
|
+
* - Mislead readers who assume "if it's here, it must matter"
|
|
9
|
+
* - Anchor obsolete patterns ("we use X here, so we must keep using X")
|
|
10
|
+
*
|
|
11
|
+
* What makes this novel:
|
|
12
|
+
* - Combines staleness, churn pattern, and TODO-density into a single
|
|
13
|
+
* "ghostliness" score, instead of just "files not edited recently".
|
|
14
|
+
* - Detects abandoned half-finished work via TODO/FIXME density + low
|
|
15
|
+
* recent commit activity.
|
|
16
|
+
* - Surfaces stale TODOs (added long ago, ignored ever since) — these
|
|
17
|
+
* are individual ghost markers, not just file-level signals.
|
|
18
|
+
*
|
|
19
|
+
* Pure data extraction. The CLI renders.
|
|
20
|
+
*/
|
|
21
|
+
import type { Commit, FileChange } from "../types.js";
|
|
22
|
+
export interface GhostFile {
|
|
23
|
+
path: string;
|
|
24
|
+
/** Days since the file was last modified by any commit. */
|
|
25
|
+
daysSinceLastTouch: number;
|
|
26
|
+
/** Number of commits that ever touched the file. */
|
|
27
|
+
totalCommits: number;
|
|
28
|
+
/** Most recent commit subject — useful clue. */
|
|
29
|
+
lastCommitSubject: string;
|
|
30
|
+
/** Lines of TODO/FIXME in the file when it was last seen (estimated). */
|
|
31
|
+
todoCount: number;
|
|
32
|
+
/**
|
|
33
|
+
* Ghostliness 0..1 — higher = more haunted.
|
|
34
|
+
* - boosted by staleness (recency-decay)
|
|
35
|
+
* - boosted by low total commits (born and forgotten)
|
|
36
|
+
* - boosted by high TODO density
|
|
37
|
+
* - dampened if many recent commits (active = not a ghost)
|
|
38
|
+
*/
|
|
39
|
+
ghostliness: number;
|
|
40
|
+
/** Why this file looks like a ghost — short label. */
|
|
41
|
+
reason: string;
|
|
42
|
+
}
|
|
43
|
+
export interface StaleTodo {
|
|
44
|
+
/** Commit that introduced the TODO. */
|
|
45
|
+
introducedIn: Commit;
|
|
46
|
+
/** Days since the TODO was added. */
|
|
47
|
+
ageDays: number;
|
|
48
|
+
/** The TODO text (subject of the introducing commit). */
|
|
49
|
+
hint: string;
|
|
50
|
+
/** Times the file was touched after the TODO without removing it. */
|
|
51
|
+
ignoredCount: number;
|
|
52
|
+
/** File path containing the TODO. */
|
|
53
|
+
filePath: string;
|
|
54
|
+
}
|
|
55
|
+
export interface GhostReport {
|
|
56
|
+
ghostFiles: GhostFile[];
|
|
57
|
+
staleTodos: StaleTodo[];
|
|
58
|
+
/** Total files analyzed. */
|
|
59
|
+
totalFiles: number;
|
|
60
|
+
/** Average ghostliness across files (0..1). */
|
|
61
|
+
averageGhostliness: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a ghost report from commits + per-commit file changes + per-file
|
|
65
|
+
* TODO counts. Caller provides:
|
|
66
|
+
* - commits sorted (any order — we sort)
|
|
67
|
+
* - changes: list of FileChange across all commits
|
|
68
|
+
* - todoCounts: optional Map<filePath, number> of TODO/FIXME counts
|
|
69
|
+
*
|
|
70
|
+
* If `todoCounts` is omitted, TODO inference falls back to scanning commit
|
|
71
|
+
* subjects for TODO/FIXME mentions.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildGhostReport(commits: Commit[], changes: FileChange[], opts?: {
|
|
74
|
+
todoCounts?: Map<string, number>;
|
|
75
|
+
nowMs?: number;
|
|
76
|
+
staleDays?: number;
|
|
77
|
+
todoStaleDays?: number;
|
|
78
|
+
minGhostliness?: number;
|
|
79
|
+
}): GhostReport;
|
|
80
|
+
//# sourceMappingURL=ghost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost.d.ts","sourceRoot":"","sources":["../../src/insights/ghost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAID;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,UAAU,EAAE,EACrB,IAAI,GAAE;IACJ,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;CACpB,GACL,WAAW,CAgHb"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const TODO_RE = /\b(TODO|FIXME|XXX|HACK|TBD)\b/i;
|
|
2
|
+
/**
|
|
3
|
+
* Build a ghost report from commits + per-commit file changes + per-file
|
|
4
|
+
* TODO counts. Caller provides:
|
|
5
|
+
* - commits sorted (any order — we sort)
|
|
6
|
+
* - changes: list of FileChange across all commits
|
|
7
|
+
* - todoCounts: optional Map<filePath, number> of TODO/FIXME counts
|
|
8
|
+
*
|
|
9
|
+
* If `todoCounts` is omitted, TODO inference falls back to scanning commit
|
|
10
|
+
* subjects for TODO/FIXME mentions.
|
|
11
|
+
*/
|
|
12
|
+
export function buildGhostReport(commits, changes, opts = {}) {
|
|
13
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
14
|
+
const staleDays = opts.staleDays ?? 180;
|
|
15
|
+
const todoStaleDays = opts.todoStaleDays ?? 90;
|
|
16
|
+
const minGhostliness = opts.minGhostliness ?? 0.4;
|
|
17
|
+
const byPath = new Map();
|
|
18
|
+
const commitByHash = new Map();
|
|
19
|
+
for (const c of commits)
|
|
20
|
+
commitByHash.set(c.hash, c);
|
|
21
|
+
for (const ch of changes) {
|
|
22
|
+
const c = commitByHash.get(ch.commitHash);
|
|
23
|
+
if (!c)
|
|
24
|
+
continue;
|
|
25
|
+
const t = new Date(c.authorDate).getTime();
|
|
26
|
+
const cur = byPath.get(ch.path);
|
|
27
|
+
if (!cur) {
|
|
28
|
+
byPath.set(ch.path, {
|
|
29
|
+
path: ch.path,
|
|
30
|
+
lastTouchMs: t,
|
|
31
|
+
firstTouchMs: t,
|
|
32
|
+
totalCommits: 1,
|
|
33
|
+
lastCommit: c,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
cur.totalCommits += 1;
|
|
38
|
+
if (t > cur.lastTouchMs) {
|
|
39
|
+
cur.lastTouchMs = t;
|
|
40
|
+
cur.lastCommit = c;
|
|
41
|
+
}
|
|
42
|
+
if (t < cur.firstTouchMs)
|
|
43
|
+
cur.firstTouchMs = t;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const ghostFiles = [];
|
|
47
|
+
let totalGhost = 0;
|
|
48
|
+
for (const stat of byPath.values()) {
|
|
49
|
+
const daysSinceLastTouch = (nowMs - stat.lastTouchMs) / 86_400_000;
|
|
50
|
+
const todoCount = opts.todoCounts?.get(stat.path) ?? 0;
|
|
51
|
+
const ghostliness = computeGhostliness(daysSinceLastTouch, stat.totalCommits, todoCount, staleDays);
|
|
52
|
+
totalGhost += ghostliness;
|
|
53
|
+
if (ghostliness >= minGhostliness) {
|
|
54
|
+
ghostFiles.push({
|
|
55
|
+
path: stat.path,
|
|
56
|
+
daysSinceLastTouch: Math.round(daysSinceLastTouch),
|
|
57
|
+
totalCommits: stat.totalCommits,
|
|
58
|
+
lastCommitSubject: stat.lastCommit.subject,
|
|
59
|
+
todoCount,
|
|
60
|
+
ghostliness: Number(ghostliness.toFixed(2)),
|
|
61
|
+
reason: explainGhost(daysSinceLastTouch, stat.totalCommits, todoCount, staleDays),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
ghostFiles.sort((a, b) => b.ghostliness - a.ghostliness);
|
|
66
|
+
// Detect stale TODOs from commit history
|
|
67
|
+
const staleTodos = [];
|
|
68
|
+
// Group commits by file: who introduced a TODO mention?
|
|
69
|
+
const fileTouchOrder = new Map();
|
|
70
|
+
for (const ch of changes) {
|
|
71
|
+
const c = commitByHash.get(ch.commitHash);
|
|
72
|
+
if (!c)
|
|
73
|
+
continue;
|
|
74
|
+
const arr = fileTouchOrder.get(ch.path) ?? [];
|
|
75
|
+
arr.push(c);
|
|
76
|
+
fileTouchOrder.set(ch.path, arr);
|
|
77
|
+
}
|
|
78
|
+
for (const [path, touches] of fileTouchOrder) {
|
|
79
|
+
touches.sort((a, b) => a.authorDate.localeCompare(b.authorDate));
|
|
80
|
+
let firstTodoCommit = null;
|
|
81
|
+
let ignoredCount = 0;
|
|
82
|
+
for (const c of touches) {
|
|
83
|
+
const text = `${c.subject}\n${c.body || ""}`;
|
|
84
|
+
if (firstTodoCommit) {
|
|
85
|
+
ignoredCount += 1;
|
|
86
|
+
}
|
|
87
|
+
else if (TODO_RE.test(text)) {
|
|
88
|
+
firstTodoCommit = c;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (firstTodoCommit) {
|
|
92
|
+
const ageDays = (nowMs - new Date(firstTodoCommit.authorDate).getTime()) / 86_400_000;
|
|
93
|
+
if (ageDays >= todoStaleDays && ignoredCount >= 1) {
|
|
94
|
+
staleTodos.push({
|
|
95
|
+
introducedIn: firstTodoCommit,
|
|
96
|
+
ageDays: Math.round(ageDays),
|
|
97
|
+
hint: firstTodoCommit.subject,
|
|
98
|
+
ignoredCount,
|
|
99
|
+
filePath: path,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
staleTodos.sort((a, b) => b.ageDays - a.ageDays);
|
|
105
|
+
return {
|
|
106
|
+
ghostFiles,
|
|
107
|
+
staleTodos,
|
|
108
|
+
totalFiles: byPath.size,
|
|
109
|
+
averageGhostliness: byPath.size === 0 ? 0 : totalGhost / byPath.size,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function computeGhostliness(daysSinceLastTouch, totalCommits, todoCount, staleDays) {
|
|
113
|
+
// staleness: 0 at 0 days, → 1 as ratio approaches 2x staleDays
|
|
114
|
+
const stale = Math.min(1, daysSinceLastTouch / (staleDays * 2));
|
|
115
|
+
// low-touch boost: peaks when totalCommits ≤ 2
|
|
116
|
+
const lowTouch = totalCommits <= 1 ? 0.3 : totalCommits <= 3 ? 0.15 : 0;
|
|
117
|
+
// todo-density: each todo adds 0.05, capped at 0.25
|
|
118
|
+
const todoBoost = Math.min(0.25, todoCount * 0.05);
|
|
119
|
+
// recent activity dampener: many commits → not a ghost
|
|
120
|
+
const damp = totalCommits >= 10 ? 0.2 : 0;
|
|
121
|
+
return Math.max(0, Math.min(1, stale + lowTouch + todoBoost - damp));
|
|
122
|
+
}
|
|
123
|
+
function explainGhost(daysSinceLastTouch, totalCommits, todoCount, staleDays) {
|
|
124
|
+
if (daysSinceLastTouch >= staleDays * 2 && totalCommits <= 2) {
|
|
125
|
+
return `born and forgotten — ${Math.round(daysSinceLastTouch)}d untouched, only ${totalCommits} commit${totalCommits === 1 ? "" : "s"} ever`;
|
|
126
|
+
}
|
|
127
|
+
if (daysSinceLastTouch >= staleDays * 2) {
|
|
128
|
+
return `long-untouched — ${Math.round(daysSinceLastTouch)}d since last edit`;
|
|
129
|
+
}
|
|
130
|
+
if (todoCount >= 3) {
|
|
131
|
+
return `dense with TODOs — ${todoCount} marker${todoCount === 1 ? "" : "s"} pending`;
|
|
132
|
+
}
|
|
133
|
+
if (totalCommits === 1) {
|
|
134
|
+
return `one-shot file — added once, never revisited`;
|
|
135
|
+
}
|
|
136
|
+
return `stale — ${Math.round(daysSinceLastTouch)}d quiet`;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=ghost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost.js","sourceRoot":"","sources":["../../src/insights/ghost.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost.test.d.ts","sourceRoot":"","sources":["../../src/insights/ghost.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildGhostReport } from "./ghost.js";
|
|
3
|
+
const NOW = new Date("2026-05-05").getTime();
|
|
4
|
+
function mk(p) {
|
|
5
|
+
return {
|
|
6
|
+
hash: p.hash,
|
|
7
|
+
shortHash: p.hash.slice(0, 7),
|
|
8
|
+
authorName: "Test",
|
|
9
|
+
authorEmail: "t@e.com",
|
|
10
|
+
authorDate: p.date,
|
|
11
|
+
committerDate: p.date,
|
|
12
|
+
subject: p.subject,
|
|
13
|
+
body: p.body ?? "",
|
|
14
|
+
files: p.files ?? [],
|
|
15
|
+
parents: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function mkChange(commitHash, path) {
|
|
19
|
+
return { commitHash, path, changeKind: "M", insertions: 5, deletions: 0 };
|
|
20
|
+
}
|
|
21
|
+
describe("buildGhostReport", () => {
|
|
22
|
+
it("returns empty when no commits or changes", () => {
|
|
23
|
+
const r = buildGhostReport([], [], { nowMs: NOW });
|
|
24
|
+
expect(r.ghostFiles).toHaveLength(0);
|
|
25
|
+
expect(r.staleTodos).toHaveLength(0);
|
|
26
|
+
expect(r.totalFiles).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
it("identifies a long-untouched file as a ghost", () => {
|
|
29
|
+
const c = mk({ hash: "a1", date: "2024-01-01", subject: "add legacy thing" });
|
|
30
|
+
const ch = [mkChange("a1", "src/legacy.ts")];
|
|
31
|
+
const r = buildGhostReport([c], ch, { nowMs: NOW, staleDays: 180, minGhostliness: 0.4 });
|
|
32
|
+
const ghost = r.ghostFiles.find((g) => g.path === "src/legacy.ts");
|
|
33
|
+
expect(ghost).toBeDefined();
|
|
34
|
+
expect(ghost.daysSinceLastTouch).toBeGreaterThan(180);
|
|
35
|
+
});
|
|
36
|
+
it("does not flag actively-edited files as ghosts", () => {
|
|
37
|
+
const commits = Array.from({ length: 12 }, (_, i) => mk({
|
|
38
|
+
hash: `c${i}`,
|
|
39
|
+
date: `2026-04-${(i + 1).toString().padStart(2, "0")}`,
|
|
40
|
+
subject: `evolve module`,
|
|
41
|
+
files: ["src/active.ts"],
|
|
42
|
+
}));
|
|
43
|
+
const changes = commits.map((c) => mkChange(c.hash, "src/active.ts"));
|
|
44
|
+
const r = buildGhostReport(commits, changes, { nowMs: NOW, minGhostliness: 0.4 });
|
|
45
|
+
const ghost = r.ghostFiles.find((g) => g.path === "src/active.ts");
|
|
46
|
+
expect(ghost).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
it("boosts ghostliness via TODO density", () => {
|
|
49
|
+
const c1 = mk({ hash: "a1", date: "2024-01-01", subject: "scaffold" });
|
|
50
|
+
const c2 = mk({ hash: "a2", date: "2024-01-15", subject: "TODO: finish error handling" });
|
|
51
|
+
const ch = [mkChange("a1", "src/half.ts"), mkChange("a2", "src/half.ts")];
|
|
52
|
+
const r = buildGhostReport([c1, c2], ch, {
|
|
53
|
+
nowMs: NOW,
|
|
54
|
+
staleDays: 180,
|
|
55
|
+
todoCounts: new Map([["src/half.ts", 5]]),
|
|
56
|
+
minGhostliness: 0.4,
|
|
57
|
+
});
|
|
58
|
+
const ghost = r.ghostFiles.find((g) => g.path === "src/half.ts");
|
|
59
|
+
expect(ghost).toBeDefined();
|
|
60
|
+
expect(ghost.todoCount).toBe(5);
|
|
61
|
+
});
|
|
62
|
+
it("detects stale TODOs from commit history", () => {
|
|
63
|
+
const c1 = mk({ hash: "a1", date: "2024-01-01", subject: "TODO: add validation" });
|
|
64
|
+
const c2 = mk({ hash: "a2", date: "2024-06-01", subject: "minor formatting" });
|
|
65
|
+
const c3 = mk({ hash: "a3", date: "2024-12-01", subject: "another tweak" });
|
|
66
|
+
const ch = [
|
|
67
|
+
mkChange("a1", "src/feat.ts"),
|
|
68
|
+
mkChange("a2", "src/feat.ts"),
|
|
69
|
+
mkChange("a3", "src/feat.ts"),
|
|
70
|
+
];
|
|
71
|
+
const r = buildGhostReport([c1, c2, c3], ch, {
|
|
72
|
+
nowMs: NOW,
|
|
73
|
+
todoStaleDays: 90,
|
|
74
|
+
});
|
|
75
|
+
expect(r.staleTodos.length).toBeGreaterThanOrEqual(1);
|
|
76
|
+
expect(r.staleTodos[0].ignoredCount).toBeGreaterThanOrEqual(1);
|
|
77
|
+
});
|
|
78
|
+
it("ghostliness is bounded to 0..1", () => {
|
|
79
|
+
const commits = [
|
|
80
|
+
mk({ hash: "a1", date: "2020-01-01", subject: "ancient" }),
|
|
81
|
+
];
|
|
82
|
+
const ch = [mkChange("a1", "src/dust.ts")];
|
|
83
|
+
const r = buildGhostReport(commits, ch, {
|
|
84
|
+
nowMs: NOW,
|
|
85
|
+
todoCounts: new Map([["src/dust.ts", 100]]),
|
|
86
|
+
minGhostliness: 0,
|
|
87
|
+
});
|
|
88
|
+
for (const g of r.ghostFiles) {
|
|
89
|
+
expect(g.ghostliness).toBeGreaterThanOrEqual(0);
|
|
90
|
+
expect(g.ghostliness).toBeLessThanOrEqual(1);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
it("orders ghostFiles by ghostliness descending", () => {
|
|
94
|
+
const commits = [
|
|
95
|
+
mk({ hash: "old", date: "2020-01-01", subject: "very old" }),
|
|
96
|
+
mk({ hash: "med", date: "2024-08-01", subject: "medium age" }),
|
|
97
|
+
mk({ hash: "fresh", date: "2026-04-01", subject: "fresh" }),
|
|
98
|
+
];
|
|
99
|
+
const ch = [
|
|
100
|
+
mkChange("old", "src/old.ts"),
|
|
101
|
+
mkChange("med", "src/med.ts"),
|
|
102
|
+
mkChange("fresh", "src/fresh.ts"),
|
|
103
|
+
];
|
|
104
|
+
const r = buildGhostReport(commits, ch, {
|
|
105
|
+
nowMs: NOW,
|
|
106
|
+
staleDays: 180,
|
|
107
|
+
minGhostliness: 0,
|
|
108
|
+
});
|
|
109
|
+
for (let i = 1; i < r.ghostFiles.length; i++) {
|
|
110
|
+
expect(r.ghostFiles[i - 1].ghostliness).toBeGreaterThanOrEqual(r.ghostFiles[i].ghostliness);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
it("reports averageGhostliness across all files", () => {
|
|
114
|
+
const commits = [
|
|
115
|
+
mk({ hash: "a1", date: "2020-01-01", subject: "ancient" }),
|
|
116
|
+
mk({ hash: "a2", date: "2026-04-01", subject: "fresh" }),
|
|
117
|
+
];
|
|
118
|
+
const ch = [mkChange("a1", "src/a.ts"), mkChange("a2", "src/b.ts")];
|
|
119
|
+
const r = buildGhostReport(commits, ch, { nowMs: NOW, minGhostliness: 0 });
|
|
120
|
+
expect(r.averageGhostliness).toBeGreaterThan(0);
|
|
121
|
+
expect(r.averageGhostliness).toBeLessThanOrEqual(1);
|
|
122
|
+
});
|
|
123
|
+
it("produces a non-empty reason for every ghost", () => {
|
|
124
|
+
const commits = [
|
|
125
|
+
mk({ hash: "a1", date: "2020-01-01", subject: "ancient" }),
|
|
126
|
+
];
|
|
127
|
+
const ch = [mkChange("a1", "src/dusty.ts")];
|
|
128
|
+
const r = buildGhostReport(commits, ch, { nowMs: NOW, minGhostliness: 0 });
|
|
129
|
+
for (const g of r.ghostFiles) {
|
|
130
|
+
expect(g.reason.length).toBeGreaterThan(0);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
it("does not flag stale TODO unless ignored at least once", () => {
|
|
134
|
+
const c1 = mk({ hash: "a1", date: "2020-01-01", subject: "TODO: clean this up" });
|
|
135
|
+
const ch = [mkChange("a1", "src/only.ts")];
|
|
136
|
+
const r = buildGhostReport([c1], ch, { nowMs: NOW, todoStaleDays: 90 });
|
|
137
|
+
// The TODO commit is the only touch — ignoredCount = 0, should not appear
|
|
138
|
+
expect(r.staleTodos.find((t) => t.filePath === "src/only.ts")).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=ghost.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost.test.js","sourceRoot":"","sources":["../../src/insights/ghost.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC;AAE7C,SAAS,EAAE,CAAC,CAAmF;IAC7F,OAAO;QACL,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7B,UAAU,EAAE,MAAM;QAClB,WAAW,EAAE,SAAS;QACtB,UAAU,EAAE,CAAC,CAAC,IAAI;QAClB,aAAa,EAAE,CAAC,CAAC,IAAI;QACrB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;QAClB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;QACpB,OAAO,EAAE,EAAE;KACZ,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,UAAkB,EAAE,IAAY;IAChD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;AAC5E,CAAC;AAED,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,GAAG,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QACnD,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAC9E,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC;QAC7C,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;QACzF,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,eAAe,CAAC,CAAC;QACnE,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAC5B,MAAM,CAAC,KAAM,CAAC,kBAAkB,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAClD,EAAE,CAAC;YACD,IAAI,EAAE,IAAI,CAAC,EAAE;YACb,IAAI,EAAE,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;YACtD,OAAO,EAAE,eAAe;YACxB,KAAK,EAAE,CAAC,eAAe,CAAC;SACzB,CAAC,CACH,CAAC;QACF,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC;QACtE,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;QAClF,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,eAAe,CAAC,CAAC;QACnE,MAAM,CAAC,KAAK,CAAC,CAAC,aAAa,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;QACvE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC,CAAC;QAC1F,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC;QAC1E,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE;YACvC,KAAK,EAAE,GAAG;YACV,SAAS,EAAE,GAAG;YACd,UAAU,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC;YACzC,cAAc,EAAE,GAAG;SACpB,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,CAAC;QACjE,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAC5B,MAAM,CAAC,KAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC,CAAC;QACnF,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,CAAC;QAC/E,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;QAC5E,MAAM,EAAE,GAAG;YACT,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAC7B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAC7B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;SAC9B,CAAC;QACF,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE;YAC3C,KAAK,EAAE,GAAG;YACV,aAAa,EAAE,EAAE;SAClB,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAE,CAAC,YAAY,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,OAAO,GAAG;YACd,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;SAC3D,CAAC;QACF,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,EAAE,EAAE,EAAE;YACtC,KAAK,EAAE,GAAG;YACV,UAAU,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC;YAC3C,cAAc,EAAE,CAAC;SAClB,CAAC,CAAC;QACH,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;YAC7B,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;YAChD,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,OAAO,GAAG;YACd,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;YAC5D,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;YAC9D,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;SAC5D,CAAC;QACF,MAAM,EAAE,GAAG;YACT,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;YAC7B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;YAC7B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;SAClC,CAAC;QACF,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,EAAE,EAAE,EAAE;YACtC,KAAK,EAAE,GAAG;YACV,SAAS,EAAE,GAAG;YACd,cAAc,EAAE,CAAC;SAClB,CAAC,CAAC;QACH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7C,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,WAAW,CAAC,CAAC,sBAAsB,CAC7D,CAAC,CAAC,UAAU,CAAC,CAAC,CAAE,CAAC,WAAW,CAC7B,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,OAAO,GAAG;YACd,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;YAC1D,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;SACzD,CAAC;QACF,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;QACpE,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC;QAC3E,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAChD,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,OAAO,GAAG;YACd,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;SAC3D,CAAC;QACF,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC;QAC5C,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC;QAC3E,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;YAC7B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC;QAClF,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC,CAAC;QACxE,0EAA0E;QAC1E,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IACjF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/insights/index.d.ts
CHANGED
|
@@ -18,4 +18,7 @@ export * from "./bus-factor.js";
|
|
|
18
18
|
export * from "./paradox.js";
|
|
19
19
|
export * from "./commit-coach.js";
|
|
20
20
|
export * from "./crystal-ball.js";
|
|
21
|
+
export * from "./time-machine.js";
|
|
22
|
+
export * from "./premortem.js";
|
|
23
|
+
export * from "./ghost.js";
|
|
21
24
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/insights/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/insights/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
package/dist/insights/index.js
CHANGED
|
@@ -18,4 +18,7 @@ export * from "./bus-factor.js";
|
|
|
18
18
|
export * from "./paradox.js";
|
|
19
19
|
export * from "./commit-coach.js";
|
|
20
20
|
export * from "./crystal-ball.js";
|
|
21
|
+
export * from "./time-machine.js";
|
|
22
|
+
export * from "./premortem.js";
|
|
23
|
+
export * from "./ghost.js";
|
|
21
24
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/insights/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/insights/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mneme premortem <intent>` — predict what will go wrong with a proposed
|
|
3
|
+
* change *before* you write it, by mining your repo's history of similar
|
|
4
|
+
* past attempts and what happened to them.
|
|
5
|
+
*
|
|
6
|
+
* Premortem inverts the postmortem: instead of "what went wrong?", we ask
|
|
7
|
+
* "what is likely to go wrong, given what's gone wrong before in this repo
|
|
8
|
+
* when we tried something like this?"
|
|
9
|
+
*
|
|
10
|
+
* What makes this novel:
|
|
11
|
+
* - Most "AI coding" tools give generic advice ("watch out for race
|
|
12
|
+
* conditions"). Premortem gives advice grounded in YOUR repo's actual
|
|
13
|
+
* failure history.
|
|
14
|
+
* - Estimates a regret probability based on hit rate of similar past
|
|
15
|
+
* intents that ended in revert/hotfix/incident.
|
|
16
|
+
* - Surfaces 3 concrete risks with citations to the past commits that
|
|
17
|
+
* caused them — actionable, not abstract.
|
|
18
|
+
*
|
|
19
|
+
* Pure data extraction — no LLM. The CLI renders.
|
|
20
|
+
*/
|
|
21
|
+
import type { Commit } from "../types.js";
|
|
22
|
+
export type RiskKind = "revert" | "hotfix" | "incident" | "rewrite";
|
|
23
|
+
export interface PastAttempt {
|
|
24
|
+
/** The commit that introduced the past attempt. */
|
|
25
|
+
attempt: Commit;
|
|
26
|
+
/** The follow-up that signals regret (if any). */
|
|
27
|
+
regret?: Commit;
|
|
28
|
+
/** What went wrong, classified by signal in the regret commit. */
|
|
29
|
+
riskKind: RiskKind | "none";
|
|
30
|
+
/** Days from attempt to regret (or 0 if no regret). */
|
|
31
|
+
daysToRegret: number;
|
|
32
|
+
/** Similarity score of this past attempt to the current intent (0..1). */
|
|
33
|
+
similarity: number;
|
|
34
|
+
}
|
|
35
|
+
export interface Risk {
|
|
36
|
+
/** Short label e.g. "cache invalidation regression". */
|
|
37
|
+
label: string;
|
|
38
|
+
/** Specific commits that exhibited this risk. */
|
|
39
|
+
evidence: Commit[];
|
|
40
|
+
/** How often this risk fired, normalized 0..1 (capped at 1). */
|
|
41
|
+
weight: number;
|
|
42
|
+
}
|
|
43
|
+
export interface PremortemResult {
|
|
44
|
+
intent: string;
|
|
45
|
+
/** Past attempts in this repo that look like the proposed intent. */
|
|
46
|
+
pastAttempts: PastAttempt[];
|
|
47
|
+
/**
|
|
48
|
+
* Probability that this kind of change will be regretted, computed as
|
|
49
|
+
* regret_count / total_similar_attempts. Range 0..1.
|
|
50
|
+
*/
|
|
51
|
+
regretProbability: number;
|
|
52
|
+
/** Top concrete risks distilled from past attempts. */
|
|
53
|
+
topRisks: Risk[];
|
|
54
|
+
/** Verdict label — "low / medium / high / very_high". */
|
|
55
|
+
verdict: "low" | "medium" | "high" | "very_high";
|
|
56
|
+
/** Short verdict prose. */
|
|
57
|
+
summary: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Score similarity between an intent string and a commit, by token overlap
|
|
61
|
+
* + a small bonus for files-mentioned matching commit-touched files.
|
|
62
|
+
*/
|
|
63
|
+
export declare function scoreSimilarity(intent: string, c: Commit): number;
|
|
64
|
+
/**
|
|
65
|
+
* Build a premortem from an intent string and the commit history. Caller
|
|
66
|
+
* passes the full commit history (newest first or oldest first — we sort).
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildPremortem(intent: string, commits: Commit[], opts?: {
|
|
69
|
+
similarityFloor?: number;
|
|
70
|
+
windowDays?: number;
|
|
71
|
+
maxAttempts?: number;
|
|
72
|
+
}): PremortemResult;
|
|
73
|
+
//# sourceMappingURL=premortem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"premortem.d.ts","sourceRoot":"","sources":["../../src/insights/premortem.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,IAAI;IACnB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B;;;OAGG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjB,yDAAyD;IACzD,OAAO,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IACjD,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAkBjE;AAsBD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EAAE,EACjB,IAAI,GAAE;IACJ,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACjB,GACL,eAAe,CA8GjB"}
|