@mneme-ai/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/insights/bus-factor.d.ts +58 -0
- package/dist/insights/bus-factor.d.ts.map +1 -0
- package/dist/insights/bus-factor.js +117 -0
- package/dist/insights/bus-factor.js.map +1 -0
- package/dist/insights/bus-factor.test.d.ts +2 -0
- package/dist/insights/bus-factor.test.d.ts.map +1 -0
- package/dist/insights/bus-factor.test.js +149 -0
- package/dist/insights/bus-factor.test.js.map +1 -0
- package/dist/insights/commit-coach.d.ts +80 -0
- package/dist/insights/commit-coach.d.ts.map +1 -0
- package/dist/insights/commit-coach.js +230 -0
- package/dist/insights/commit-coach.js.map +1 -0
- package/dist/insights/commit-coach.test.d.ts +2 -0
- package/dist/insights/commit-coach.test.d.ts.map +1 -0
- package/dist/insights/commit-coach.test.js +163 -0
- package/dist/insights/commit-coach.test.js.map +1 -0
- package/dist/insights/crystal-ball.d.ts +76 -0
- package/dist/insights/crystal-ball.d.ts.map +1 -0
- package/dist/insights/crystal-ball.js +219 -0
- package/dist/insights/crystal-ball.js.map +1 -0
- package/dist/insights/crystal-ball.test.d.ts +2 -0
- package/dist/insights/crystal-ball.test.d.ts.map +1 -0
- package/dist/insights/crystal-ball.test.js +157 -0
- package/dist/insights/crystal-ball.test.js.map +1 -0
- package/dist/insights/ghost.d.ts +80 -0
- package/dist/insights/ghost.d.ts.map +1 -0
- package/dist/insights/ghost.js +138 -0
- package/dist/insights/ghost.js.map +1 -0
- package/dist/insights/ghost.test.d.ts +2 -0
- package/dist/insights/ghost.test.d.ts.map +1 -0
- package/dist/insights/ghost.test.js +141 -0
- package/dist/insights/ghost.test.js.map +1 -0
- package/dist/insights/index.d.ts +8 -0
- package/dist/insights/index.d.ts.map +1 -1
- package/dist/insights/index.js +8 -0
- package/dist/insights/index.js.map +1 -1
- package/dist/insights/paradox.d.ts +36 -0
- package/dist/insights/paradox.d.ts.map +1 -0
- package/dist/insights/paradox.js +201 -0
- package/dist/insights/paradox.js.map +1 -0
- package/dist/insights/paradox.test.d.ts +2 -0
- package/dist/insights/paradox.test.d.ts.map +1 -0
- package/dist/insights/paradox.test.js +88 -0
- package/dist/insights/paradox.test.js.map +1 -0
- package/dist/insights/premortem.d.ts +73 -0
- package/dist/insights/premortem.d.ts.map +1 -0
- package/dist/insights/premortem.js +209 -0
- package/dist/insights/premortem.js.map +1 -0
- package/dist/insights/premortem.test.d.ts +2 -0
- package/dist/insights/premortem.test.d.ts.map +1 -0
- package/dist/insights/premortem.test.js +169 -0
- package/dist/insights/premortem.test.js.map +1 -0
- package/dist/insights/regret.d.ts +57 -0
- package/dist/insights/regret.d.ts.map +1 -0
- package/dist/insights/regret.js +137 -0
- package/dist/insights/regret.js.map +1 -0
- package/dist/insights/regret.test.d.ts +2 -0
- package/dist/insights/regret.test.d.ts.map +1 -0
- package/dist/insights/regret.test.js +153 -0
- package/dist/insights/regret.test.js.map +1 -0
- package/dist/insights/time-machine.d.ts +70 -0
- package/dist/insights/time-machine.d.ts.map +1 -0
- package/dist/insights/time-machine.js +177 -0
- package/dist/insights/time-machine.js.map +1 -0
- package/dist/insights/time-machine.test.d.ts +2 -0
- package/dist/insights/time-machine.test.d.ts.map +1 -0
- package/dist/insights/time-machine.test.js +141 -0
- package/dist/insights/time-machine.test.js.map +1 -0
- package/dist/insights/who-knows.d.ts +18 -0
- package/dist/insights/who-knows.d.ts.map +1 -1
- package/dist/insights/who-knows.js +29 -0
- package/dist/insights/who-knows.js.map +1 -1
- package/dist/insights/who-knows.test.js +63 -1
- package/dist/insights/who-knows.test.js.map +1 -1
- package/dist/quant/alpha.d.ts +87 -0
- package/dist/quant/alpha.d.ts.map +1 -0
- package/dist/quant/alpha.js +103 -0
- package/dist/quant/alpha.js.map +1 -0
- package/dist/quant/alpha.test.d.ts +2 -0
- package/dist/quant/alpha.test.d.ts.map +1 -0
- package/dist/quant/alpha.test.js +147 -0
- package/dist/quant/alpha.test.js.map +1 -0
- package/dist/quant/backtest.d.ts +57 -0
- package/dist/quant/backtest.d.ts.map +1 -0
- package/dist/quant/backtest.js +90 -0
- package/dist/quant/backtest.js.map +1 -0
- package/dist/quant/backtest.test.d.ts +2 -0
- package/dist/quant/backtest.test.d.ts.map +1 -0
- package/dist/quant/backtest.test.js +133 -0
- package/dist/quant/backtest.test.js.map +1 -0
- package/dist/quant/black-swan.d.ts +45 -0
- package/dist/quant/black-swan.d.ts.map +1 -0
- package/dist/quant/black-swan.js +112 -0
- package/dist/quant/black-swan.js.map +1 -0
- package/dist/quant/black-swan.test.d.ts +2 -0
- package/dist/quant/black-swan.test.d.ts.map +1 -0
- package/dist/quant/black-swan.test.js +131 -0
- package/dist/quant/black-swan.test.js.map +1 -0
- package/dist/quant/correlation-matrix.d.ts +54 -0
- package/dist/quant/correlation-matrix.d.ts.map +1 -0
- package/dist/quant/correlation-matrix.js +103 -0
- package/dist/quant/correlation-matrix.js.map +1 -0
- package/dist/quant/correlation-matrix.test.d.ts +2 -0
- package/dist/quant/correlation-matrix.test.d.ts.map +1 -0
- package/dist/quant/correlation-matrix.test.js +118 -0
- package/dist/quant/correlation-matrix.test.js.map +1 -0
- package/dist/quant/drawdown.d.ts +51 -0
- package/dist/quant/drawdown.d.ts.map +1 -0
- package/dist/quant/drawdown.js +96 -0
- package/dist/quant/drawdown.js.map +1 -0
- package/dist/quant/drawdown.test.d.ts +2 -0
- package/dist/quant/drawdown.test.d.ts.map +1 -0
- package/dist/quant/drawdown.test.js +166 -0
- package/dist/quant/drawdown.test.js.map +1 -0
- package/dist/quant/greek.d.ts +55 -0
- package/dist/quant/greek.d.ts.map +1 -0
- package/dist/quant/greek.js +157 -0
- package/dist/quant/greek.js.map +1 -0
- package/dist/quant/greek.test.d.ts +2 -0
- package/dist/quant/greek.test.d.ts.map +1 -0
- package/dist/quant/greek.test.js +138 -0
- package/dist/quant/greek.test.js.map +1 -0
- package/dist/quant/implied-volatility.d.ts +65 -0
- package/dist/quant/implied-volatility.d.ts.map +1 -0
- package/dist/quant/implied-volatility.js +149 -0
- package/dist/quant/implied-volatility.js.map +1 -0
- package/dist/quant/implied-volatility.test.d.ts +2 -0
- package/dist/quant/implied-volatility.test.d.ts.map +1 -0
- package/dist/quant/implied-volatility.test.js +127 -0
- package/dist/quant/implied-volatility.test.js.map +1 -0
- package/dist/quant/index.d.ts +28 -0
- package/dist/quant/index.d.ts.map +1 -0
- package/dist/quant/index.js +28 -0
- package/dist/quant/index.js.map +1 -0
- package/dist/quant/insider-trading.d.ts +56 -0
- package/dist/quant/insider-trading.d.ts.map +1 -0
- package/dist/quant/insider-trading.js +129 -0
- package/dist/quant/insider-trading.js.map +1 -0
- package/dist/quant/insider-trading.test.d.ts +2 -0
- package/dist/quant/insider-trading.test.d.ts.map +1 -0
- package/dist/quant/insider-trading.test.js +130 -0
- package/dist/quant/insider-trading.test.js.map +1 -0
- package/dist/quant/moneyball.d.ts +48 -0
- package/dist/quant/moneyball.d.ts.map +1 -0
- package/dist/quant/moneyball.js +110 -0
- package/dist/quant/moneyball.js.map +1 -0
- package/dist/quant/moneyball.test.d.ts +2 -0
- package/dist/quant/moneyball.test.d.ts.map +1 -0
- package/dist/quant/moneyball.test.js +137 -0
- package/dist/quant/moneyball.test.js.map +1 -0
- package/dist/quant/tax-loss-harvest.d.ts +59 -0
- package/dist/quant/tax-loss-harvest.d.ts.map +1 -0
- package/dist/quant/tax-loss-harvest.js +126 -0
- package/dist/quant/tax-loss-harvest.js.map +1 -0
- package/dist/quant/tax-loss-harvest.test.d.ts +2 -0
- package/dist/quant/tax-loss-harvest.test.d.ts.map +1 -0
- package/dist/quant/tax-loss-harvest.test.js +126 -0
- package/dist/quant/tax-loss-harvest.test.js.map +1 -0
- package/dist/retrieve/synthesize.d.ts.map +1 -1
- package/dist/retrieve/synthesize.js +56 -25
- package/dist/retrieve/synthesize.js.map +1 -1
- package/dist/retrieve/synthesize.test.js +26 -15
- package/dist/retrieve/synthesize.test.js.map +1 -1
- package/dist/store/schema.d.ts +2 -2
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +6 -2
- package/dist/store/schema.js.map +1 -1
- package/dist/store/sqlite.d.ts +2 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +24 -0
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sqlite.test.js +1 -1
- package/dist/util/index.d.ts +4 -0
- package/dist/util/index.d.ts.map +1 -1
- package/dist/util/index.js +26 -0
- package/dist/util/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mneme crystal-ball` — predict the probability that the staged change
|
|
3
|
+
* will fail CI, BEFORE you push.
|
|
4
|
+
*
|
|
5
|
+
* Approach (Bayesian-ish over historical signals):
|
|
6
|
+
* 1. Compute a fingerprint of the staged diff:
|
|
7
|
+
* modules touched, file types, add/remove ratio, test changes Y/N.
|
|
8
|
+
* 2. Find historical commits with similar fingerprints.
|
|
9
|
+
* 3. Look at each one's fate within `windowDays`:
|
|
10
|
+
* - revert / hotfix / fix follow-up = "trouble"
|
|
11
|
+
* - else = "clean"
|
|
12
|
+
* 4. Output: prediction p(clean) = clean / total similar.
|
|
13
|
+
*
|
|
14
|
+
* Without integrated CI run history (which Mneme doesn't index by default),
|
|
15
|
+
* follow-up commits are our best proxy: a commit that needed a hotfix is
|
|
16
|
+
* indistinguishable from one that broke CI from the user's perspective.
|
|
17
|
+
*
|
|
18
|
+
* The output is honest about its limits: if `n` is small (< 5), we say
|
|
19
|
+
* "low signal" instead of pretending to be confident.
|
|
20
|
+
*/
|
|
21
|
+
import { parseDiff } from "./commit-coach.js";
|
|
22
|
+
import { detectRegrets } from "./regret.js";
|
|
23
|
+
export function fingerprint(diff) {
|
|
24
|
+
const exts = new Set();
|
|
25
|
+
for (const f of diff.files) {
|
|
26
|
+
const m = f.match(/\.([a-z0-9]+)$/i);
|
|
27
|
+
if (m)
|
|
28
|
+
exts.add(m[1].toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
const totalLines = diff.added + diff.removed;
|
|
31
|
+
const size = totalLines < 20 ? "tiny" : totalLines < 100 ? "small" : totalLines < 500 ? "medium" : "large";
|
|
32
|
+
const hasTests = diff.files.some((f) => /(\.test\.|\.spec\.|\/tests?\/|\/specs?\/)/.test(f));
|
|
33
|
+
return {
|
|
34
|
+
modules: [...new Set(diff.modules)].sort(),
|
|
35
|
+
extensions: [...exts].sort(),
|
|
36
|
+
shape: diff.shape,
|
|
37
|
+
size,
|
|
38
|
+
hasTests,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Score similarity between two fingerprints. Returns 0..1.
|
|
43
|
+
*
|
|
44
|
+
* Each dimension contributes a weight; the score is a weighted average:
|
|
45
|
+
* modules: 0.40 (Jaccard)
|
|
46
|
+
* extensions: 0.20 (Jaccard)
|
|
47
|
+
* shape: 0.20 (1.0 if equal, else 0.0)
|
|
48
|
+
* size: 0.10 (linear distance over 4 buckets)
|
|
49
|
+
* hasTests: 0.10 (1.0 if equal, else 0.0)
|
|
50
|
+
*/
|
|
51
|
+
export function similarity(a, b) {
|
|
52
|
+
const jaccard = (xs, ys) => {
|
|
53
|
+
if (xs.length === 0 && ys.length === 0)
|
|
54
|
+
return 1;
|
|
55
|
+
const A = new Set(xs);
|
|
56
|
+
const B = new Set(ys);
|
|
57
|
+
let inter = 0;
|
|
58
|
+
for (const x of A)
|
|
59
|
+
if (B.has(x))
|
|
60
|
+
inter += 1;
|
|
61
|
+
const union = A.size + B.size - inter;
|
|
62
|
+
return union === 0 ? 0 : inter / union;
|
|
63
|
+
};
|
|
64
|
+
const modules = jaccard(a.modules, b.modules);
|
|
65
|
+
const exts = jaccard(a.extensions, b.extensions);
|
|
66
|
+
const shape = a.shape === b.shape ? 1 : 0;
|
|
67
|
+
const sizes = ["tiny", "small", "medium", "large"];
|
|
68
|
+
const sizeDist = Math.abs(sizes.indexOf(a.size) - sizes.indexOf(b.size));
|
|
69
|
+
const size = 1 - sizeDist / 3;
|
|
70
|
+
const tests = a.hasTests === b.hasTests ? 1 : 0;
|
|
71
|
+
return 0.4 * modules + 0.2 * exts + 0.2 * shape + 0.1 * size + 0.1 * tests;
|
|
72
|
+
}
|
|
73
|
+
const SIMILARITY_THRESHOLD = 0.5;
|
|
74
|
+
/**
|
|
75
|
+
* Predict CI/follow-up trouble probability for the given diff.
|
|
76
|
+
*
|
|
77
|
+
* @param store - the indexed store
|
|
78
|
+
* @param diffText - unified diff text (e.g. `git diff --staged` output)
|
|
79
|
+
* @param windowDays - how long after a past commit do we still count a
|
|
80
|
+
* follow-up as "trouble" (default 14)
|
|
81
|
+
*/
|
|
82
|
+
export function predict(store, diffText, windowDays = 14) {
|
|
83
|
+
const parsed = parseDiff(diffText);
|
|
84
|
+
const fp = fingerprint(parsed);
|
|
85
|
+
// Pull all commits from the store + their files.
|
|
86
|
+
// Only consider recent-enough commits (skip ancient history).
|
|
87
|
+
const commitRows = store.db
|
|
88
|
+
.prepare(`SELECT * FROM commits ORDER BY author_date DESC LIMIT 1000`)
|
|
89
|
+
.all();
|
|
90
|
+
if (commitRows.length === 0) {
|
|
91
|
+
return {
|
|
92
|
+
fingerprint: fp,
|
|
93
|
+
similarN: 0,
|
|
94
|
+
cleanN: 0,
|
|
95
|
+
pClean: 0,
|
|
96
|
+
verdict: "unknown",
|
|
97
|
+
recommendation: "Index the repo first (mneme index) — no historical signal yet.",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const commits = [];
|
|
101
|
+
for (const r of commitRows) {
|
|
102
|
+
const hash = String(r.hash);
|
|
103
|
+
const files = store.db.prepare("SELECT path FROM file_changes WHERE commit_hash = ?").all(hash).map((x) => x.path);
|
|
104
|
+
commits.push({
|
|
105
|
+
hash,
|
|
106
|
+
shortHash: String(r.short_hash),
|
|
107
|
+
authorName: String(r.author_name),
|
|
108
|
+
authorEmail: String(r.author_email),
|
|
109
|
+
authorDate: String(r.author_date),
|
|
110
|
+
committerDate: String(r.committer_date),
|
|
111
|
+
subject: String(r.subject),
|
|
112
|
+
body: String(r.body || ""),
|
|
113
|
+
parents: r.parents ? String(r.parents).split(/\s+/).filter(Boolean) : [],
|
|
114
|
+
files,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Compute similarity for each historical commit.
|
|
118
|
+
const scored = [];
|
|
119
|
+
for (const c of commits) {
|
|
120
|
+
const cFp = fingerprintFromCommit(c);
|
|
121
|
+
const sim = similarity(fp, cFp);
|
|
122
|
+
if (sim >= SIMILARITY_THRESHOLD)
|
|
123
|
+
scored.push({ commit: c, sim, fp: cFp });
|
|
124
|
+
}
|
|
125
|
+
scored.sort((a, b) => b.sim - a.sim);
|
|
126
|
+
// Top 50 most-similar commits are our base.
|
|
127
|
+
const similar = scored.slice(0, 50);
|
|
128
|
+
const similarN = similar.length;
|
|
129
|
+
if (similarN === 0) {
|
|
130
|
+
return {
|
|
131
|
+
fingerprint: fp,
|
|
132
|
+
similarN: 0,
|
|
133
|
+
cleanN: 0,
|
|
134
|
+
pClean: 0,
|
|
135
|
+
verdict: "unknown",
|
|
136
|
+
recommendation: "No similar past changes in this repo. Treat as exploratory — small commits, careful review.",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Detect regrets ONLY among the similar commits (constrain corpus).
|
|
140
|
+
// Use only strong regret signals (revert/hotfix/fix) — the weaker
|
|
141
|
+
// 'sameFiles' kind is too noisy for failure prediction.
|
|
142
|
+
const similarHashes = new Set(similar.map((s) => s.commit.hash));
|
|
143
|
+
const allRegrets = detectRegrets(commits, { windowDays });
|
|
144
|
+
const troubledHashes = new Set(allRegrets
|
|
145
|
+
.filter((r) => r.kind !== "sameFiles" && similarHashes.has(r.shipped.hash))
|
|
146
|
+
.map((r) => r.shipped.hash));
|
|
147
|
+
const cleanN = similarN - troubledHashes.size;
|
|
148
|
+
const pClean = cleanN / similarN;
|
|
149
|
+
// Find most-similar past commit (top of scored list) and its outcome.
|
|
150
|
+
const top = similar[0];
|
|
151
|
+
const mostSimilar = {
|
|
152
|
+
hash: top.commit.shortHash || top.commit.hash.slice(0, 7),
|
|
153
|
+
subject: top.commit.subject,
|
|
154
|
+
date: top.commit.authorDate.slice(0, 10),
|
|
155
|
+
outcome: troubledHashes.has(top.commit.hash) ? "trouble" : "clean",
|
|
156
|
+
};
|
|
157
|
+
const verdict = similarN < 5
|
|
158
|
+
? "unknown"
|
|
159
|
+
: pClean >= 0.85
|
|
160
|
+
? "clear"
|
|
161
|
+
: pClean >= 0.6
|
|
162
|
+
? "moderate"
|
|
163
|
+
: "risky";
|
|
164
|
+
const recommendation = buildRecommendation(verdict, similarN, troubledHashes.size, pClean);
|
|
165
|
+
return {
|
|
166
|
+
fingerprint: fp,
|
|
167
|
+
similarN,
|
|
168
|
+
cleanN,
|
|
169
|
+
pClean,
|
|
170
|
+
verdict,
|
|
171
|
+
mostSimilar,
|
|
172
|
+
recommendation,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function fingerprintFromCommit(c) {
|
|
176
|
+
// Reconstruct a parsed-diff-like view from commit metadata.
|
|
177
|
+
const files = c.files ?? [];
|
|
178
|
+
const modules = [
|
|
179
|
+
...new Set(files.map((f) => {
|
|
180
|
+
const parts = f.split("/");
|
|
181
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
182
|
+
})),
|
|
183
|
+
].sort();
|
|
184
|
+
const exts = new Set();
|
|
185
|
+
for (const f of files) {
|
|
186
|
+
const m = f.match(/\.([a-z0-9]+)$/i);
|
|
187
|
+
if (m)
|
|
188
|
+
exts.add(m[1].toLowerCase());
|
|
189
|
+
}
|
|
190
|
+
// Shape from subject conventions.
|
|
191
|
+
const subjectLower = c.subject.toLowerCase();
|
|
192
|
+
const shape = /^fix|^hotfix|^bug/.test(subjectLower)
|
|
193
|
+
? "fix"
|
|
194
|
+
: /^refactor/.test(subjectLower)
|
|
195
|
+
? "refactor"
|
|
196
|
+
: /^docs?|^chore/.test(subjectLower)
|
|
197
|
+
? "docs"
|
|
198
|
+
: /^test/.test(subjectLower)
|
|
199
|
+
? "test"
|
|
200
|
+
: /^perf/.test(subjectLower)
|
|
201
|
+
? "perf"
|
|
202
|
+
: "feat";
|
|
203
|
+
// We don't have line counts; treat as "small" by default.
|
|
204
|
+
const hasTests = files.some((f) => /(\.test\.|\.spec\.|\/tests?\/)/.test(f));
|
|
205
|
+
return { modules, extensions: [...exts].sort(), shape, size: "small", hasTests };
|
|
206
|
+
}
|
|
207
|
+
function buildRecommendation(verdict, similarN, troubledN, pClean) {
|
|
208
|
+
switch (verdict) {
|
|
209
|
+
case "unknown":
|
|
210
|
+
return `Only ${similarN} similar past change${similarN === 1 ? "" : "s"} in history — too thin to predict. Run tests locally before pushing.`;
|
|
211
|
+
case "clear":
|
|
212
|
+
return `${(pClean * 100).toFixed(0)}% of ${similarN} similar past changes shipped cleanly. Looks like a routine change.`;
|
|
213
|
+
case "moderate":
|
|
214
|
+
return `${troubledN} of ${similarN} similar past changes needed a follow-up fix. Run lint + tests locally first.`;
|
|
215
|
+
case "risky":
|
|
216
|
+
return `${troubledN} of ${similarN} similar past changes needed a follow-up fix (${(pClean * 100).toFixed(0)}% clean rate). Strongly consider splitting this commit + extra review.`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=crystal-ball.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crystal-ball.js","sourceRoot":"","sources":["../../src/insights/crystal-ball.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crystal-ball.test.d.ts","sourceRoot":"","sources":["../../src/insights/crystal-ball.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,157 @@
|
|
|
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 { fingerprint, similarity, predict } from "./crystal-ball.js";
|
|
7
|
+
import { parseDiff } from "./commit-coach.js";
|
|
8
|
+
let tmpDir;
|
|
9
|
+
let store;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = mkdtempSync(join(tmpdir(), "mneme-cb-test-"));
|
|
12
|
+
store = new MnemeStore(join(tmpDir, "mneme.db"));
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
store.close();
|
|
16
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
const cmt = (hash, author, date, subject, files) => ({
|
|
19
|
+
hash,
|
|
20
|
+
shortHash: hash.slice(0, 7),
|
|
21
|
+
authorName: author,
|
|
22
|
+
authorEmail: `${author}@x`,
|
|
23
|
+
authorDate: `${date}T00:00:00Z`,
|
|
24
|
+
committerDate: `${date}T00:00:00Z`,
|
|
25
|
+
subject,
|
|
26
|
+
body: "",
|
|
27
|
+
parents: [],
|
|
28
|
+
files,
|
|
29
|
+
});
|
|
30
|
+
function seed(commits) {
|
|
31
|
+
store.upsertCommits(commits);
|
|
32
|
+
for (const c of commits) {
|
|
33
|
+
store.upsertFileChanges(c.files.map((f) => ({
|
|
34
|
+
commitHash: c.hash,
|
|
35
|
+
path: f,
|
|
36
|
+
changeKind: "M",
|
|
37
|
+
insertions: 1,
|
|
38
|
+
deletions: 0,
|
|
39
|
+
})));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
describe("fingerprint", () => {
|
|
43
|
+
it("extracts modules, extensions, shape, size, hasTests", () => {
|
|
44
|
+
const diff = parseDiff(`diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts
|
|
45
|
+
+const x = 1;`);
|
|
46
|
+
const fp = fingerprint(diff);
|
|
47
|
+
expect(fp.modules).toEqual(["src/auth"]);
|
|
48
|
+
expect(fp.extensions).toEqual(["ts"]);
|
|
49
|
+
expect(fp.size).toBe("tiny");
|
|
50
|
+
expect(fp.hasTests).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("flags hasTests when *.test.ts present", () => {
|
|
53
|
+
const diff = parseDiff(`diff --git a/src/foo.test.ts b/src/foo.test.ts
|
|
54
|
+
+test('x', () => {});`);
|
|
55
|
+
expect(fingerprint(diff).hasTests).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it("size buckets are tiny/small/medium/large by line count", () => {
|
|
58
|
+
const tiny = fingerprint({ files: [], modules: [], added: 5, removed: 5, shape: "feat" });
|
|
59
|
+
const small = fingerprint({ files: [], modules: [], added: 50, removed: 30, shape: "feat" });
|
|
60
|
+
const medium = fingerprint({ files: [], modules: [], added: 200, removed: 200, shape: "feat" });
|
|
61
|
+
const large = fingerprint({ files: [], modules: [], added: 800, removed: 100, shape: "feat" });
|
|
62
|
+
expect(tiny.size).toBe("tiny");
|
|
63
|
+
expect(small.size).toBe("small");
|
|
64
|
+
expect(medium.size).toBe("medium");
|
|
65
|
+
expect(large.size).toBe("large");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe("similarity", () => {
|
|
69
|
+
const fp = (overrides = {}) => ({
|
|
70
|
+
modules: ["src/auth"],
|
|
71
|
+
extensions: ["ts"],
|
|
72
|
+
shape: "feat",
|
|
73
|
+
size: "small",
|
|
74
|
+
hasTests: false,
|
|
75
|
+
...overrides,
|
|
76
|
+
});
|
|
77
|
+
it("identical fingerprints score 1.0", () => {
|
|
78
|
+
expect(similarity(fp(), fp())).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
it("module overlap dominates the score (40% weight)", () => {
|
|
81
|
+
const a = fp({ modules: ["src/auth"] });
|
|
82
|
+
const b = fp({ modules: ["src/payment"] });
|
|
83
|
+
// No module overlap → modules contributes 0
|
|
84
|
+
// Same exts (1.0 × 0.2), shape (0.2), size (0.1), tests (0.1) = 0.6
|
|
85
|
+
expect(similarity(a, b)).toBeCloseTo(0.6, 1);
|
|
86
|
+
});
|
|
87
|
+
it("size distance reduces score linearly", () => {
|
|
88
|
+
const a = fp({ size: "tiny" });
|
|
89
|
+
const b = fp({ size: "large" });
|
|
90
|
+
const s = similarity(a, b);
|
|
91
|
+
// Other dims identical (0.9 weight), size dim 0
|
|
92
|
+
expect(s).toBeCloseTo(0.9, 1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("predict — empty repo edge case", () => {
|
|
96
|
+
it("returns 'unknown' verdict when repo is empty", () => {
|
|
97
|
+
const p = predict(store, "diff --git a/src/x.ts b/src/x.ts\n+const x = 1;");
|
|
98
|
+
expect(p.verdict).toBe("unknown");
|
|
99
|
+
expect(p.similarN).toBe(0);
|
|
100
|
+
expect(p.recommendation.toLowerCase()).toMatch(/index|signal/);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("predict — clean prediction", () => {
|
|
104
|
+
it("predicts 'clear' when most similar past changes shipped cleanly", () => {
|
|
105
|
+
const seedCommits = Array.from({ length: 20 }, (_, i) => cmt(`a${i}1234567`, "alice", `2024-08-${String((i % 28) + 1).padStart(2, "0")}`, `feat(auth): add thing ${i}`, ["src/auth/jwt.ts"]));
|
|
106
|
+
seed(seedCommits);
|
|
107
|
+
const p = predict(store, `diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts
|
|
108
|
+
+const x = 1;`);
|
|
109
|
+
expect(p.similarN).toBeGreaterThan(0);
|
|
110
|
+
expect(p.verdict).toBe("clear");
|
|
111
|
+
expect(p.pClean).toBeGreaterThan(0.8);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("predict — risky prediction", () => {
|
|
115
|
+
it("predicts 'risky' when many similar past changes needed follow-up fixes", () => {
|
|
116
|
+
const commits = [];
|
|
117
|
+
// 10 pairs of (shipped, fix-2-days-later) on src/auth
|
|
118
|
+
for (let i = 0; i < 10; i++) {
|
|
119
|
+
const day = String(i * 3 + 1).padStart(2, "0");
|
|
120
|
+
const fixDay = String(i * 3 + 3).padStart(2, "0");
|
|
121
|
+
commits.push(cmt(`a${i}_____`, "alice", `2024-08-${day}`, `feat(auth): add thing ${i}`, ["src/auth/jwt.ts"]));
|
|
122
|
+
commits.push(cmt(`b${i}_____`, "alice", `2024-08-${fixDay}`, `fix: thing ${i} broke`, ["src/auth/jwt.ts"]));
|
|
123
|
+
}
|
|
124
|
+
seed(commits);
|
|
125
|
+
const p = predict(store, `diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts
|
|
126
|
+
+const x = 1;`);
|
|
127
|
+
expect(p.similarN).toBeGreaterThanOrEqual(5);
|
|
128
|
+
expect(["risky", "moderate"]).toContain(p.verdict);
|
|
129
|
+
expect(p.pClean).toBeLessThan(0.7);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe("predict — low signal verdict", () => {
|
|
133
|
+
it("returns 'unknown' when similar count is below 5", () => {
|
|
134
|
+
seed([
|
|
135
|
+
cmt("a1", "alice", "2024-08-01", "feat: thing", ["src/auth/jwt.ts"]),
|
|
136
|
+
cmt("a2", "alice", "2024-08-02", "feat: thing 2", ["src/auth/jwt.ts"]),
|
|
137
|
+
]);
|
|
138
|
+
const p = predict(store, `diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts
|
|
139
|
+
+const x = 1;`);
|
|
140
|
+
expect(["unknown", "clear"]).toContain(p.verdict);
|
|
141
|
+
if (p.similarN < 5)
|
|
142
|
+
expect(p.verdict).toBe("unknown");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("predict — most-similar reference", () => {
|
|
146
|
+
it("returns the most-similar past commit alongside the prediction", () => {
|
|
147
|
+
seed([
|
|
148
|
+
cmt("a1xxxxxx", "alice", "2024-08-01", "feat(auth): unique subject", ["src/auth/jwt.ts"]),
|
|
149
|
+
...Array.from({ length: 10 }, (_, i) => cmt(`b${i}xxxxxx`, "alice", `2024-09-${String(i + 1).padStart(2, "0")}`, `feat: misc`, ["src/other/foo.ts"])),
|
|
150
|
+
]);
|
|
151
|
+
const p = predict(store, `diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts
|
|
152
|
+
+const x = 1;`);
|
|
153
|
+
expect(p.mostSimilar?.hash).toBeDefined();
|
|
154
|
+
expect(["clean", "trouble"]).toContain(p.mostSimilar?.outcome);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
//# sourceMappingURL=crystal-ball.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crystal-ball.test.js","sourceRoot":"","sources":["../../src/insights/crystal-ball.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -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":""}
|