diffintel 0.1.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/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # diffintel
2
+
3
+ Experimantal(!) structural diff explainer for pull requests. Parses changed JS/TS files with Tree-sitter, detects structural changes (functions, classes, imports, exports), and uses an LLM to generate a concise explanation with risk assessment. Outputs a self-contained HTML report. Helpful for getting a concise explanation on diffs. Inspired by the hard-to-ingest-code-masses the ai-agents write.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g diffintel
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ export ANTHROPIC_API_KEY=sk-ant-...
15
+ diffintel explain --base main --out report.html
16
+ ```
17
+
18
+ | Flag | Default | Description |
19
+ |---|---|---|
20
+ | `--base <ref>` | `origin/main` | Base git ref |
21
+ | `--head <ref>` | `HEAD` | Head git ref |
22
+ | `--out <file>` | `explain-report.html` | Output path |
23
+
24
+ ## GitHub Action
25
+
26
+ ```yaml
27
+ name: PR Explain
28
+ on:
29
+ pull_request:
30
+ types: [opened, synchronize]
31
+
32
+ jobs:
33
+ explain:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ with:
38
+ fetch-depth: 0
39
+ - uses: actions/setup-node@v4
40
+ with:
41
+ node-version: 20
42
+ - run: npm install -g diffintel
43
+ - name: Generate report
44
+ env:
45
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
46
+ run: diffintel explain --base origin/${{ github.base_ref }}
47
+ - uses: actions/upload-artifact@v4
48
+ with:
49
+ name: explain-report
50
+ path: explain-report.html
51
+ ```
52
+
53
+ ## Development
54
+
55
+ ```bash
56
+ npm install
57
+ npm test
58
+ npx tsx src/cli.ts explain --base HEAD~3
59
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const package_json_1 = __importDefault(require("../package.json"));
9
+ commander_1.program
10
+ .name("diffintel")
11
+ .description("AI-powered structural diff explainer for pull requests")
12
+ .version(package_json_1.default.version);
13
+ commander_1.program
14
+ .command("explain")
15
+ .description("Generate an HTML report explaining code changes between refs")
16
+ .option("--base <ref>", "Base ref to diff from (default: origin/main)")
17
+ .option("--head <ref>", "Head ref to diff to (default: HEAD)")
18
+ .option("--out <file>", "Output HTML file path (default: explain-report.html)")
19
+ .action(async (opts) => {
20
+ require("dotenv").config();
21
+ const { run } = require("./commands/explain");
22
+ const code = await run(opts);
23
+ process.exit(code);
24
+ });
25
+ commander_1.program.parse();
26
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;AAEA,yCAAoC;AACpC,mEAAkC;AAElC,mBAAO;KACJ,IAAI,CAAC,WAAW,CAAC;KACjB,WAAW,CAAC,wDAAwD,CAAC;KACrE,OAAO,CAAC,sBAAG,CAAC,OAAO,CAAC,CAAC;AAExB,mBAAO;KACJ,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,8DAA8D,CAAC;KAC3E,MAAM,CAAC,cAAc,EAAE,8CAA8C,CAAC;KACtE,MAAM,CAAC,cAAc,EAAE,qCAAqC,CAAC;KAC7D,MAAM,CAAC,cAAc,EAAE,sDAAsD,CAAC;KAC9E,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;IACrB,OAAO,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC,CAAC,CAAC;AAEL,mBAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,7 @@
1
+ interface ExplainOptions {
2
+ base?: string;
3
+ head?: string;
4
+ out?: string;
5
+ }
6
+ export declare function run(opts: ExplainOptions): Promise<number>;
7
+ export {};
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.run = run;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const git_diff_1 = require("../explain/git-diff");
9
+ const ast_diff_1 = require("../explain/ast-diff");
10
+ const llm_explain_1 = require("../explain/llm-explain");
11
+ const html_report_1 = require("../explain/html-report");
12
+ async function run(opts) {
13
+ const baseRef = opts.base || "origin/main";
14
+ const headRef = opts.head || "HEAD";
15
+ const outFile = opts.out || "explain-report.html";
16
+ if (!process.env.ANTHROPIC_API_KEY) {
17
+ console.error("Error: ANTHROPIC_API_KEY environment variable is required.");
18
+ return 1;
19
+ }
20
+ try {
21
+ console.error(`Analyzing diff: ${baseRef}...${headRef}`);
22
+ const { files: fileDiffs, rawDiff } = (0, git_diff_1.getDiff)(baseRef, headRef);
23
+ if (fileDiffs.length === 0) {
24
+ console.error("No changes found between refs.");
25
+ return 0;
26
+ }
27
+ console.error(`Found ${fileDiffs.length} changed file(s). Analyzing...`);
28
+ const fileAnalyses = fileDiffs.map(ast_diff_1.analyzeFile);
29
+ const totalChanges = fileAnalyses.reduce((sum, f) => sum + f.structuralChanges.length, 0);
30
+ console.error(`Detected ${totalChanges} structural change(s). Calling LLM...`);
31
+ const explanation = await (0, llm_explain_1.explainChanges)(fileAnalyses, rawDiff);
32
+ const totalAdditions = fileDiffs.reduce((sum, f) => sum + f.additions, 0);
33
+ const totalDeletions = fileDiffs.reduce((sum, f) => sum + f.deletions, 0);
34
+ const report = {
35
+ generatedAt: new Date().toISOString(),
36
+ baseRef,
37
+ headRef,
38
+ summary: {
39
+ filesChanged: fileDiffs.length,
40
+ additions: totalAdditions,
41
+ deletions: totalDeletions,
42
+ },
43
+ explanation,
44
+ files: fileAnalyses,
45
+ };
46
+ const html = (0, html_report_1.renderReport)(report);
47
+ fs_1.default.writeFileSync(outFile, html, "utf-8");
48
+ console.error(`\nTitle: ${explanation.title}`);
49
+ console.error(`Files: ${fileDiffs.length} | +${totalAdditions} -${totalDeletions}`);
50
+ console.error(`Risks: ${explanation.risks.length > 0 ? explanation.risks.map((r) => `${r.level}: ${r.description}`).join("; ") : "none"}`);
51
+ console.error(`Tokens: ${explanation.tokenUsage.input} in / ${explanation.tokenUsage.output} out`);
52
+ console.error(`\nReport written to: ${outFile}`);
53
+ return 0;
54
+ }
55
+ catch (err) {
56
+ console.error(`Error: ${err.message}`);
57
+ return 1;
58
+ }
59
+ }
60
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explain.js","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { FileDiff, FileAnalysis, ChangeType } from "./types";
2
+ interface Declaration {
3
+ name: string;
4
+ type: ChangeType;
5
+ text: string;
6
+ startLine: number;
7
+ }
8
+ export declare function analyzeFile(diff: FileDiff): FileAnalysis;
9
+ export declare function extractDeclarations(source: string, ext: string): Declaration[];
10
+ export {};
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.analyzeFile = analyzeFile;
7
+ exports.extractDeclarations = extractDeclarations;
8
+ const path_1 = __importDefault(require("path"));
9
+ const parser_1 = require("../parsing/parser");
10
+ const SUPPORTED_EXTS = new Set([".js", ".ts", ".tsx", ".jsx"]);
11
+ function analyzeFile(diff) {
12
+ const ext = path_1.default.extname(diff.path);
13
+ if (!SUPPORTED_EXTS.has(ext)) {
14
+ return {
15
+ path: diff.path,
16
+ status: diff.status,
17
+ language: null,
18
+ structuralChanges: [],
19
+ rawDiff: diff.hunks,
20
+ };
21
+ }
22
+ const language = ext.replace(".", "");
23
+ const structuralChanges = [];
24
+ if (diff.status === "added" && diff.newContent) {
25
+ const decls = extractDeclarations(diff.newContent, ext);
26
+ for (const d of decls) {
27
+ structuralChanges.push({
28
+ file: diff.path,
29
+ type: d.type,
30
+ action: "added",
31
+ name: d.name,
32
+ startLine: d.startLine,
33
+ });
34
+ }
35
+ }
36
+ else if (diff.status === "deleted" && diff.oldContent) {
37
+ const decls = extractDeclarations(diff.oldContent, ext);
38
+ for (const d of decls) {
39
+ structuralChanges.push({
40
+ file: diff.path,
41
+ type: d.type,
42
+ action: "removed",
43
+ name: d.name,
44
+ startLine: d.startLine,
45
+ });
46
+ }
47
+ }
48
+ else if ((diff.status === "modified" || diff.status === "renamed") && diff.oldContent && diff.newContent) {
49
+ const oldDecls = extractDeclarations(diff.oldContent, ext);
50
+ const newDecls = extractDeclarations(diff.newContent, ext);
51
+ const oldMap = new Map(oldDecls.map((d) => [d.name, d]));
52
+ const newMap = new Map(newDecls.map((d) => [d.name, d]));
53
+ // Removed declarations
54
+ for (const [name, decl] of oldMap) {
55
+ if (!newMap.has(name)) {
56
+ structuralChanges.push({
57
+ file: diff.path,
58
+ type: decl.type,
59
+ action: "removed",
60
+ name,
61
+ startLine: decl.startLine,
62
+ });
63
+ }
64
+ }
65
+ // Added or modified declarations
66
+ for (const [name, decl] of newMap) {
67
+ const old = oldMap.get(name);
68
+ if (!old) {
69
+ structuralChanges.push({
70
+ file: diff.path,
71
+ type: decl.type,
72
+ action: "added",
73
+ name,
74
+ startLine: decl.startLine,
75
+ });
76
+ }
77
+ else if (old.text !== decl.text) {
78
+ structuralChanges.push({
79
+ file: diff.path,
80
+ type: decl.type,
81
+ action: "modified",
82
+ name,
83
+ startLine: decl.startLine,
84
+ });
85
+ }
86
+ }
87
+ }
88
+ return {
89
+ path: diff.path,
90
+ status: diff.status,
91
+ language,
92
+ structuralChanges,
93
+ rawDiff: diff.hunks,
94
+ };
95
+ }
96
+ function extractDeclarations(source, ext) {
97
+ if (!source.trim())
98
+ return [];
99
+ const { tree } = (0, parser_1.parseSource)(source, ext);
100
+ const decls = [];
101
+ const root = tree.rootNode;
102
+ for (let i = 0; i < root.childCount; i++) {
103
+ const node = root.child(i);
104
+ const extracted = extractFromNode(node);
105
+ if (extracted) {
106
+ decls.push(...extracted);
107
+ }
108
+ }
109
+ return decls;
110
+ }
111
+ function extractFromNode(node) {
112
+ const type = node.type;
113
+ if (type === "function_declaration") {
114
+ const name = node.childForFieldName("name")?.text || "<anonymous>";
115
+ return [{ name, type: "function", text: node.text, startLine: node.startPosition.row + 1 }];
116
+ }
117
+ if (type === "class_declaration") {
118
+ const name = node.childForFieldName("name")?.text || "<anonymous>";
119
+ return [{ name, type: "class", text: node.text, startLine: node.startPosition.row + 1 }];
120
+ }
121
+ if (type === "import_statement") {
122
+ const source = node.childForFieldName("source")?.text || node.text;
123
+ return [{ name: source, type: "import", text: node.text, startLine: node.startPosition.row + 1 }];
124
+ }
125
+ if (type === "export_statement") {
126
+ // Named export with declaration inside
127
+ const decl = node.childForFieldName("declaration");
128
+ if (decl) {
129
+ const inner = extractFromNode(decl);
130
+ if (inner) {
131
+ return inner.map((d) => ({ ...d, type: "export" }));
132
+ }
133
+ }
134
+ const name = node.text.slice(0, 60);
135
+ return [{ name, type: "export", text: node.text, startLine: node.startPosition.row + 1 }];
136
+ }
137
+ if (type === "variable_declaration" || type === "lexical_declaration") {
138
+ const results = [];
139
+ for (let i = 0; i < node.childCount; i++) {
140
+ const child = node.child(i);
141
+ if (child.type === "variable_declarator") {
142
+ const name = child.childForFieldName("name")?.text || "<unknown>";
143
+ // Check if value is an arrow function or function expression
144
+ const value = child.childForFieldName("value");
145
+ const isFn = value && (value.type === "arrow_function" || value.type === "function");
146
+ results.push({
147
+ name,
148
+ type: isFn ? "function" : "variable",
149
+ text: node.text,
150
+ startLine: node.startPosition.row + 1,
151
+ });
152
+ }
153
+ }
154
+ if (results.length)
155
+ return results;
156
+ }
157
+ // Expression statements like module.exports = ...
158
+ if (type === "expression_statement") {
159
+ const expr = node.child(0);
160
+ if (expr?.type === "assignment_expression") {
161
+ const left = expr.childForFieldName("left");
162
+ if (left?.text?.startsWith("module.exports")) {
163
+ return [{ name: "module.exports", type: "export", text: node.text, startLine: node.startPosition.row + 1 }];
164
+ }
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ //# sourceMappingURL=ast-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast-diff.js","sourceRoot":"","sources":["../../src/explain/ast-diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { FileDiff } from "./types";
2
+ /**
3
+ * Get the unified diff between two refs.
4
+ * If headRef is omitted, diffs against the working tree.
5
+ */
6
+ export declare function getDiff(baseRef: string, headRef?: string): {
7
+ files: FileDiff[];
8
+ rawDiff: string;
9
+ };
10
+ /**
11
+ * Parse unified diff text into per-file FileDiff objects.
12
+ */
13
+ export declare function parseDiffText(text: string): Array<Pick<FileDiff, "path" | "hunks" | "additions" | "deletions">>;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDiff = getDiff;
4
+ exports.parseDiffText = parseDiffText;
5
+ const child_process_1 = require("child_process");
6
+ /**
7
+ * Get the unified diff between two refs.
8
+ * If headRef is omitted, diffs against the working tree.
9
+ */
10
+ function getDiff(baseRef, headRef) {
11
+ const range = headRef ? `${baseRef}...${headRef}` : baseRef;
12
+ const nameStatus = (0, child_process_1.execSync)(`git diff --name-status ${range}`, { encoding: "utf-8" });
13
+ const rawDiff = (0, child_process_1.execSync)(`git diff ${range}`, { encoding: "utf-8" });
14
+ const fileStatuses = parseNameStatus(nameStatus);
15
+ const fileDiffs = parseDiffText(rawDiff);
16
+ // Merge name-status info with parsed diffs
17
+ const merged = [];
18
+ for (const fs of fileStatuses) {
19
+ const diff = fileDiffs.find((d) => d.path === fs.path) || {
20
+ path: fs.path,
21
+ hunks: "",
22
+ additions: 0,
23
+ deletions: 0,
24
+ };
25
+ const oldContent = fs.status !== "added"
26
+ ? getFileContent(baseRef, fs.oldPath || fs.path)
27
+ : undefined;
28
+ const newContent = fs.status !== "deleted"
29
+ ? getFileContent(headRef || "HEAD", fs.path)
30
+ : undefined;
31
+ merged.push({
32
+ path: fs.path,
33
+ oldPath: fs.oldPath,
34
+ status: fs.status,
35
+ hunks: diff.hunks,
36
+ oldContent,
37
+ newContent,
38
+ additions: diff.additions,
39
+ deletions: diff.deletions,
40
+ });
41
+ }
42
+ return { files: merged, rawDiff };
43
+ }
44
+ function parseNameStatus(text) {
45
+ const results = [];
46
+ for (const line of text.trim().split("\n")) {
47
+ if (!line.trim())
48
+ continue;
49
+ const parts = line.split("\t");
50
+ const code = parts[0].charAt(0);
51
+ if (code === "R") {
52
+ results.push({ path: parts[2], oldPath: parts[1], status: "renamed" });
53
+ }
54
+ else if (code === "A") {
55
+ results.push({ path: parts[1], status: "added" });
56
+ }
57
+ else if (code === "D") {
58
+ results.push({ path: parts[1], status: "deleted" });
59
+ }
60
+ else {
61
+ results.push({ path: parts[1], status: "modified" });
62
+ }
63
+ }
64
+ return results;
65
+ }
66
+ /**
67
+ * Parse unified diff text into per-file FileDiff objects.
68
+ */
69
+ function parseDiffText(text) {
70
+ if (!text.trim())
71
+ return [];
72
+ const files = [];
73
+ // Split on diff headers
74
+ const parts = text.split(/^diff --git /m).filter(Boolean);
75
+ for (const part of parts) {
76
+ const lines = part.split("\n");
77
+ // First line: a/path b/path
78
+ const headerMatch = lines[0].match(/a\/(.+?) b\/(.+)/);
79
+ if (!headerMatch)
80
+ continue;
81
+ const path = headerMatch[2];
82
+ // Find the start of hunks (@@)
83
+ let hunkStart = 0;
84
+ for (let i = 1; i < lines.length; i++) {
85
+ if (lines[i].startsWith("@@")) {
86
+ hunkStart = i;
87
+ break;
88
+ }
89
+ }
90
+ const hunkLines = hunkStart > 0 ? lines.slice(hunkStart) : [];
91
+ const hunks = hunkLines.join("\n");
92
+ let additions = 0;
93
+ let deletions = 0;
94
+ for (const line of hunkLines) {
95
+ if (line.startsWith("+") && !line.startsWith("+++"))
96
+ additions++;
97
+ if (line.startsWith("-") && !line.startsWith("---"))
98
+ deletions++;
99
+ }
100
+ files.push({ path, hunks, additions, deletions });
101
+ }
102
+ return files;
103
+ }
104
+ function getFileContent(ref, filePath) {
105
+ try {
106
+ return (0, child_process_1.execSync)(`git show ${ref}:${filePath}`, { encoding: "utf-8" });
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ //# sourceMappingURL=git-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-diff.js","sourceRoot":"","sources":["../../src/explain/git-diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { ExplainReport } from "./types";
2
+ export declare function renderReport(report: ExplainReport): string;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderReport = renderReport;
4
+ function escapeHtml(str) {
5
+ return str
6
+ .replace(/&/g, "&amp;")
7
+ .replace(/</g, "&lt;")
8
+ .replace(/>/g, "&gt;")
9
+ .replace(/"/g, "&quot;");
10
+ }
11
+ const STATUS_ICON = {
12
+ added: "&#43;", // +
13
+ modified: "&#9998;", // pencil
14
+ deleted: "&#10005;", // x
15
+ renamed: "&#8594;", // arrow
16
+ };
17
+ const ACTION_BADGE = {
18
+ added: { label: "added", color: "#22c55e" },
19
+ removed: { label: "removed", color: "#ef4444" },
20
+ modified: { label: "modified", color: "#f59e0b" },
21
+ };
22
+ const RISK_COLORS = {
23
+ low: { bg: "#dcfce7", text: "#166534" },
24
+ medium: { bg: "#fef3c7", text: "#92400e" },
25
+ high: { bg: "#fee2e2", text: "#991b1b" },
26
+ };
27
+ function renderReport(report) {
28
+ const { explanation, summary, files } = report;
29
+ const riskBadges = explanation.risks.length > 0
30
+ ? explanation.risks.map((r) => {
31
+ const c = RISK_COLORS[r.level] || RISK_COLORS.low;
32
+ return `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:13px;background:${c.bg};color:${c.text};margin-right:6px;">${r.level.toUpperCase()}: ${escapeHtml(r.description)}</span>`;
33
+ }).join("\n")
34
+ : '<span style="color:#166534;">No risks identified</span>';
35
+ const filesTable = files.map((f) => {
36
+ const icon = STATUS_ICON[f.status] || "";
37
+ const changeCount = f.structuralChanges.length;
38
+ return `<tr>
39
+ <td>${icon}</td>
40
+ <td style="font-family:monospace;font-size:13px;">${escapeHtml(f.path)}</td>
41
+ <td>${f.status}</td>
42
+ <td>${f.language || "—"}</td>
43
+ <td>${changeCount}</td>
44
+ </tr>`;
45
+ }).join("\n");
46
+ const fileDetails = files.map((f) => {
47
+ const changesList = f.structuralChanges.length > 0
48
+ ? "<ul style=\"margin:8px 0;padding-left:20px;\">" +
49
+ f.structuralChanges.map((c) => {
50
+ const badge = ACTION_BADGE[c.action] || ACTION_BADGE.modified;
51
+ return `<li><span style="display:inline-block;padding:1px 6px;border-radius:3px;font-size:11px;background:${badge.color};color:white;margin-right:4px;">${badge.label}</span> <code>${escapeHtml(c.name)}</code> <span style="color:#666;">(${c.type})</span></li>`;
52
+ }).join("\n") +
53
+ "</ul>"
54
+ : "<p style=\"color:#888;margin:8px 0;\">No structural changes detected</p>";
55
+ const diffBlock = f.rawDiff
56
+ ? `<pre style="background:#1e1e1e;color:#d4d4d4;padding:12px;border-radius:6px;overflow-x:auto;font-size:12px;line-height:1.5;margin:8px 0;">${colorDiff(f.rawDiff)}</pre>`
57
+ : "<p style=\"color:#888;\">No diff available</p>";
58
+ return `<details style="margin-bottom:12px;border:1px solid #e5e7eb;border-radius:8px;padding:0;">
59
+ <summary style="padding:10px 14px;cursor:pointer;font-family:monospace;font-size:14px;background:#f9fafb;border-radius:8px;">${STATUS_ICON[f.status] || ""} ${escapeHtml(f.path)} <span style="color:#888;">(${f.structuralChanges.length} changes)</span></summary>
60
+ <div style="padding:10px 14px;">
61
+ ${changesList}
62
+ ${diffBlock}
63
+ </div>
64
+ </details>`;
65
+ }).join("\n");
66
+ return `<!DOCTYPE html>
67
+ <html lang="en">
68
+ <head>
69
+ <meta charset="utf-8">
70
+ <meta name="viewport" content="width=device-width, initial-scale=1">
71
+ <title>${escapeHtml(explanation.title)}</title>
72
+ <style>
73
+ * { box-sizing: border-box; margin: 0; padding: 0; }
74
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 24px; color: #1f2937; background: #fff; line-height: 1.6; }
75
+ h1 { font-size: 22px; margin-bottom: 6px; }
76
+ h2 { font-size: 16px; margin: 24px 0 10px; color: #374151; border-bottom: 1px solid #e5e7eb; padding-bottom: 6px; }
77
+ table { width: 100%; border-collapse: collapse; margin: 10px 0; }
78
+ th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid #f3f4f6; font-size: 13px; }
79
+ th { background: #f9fafb; font-weight: 600; }
80
+ .stats { display: flex; gap: 16px; margin: 12px 0; }
81
+ .stat { padding: 8px 14px; border-radius: 6px; background: #f3f4f6; font-size: 14px; }
82
+ .stat b { font-size: 18px; }
83
+ .meta { color: #6b7280; font-size: 12px; margin-bottom: 16px; }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <h1>${escapeHtml(explanation.title)}</h1>
88
+ <div class="meta">${escapeHtml(report.baseRef)} &rarr; ${escapeHtml(report.headRef)} &middot; ${escapeHtml(report.generatedAt)}</div>
89
+
90
+ <div class="stats">
91
+ <div class="stat"><b>${summary.filesChanged}</b> files changed</div>
92
+ <div class="stat" style="color:#166534;"><b>+${summary.additions}</b> additions</div>
93
+ <div class="stat" style="color:#991b1b;"><b>-${summary.deletions}</b> deletions</div>
94
+ </div>
95
+
96
+ <h2>Description</h2>
97
+ <p style="margin:8px 0;">${escapeHtml(explanation.description)}</p>
98
+
99
+ <h2>Risk Assessment</h2>
100
+ <div style="margin:8px 0;">${riskBadges}</div>
101
+
102
+ <h2>Files Overview</h2>
103
+ <table>
104
+ <thead><tr><th></th><th>File</th><th>Status</th><th>Language</th><th>Changes</th></tr></thead>
105
+ <tbody>${filesTable}</tbody>
106
+ </table>
107
+
108
+ <h2>File Details</h2>
109
+ ${fileDetails}
110
+
111
+ <div style="margin-top:32px;padding-top:12px;border-top:1px solid #e5e7eb;color:#9ca3af;font-size:11px;">
112
+ Generated by intent-spec explain &middot; Tokens: ${explanation.tokenUsage.input} in / ${explanation.tokenUsage.output} out
113
+ </div>
114
+ </body>
115
+ </html>`;
116
+ }
117
+ function colorDiff(diff) {
118
+ return escapeHtml(diff)
119
+ .split("\n")
120
+ .map((line) => {
121
+ if (line.startsWith("+"))
122
+ return `<span style="color:#4ade80;">${line}</span>`;
123
+ if (line.startsWith("-"))
124
+ return `<span style="color:#f87171;">${line}</span>`;
125
+ if (line.startsWith("@@"))
126
+ return `<span style="color:#60a5fa;">${line}</span>`;
127
+ return line;
128
+ })
129
+ .join("\n");
130
+ }
131
+ //# sourceMappingURL=html-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-report.js","sourceRoot":"","sources":["../../src/explain/html-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { FileAnalysis, LLMExplanation } from "./types";
2
+ export declare function explainChanges(files: FileAnalysis[], rawDiff: string): Promise<LLMExplanation>;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.explainChanges = explainChanges;
7
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
8
+ const SYSTEM_PROMPT = "You analyze code changes. Respond ONLY with valid JSON. Be extremely concise.";
9
+ const ACTION_ICON = {
10
+ added: "+",
11
+ removed: "-",
12
+ modified: "~",
13
+ };
14
+ async function explainChanges(files, rawDiff) {
15
+ const structuralSummary = files
16
+ .filter((f) => f.structuralChanges.length > 0)
17
+ .map((f) => {
18
+ const changes = f.structuralChanges
19
+ .map((c) => `${ACTION_ICON[c.action]}${c.name} (${c.type})`)
20
+ .join(", ");
21
+ return `- ${f.path} (${f.status}): ${changes}`;
22
+ })
23
+ .join("\n");
24
+ // Truncate diff to ~4000 chars, prioritizing modified files
25
+ const truncatedDiff = truncateDiff(rawDiff, 4000);
26
+ const prompt = `## Structural changes
27
+ ${structuralSummary || "(no structural changes detected)"}
28
+
29
+ ## Diff (truncated)
30
+ ${truncatedDiff || "(empty diff)"}
31
+
32
+ Respond with JSON:
33
+ { "title": "<60 char PR title>",
34
+ "description": "<2-4 sentences summarizing the changes>",
35
+ "risks": [{"level":"low|medium|high","description":"<one sentence>"}] }`;
36
+ const client = new sdk_1.default();
37
+ const message = await client.messages.create({
38
+ model: "claude-sonnet-4-5-20250929",
39
+ max_tokens: 512,
40
+ system: SYSTEM_PROMPT,
41
+ messages: [{ role: "user", content: prompt }],
42
+ });
43
+ const text = message.content
44
+ .filter((block) => block.type === "text")
45
+ .map((block) => block.text)
46
+ .join("\n");
47
+ const tokenUsage = {
48
+ input: message.usage?.input_tokens || 0,
49
+ output: message.usage?.output_tokens || 0,
50
+ };
51
+ let parsed;
52
+ try {
53
+ const cleaned = text.replace(/^```json\s*/m, "").replace(/```\s*$/m, "").trim();
54
+ parsed = JSON.parse(cleaned);
55
+ }
56
+ catch {
57
+ // Fallback if LLM doesn't return valid JSON
58
+ parsed = {
59
+ title: "Code changes",
60
+ description: text.slice(0, 200),
61
+ risks: [{ level: "low", description: "LLM response was not valid JSON; showing raw text." }],
62
+ };
63
+ }
64
+ return {
65
+ title: parsed.title || "Code changes",
66
+ description: parsed.description || "",
67
+ risks: Array.isArray(parsed.risks) ? parsed.risks : [],
68
+ tokenUsage,
69
+ };
70
+ }
71
+ function truncateDiff(diff, maxLen) {
72
+ if (diff.length <= maxLen)
73
+ return diff;
74
+ // Split by file sections, keep as many as fit
75
+ const sections = diff.split(/^diff --git /m).filter(Boolean);
76
+ let result = "";
77
+ for (const section of sections) {
78
+ const chunk = `diff --git ${section}`;
79
+ if (result.length + chunk.length > maxLen) {
80
+ result += "\n... (diff truncated)";
81
+ break;
82
+ }
83
+ result += chunk;
84
+ }
85
+ return result || diff.slice(0, maxLen) + "\n... (truncated)";
86
+ }
87
+ //# sourceMappingURL=llm-explain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-explain.js","sourceRoot":"","sources":["../../src/explain/llm-explain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ export type FileStatus = "added" | "modified" | "deleted" | "renamed";
2
+ export interface FileDiff {
3
+ path: string;
4
+ oldPath?: string;
5
+ status: FileStatus;
6
+ hunks: string;
7
+ oldContent?: string;
8
+ newContent?: string;
9
+ additions: number;
10
+ deletions: number;
11
+ }
12
+ export type ChangeAction = "added" | "removed" | "modified";
13
+ export type ChangeType = "function" | "import" | "export" | "class" | "variable" | "route" | "other";
14
+ export interface StructuralChange {
15
+ file: string;
16
+ type: ChangeType;
17
+ action: ChangeAction;
18
+ name: string;
19
+ startLine?: number;
20
+ detail?: string;
21
+ }
22
+ export interface FileAnalysis {
23
+ path: string;
24
+ status: FileStatus;
25
+ language: string | null;
26
+ structuralChanges: StructuralChange[];
27
+ rawDiff: string;
28
+ }
29
+ export interface Risk {
30
+ level: "low" | "medium" | "high";
31
+ description: string;
32
+ }
33
+ export interface LLMExplanation {
34
+ title: string;
35
+ description: string;
36
+ risks: Risk[];
37
+ tokenUsage: {
38
+ input: number;
39
+ output: number;
40
+ };
41
+ }
42
+ export interface ExplainReport {
43
+ generatedAt: string;
44
+ baseRef: string;
45
+ headRef: string;
46
+ summary: {
47
+ filesChanged: number;
48
+ additions: number;
49
+ deletions: number;
50
+ };
51
+ explanation: LLMExplanation;
52
+ files: FileAnalysis[];
53
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/explain/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export { parseFile, parseSource, Query, language, getLanguageForExt } from "./parser";
2
+ export type { Tree, SyntaxNode, QueryMatch, QueryCapture } from "./parser";
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLanguageForExt = exports.language = exports.Query = exports.parseSource = exports.parseFile = void 0;
4
+ var parser_1 = require("./parser");
5
+ Object.defineProperty(exports, "parseFile", { enumerable: true, get: function () { return parser_1.parseFile; } });
6
+ Object.defineProperty(exports, "parseSource", { enumerable: true, get: function () { return parser_1.parseSource; } });
7
+ Object.defineProperty(exports, "Query", { enumerable: true, get: function () { return parser_1.Query; } });
8
+ Object.defineProperty(exports, "language", { enumerable: true, get: function () { return parser_1.language; } });
9
+ Object.defineProperty(exports, "getLanguageForExt", { enumerable: true, get: function () { return parser_1.getLanguageForExt; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parsing/index.ts"],"names":[],"mappings":";;;AAAA,mCAAsF;AAA7E,mGAAA,SAAS,OAAA;AAAE,qGAAA,WAAW,OAAA;AAAE,+FAAA,KAAK,OAAA;AAAE,kGAAA,QAAQ,OAAA;AAAE,2GAAA,iBAAiB,OAAA"}
@@ -0,0 +1,18 @@
1
+ import Parser from "tree-sitter";
2
+ export type { Parser };
3
+ export type Tree = Parser.Tree;
4
+ export type SyntaxNode = Parser.SyntaxNode;
5
+ export type QueryMatch = Parser.QueryMatch;
6
+ export type QueryCapture = Parser.QueryCapture;
7
+ export declare const Query: typeof Parser.Query;
8
+ /** Backward-compat export: the JavaScript language */
9
+ export declare const language: Parser.Language;
10
+ export declare function getLanguageForExt(ext: string): Parser.Language;
11
+ export declare function parseSource(source: string, ext?: string): {
12
+ tree: Tree;
13
+ source: string;
14
+ };
15
+ export declare function parseFile(filePath: string): {
16
+ tree: Tree;
17
+ source: string;
18
+ };
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.language = exports.Query = void 0;
7
+ exports.getLanguageForExt = getLanguageForExt;
8
+ exports.parseSource = parseSource;
9
+ exports.parseFile = parseFile;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const tree_sitter_1 = __importDefault(require("tree-sitter"));
13
+ const tree_sitter_javascript_1 = __importDefault(require("tree-sitter-javascript"));
14
+ const tree_sitter_typescript_1 = __importDefault(require("tree-sitter-typescript"));
15
+ exports.Query = tree_sitter_1.default.Query;
16
+ const jsLanguage = tree_sitter_javascript_1.default;
17
+ const tsLanguage = tree_sitter_typescript_1.default.typescript;
18
+ const tsxLanguage = tree_sitter_typescript_1.default.tsx;
19
+ /** Backward-compat export: the JavaScript language */
20
+ exports.language = jsLanguage;
21
+ const parsers = new Map();
22
+ function getParser(lang) {
23
+ let p = parsers.get(lang);
24
+ if (!p) {
25
+ p = new tree_sitter_1.default();
26
+ p.setLanguage(lang);
27
+ parsers.set(lang, p);
28
+ }
29
+ return p;
30
+ }
31
+ const extToLanguage = {
32
+ ".js": jsLanguage,
33
+ ".ts": tsLanguage,
34
+ ".tsx": tsxLanguage,
35
+ };
36
+ function getLanguageForExt(ext) {
37
+ return extToLanguage[ext] || jsLanguage;
38
+ }
39
+ function parseSource(source, ext = ".js") {
40
+ const lang = getLanguageForExt(ext);
41
+ const tree = getParser(lang).parse(source);
42
+ return { tree, source };
43
+ }
44
+ function parseFile(filePath) {
45
+ const source = fs_1.default.readFileSync(filePath, "utf-8");
46
+ const ext = path_1.default.extname(filePath);
47
+ return parseSource(source, ext);
48
+ }
49
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../src/parsing/parser.ts"],"names":[],"mappings":";;;;;;AAsCA,8CAEC;AAED,kCAOC;AAED,8BAIC;AAvDD,4CAAoB;AACpB,gDAAwB;AACxB,8DAAiC;AACjC,oFAAgD;AAChD,oFAAyD;AAO5C,QAAA,KAAK,GAAG,qBAAM,CAAC,KAAK,CAAC;AAElC,MAAM,UAAU,GAAG,gCAAwC,CAAC;AAC5D,MAAM,UAAU,GAAG,gCAAmB,CAAC,UAAwC,CAAC;AAChF,MAAM,WAAW,GAAG,gCAAmB,CAAC,GAAiC,CAAC;AAE1E,sDAAsD;AACzC,QAAA,QAAQ,GAAG,UAAU,CAAC;AAEnC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEnD,SAAS,SAAS,CAAC,IAAqB;IACtC,IAAI,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,GAAG,IAAI,qBAAM,EAAE,CAAC;QACjB,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,aAAa,GAAoC;IACrD,KAAK,EAAE,UAAU;IACjB,KAAK,EAAE,UAAU;IACjB,MAAM,EAAE,WAAW;CACpB,CAAC;AAEF,SAAgB,iBAAiB,CAAC,GAAW;IAC3C,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC;AAC1C,CAAC;AAED,SAAgB,WAAW,CACzB,MAAc,EACd,MAAc,KAAK;IAEnB,MAAM,IAAI,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED,SAAgB,SAAS,CAAC,QAAgB;IACxC,MAAM,MAAM,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,OAAO,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "diffintel",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered structural diff explainer for pull requests",
5
+ "bin": {
6
+ "diffintel": "dist/cli.js"
7
+ },
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "tsc && node --import tsx --test test/unit/*.test.ts",
12
+ "prepublishOnly": "tsc"
13
+ },
14
+ "keywords": ["diff", "pull-request", "code-review", "ast", "tree-sitter", "ai", "llm"],
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@anthropic-ai/sdk": "^0.39.0",
18
+ "commander": "^14.0.3",
19
+ "dotenv": "^17.2.4",
20
+ "tree-sitter": "^0.25.0",
21
+ "tree-sitter-javascript": "^0.25.0",
22
+ "tree-sitter-typescript": "^0.23.2"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.19.11",
26
+ "tsx": "^4.21.0",
27
+ "typescript": "^5.9.3"
28
+ }
29
+ }