@mneme-ai/core 0.8.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entities/go-parser.d.ts +47 -0
- package/dist/entities/go-parser.d.ts.map +1 -0
- package/dist/entities/go-parser.js +315 -0
- package/dist/entities/go-parser.js.map +1 -0
- package/dist/entities/go-parser.test.d.ts +2 -0
- package/dist/entities/go-parser.test.d.ts.map +1 -0
- package/dist/entities/go-parser.test.js +147 -0
- package/dist/entities/go-parser.test.js.map +1 -0
- package/dist/entities/index.d.ts +1 -0
- package/dist/entities/index.d.ts.map +1 -1
- package/dist/entities/index.js +1 -0
- package/dist/entities/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/indexer/indexer.d.ts +12 -0
- package/dist/indexer/indexer.d.ts.map +1 -1
- package/dist/indexer/indexer.js +28 -1
- package/dist/indexer/indexer.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/decisions.d.ts +38 -0
- package/dist/insights/decisions.d.ts.map +1 -0
- package/dist/insights/decisions.js +125 -0
- package/dist/insights/decisions.js.map +1 -0
- package/dist/insights/decisions.test.d.ts +2 -0
- package/dist/insights/decisions.test.d.ts.map +1 -0
- package/dist/insights/decisions.test.js +141 -0
- package/dist/insights/decisions.test.js.map +1 -0
- package/dist/insights/dream.d.ts +71 -0
- package/dist/insights/dream.d.ts.map +1 -0
- package/dist/insights/dream.js +235 -0
- package/dist/insights/dream.js.map +1 -0
- package/dist/insights/dream.test.d.ts +2 -0
- package/dist/insights/dream.test.d.ts.map +1 -0
- package/dist/insights/dream.test.js +127 -0
- package/dist/insights/dream.test.js.map +1 -0
- package/dist/insights/index.d.ts +21 -0
- package/dist/insights/index.d.ts.map +1 -0
- package/dist/insights/index.js +21 -0
- package/dist/insights/index.js.map +1 -0
- package/dist/insights/obsidian.d.ts +42 -0
- package/dist/insights/obsidian.d.ts.map +1 -0
- package/dist/insights/obsidian.js +263 -0
- package/dist/insights/obsidian.js.map +1 -0
- package/dist/insights/obsidian.test.d.ts +2 -0
- package/dist/insights/obsidian.test.d.ts.map +1 -0
- package/dist/insights/obsidian.test.js +241 -0
- package/dist/insights/obsidian.test.js.map +1 -0
- 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/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/stack-trace.d.ts +40 -0
- package/dist/insights/stack-trace.d.ts.map +1 -0
- package/dist/insights/stack-trace.js +127 -0
- package/dist/insights/stack-trace.js.map +1 -0
- package/dist/insights/stack-trace.test.d.ts +2 -0
- package/dist/insights/stack-trace.test.d.ts.map +1 -0
- package/dist/insights/stack-trace.test.js +103 -0
- package/dist/insights/stack-trace.test.js.map +1 -0
- package/dist/insights/story.d.ts +34 -0
- package/dist/insights/story.d.ts.map +1 -0
- package/dist/insights/story.js +100 -0
- package/dist/insights/story.js.map +1 -0
- package/dist/insights/story.test.d.ts +2 -0
- package/dist/insights/story.test.d.ts.map +1 -0
- package/dist/insights/story.test.js +99 -0
- package/dist/insights/story.test.js.map +1 -0
- package/dist/insights/suggest.d.ts +29 -0
- package/dist/insights/suggest.d.ts.map +1 -0
- package/dist/insights/suggest.js +93 -0
- package/dist/insights/suggest.js.map +1 -0
- package/dist/insights/suggest.test.d.ts +2 -0
- package/dist/insights/suggest.test.d.ts.map +1 -0
- package/dist/insights/suggest.test.js +71 -0
- package/dist/insights/suggest.test.js.map +1 -0
- package/dist/insights/who-knows.d.ts +66 -0
- package/dist/insights/who-knows.d.ts.map +1 -0
- package/dist/insights/who-knows.js +125 -0
- package/dist/insights/who-knows.js.map +1 -0
- package/dist/insights/who-knows.test.d.ts +2 -0
- package/dist/insights/who-knows.test.d.ts.map +1 -0
- package/dist/insights/who-knows.test.js +109 -0
- package/dist/insights/who-knows.test.js.map +1 -0
- 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/index.d.ts +2 -0
- package/dist/retrieve/index.d.ts.map +1 -1
- package/dist/retrieve/index.js +2 -0
- package/dist/retrieve/index.js.map +1 -1
- package/dist/retrieve/intent.d.ts +32 -0
- package/dist/retrieve/intent.d.ts.map +1 -0
- package/dist/retrieve/intent.js +104 -0
- package/dist/retrieve/intent.js.map +1 -0
- package/dist/retrieve/intent.test.d.ts +2 -0
- package/dist/retrieve/intent.test.d.ts.map +1 -0
- package/dist/retrieve/intent.test.js +106 -0
- package/dist/retrieve/intent.test.js.map +1 -0
- package/dist/retrieve/search.d.ts +30 -0
- package/dist/retrieve/search.d.ts.map +1 -1
- package/dist/retrieve/search.js +48 -0
- package/dist/retrieve/search.js.map +1 -1
- package/dist/retrieve/search.test.js +84 -1
- package/dist/retrieve/search.test.js.map +1 -1
- package/dist/retrieve/synthesize.d.ts +57 -0
- package/dist/retrieve/synthesize.d.ts.map +1 -0
- package/dist/retrieve/synthesize.js +191 -0
- package/dist/retrieve/synthesize.js.map +1 -0
- package/dist/retrieve/synthesize.test.d.ts +2 -0
- package/dist/retrieve/synthesize.test.d.ts.map +1 -0
- package/dist/retrieve/synthesize.test.js +127 -0
- package/dist/retrieve/synthesize.test.js.map +1 -0
- package/dist/store/schema.d.ts +2 -2
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +60 -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 +2 -0
- package/dist/util/index.d.ts.map +1 -1
- package/dist/util/index.js +2 -1
- package/dist/util/index.js.map +1 -1
- package/dist/util/redact.d.ts +58 -0
- package/dist/util/redact.d.ts.map +1 -0
- package/dist/util/redact.js +129 -0
- package/dist/util/redact.js.map +1 -0
- package/dist/util/redact.test.d.ts +2 -0
- package/dist/util/redact.test.d.ts.map +1 -0
- package/dist/util/redact.test.js +148 -0
- package/dist/util/redact.test.js.map +1 -0
- package/dist/wisdom/calibrator.d.ts +43 -0
- package/dist/wisdom/calibrator.d.ts.map +1 -0
- package/dist/wisdom/calibrator.js +120 -0
- package/dist/wisdom/calibrator.js.map +1 -0
- package/dist/wisdom/feedback.d.ts +45 -0
- package/dist/wisdom/feedback.d.ts.map +1 -0
- package/dist/wisdom/feedback.js +116 -0
- package/dist/wisdom/feedback.js.map +1 -0
- package/dist/wisdom/index.d.ts +15 -0
- package/dist/wisdom/index.d.ts.map +1 -0
- package/dist/wisdom/index.js +15 -0
- package/dist/wisdom/index.js.map +1 -0
- package/dist/wisdom/types.d.ts +67 -0
- package/dist/wisdom/types.d.ts.map +1 -0
- package/dist/wisdom/types.js +20 -0
- package/dist/wisdom/types.js.map +1 -0
- package/dist/wisdom/wisdom.test.d.ts +2 -0
- package/dist/wisdom/wisdom.test.d.ts.map +1 -0
- package/dist/wisdom/wisdom.test.js +144 -0
- package/dist/wisdom/wisdom.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildStory } from "./story.js";
|
|
3
|
+
const cmt = (hash, date, subject, body = "") => ({
|
|
4
|
+
hash,
|
|
5
|
+
shortHash: hash.slice(0, 7),
|
|
6
|
+
authorName: "alice",
|
|
7
|
+
authorEmail: "alice@example.com",
|
|
8
|
+
authorDate: `${date}T00:00:00Z`,
|
|
9
|
+
committerDate: `${date}T00:00:00Z`,
|
|
10
|
+
subject,
|
|
11
|
+
body,
|
|
12
|
+
parents: [],
|
|
13
|
+
files: [],
|
|
14
|
+
});
|
|
15
|
+
describe("buildStory — basic structure", () => {
|
|
16
|
+
it("returns empty story for empty commit list", () => {
|
|
17
|
+
const s = buildStory("auth", []);
|
|
18
|
+
expect(s.acts).toEqual([]);
|
|
19
|
+
expect(s.totalCommits).toBe(0);
|
|
20
|
+
expect(s.spanDays).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
it("first commit always opens Act I", () => {
|
|
23
|
+
const s = buildStory("auth", [cmt("a1", "2024-01-01", "feat: add passport.js")]);
|
|
24
|
+
expect(s.acts[0].id).toBe("initial");
|
|
25
|
+
expect(s.acts[0].title).toContain("Beginning");
|
|
26
|
+
});
|
|
27
|
+
it("totalCommits and spanDays reflect input", () => {
|
|
28
|
+
const s = buildStory("auth", [
|
|
29
|
+
cmt("a1", "2024-01-01", "feat: passport"),
|
|
30
|
+
cmt("a2", "2024-04-01", "refactor: replace passport"),
|
|
31
|
+
]);
|
|
32
|
+
expect(s.totalCommits).toBe(2);
|
|
33
|
+
expect(s.spanDays).toBeGreaterThanOrEqual(89); // about 90 days between Jan 1 and Apr 1
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("buildStory — act detection", () => {
|
|
37
|
+
it("groups consecutive refactor-style commits into one Refactor act", () => {
|
|
38
|
+
const s = buildStory("auth", [
|
|
39
|
+
cmt("a1", "2024-01-01", "feat: passport"),
|
|
40
|
+
cmt("a2", "2024-04-01", "refactor: replace passport with custom"),
|
|
41
|
+
cmt("a3", "2024-04-08", "refactor: switch to JWT signing"),
|
|
42
|
+
cmt("a4", "2024-04-15", "refactor: migrate session middleware"),
|
|
43
|
+
]);
|
|
44
|
+
const refactorActs = s.acts.filter((a) => a.id === "refactor");
|
|
45
|
+
expect(refactorActs).toHaveLength(1);
|
|
46
|
+
expect(refactorActs[0].commits).toHaveLength(3);
|
|
47
|
+
});
|
|
48
|
+
it("flags 'hotfix' and 'incident' commits as incident acts", () => {
|
|
49
|
+
const s = buildStory("auth", [
|
|
50
|
+
cmt("a1", "2024-01-01", "feat: passport"),
|
|
51
|
+
cmt("a2", "2024-02-01", "hotfix: CSRF bypass after refactor", ""),
|
|
52
|
+
cmt("a3", "2024-02-02", "revert: PR #42 caused outage"),
|
|
53
|
+
]);
|
|
54
|
+
expect(s.acts.find((a) => a.id === "incident")).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
it("commits without keywords flagged as evolution", () => {
|
|
57
|
+
const s = buildStory("auth", [
|
|
58
|
+
cmt("a1", "2024-01-01", "feat: passport"),
|
|
59
|
+
cmt("a2", "2024-01-15", "feat: add /me endpoint"),
|
|
60
|
+
cmt("a3", "2024-02-01", "feat: add /logout endpoint"),
|
|
61
|
+
]);
|
|
62
|
+
expect(s.acts.find((a) => a.id === "evolution")).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
it("transitions across flavors close+open separate acts", () => {
|
|
65
|
+
const s = buildStory("auth", [
|
|
66
|
+
cmt("a1", "2024-01-01", "feat: passport"), // initial
|
|
67
|
+
cmt("a2", "2024-01-10", "feat: add session cookies"), // evolution
|
|
68
|
+
cmt("a3", "2024-02-01", "refactor: replace passport"), // refactor
|
|
69
|
+
cmt("a4", "2024-02-15", "feat: add audit logs"), // evolution
|
|
70
|
+
cmt("a5", "2024-03-01", "hotfix: critical XSS"), // incident
|
|
71
|
+
]);
|
|
72
|
+
const ids = s.acts.map((a) => a.id);
|
|
73
|
+
expect(ids[0]).toBe("initial");
|
|
74
|
+
expect(ids).toContain("refactor");
|
|
75
|
+
expect(ids).toContain("evolution");
|
|
76
|
+
expect(ids).toContain("incident");
|
|
77
|
+
});
|
|
78
|
+
it("appends a Stable State act when the latest commit is > 90 days old", () => {
|
|
79
|
+
const oneYearAgo = new Date(Date.now() - 400 * 86_400_000).toISOString().slice(0, 10);
|
|
80
|
+
const s = buildStory("auth", [cmt("a1", oneYearAgo, "feat: passport")]);
|
|
81
|
+
const stable = s.acts.find((a) => a.id === "stable");
|
|
82
|
+
expect(stable).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("buildStory — date metadata", () => {
|
|
86
|
+
it("each act records fromDate and toDate", () => {
|
|
87
|
+
const s = buildStory("auth", [
|
|
88
|
+
cmt("a1", "2024-01-01", "feat: passport"),
|
|
89
|
+
cmt("a2", "2024-04-01", "refactor: replace passport"),
|
|
90
|
+
cmt("a3", "2024-04-15", "refactor: switch to JWT"),
|
|
91
|
+
]);
|
|
92
|
+
for (const act of s.acts) {
|
|
93
|
+
expect(act.fromDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
94
|
+
expect(act.toDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
95
|
+
expect(act.fromDate <= act.toDate).toBe(true);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=story.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"story.test.js","sourceRoot":"","sources":["../../src/insights/story.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart suggestions — given a question and its results, propose 3 follow-up
|
|
3
|
+
* commands the user might want to run next.
|
|
4
|
+
*
|
|
5
|
+
* Pure function. Heuristic, not LLM. Deterministic so tests can verify it.
|
|
6
|
+
*
|
|
7
|
+
* Design principle: every suggestion must lead to an actionable command
|
|
8
|
+
* the user can copy-paste. Vague hints ("explore more", "look around") are
|
|
9
|
+
* useless; concrete commands compound.
|
|
10
|
+
*/
|
|
11
|
+
import type { SearchResult } from "../types.js";
|
|
12
|
+
export interface Suggestion {
|
|
13
|
+
/** The CLI command, ready to copy-paste. */
|
|
14
|
+
command: string;
|
|
15
|
+
/** One-line "why this is interesting". */
|
|
16
|
+
reason: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build follow-up suggestions for an `ask` query.
|
|
20
|
+
* Returns 0..3 suggestions, ordered by likely usefulness.
|
|
21
|
+
*/
|
|
22
|
+
export declare function suggestFollowUps(question: string, results: SearchResult[]): Suggestion[];
|
|
23
|
+
/**
|
|
24
|
+
* Extract a "topic word" from a question — the most concrete noun, suitable
|
|
25
|
+
* for plugging into `story <topic>` or `who-knows <topic>`. Falsy when nothing
|
|
26
|
+
* concrete is present.
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractTopicWord(question: string): string | undefined;
|
|
29
|
+
//# sourceMappingURL=suggest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../src/insights/suggest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,UAAU,EAAE,CAiDxF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA0BrE"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart suggestions — given a question and its results, propose 3 follow-up
|
|
3
|
+
* commands the user might want to run next.
|
|
4
|
+
*
|
|
5
|
+
* Pure function. Heuristic, not LLM. Deterministic so tests can verify it.
|
|
6
|
+
*
|
|
7
|
+
* Design principle: every suggestion must lead to an actionable command
|
|
8
|
+
* the user can copy-paste. Vague hints ("explore more", "look around") are
|
|
9
|
+
* useless; concrete commands compound.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Build follow-up suggestions for an `ask` query.
|
|
13
|
+
* Returns 0..3 suggestions, ordered by likely usefulness.
|
|
14
|
+
*/
|
|
15
|
+
export function suggestFollowUps(question, results) {
|
|
16
|
+
const out = [];
|
|
17
|
+
if (results.length === 0)
|
|
18
|
+
return out;
|
|
19
|
+
const top = results[0];
|
|
20
|
+
const topAuthor = top.commit.authorName;
|
|
21
|
+
const topFile = (top.commit.files ?? [])[0];
|
|
22
|
+
// 1. If the top commit has files, suggest `mneme why <file>:<line>` for the
|
|
23
|
+
// most-changed file. Anchor question into a specific location.
|
|
24
|
+
if (topFile) {
|
|
25
|
+
out.push({
|
|
26
|
+
command: `mneme why ${topFile}`,
|
|
27
|
+
reason: `Walk the blame + history of the file most changed in the top commit`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// 2. Story command — narrate the topic across acts.
|
|
31
|
+
const topicWord = extractTopicWord(question);
|
|
32
|
+
if (topicWord) {
|
|
33
|
+
out.push({
|
|
34
|
+
command: `mneme story ${topicWord}`,
|
|
35
|
+
reason: `See how "${topicWord}" evolved across acts (initial / refactor / incidents)`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// 3. Who-knows — surface the human expert.
|
|
39
|
+
if (topicWord && topAuthor) {
|
|
40
|
+
out.push({
|
|
41
|
+
command: `mneme who-knows ${topicWord}`,
|
|
42
|
+
reason: `Find people most likely to know about "${topicWord}" (top hit so far: ${topAuthor})`,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else if (topAuthor) {
|
|
46
|
+
out.push({
|
|
47
|
+
command: `mneme who-knows ${topAuthor.split(/\s+/)[0]?.toLowerCase()}`,
|
|
48
|
+
reason: `See where ${topAuthor} has been most active recently`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// 4. Blast radius if the top commit looks recent.
|
|
52
|
+
if (results.length >= 2 && top.commit.shortHash) {
|
|
53
|
+
out.push({
|
|
54
|
+
command: `mneme blast ${top.commit.shortHash}`,
|
|
55
|
+
reason: `Predict incidents likely to follow shipping the top commit (base-rate verdict)`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return out.slice(0, 3);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract a "topic word" from a question — the most concrete noun, suitable
|
|
62
|
+
* for plugging into `story <topic>` or `who-knows <topic>`. Falsy when nothing
|
|
63
|
+
* concrete is present.
|
|
64
|
+
*/
|
|
65
|
+
export function extractTopicWord(question) {
|
|
66
|
+
// 1. Look for camelCase / PascalCase identifiers (case-sensitive on the original).
|
|
67
|
+
const camel = question.match(/[a-z]+[A-Z][a-zA-Z]+/);
|
|
68
|
+
if (camel)
|
|
69
|
+
return camel[0];
|
|
70
|
+
// 2. Look for path-like tokens (lowercased — paths are usually lowercase anyway).
|
|
71
|
+
const q = question.toLowerCase();
|
|
72
|
+
const path = q.match(/[a-z]+\/[a-z]+/);
|
|
73
|
+
if (path)
|
|
74
|
+
return path[0];
|
|
75
|
+
// 3. Strip stop-words and punctuation and pick the longest remaining token.
|
|
76
|
+
const STOP = new Set([
|
|
77
|
+
"the", "a", "an", "is", "are", "was", "were", "do", "does", "did",
|
|
78
|
+
"why", "when", "what", "who", "how", "where", "this", "that",
|
|
79
|
+
"to", "from", "on", "in", "of", "for", "with", "and", "or", "but",
|
|
80
|
+
"code", "file", "method", "module", "any",
|
|
81
|
+
"use", "uses", "used", "using",
|
|
82
|
+
]);
|
|
83
|
+
const words = q
|
|
84
|
+
.replace(/[?.,!;:'"()[\]{}]/g, " ")
|
|
85
|
+
.split(/\s+/)
|
|
86
|
+
.filter((w) => w.length >= 4 && !STOP.has(w));
|
|
87
|
+
if (words.length === 0)
|
|
88
|
+
return undefined;
|
|
89
|
+
// Prefer longest distinct-looking word.
|
|
90
|
+
words.sort((a, b) => b.length - a.length);
|
|
91
|
+
return words[0];
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=suggest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggest.js","sourceRoot":"","sources":["../../src/insights/suggest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggest.test.d.ts","sourceRoot":"","sources":["../../src/insights/suggest.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { suggestFollowUps, extractTopicWord } from "./suggest.js";
|
|
3
|
+
const cmt = (hash, subject, author = "alice", files = []) => ({
|
|
4
|
+
hash,
|
|
5
|
+
shortHash: hash.slice(0, 7),
|
|
6
|
+
authorName: author,
|
|
7
|
+
authorEmail: `${author}@example.com`,
|
|
8
|
+
authorDate: "2024-08-12T00:00:00Z",
|
|
9
|
+
committerDate: "2024-08-12T00:00:00Z",
|
|
10
|
+
subject,
|
|
11
|
+
body: "",
|
|
12
|
+
parents: [],
|
|
13
|
+
files,
|
|
14
|
+
});
|
|
15
|
+
const result = (hash, score, files = []) => ({
|
|
16
|
+
commit: cmt(hash, "subj", "alice", files),
|
|
17
|
+
score,
|
|
18
|
+
matchedChunks: [],
|
|
19
|
+
});
|
|
20
|
+
describe("extractTopicWord", () => {
|
|
21
|
+
it("returns camelCase identifier when present", () => {
|
|
22
|
+
expect(extractTopicWord("why does parseAmount throw?")).toBe("parseAmount");
|
|
23
|
+
});
|
|
24
|
+
it("returns path-like token when present", () => {
|
|
25
|
+
expect(extractTopicWord("what about src/payment changes?")).toBe("src/payment");
|
|
26
|
+
});
|
|
27
|
+
it("returns the longest meaningful word otherwise", () => {
|
|
28
|
+
expect(extractTopicWord("why is authentication broken")).toBe("authentication");
|
|
29
|
+
});
|
|
30
|
+
it("filters stop-words and code-noise words", () => {
|
|
31
|
+
// 'the', 'how', 'does', 'use' filtered → longest remaining is "function"
|
|
32
|
+
expect(extractTopicWord("how does the function use this")).toBe("function");
|
|
33
|
+
});
|
|
34
|
+
it("returns undefined when nothing concrete remains", () => {
|
|
35
|
+
expect(extractTopicWord("why")).toBeUndefined();
|
|
36
|
+
expect(extractTopicWord("the and or")).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("suggestFollowUps", () => {
|
|
40
|
+
it("returns no suggestions when results are empty", () => {
|
|
41
|
+
expect(suggestFollowUps("anything", [])).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
it("suggests `mneme why <file>` when top result has files", () => {
|
|
44
|
+
const out = suggestFollowUps("why does X exist?", [result("a", 0.05, ["src/payment.ts"])]);
|
|
45
|
+
expect(out.find((s) => s.command.startsWith("mneme why"))).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
it("suggests `mneme story <topic>` when topic word is extractable", () => {
|
|
48
|
+
const out = suggestFollowUps("how does authentication work?", [result("a", 0.05)]);
|
|
49
|
+
expect(out.find((s) => s.command.startsWith("mneme story"))).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
it("suggests `mneme who-knows <topic>` with the topic", () => {
|
|
52
|
+
const out = suggestFollowUps("authentication question", [result("a", 0.05)]);
|
|
53
|
+
expect(out.find((s) => s.command.startsWith("mneme who-knows"))).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
it("suggests `mneme blast <commit>` when there are 2+ results", () => {
|
|
56
|
+
const out = suggestFollowUps("auth", [result("abc1234", 0.05), result("def5678", 0.04)]);
|
|
57
|
+
expect(out.find((s) => s.command.includes("blast"))).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
it("caps suggestions at 3", () => {
|
|
60
|
+
const out = suggestFollowUps("authentication question", [result("abc1234", 0.05, ["src/auth.ts"]), result("def5678", 0.04)]);
|
|
61
|
+
expect(out.length).toBeLessThanOrEqual(3);
|
|
62
|
+
});
|
|
63
|
+
it("every suggestion has a non-empty command and reason", () => {
|
|
64
|
+
const out = suggestFollowUps("stripe webhook bigint", [result("a", 0.05, ["src/webhook.ts"]), result("b", 0.04)]);
|
|
65
|
+
for (const s of out) {
|
|
66
|
+
expect(s.command.length).toBeGreaterThan(5);
|
|
67
|
+
expect(s.reason.length).toBeGreaterThan(10);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=suggest.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggest.test.js","sourceRoot":"","sources":["../../src/insights/suggest.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `who-knows` — surface the people most likely to answer a question about
|
|
3
|
+
* a topic, ranked by their git activity on commits/files matching that topic.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: every team has tribal knowledge. "Ask alice about Stripe"
|
|
6
|
+
* is institutional memory. Mneme already indexes commits — making the
|
|
7
|
+
* institutional memory queryable is a small step from the engine the project
|
|
8
|
+
* already has.
|
|
9
|
+
*
|
|
10
|
+
* Scoring is intentionally simple and explainable. We avoid any "rank by
|
|
11
|
+
* cosine similarity of author embedding" trickery: this is a counting
|
|
12
|
+
* problem, and the user should be able to verify by running `git log
|
|
13
|
+
* --author=alice -- <files>`.
|
|
14
|
+
*/
|
|
15
|
+
import type { MnemeStore } from "../store/sqlite.js";
|
|
16
|
+
export interface ExpertCandidate {
|
|
17
|
+
/** Author name as it appears in git. */
|
|
18
|
+
name: string;
|
|
19
|
+
email: string;
|
|
20
|
+
/** Number of commits authored that match the topic. */
|
|
21
|
+
commitCount: number;
|
|
22
|
+
/** Last commit author-date in ISO; lets the caller flag stale experts. */
|
|
23
|
+
lastTouch: string;
|
|
24
|
+
/** Distinct files touched (capped at 1000 for memory hygiene). */
|
|
25
|
+
filesTouched: number;
|
|
26
|
+
/** Score: log(commits + 1) × recency bonus. Higher = more authoritative. */
|
|
27
|
+
score: number;
|
|
28
|
+
/** Tier label derived from score + recency. */
|
|
29
|
+
tier: "definitive" | "active" | "stale" | "occasional";
|
|
30
|
+
}
|
|
31
|
+
export interface WhoKnowsOptions {
|
|
32
|
+
topic: string;
|
|
33
|
+
/** Cap on candidates returned (default 5). */
|
|
34
|
+
topN?: number;
|
|
35
|
+
/** ISO date — ignore commits older than this for the recency bonus. */
|
|
36
|
+
since?: string;
|
|
37
|
+
/** Today, in ISO format. Lets tests inject a deterministic clock. */
|
|
38
|
+
now?: Date;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find people likely to know about `topic`. Searches commit messages,
|
|
42
|
+
* subjects, bodies, and PR titles via FTS, then aggregates by author.
|
|
43
|
+
*/
|
|
44
|
+
export declare function whoKnows(store: MnemeStore, opts: WhoKnowsOptions): ExpertCandidate[];
|
|
45
|
+
/** log-based commit weight × recency multiplier. Pure function, easy to test. */
|
|
46
|
+
export declare function scoreCandidate(commitCount: number, lastTouchIso: string, now: Date): number;
|
|
47
|
+
export declare function tierOf(commitCount: number, lastTouchIso: string, now: Date): ExpertCandidate["tier"];
|
|
48
|
+
/**
|
|
49
|
+
* Verdict-shaped summary built from the full candidate list. Surfaces the
|
|
50
|
+
* single most likely person to ask + a confidence percentage + a fallback
|
|
51
|
+
* recommendation, instead of letting the user scan a list of names.
|
|
52
|
+
*/
|
|
53
|
+
export interface ExpertVerdict {
|
|
54
|
+
/** The single most likely expert (or undefined when no candidates). */
|
|
55
|
+
topExpert?: ExpertCandidate;
|
|
56
|
+
/** Confidence percentage 0..100 — share of total commits attributed to the top. */
|
|
57
|
+
confidencePct: number;
|
|
58
|
+
/** Backup expert if the top is stale or has insufficient share. */
|
|
59
|
+
backup?: ExpertCandidate;
|
|
60
|
+
/** A 1-line risk note when the top expert is stale or shares too thinly. */
|
|
61
|
+
risk?: string;
|
|
62
|
+
/** Total commits matched across all candidates (sum of commitCount). */
|
|
63
|
+
totalCommits: number;
|
|
64
|
+
}
|
|
65
|
+
export declare function whoKnowsVerdict(candidates: ExpertCandidate[]): ExpertVerdict;
|
|
66
|
+
//# sourceMappingURL=who-knows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"who-knows.d.ts","sourceRoot":"","sources":["../../src/insights/who-knows.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,IAAI,EAAE,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;CACxD;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAKD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,GAAG,eAAe,EAAE,CA6DpF;AAED,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,CAQ3F;AAED,wBAAgB,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,CAOpG;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,mFAAmF;IACnF,aAAa,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,aAAa,CAoB5E"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `who-knows` — surface the people most likely to answer a question about
|
|
3
|
+
* a topic, ranked by their git activity on commits/files matching that topic.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: every team has tribal knowledge. "Ask alice about Stripe"
|
|
6
|
+
* is institutional memory. Mneme already indexes commits — making the
|
|
7
|
+
* institutional memory queryable is a small step from the engine the project
|
|
8
|
+
* already has.
|
|
9
|
+
*
|
|
10
|
+
* Scoring is intentionally simple and explainable. We avoid any "rank by
|
|
11
|
+
* cosine similarity of author embedding" trickery: this is a counting
|
|
12
|
+
* problem, and the user should be able to verify by running `git log
|
|
13
|
+
* --author=alice -- <files>`.
|
|
14
|
+
*/
|
|
15
|
+
const STALE_DAYS = 180;
|
|
16
|
+
const ACTIVE_DAYS = 90;
|
|
17
|
+
/**
|
|
18
|
+
* Find people likely to know about `topic`. Searches commit messages,
|
|
19
|
+
* subjects, bodies, and PR titles via FTS, then aggregates by author.
|
|
20
|
+
*/
|
|
21
|
+
export function whoKnows(store, opts) {
|
|
22
|
+
const topN = opts.topN ?? 5;
|
|
23
|
+
const now = opts.now ?? new Date();
|
|
24
|
+
// Pull commits whose chunks match the topic via FTS.
|
|
25
|
+
const ftsHits = store.ftsSearch(opts.topic, 200);
|
|
26
|
+
if (ftsHits.length === 0)
|
|
27
|
+
return [];
|
|
28
|
+
// Aggregate by commit hash (one chunk per commit can appear multiple times).
|
|
29
|
+
const commitHashes = new Set(ftsHits.map((h) => h.commitHash));
|
|
30
|
+
const buckets = new Map();
|
|
31
|
+
for (const hash of commitHashes) {
|
|
32
|
+
const c = store.getCommit(hash);
|
|
33
|
+
if (!c)
|
|
34
|
+
continue;
|
|
35
|
+
if (opts.since && c.authorDate < opts.since)
|
|
36
|
+
continue;
|
|
37
|
+
const key = `${c.authorName}|${c.authorEmail}`;
|
|
38
|
+
if (!buckets.has(key)) {
|
|
39
|
+
buckets.set(key, {
|
|
40
|
+
name: c.authorName,
|
|
41
|
+
email: c.authorEmail,
|
|
42
|
+
commitCount: 0,
|
|
43
|
+
lastTouch: c.authorDate,
|
|
44
|
+
files: new Set(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const b = buckets.get(key);
|
|
48
|
+
b.commitCount += 1;
|
|
49
|
+
if (c.authorDate > b.lastTouch)
|
|
50
|
+
b.lastTouch = c.authorDate;
|
|
51
|
+
for (const f of (c.files ?? []).slice(0, 50))
|
|
52
|
+
b.files.add(f);
|
|
53
|
+
if (b.files.size > 1000) {
|
|
54
|
+
// Hygiene cap — extremely active authors on huge repos.
|
|
55
|
+
const arr = [...b.files];
|
|
56
|
+
b.files = new Set(arr.slice(0, 1000));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const candidates = [];
|
|
60
|
+
for (const b of buckets.values()) {
|
|
61
|
+
const score = scoreCandidate(b.commitCount, b.lastTouch, now);
|
|
62
|
+
candidates.push({
|
|
63
|
+
name: b.name,
|
|
64
|
+
email: b.email,
|
|
65
|
+
commitCount: b.commitCount,
|
|
66
|
+
lastTouch: b.lastTouch,
|
|
67
|
+
filesTouched: b.files.size,
|
|
68
|
+
score,
|
|
69
|
+
tier: tierOf(b.commitCount, b.lastTouch, now),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
73
|
+
return candidates.slice(0, topN);
|
|
74
|
+
}
|
|
75
|
+
/** log-based commit weight × recency multiplier. Pure function, easy to test. */
|
|
76
|
+
export function scoreCandidate(commitCount, lastTouchIso, now) {
|
|
77
|
+
const lastTouch = new Date(lastTouchIso);
|
|
78
|
+
const daysAgo = Math.max(0, (now.getTime() - lastTouch.getTime()) / (1000 * 60 * 60 * 24));
|
|
79
|
+
// Recency bonus: 1.0 within 30 days, decays linearly to 0.3 at 365 days, then floor 0.3.
|
|
80
|
+
const recency = daysAgo < 30 ? 1.0 : Math.max(0.3, 1.0 - (daysAgo - 30) / 480);
|
|
81
|
+
// Volume: log scale to avoid one mega-contributor dominating.
|
|
82
|
+
const volume = Math.log2(commitCount + 1);
|
|
83
|
+
return volume * recency;
|
|
84
|
+
}
|
|
85
|
+
export function tierOf(commitCount, lastTouchIso, now) {
|
|
86
|
+
const lastTouch = new Date(lastTouchIso);
|
|
87
|
+
const daysAgo = (now.getTime() - lastTouch.getTime()) / (1000 * 60 * 60 * 24);
|
|
88
|
+
if (commitCount >= 10 && daysAgo <= ACTIVE_DAYS)
|
|
89
|
+
return "definitive";
|
|
90
|
+
if (commitCount >= 3 && daysAgo <= ACTIVE_DAYS)
|
|
91
|
+
return "active";
|
|
92
|
+
if (daysAgo > STALE_DAYS)
|
|
93
|
+
return "stale";
|
|
94
|
+
return "occasional";
|
|
95
|
+
}
|
|
96
|
+
export function whoKnowsVerdict(candidates) {
|
|
97
|
+
if (candidates.length === 0) {
|
|
98
|
+
return { confidencePct: 0, totalCommits: 0 };
|
|
99
|
+
}
|
|
100
|
+
const sorted = [...candidates].sort((a, b) => b.score - a.score);
|
|
101
|
+
const top = sorted[0];
|
|
102
|
+
const total = candidates.reduce((s, c) => s + c.commitCount, 0);
|
|
103
|
+
const pct = total === 0 ? 0 : Math.round((top.commitCount / total) * 100);
|
|
104
|
+
const backup = sorted[1];
|
|
105
|
+
let risk;
|
|
106
|
+
if (top.tier === "stale") {
|
|
107
|
+
risk = `Top expert is STALE — last touched ${formatDaysAgo(top.lastTouch)} ago. Consider asking ${backup?.name ?? "someone with recent activity"} instead.`;
|
|
108
|
+
}
|
|
109
|
+
else if (pct < 30 && candidates.length >= 3) {
|
|
110
|
+
risk = `No single dominant expert — ${candidates.length} people share the work. Knowledge is well-distributed but no one will know everything.`;
|
|
111
|
+
}
|
|
112
|
+
else if (top.commitCount === 1) {
|
|
113
|
+
risk = "Top expert touched the topic only once — treat as weak signal.";
|
|
114
|
+
}
|
|
115
|
+
return { topExpert: top, confidencePct: pct, backup, risk, totalCommits: total };
|
|
116
|
+
}
|
|
117
|
+
function formatDaysAgo(iso) {
|
|
118
|
+
const d = (Date.now() - new Date(iso).getTime()) / 86_400_000;
|
|
119
|
+
if (d < 30)
|
|
120
|
+
return `${Math.round(d)}d`;
|
|
121
|
+
if (d < 365)
|
|
122
|
+
return `${Math.round(d / 30)}mo`;
|
|
123
|
+
return `${(d / 365).toFixed(1)}y`;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=who-knows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"who-knows.js","sourceRoot":"","sources":["../../src/insights/who-knows.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"who-knows.test.d.ts","sourceRoot":"","sources":["../../src/insights/who-knows.test.ts"],"names":[],"mappings":""}
|