@mneme-ai/core 0.8.3 → 0.9.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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/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 +16 -0
- package/dist/insights/index.d.ts.map +1 -0
- package/dist/insights/index.js +16 -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/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 +48 -0
- package/dist/insights/who-knows.d.ts.map +1 -0
- package/dist/insights/who-knows.js +96 -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 +47 -0
- package/dist/insights/who-knows.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 +160 -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 +116 -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 +55 -1
- package/dist/store/schema.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,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `stack-trace` — given an error / stack trace, find the commits that
|
|
3
|
+
* touched each frame and any past incidents at the same locations.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: when a prod bug fires, the most useful question is
|
|
6
|
+
* "have we seen this here before?" Mneme already indexes git + incidents.
|
|
7
|
+
* Parsing a stack trace and querying both is a small composition of pieces
|
|
8
|
+
* we already have.
|
|
9
|
+
*
|
|
10
|
+
* Supported formats (frame parser):
|
|
11
|
+
* - JS/TS: " at functionName (path/to/file.ts:42:15)"
|
|
12
|
+
* - Python: " File "path/to/file.py", line 42, in functionName"
|
|
13
|
+
* - Go: "goroutine 1 [running]: ... path/to/file.go:42 +0x..."
|
|
14
|
+
* - Java: " at com.example.Foo.bar(Foo.java:42)"
|
|
15
|
+
*
|
|
16
|
+
* Pure parsing — no I/O. Composing with retrieval lives in the CLI command.
|
|
17
|
+
*/
|
|
18
|
+
const PATTERNS = [
|
|
19
|
+
// JS/TS — V8 / Node.js / browser
|
|
20
|
+
// " at functionName (path/to/file.ts:42:15)"
|
|
21
|
+
// " at path/to/file.ts:42:15"
|
|
22
|
+
{
|
|
23
|
+
language: "js",
|
|
24
|
+
re: /\bat\s+(?:(?<fn>[\w$.<>[\]\s]+?)\s+\()?(?<file>[^():\n]+\.[a-zA-Z]+):(?<line>\d+)(?::\d+)?\)?/g,
|
|
25
|
+
map: (m) => {
|
|
26
|
+
const file = m.groups?.file?.trim();
|
|
27
|
+
if (!file)
|
|
28
|
+
return null;
|
|
29
|
+
return {
|
|
30
|
+
file,
|
|
31
|
+
line: Number(m.groups?.line ?? 0),
|
|
32
|
+
function: m.groups?.fn?.trim() || undefined,
|
|
33
|
+
language: "js",
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
// Python — CPython tracebacks
|
|
38
|
+
// ' File "path/to/file.py", line 42, in functionName'
|
|
39
|
+
{
|
|
40
|
+
language: "python",
|
|
41
|
+
re: /File\s+"(?<file>[^"]+\.py[i]?)",\s+line\s+(?<line>\d+)(?:,\s+in\s+(?<fn>\w+))?/g,
|
|
42
|
+
map: (m) => ({
|
|
43
|
+
file: m.groups.file,
|
|
44
|
+
line: Number(m.groups.line),
|
|
45
|
+
function: m.groups?.fn || undefined,
|
|
46
|
+
language: "python",
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
// Go — runtime panic format
|
|
50
|
+
// " /home/x/main.go:42 +0x..."
|
|
51
|
+
// "main.foo(0x0, 0x0)"
|
|
52
|
+
// " /home/x/foo.go:42"
|
|
53
|
+
{
|
|
54
|
+
language: "go",
|
|
55
|
+
re: /(?<file>[^\s():]+\.go):(?<line>\d+)/g,
|
|
56
|
+
map: (m) => ({
|
|
57
|
+
file: m.groups.file,
|
|
58
|
+
line: Number(m.groups.line),
|
|
59
|
+
language: "go",
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
// Java
|
|
63
|
+
// " at com.example.Foo.bar(Foo.java:42)"
|
|
64
|
+
{
|
|
65
|
+
language: "java",
|
|
66
|
+
re: /\bat\s+(?<fn>[\w.$]+)\((?<file>[^()]+\.java):(?<line>\d+)\)/g,
|
|
67
|
+
map: (m) => ({
|
|
68
|
+
file: m.groups.file,
|
|
69
|
+
line: Number(m.groups.line),
|
|
70
|
+
function: m.groups?.fn || undefined,
|
|
71
|
+
language: "java",
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Parse a multi-line stack trace into ordered frames. Frames are deduped
|
|
77
|
+
* when the same (file, line) appears consecutively. Languages that share
|
|
78
|
+
* patterns (e.g. .ts files might also match the python file regex if
|
|
79
|
+
* weirdly formatted) are disambiguated by extension first.
|
|
80
|
+
*/
|
|
81
|
+
export function parseStackTrace(text) {
|
|
82
|
+
if (!text || !text.trim())
|
|
83
|
+
return [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const frames = [];
|
|
86
|
+
for (const p of PATTERNS) {
|
|
87
|
+
const re = new RegExp(p.re.source, p.re.flags);
|
|
88
|
+
let m;
|
|
89
|
+
while ((m = re.exec(text)) !== null) {
|
|
90
|
+
const frame = p.map(m);
|
|
91
|
+
if (!frame)
|
|
92
|
+
continue;
|
|
93
|
+
// Skip language-mismatch (e.g. js regex matching a Java line).
|
|
94
|
+
const ext = frame.file.split(".").pop()?.toLowerCase();
|
|
95
|
+
if (frame.language === "js" && ext && !["js", "ts", "jsx", "tsx", "mjs", "cjs"].includes(ext))
|
|
96
|
+
continue;
|
|
97
|
+
const key = `${frame.file}:${frame.line}`;
|
|
98
|
+
if (seen.has(key))
|
|
99
|
+
continue;
|
|
100
|
+
seen.add(key);
|
|
101
|
+
frames.push(frame);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Sort by appearance order in original text (so most-recent-frame-first
|
|
105
|
+
// for languages where the trace is bottom-up like Python).
|
|
106
|
+
// Detection: count file paths — the first frame in the input is what we keep first.
|
|
107
|
+
return frames.sort((a, b) => text.indexOf(a.file) - text.indexOf(b.file));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Heuristic: detect whether the trace appears to be from a particular
|
|
111
|
+
* language. Useful when displaying frame analysis ("Python traceback").
|
|
112
|
+
*/
|
|
113
|
+
export function detectLanguage(text) {
|
|
114
|
+
const t = text.toLowerCase();
|
|
115
|
+
if (t.includes("traceback") || t.includes('file "') || t.includes(".py"))
|
|
116
|
+
return t.includes(".py") ? "python" : "unknown";
|
|
117
|
+
if (t.includes("goroutine") || /\.go:\d+/.test(t))
|
|
118
|
+
return "go";
|
|
119
|
+
if (/\.java:\d+/.test(t))
|
|
120
|
+
return "java";
|
|
121
|
+
if (/\b(typeerror|referenceerror|syntaxerror|rangeerror)\b/.test(t))
|
|
122
|
+
return "js";
|
|
123
|
+
if (/\.[jt]sx?:\d+/.test(t))
|
|
124
|
+
return "js";
|
|
125
|
+
return "unknown";
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=stack-trace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-trace.js","sourceRoot":"","sources":["../../src/insights/stack-trace.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-trace.test.d.ts","sourceRoot":"","sources":["../../src/insights/stack-trace.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseStackTrace, detectLanguage } from "./stack-trace.js";
|
|
3
|
+
describe("parseStackTrace — JavaScript/TypeScript", () => {
|
|
4
|
+
it("parses V8-style frames with function name", () => {
|
|
5
|
+
const trace = `TypeError: Cannot read property 'amount' of undefined
|
|
6
|
+
at parseAmount (src/payment.ts:42:15)
|
|
7
|
+
at processCharge (src/payment.ts:78:9)
|
|
8
|
+
at Object.<anonymous> (src/index.ts:5:1)`;
|
|
9
|
+
const frames = parseStackTrace(trace);
|
|
10
|
+
expect(frames).toHaveLength(3);
|
|
11
|
+
expect(frames[0]).toMatchObject({ file: "src/payment.ts", line: 42, function: "parseAmount", language: "js" });
|
|
12
|
+
expect(frames[1].function).toBe("processCharge");
|
|
13
|
+
});
|
|
14
|
+
it("parses anonymous frames (no function name)", () => {
|
|
15
|
+
const trace = `Error: kaboom
|
|
16
|
+
at /app/dist/main.js:1:42`;
|
|
17
|
+
const frames = parseStackTrace(trace);
|
|
18
|
+
expect(frames[0]?.file).toBe("/app/dist/main.js");
|
|
19
|
+
expect(frames[0]?.function).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("parseStackTrace — Python", () => {
|
|
23
|
+
it("parses CPython tracebacks", () => {
|
|
24
|
+
const trace = `Traceback (most recent call last):
|
|
25
|
+
File "/app/main.py", line 42, in process_charge
|
|
26
|
+
amount = parse_amount(payload["amount"])
|
|
27
|
+
File "/app/payment.py", line 17, in parse_amount
|
|
28
|
+
return Decimal(value)
|
|
29
|
+
ValueError: not a number`;
|
|
30
|
+
const frames = parseStackTrace(trace);
|
|
31
|
+
expect(frames.length).toBeGreaterThanOrEqual(2);
|
|
32
|
+
const main = frames.find((f) => f.file.endsWith("main.py"));
|
|
33
|
+
expect(main).toMatchObject({ line: 42, function: "process_charge", language: "python" });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("parseStackTrace — Go", () => {
|
|
37
|
+
it("parses Go panic file:line references", () => {
|
|
38
|
+
const trace = `goroutine 1 [running]:
|
|
39
|
+
main.foo(0x0)
|
|
40
|
+
/home/dev/app/main.go:42 +0x12
|
|
41
|
+
runtime.main()
|
|
42
|
+
/usr/local/go/src/runtime/proc.go:250`;
|
|
43
|
+
const frames = parseStackTrace(trace);
|
|
44
|
+
expect(frames.length).toBeGreaterThanOrEqual(2);
|
|
45
|
+
expect(frames.find((f) => f.file === "/home/dev/app/main.go")?.line).toBe(42);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("parseStackTrace — Java", () => {
|
|
49
|
+
it("parses Java stack frames with class.method(File.java:line)", () => {
|
|
50
|
+
const trace = `Exception in thread "main" java.lang.NullPointerException
|
|
51
|
+
at com.example.Foo.bar(Foo.java:42)
|
|
52
|
+
at com.example.App.main(App.java:7)`;
|
|
53
|
+
const frames = parseStackTrace(trace);
|
|
54
|
+
expect(frames).toHaveLength(2);
|
|
55
|
+
expect(frames[0]).toMatchObject({ file: "Foo.java", line: 42, language: "java" });
|
|
56
|
+
expect(frames[0].function).toContain("Foo.bar");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("parseStackTrace — edge cases", () => {
|
|
60
|
+
it("returns empty array for empty input", () => {
|
|
61
|
+
expect(parseStackTrace("")).toEqual([]);
|
|
62
|
+
expect(parseStackTrace(" \n \n")).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
it("dedupes consecutive identical (file, line) frames", () => {
|
|
65
|
+
const trace = `at a (foo.ts:42:1)
|
|
66
|
+
at a (foo.ts:42:1)
|
|
67
|
+
at a (foo.ts:42:1)`;
|
|
68
|
+
expect(parseStackTrace(trace)).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
it("filters out non-matching language extensions", () => {
|
|
71
|
+
// ".java" file should not match the JS regex even if the format looks similar.
|
|
72
|
+
const trace = `at com.x.Foo.bar(Foo.java:42)`;
|
|
73
|
+
const frames = parseStackTrace(trace);
|
|
74
|
+
expect(frames.every((f) => f.language === "java")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it("handles a mixed trace (multiple languages in one input)", () => {
|
|
77
|
+
const trace = `TypeError: kaboom
|
|
78
|
+
at handler (src/api.ts:42:1)
|
|
79
|
+
And in the worker:
|
|
80
|
+
File "/app/worker.py", line 7, in run_job`;
|
|
81
|
+
const frames = parseStackTrace(trace);
|
|
82
|
+
expect(frames.find((f) => f.language === "js")).toBeDefined();
|
|
83
|
+
expect(frames.find((f) => f.language === "python")).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe("detectLanguage — heuristic top-level detection", () => {
|
|
87
|
+
it("python (Traceback header)", () => {
|
|
88
|
+
expect(detectLanguage("Traceback (most recent call last):\n File \"x.py\", line 1, in f")).toBe("python");
|
|
89
|
+
});
|
|
90
|
+
it("go (goroutine header)", () => {
|
|
91
|
+
expect(detectLanguage("goroutine 1 [running]:\n/x/main.go:1 +0x")).toBe("go");
|
|
92
|
+
});
|
|
93
|
+
it("java (.java:N pattern)", () => {
|
|
94
|
+
expect(detectLanguage("at com.x.Foo.bar(Foo.java:42)")).toBe("java");
|
|
95
|
+
});
|
|
96
|
+
it("js (TypeError keyword)", () => {
|
|
97
|
+
expect(detectLanguage("TypeError: Cannot read property 'x' of undefined")).toBe("js");
|
|
98
|
+
});
|
|
99
|
+
it("unknown when no marker", () => {
|
|
100
|
+
expect(detectLanguage("just some random text")).toBe("unknown");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=stack-trace.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-trace.test.js","sourceRoot":"","sources":["../../src/insights/stack-trace.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEnE,QAAQ,CAAC,yCAAyC,EAAE,GAAG,EAAE;IACvD,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,KAAK,GAAG;;;6CAG2B,CAAC;QAC1C,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/G,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,KAAK,GAAG;8BACY,CAAC;QAC3B,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAClD,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,aAAa,EAAE,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,KAAK,GAAG;;;;;yBAKO,CAAC;QACtB,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;QAChD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC3F,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,KAAK,GAAG;;;;uCAIqB,CAAC;QACpC,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,uBAAuB,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,KAAK,GAAG;;wCAEsB,CAAC;QACrC,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAClF,MAAM,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACxC,MAAM,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,KAAK,GAAG;;mBAEC,CAAC;QAChB,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,+EAA+E;QAC/E,MAAM,KAAK,GAAG,+BAA+B,CAAC;QAC9C,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,KAAK,GAAG;;;4CAG0B,CAAC;QACzC,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC9D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IACpE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gDAAgD,EAAE,GAAG,EAAE;IAC9D,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,cAAc,CAAC,mEAAmE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7G,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC/B,MAAM,CAAC,cAAc,CAAC,0CAA0C,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,cAAc,CAAC,+BAA+B,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,cAAc,CAAC,kDAAkD,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,cAAc,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `story <topic>` — narrative generation. Take a topic, find every commit
|
|
3
|
+
* that touched it, group into "acts" (initial / refactor / incidents /
|
|
4
|
+
* stable), and emit a structured timeline. The LLM (when available) can
|
|
5
|
+
* polish the act labels and write a short prose summary.
|
|
6
|
+
*
|
|
7
|
+
* Pure data extraction in this file. The CLI command renders + optionally
|
|
8
|
+
* pipes through an LLM for prose.
|
|
9
|
+
*/
|
|
10
|
+
import type { Commit } from "../types.js";
|
|
11
|
+
export interface StoryAct {
|
|
12
|
+
/** Stable id used for the section header. */
|
|
13
|
+
id: "initial" | "refactor" | "incident" | "evolution" | "stable";
|
|
14
|
+
/** Human-readable title — "Act I — The Beginning" etc. */
|
|
15
|
+
title: string;
|
|
16
|
+
/** Commits in chronological order (oldest first). */
|
|
17
|
+
commits: Commit[];
|
|
18
|
+
/** Date range of the act. */
|
|
19
|
+
fromDate: string;
|
|
20
|
+
toDate: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Story {
|
|
23
|
+
topic: string;
|
|
24
|
+
acts: StoryAct[];
|
|
25
|
+
totalCommits: number;
|
|
26
|
+
spanDays: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build a Story from a list of commits matching the topic. Commits should
|
|
30
|
+
* already be filtered by relevance (e.g. via FTS) and sorted chronologically
|
|
31
|
+
* by the caller.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildStory(topic: string, commits: Commit[]): Story;
|
|
34
|
+
//# sourceMappingURL=story.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"story.d.ts","sourceRoot":"","sources":["../../src/insights/story.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,QAAQ;IACvB,6CAA6C;IAC7C,EAAE,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,CAAC;IACjE,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,CAqElE"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `story <topic>` — narrative generation. Take a topic, find every commit
|
|
3
|
+
* that touched it, group into "acts" (initial / refactor / incidents /
|
|
4
|
+
* stable), and emit a structured timeline. The LLM (when available) can
|
|
5
|
+
* polish the act labels and write a short prose summary.
|
|
6
|
+
*
|
|
7
|
+
* Pure data extraction in this file. The CLI command renders + optionally
|
|
8
|
+
* pipes through an LLM for prose.
|
|
9
|
+
*/
|
|
10
|
+
const REFACTOR_KEYWORDS = /\b(refactor|rewrite|migrate|switch(?:ed)?|replace[ds]?|deprecate)\b/i;
|
|
11
|
+
const INCIDENT_KEYWORDS = /\b(incident|outage|hotfix|revert|rollback|broken|critical|emergency|p[01])\b/i;
|
|
12
|
+
/**
|
|
13
|
+
* Build a Story from a list of commits matching the topic. Commits should
|
|
14
|
+
* already be filtered by relevance (e.g. via FTS) and sorted chronologically
|
|
15
|
+
* by the caller.
|
|
16
|
+
*/
|
|
17
|
+
export function buildStory(topic, commits) {
|
|
18
|
+
const sorted = [...commits].sort((a, b) => a.authorDate.localeCompare(b.authorDate));
|
|
19
|
+
if (sorted.length === 0) {
|
|
20
|
+
return { topic, acts: [], totalCommits: 0, spanDays: 0 };
|
|
21
|
+
}
|
|
22
|
+
const acts = [];
|
|
23
|
+
// Act I — Initial: the very first commit on the topic.
|
|
24
|
+
const initial = sorted[0];
|
|
25
|
+
acts.push({
|
|
26
|
+
id: "initial",
|
|
27
|
+
title: "Act I — The Beginning",
|
|
28
|
+
commits: [initial],
|
|
29
|
+
fromDate: initial.authorDate.slice(0, 10),
|
|
30
|
+
toDate: initial.authorDate.slice(0, 10),
|
|
31
|
+
});
|
|
32
|
+
const remaining = sorted.slice(1);
|
|
33
|
+
let currentFlavor = null;
|
|
34
|
+
let currentBucket = [];
|
|
35
|
+
const flush = () => {
|
|
36
|
+
if (currentBucket.length === 0 || !currentFlavor)
|
|
37
|
+
return;
|
|
38
|
+
acts.push({
|
|
39
|
+
id: currentFlavor,
|
|
40
|
+
title: titleFor(currentFlavor, acts.length),
|
|
41
|
+
commits: [...currentBucket],
|
|
42
|
+
fromDate: currentBucket[0].authorDate.slice(0, 10),
|
|
43
|
+
toDate: currentBucket[currentBucket.length - 1].authorDate.slice(0, 10),
|
|
44
|
+
});
|
|
45
|
+
currentBucket = [];
|
|
46
|
+
currentFlavor = null;
|
|
47
|
+
};
|
|
48
|
+
for (const c of remaining) {
|
|
49
|
+
const flavor = detectFlavor(c);
|
|
50
|
+
if (flavor !== currentFlavor)
|
|
51
|
+
flush();
|
|
52
|
+
currentFlavor = flavor;
|
|
53
|
+
currentBucket.push(c);
|
|
54
|
+
}
|
|
55
|
+
flush();
|
|
56
|
+
// Final act — if the most recent commit is older than 90 days, consider
|
|
57
|
+
// the topic "stable" (no recent change).
|
|
58
|
+
const last = sorted[sorted.length - 1];
|
|
59
|
+
const ageDays = (Date.now() - new Date(last.authorDate).getTime()) / 86_400_000;
|
|
60
|
+
if (ageDays > 90 && acts[acts.length - 1].id !== "stable") {
|
|
61
|
+
acts.push({
|
|
62
|
+
id: "stable",
|
|
63
|
+
title: `${acts.length === 0 ? "Act I" : romanFor(acts.length + 1)} — The Stable State`,
|
|
64
|
+
commits: [last],
|
|
65
|
+
fromDate: last.authorDate.slice(0, 10),
|
|
66
|
+
toDate: new Date().toISOString().slice(0, 10),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const span = Math.round((new Date(last.authorDate).getTime() - new Date(initial.authorDate).getTime()) / 86_400_000);
|
|
70
|
+
return {
|
|
71
|
+
topic,
|
|
72
|
+
acts,
|
|
73
|
+
totalCommits: sorted.length,
|
|
74
|
+
spanDays: span,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function detectFlavor(c) {
|
|
78
|
+
const text = `${c.subject}\n${c.body || ""}`;
|
|
79
|
+
if (INCIDENT_KEYWORDS.test(text))
|
|
80
|
+
return "incident";
|
|
81
|
+
if (REFACTOR_KEYWORDS.test(text))
|
|
82
|
+
return "refactor";
|
|
83
|
+
return "evolution";
|
|
84
|
+
}
|
|
85
|
+
function titleFor(flavor, actIndex) {
|
|
86
|
+
const roman = romanFor(actIndex + 1);
|
|
87
|
+
switch (flavor) {
|
|
88
|
+
case "refactor":
|
|
89
|
+
return `${roman} — The Refactor`;
|
|
90
|
+
case "incident":
|
|
91
|
+
return `${roman} — Incidents Strike`;
|
|
92
|
+
case "evolution":
|
|
93
|
+
return `${roman} — Steady Evolution`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function romanFor(n) {
|
|
97
|
+
const roman = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"];
|
|
98
|
+
return n <= 10 ? `Act ${roman[n - 1]}` : `Act ${n}`;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=story.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"story.js","sourceRoot":"","sources":["../../src/insights/story.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"story.test.d.ts","sourceRoot":"","sources":["../../src/insights/story.test.ts"],"names":[],"mappings":""}
|
|
@@ -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
|