codeblast 0.3.1 → 0.3.2
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 +25 -8
- package/README.zh-CN.md +1 -7
- package/SKILL.md +13 -2
- package/dist/bin.js +215 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Built for humans (CLI / interactive HTML / PR comments) and for AI agents ([SKIL
|
|
|
49
49
|
```bash
|
|
50
50
|
npx codeblast demo # build a graph of the current repo, run one impact query, emit the map
|
|
51
51
|
npm i -g codeblast # or install globally; needs Node ≥ 22.13 (built-in sqlite) or Bun
|
|
52
|
-
# npm serves 0.3.
|
|
52
|
+
# npm serves 0.3.1
|
|
53
53
|
|
|
54
54
|
# Install as an agent skill (Claude Code, Codex, Cursor, and 14 more harnesses)
|
|
55
55
|
npx skills add alloevil/codeblast
|
|
@@ -113,7 +113,8 @@ codeblast change <repo> main~5 main --json
|
|
|
113
113
|
|
|
114
114
|
# ③ Architecture Map — interactive HTML: module → file → symbol drill-down,
|
|
115
115
|
# symbols link to source lines
|
|
116
|
-
codeblast archmap graph.db --out arch.html --repo-url <github-url>
|
|
116
|
+
codeblast archmap graph.db --out arch.html --repo-url <github-url> \
|
|
117
|
+
--site-url https://example.github.io/repo --og-image <share-image-url>
|
|
117
118
|
|
|
118
119
|
# Optional: mine git co-change coupling (protocol pairs, config + consumers —
|
|
119
120
|
# edges static analysis can't see)
|
|
@@ -157,6 +158,20 @@ authoring agent rated its own comments; both numbers and the fixes that followed
|
|
|
157
158
|
- **You want cross-service / cross-repo edges, or Java.** The graph model reserves the node types, v1 does not fill those edges, and Java is explicitly not implemented ([intent.md](intent.md)).
|
|
158
159
|
- **You want the PR bot to replace a reviewer.** It is a structural-change signal whose usefulness measured between 20% and 75% depending on the review round.
|
|
159
160
|
|
|
161
|
+
|
|
162
|
+
### Unified change safety check
|
|
163
|
+
|
|
164
|
+
For agents and CI that need one decision instead of composing `change` and `impact`, run:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
codeblast check-change <repo> <base-sha> <head-sha> --json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The output contains `decision` (`safe-to-review`, `targeted-review`, or `review`), `risk`, reasons,
|
|
171
|
+
recommended actions, structural facts, predicted affected-test count, blind-spot count, truncation,
|
|
172
|
+
and the complete graph diff. It is a conservative routing decision, not a claim that the change is
|
|
173
|
+
safe to merge. `review` is required when API surface contracts, removed symbols, or incomplete impact
|
|
174
|
+
results are detected.
|
|
160
175
|
## For AI agents
|
|
161
176
|
|
|
162
177
|
```
|
|
@@ -179,14 +194,16 @@ Agent conventions: [AGENTS.md](AGENTS.md).
|
|
|
179
194
|
|
|
180
195
|
**Where are the numbers I can check?** Machine-readable claims with metric, method, repro command and evidence path are published at [claims.json](https://alloevil.github.io/codeblast/claims.json); the raw mutation and PR-replay runs are archived under [`eval/`](eval/) as records of runs that were made (the harness writes to `/tmp`, so the files are manual copies with no commit or version pin, and `mutation_check.py` picks candidates with `ORDER BY RANDOM()` — read them as archived runs, not as one-command regenerations), and the acceptance log with every downgrade and rejected optimization is [intent.md](intent.md).
|
|
181
196
|
|
|
197
|
+
### PR bot feedback
|
|
198
|
+
|
|
199
|
+
Feedback is intentionally issue-based and privacy-safe. Report only the public PR URL, the affected
|
|
200
|
+
decision category, and a redacted explanation. Do not include source code, secrets, proprietary diffs,
|
|
201
|
+
or full repository contents. The useful labels are false positive, missed impact, noise, incorrect
|
|
202
|
+
silence, or incorrect recommendation. Feedback is qualitative calibration data; it is not telemetry
|
|
203
|
+
and codeblast does not require access to customer repositories.
|
|
204
|
+
|
|
182
205
|
## Status & roadmap
|
|
183
206
|
|
|
184
207
|
M0 graph engine → M1 Impact → M3 architecture map → M4 graph diff + PR bot → M5 precision extensions — **all shipped**; two acceptance steps were downgraded rather than passed (M2's verification method, and M3's original "10 minutes, 5 questions" test which was never run — ⚠️ in [intent.md](intent.md)), and the SemArc alignment check was dropped. Single source of truth for design and acceptance criteria: [intent.md](intent.md).
|
|
185
208
|
|
|
186
209
|
MIT © 2026
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
<p align="center">
|
|
191
|
-
<a href="https://github.com/oil-oil/beautify-github-readme"><img src="./assets/readme/made-with-beautify.svg" width="300" alt="README made with beautify-github-readme"></a>
|
|
192
|
-
</p>
|
package/README.zh-CN.md
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
```bash
|
|
34
34
|
npx codeblast demo # 给当前仓库建图、跑一次 impact 查询、导出架构图
|
|
35
35
|
npm i -g codeblast # 或全局安装;需要 Node ≥ 22.13(内置 sqlite)或 Bun
|
|
36
|
-
# npm 上的版本是 0.3.
|
|
36
|
+
# npm 上的版本是 0.3.1,与本仓库一致
|
|
37
37
|
|
|
38
38
|
# 作为 agent skill 安装(Claude Code、Codex、Cursor 等)
|
|
39
39
|
npx skills add alloevil/codeblast
|
|
@@ -132,9 +132,3 @@ Agent 规范另见 [AGENTS.md](AGENTS.md)。
|
|
|
132
132
|
M0 图谱引擎 → M1 Impact → M3 架构图 → M4 图 diff + PR bot → M5 精度扩展,**全部交付**;其中两项验收是降级而非通过(M2 的验证方式、M3 原定的"陌生工程师 10 分钟 5 问"从未执行——见 intent.md 的 ⚠️),SemArc 对齐检查已作废。方案与验收标准的单一事实源:[intent.md](intent.md)。
|
|
133
133
|
|
|
134
134
|
MIT © 2026
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
<p align="center">
|
|
139
|
-
<a href="https://github.com/oil-oil/beautify-github-readme"><img src="./assets/readme/made-with-beautify.svg" width="300" alt="README made with beautify-github-readme"></a>
|
|
140
|
-
</p>
|
package/SKILL.md
CHANGED
|
@@ -53,8 +53,7 @@ codeblast index <repo-root> --db /tmp/graph.db
|
|
|
53
53
|
|
|
54
54
|
Auto-discovers every package `tsconfig.json` in a monorepo and ingests Python via AST. Re-running only
|
|
55
55
|
processes files whose content hash changed. Stdout is one JSON object — this is a real run of
|
|
56
|
-
`codeblast index` against [tRPC](https://github.com/trpc/trpc) at commit `66d0544` with codeblast 0.3.0
|
|
57
|
-
(timing is machine-dependent; the counts are not):
|
|
56
|
+
`codeblast index` against [tRPC](https://github.com/trpc/trpc) at commit `66d0544` with the historical codeblast 0.3.0 benchmark
|
|
58
57
|
|
|
59
58
|
```json
|
|
60
59
|
{ "db": "/tmp/graph.db", "seconds": 5.2, "tsconfigs": 34, "files_indexed": 957, "files_skipped": 0,
|
|
@@ -93,9 +92,21 @@ Output (`--json`):
|
|
|
93
92
|
via_file: string; via_line: number; // where the dependency occurs (rule 6)
|
|
94
93
|
}>;
|
|
95
94
|
co_change_hints: Array<{ file: string; co_commits: number; evidence: string }>; // rule 5
|
|
95
|
+
guidance: {
|
|
96
|
+
review_first: string[]; // call-channel non-test nodes
|
|
97
|
+
run_tests: string[]; // test file paths, de-duplicate before running
|
|
98
|
+
conservative: string[]; // non-test file-channel nodes; never discard
|
|
99
|
+
warnings: string[]; // truncation and blind-spot warnings
|
|
100
|
+
};
|
|
96
101
|
}
|
|
97
102
|
```
|
|
98
103
|
|
|
104
|
+
The `guidance` object is a convenience projection of `items`; it does not add analysis results.
|
|
105
|
+
Use `review_first` for the initial callsite checklist, `run_tests` for the affected test files,
|
|
106
|
+
and `conservative` as the import/re-export safety net. `warnings` is non-empty when the result is
|
|
107
|
+
incomplete or the target file contains unresolved analysis. Always retain `via_file` and `via_line`
|
|
108
|
+
when reporting a dependency.
|
|
109
|
+
|
|
99
110
|
How to use it: `items.filter(level === "direct")` is the callsite checklist. `items.filter(level ===
|
|
100
111
|
"tests")` de-duplicated by `file` is the test set to run. Test-directory fixtures are included
|
|
101
112
|
conservatively; estimate test cost by distinct files, not item count.
|
package/dist/bin.js
CHANGED
|
@@ -1131,7 +1131,16 @@ var init_impact_cli = __esm(() => {
|
|
|
1131
1131
|
result = impact(db2, targetId, maxNodes);
|
|
1132
1132
|
ms = (performance.now() - t02).toFixed(0);
|
|
1133
1133
|
if (process.argv.includes("--json")) {
|
|
1134
|
-
|
|
1134
|
+
const guidance = {
|
|
1135
|
+
review_first: result.items.filter((it) => it.channel === "call" && it.level !== "tests").map((it) => it.id),
|
|
1136
|
+
run_tests: result.items.filter((it) => it.level === "tests").map((it) => it.file),
|
|
1137
|
+
conservative: result.items.filter((it) => it.channel === "file" && it.level !== "tests").map((it) => it.id),
|
|
1138
|
+
warnings: [
|
|
1139
|
+
...result.truncated ? ["impact_truncated_run_full_test_suite"] : [],
|
|
1140
|
+
...result.blind_spot_count > 0 ? ["blind_spots_may_underestimate_impact"] : []
|
|
1141
|
+
]
|
|
1142
|
+
};
|
|
1143
|
+
process.stdout.write(JSON.stringify({ ...result, guidance }) + `
|
|
1135
1144
|
`);
|
|
1136
1145
|
db2.close();
|
|
1137
1146
|
process.exitCode = 0;
|
|
@@ -1436,7 +1445,7 @@ async function layoutGraph(nodes, edges) {
|
|
|
1436
1445
|
}))
|
|
1437
1446
|
};
|
|
1438
1447
|
}
|
|
1439
|
-
var CLIENT_JS, dbPath3, outFlag2, outPath2, overlayFlag, overlayPath, repoFlag, repoUrl, impactFlag, impactTarget, diffFlag, diffBase, db3, overlay, TEST_RE, moduleOf = (file) => {
|
|
1448
|
+
var CLIENT_JS, dbPath3, outFlag2, outPath2, overlayFlag, overlayPath, repoFlag, repoUrl, impactFlag, impactTarget, diffFlag, diffBase, siteFlag, siteUrl, ogImageFlag, ogImage, repoLabel, pageUrl, pageKind, pageTitle, pageDesc, escAttr = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """), headMeta, db3, overlay, TEST_RE, moduleOf = (file) => {
|
|
1440
1449
|
if (TEST_RE.test(file))
|
|
1441
1450
|
return "tests";
|
|
1442
1451
|
const ix = file.indexOf("/");
|
|
@@ -1460,6 +1469,51 @@ var init_archmap_html = __esm(async () => {
|
|
|
1460
1469
|
impactTarget = impactFlag >= 0 ? process.argv[impactFlag + 1] : undefined;
|
|
1461
1470
|
diffFlag = process.argv.indexOf("--diff");
|
|
1462
1471
|
diffBase = diffFlag >= 0 ? process.argv[diffFlag + 1] : undefined;
|
|
1472
|
+
siteFlag = process.argv.indexOf("--site-url");
|
|
1473
|
+
siteUrl = siteFlag >= 0 ? process.argv[siteFlag + 1].replace(/\/+$/, "") : undefined;
|
|
1474
|
+
ogImageFlag = process.argv.indexOf("--og-image");
|
|
1475
|
+
ogImage = ogImageFlag >= 0 ? process.argv[ogImageFlag + 1] : undefined;
|
|
1476
|
+
repoLabel = (() => {
|
|
1477
|
+
const m = repoUrl?.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
1478
|
+
if (m)
|
|
1479
|
+
return m[1].replace(/\.git$/, "");
|
|
1480
|
+
const base = outPath2.split("/").pop() ?? outPath2;
|
|
1481
|
+
return base.replace(/\.html$/, "");
|
|
1482
|
+
})();
|
|
1483
|
+
pageUrl = siteUrl ? `${siteUrl}/${outPath2.split("/").pop()}` : undefined;
|
|
1484
|
+
pageKind = impactTarget ? "blast-radius map" : diffBase ? "change map" : "architecture map";
|
|
1485
|
+
pageTitle = `${repoLabel} — ${pageKind}`;
|
|
1486
|
+
pageDesc = `Interactive architecture map of ${repoLabel} generated by codeblast: a module-level` + ` dependency graph where hovering a node lights up its fan-in and clicking it shows its files,` + ` symbols and import edges.` + (impactTarget ? ` Includes the blast-radius overlay for ${impactTarget}.` : "") + (overlayPath ? " Includes the change overlay." : "");
|
|
1487
|
+
headMeta = [
|
|
1488
|
+
`<title>${escAttr(pageTitle)}</title>`,
|
|
1489
|
+
`<meta name="description" content="${escAttr(pageDesc)}">`,
|
|
1490
|
+
...pageUrl ? [`<link rel="canonical" href="${escAttr(pageUrl)}">`] : [],
|
|
1491
|
+
`<meta property="og:type" content="website">`,
|
|
1492
|
+
`<meta property="og:title" content="${escAttr(pageTitle)}">`,
|
|
1493
|
+
`<meta property="og:description" content="${escAttr(pageDesc)}">`,
|
|
1494
|
+
...pageUrl ? [`<meta property="og:url" content="${escAttr(pageUrl)}">`] : [],
|
|
1495
|
+
`<meta property="og:site_name" content="codeblast">`,
|
|
1496
|
+
...ogImage ? [`<meta property="og:image" content="${escAttr(ogImage)}">`] : [],
|
|
1497
|
+
`<meta name="twitter:card" content="${ogImage ? "summary_large_image" : "summary"}">`,
|
|
1498
|
+
`<meta name="twitter:title" content="${escAttr(pageTitle)}">`,
|
|
1499
|
+
`<meta name="twitter:description" content="${escAttr(pageDesc)}">`,
|
|
1500
|
+
...ogImage ? [`<meta name="twitter:image" content="${escAttr(ogImage)}">`] : [],
|
|
1501
|
+
`<script type="application/ld+json">`,
|
|
1502
|
+
JSON.stringify({
|
|
1503
|
+
"@context": "https://schema.org",
|
|
1504
|
+
"@type": "WebPage",
|
|
1505
|
+
name: pageTitle,
|
|
1506
|
+
description: pageDesc,
|
|
1507
|
+
inLanguage: "zh-CN",
|
|
1508
|
+
...pageUrl ? { url: pageUrl } : {},
|
|
1509
|
+
...repoUrl ? {
|
|
1510
|
+
isBasedOn: repoUrl,
|
|
1511
|
+
about: { "@type": "SoftwareSourceCode", name: repoLabel, codeRepository: repoUrl }
|
|
1512
|
+
} : {}
|
|
1513
|
+
}, null, 2),
|
|
1514
|
+
`</script>`
|
|
1515
|
+
].join(`
|
|
1516
|
+
`);
|
|
1463
1517
|
if (!dbPath3) {
|
|
1464
1518
|
console.error("usage: codeblast archmap <graph.db> --out arch.html");
|
|
1465
1519
|
process.exit(1);
|
|
@@ -1629,10 +1683,10 @@ var init_archmap_html = __esm(async () => {
|
|
|
1629
1683
|
fileEdges: importRows
|
|
1630
1684
|
};
|
|
1631
1685
|
html = `<!DOCTYPE html>
|
|
1632
|
-
<html lang="zh">
|
|
1686
|
+
<html lang="zh-CN">
|
|
1633
1687
|
<head>
|
|
1634
1688
|
<meta charset="utf-8">
|
|
1635
|
-
|
|
1689
|
+
${headMeta}
|
|
1636
1690
|
<style>
|
|
1637
1691
|
:root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#e6edf3; --dim:#8b949e;
|
|
1638
1692
|
--accent:#58a6ff; --warn:#f85149; --ok:#3fb950; }
|
|
@@ -1671,6 +1725,11 @@ var init_archmap_html = __esm(async () => {
|
|
|
1671
1725
|
.edge.cyclic { stroke: var(--warn); stroke-dasharray: 5 3; stroke-width: 2;
|
|
1672
1726
|
animation: cycflow 1.2s linear infinite; }
|
|
1673
1727
|
@keyframes cycflow { to { stroke-dashoffset: -16; } }
|
|
1728
|
+
/* 循环依赖用虚线静止表示即可 —— 虚线与颜色承载信息,流动只是让它更显眼。
|
|
1729
|
+
reduced-motion 下停掉动画,而不是去掉虚线。 */
|
|
1730
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1731
|
+
.edge.cyclic { animation: none; }
|
|
1732
|
+
}
|
|
1674
1733
|
.edge.hi { stroke: #d29922 !important; stroke-width: 2.6; stroke-opacity: 1; }
|
|
1675
1734
|
.edge.dim { stroke-opacity: 0.1; }
|
|
1676
1735
|
.edge.faint { stroke-opacity: 0.06; }
|
|
@@ -1955,6 +2014,54 @@ var init_pr_silence = __esm(() => {
|
|
|
1955
2014
|
AUX_RE = /^(www|docs|examples)\//;
|
|
1956
2015
|
});
|
|
1957
2016
|
|
|
2017
|
+
// src/pr-decision.ts
|
|
2018
|
+
function reviewDecision(input) {
|
|
2019
|
+
const { diff, prodNodesAdded, bodyChanged, affectedTests, truncated, blindSpotCount } = input;
|
|
2020
|
+
const apiContractions = diff.visibilityChanged.filter((v) => !v.nowExported).length;
|
|
2021
|
+
const apiChanges = apiContractions + diff.signatureChanged.length;
|
|
2022
|
+
const removals = diff.nodesRemoved.length;
|
|
2023
|
+
const behaviorChanges = bodyChanged.length;
|
|
2024
|
+
const reasons = [];
|
|
2025
|
+
const recommendedActions = [];
|
|
2026
|
+
let risk = "low";
|
|
2027
|
+
if (apiContractions > 0 || removals > 0 || truncated)
|
|
2028
|
+
risk = "high";
|
|
2029
|
+
else if (apiChanges > 0 || behaviorChanges > 0 || affectedTests > 0 || blindSpotCount > 0)
|
|
2030
|
+
risk = "medium";
|
|
2031
|
+
if (apiContractions > 0)
|
|
2032
|
+
reasons.push(`${apiContractions} exported symbol${apiContractions === 1 ? " is" : "s are"} no longer public`);
|
|
2033
|
+
if (removals > 0)
|
|
2034
|
+
reasons.push(`${removals} symbol${removals === 1 ? " was" : "s were"} removed`);
|
|
2035
|
+
if (diff.signatureChanged.length > 0)
|
|
2036
|
+
reasons.push(`${diff.signatureChanged.length} exported signature${diff.signatureChanged.length === 1 ? " changed" : "s changed"}`);
|
|
2037
|
+
if (behaviorChanges > 0)
|
|
2038
|
+
reasons.push(`${behaviorChanges} function bod${behaviorChanges === 1 ? "y" : "ies"} changed`);
|
|
2039
|
+
if (affectedTests > 0)
|
|
2040
|
+
reasons.push(`${affectedTests} test file${affectedTests === 1 ? " is" : "s are"} in the predicted impact set`);
|
|
2041
|
+
if (truncated)
|
|
2042
|
+
reasons.push("the impact set exceeded the reporting limit");
|
|
2043
|
+
if (blindSpotCount > 0)
|
|
2044
|
+
reasons.push(`${blindSpotCount} static-analysis blind spot${blindSpotCount === 1 ? "" : "s"} may hide impact`);
|
|
2045
|
+
if (reasons.length === 0 && prodNodesAdded.length > 0)
|
|
2046
|
+
reasons.push(`${prodNodesAdded.length} production symbol${prodNodesAdded.length === 1 ? " was" : "s were"} added`);
|
|
2047
|
+
if (reasons.length === 0)
|
|
2048
|
+
reasons.push("only low-risk structural additions were found");
|
|
2049
|
+
if (apiChanges > 0 || removals > 0)
|
|
2050
|
+
recommendedActions.push("Review the public API changes and migration impact.");
|
|
2051
|
+
if (truncated)
|
|
2052
|
+
recommendedActions.push("Run the full test suite; the reported impact list is truncated.");
|
|
2053
|
+
else if (affectedTests > 0)
|
|
2054
|
+
recommendedActions.push(`Run the ${affectedTests} predicted affected test file${affectedTests === 1 ? "" : "s"}.`);
|
|
2055
|
+
if (blindSpotCount > 0)
|
|
2056
|
+
recommendedActions.push("Inspect the reported blind spots before treating the impact set as complete.");
|
|
2057
|
+
if (behaviorChanges > 0)
|
|
2058
|
+
recommendedActions.push("Review the changed function bodies even where the exported shape is unchanged.");
|
|
2059
|
+
if (recommendedActions.length === 0)
|
|
2060
|
+
recommendedActions.push("Review the named structural additions; no existing API contraction was detected.");
|
|
2061
|
+
const summary = risk === "high" ? "Review before merge: the change removes API surface, deletes symbols, or has an incomplete impact set." : risk === "medium" ? "Targeted review recommended: behavior, API shape, tests, or blind spots changed." : "Low structural risk: only additive changes with no predicted affected tests were found.";
|
|
2062
|
+
return { risk, summary, reasons, recommendedActions };
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1958
2065
|
// src/pr-comment.ts
|
|
1959
2066
|
var exports_pr_comment = {};
|
|
1960
2067
|
import fs7 from "node:fs";
|
|
@@ -1993,7 +2100,7 @@ var args3, repo2, baseSha, headSha, urlFlag, repoUrl2, dbPathA = "/tmp/codeblast
|
|
|
1993
2100
|
const wrap = s.kind === "interface" || s.kind === "const" ? (t) => t : (t) => `(${t})`;
|
|
1994
2101
|
const what = s.kind === "interface" ? "成员变化" : s.kind === "const" ? "类型变化" : "";
|
|
1995
2102
|
return `- \`${s.name}\`${what ? ` ${what}` : ""}: \`${wrap(clip(s.from))}\` → \`${wrap(clip(s.to))}\` (${link(s.file, s.line)})`;
|
|
1996
|
-
}, apiSig, testSig, uncovered, impactRows, prodNodesAdded, diffLineCount, bodySignal, coreNamed;
|
|
2103
|
+
}, apiSig, testSig, uncovered, impactRows, affectedTestCount = 0, anyImpactTruncated = false, totalBlindSpots = 0, prodNodesAdded, diffLineCount, bodySignal, coreNamed, decision;
|
|
1997
2104
|
var init_pr_comment = __esm(async () => {
|
|
1998
2105
|
init_db();
|
|
1999
2106
|
init_proc();
|
|
@@ -2106,6 +2213,9 @@ var init_pr_comment = __esm(async () => {
|
|
|
2106
2213
|
const r = impact(dbB2, n.id, 2000);
|
|
2107
2214
|
const callItems = r.items.filter((i) => i.channel === "call");
|
|
2108
2215
|
const tests = callItems.filter((i) => i.level === "tests").length;
|
|
2216
|
+
affectedTestCount += tests;
|
|
2217
|
+
anyImpactTruncated ||= r.truncated;
|
|
2218
|
+
totalBlindSpots += r.blind_spot_count;
|
|
2109
2219
|
impactRows.push(`| \`${n.name}\` | ${n.kind} | ${callItems.length} | ${tests} | ${link(n.file, n.line)} |`);
|
|
2110
2220
|
const blind = r.blind_spot_count > 0;
|
|
2111
2221
|
if (tests === 0 && n.kind !== "interface" && n.kind !== "const") {
|
|
@@ -2153,16 +2263,101 @@ var init_pr_comment = __esm(async () => {
|
|
|
2153
2263
|
if (total2 === 0 && bodyChanged.length > 0) {
|
|
2154
2264
|
lines3[2] = `**无结构变更**,但有 ${bodyChanged.length} 个函数体内改动(见下)`;
|
|
2155
2265
|
}
|
|
2266
|
+
decision = reviewDecision({
|
|
2267
|
+
diff: diff2,
|
|
2268
|
+
prodNodesAdded,
|
|
2269
|
+
bodyChanged,
|
|
2270
|
+
affectedTests: affectedTestCount,
|
|
2271
|
+
truncated: anyImpactTruncated,
|
|
2272
|
+
blindSpotCount: totalBlindSpots
|
|
2273
|
+
});
|
|
2274
|
+
lines3.splice(3, 0, `> **Review decision · ${decision.risk.toUpperCase()}** — ${decision.summary}`, `> ${decision.reasons.join("; ")}.`, ``, `### Recommended checks`, ``, ...decision.recommendedActions.map((action) => `- ${action}`), ``);
|
|
2156
2275
|
lines3.push(`<sub>由 [codeblast](https://github.com/alloevil/codeblast) 生成 · 每条结论基于静态分析,含证据链接 · 动态调用盲区不在本报告内 · 评论不准?[30 秒反馈](https://github.com/alloevil/codeblast/issues/new?template=bot-feedback.yml&title=${encodeURIComponent(`[feedback] ${baseSha.slice(0, 7)}..${headSha.slice(0, 7)}`)})</sub>`);
|
|
2157
2276
|
console.log(lines3.join(`
|
|
2158
2277
|
`));
|
|
2159
2278
|
});
|
|
2160
2279
|
|
|
2280
|
+
// src/check-change.ts
|
|
2281
|
+
var exports_check_change = {};
|
|
2282
|
+
import fs8 from "node:fs";
|
|
2283
|
+
function buildGraphAt3(ref, db) {
|
|
2284
|
+
const wt = `/tmp/codeblast-check-${ref.replace(/[^\w]/g, "_")}`;
|
|
2285
|
+
spawnSync(["git", "worktree", "remove", "--force", wt], { cwd: repo3 });
|
|
2286
|
+
const add = spawnSync(["git", "worktree", "add", "--detach", wt, ref], { cwd: repo3 });
|
|
2287
|
+
if (add.exitCode !== 0)
|
|
2288
|
+
throw new Error(add.stderr.slice(0, 300));
|
|
2289
|
+
try {
|
|
2290
|
+
const built = spawnSync(selfCommand("index", wt, "--db", db));
|
|
2291
|
+
if (built.exitCode !== 0)
|
|
2292
|
+
throw new Error(built.stderr.slice(0, 500));
|
|
2293
|
+
} finally {
|
|
2294
|
+
spawnSync(["git", "worktree", "remove", "--force", wt], { cwd: repo3 });
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
var repo3, baseSha2, headSha2, dbAPath = "/tmp/codeblast-check-base.db", dbBPath = "/tmp/codeblast-check-head.db", dbA3, dbB3, diff3, prodNodesAdded2, bodyChanged2, total3, affectedTests = 0, truncated = false, blindSpots = 0, diffLineCount2, decision2, output;
|
|
2298
|
+
var init_check_change = __esm(() => {
|
|
2299
|
+
init_db();
|
|
2300
|
+
init_proc();
|
|
2301
|
+
init_impact();
|
|
2302
|
+
init_pr_silence();
|
|
2303
|
+
[repo3, baseSha2, headSha2] = process.argv.slice(2);
|
|
2304
|
+
if (!repo3 || !baseSha2 || !headSha2) {
|
|
2305
|
+
console.error("usage: codeblast check-change <repo> <base-sha> <head-sha> --json");
|
|
2306
|
+
process.exit(1);
|
|
2307
|
+
}
|
|
2308
|
+
for (const f of [dbAPath, dbBPath])
|
|
2309
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
2310
|
+
fs8.rmSync(f + suffix, { force: true });
|
|
2311
|
+
buildGraphAt3(baseSha2, dbAPath);
|
|
2312
|
+
buildGraphAt3(headSha2, dbBPath);
|
|
2313
|
+
dbA3 = openDatabase(dbAPath, { readonly: true });
|
|
2314
|
+
dbB3 = openDatabase(dbBPath, { readonly: true });
|
|
2315
|
+
diff3 = graphDiff(dbA3, dbB3);
|
|
2316
|
+
prodNodesAdded2 = diff3.nodesAdded.filter((n) => !TEST_RE3.test(n.file));
|
|
2317
|
+
bodyChanged2 = [];
|
|
2318
|
+
total3 = structuralTotal(diff3);
|
|
2319
|
+
for (const node of [...diff3.nodesAdded, ...diff3.renamed.map((r) => ({ id: `${r.file}#${r.to}`, kind: r.kind, name: r.to, file: r.file, line: 0 }))].slice(0, 15)) {
|
|
2320
|
+
try {
|
|
2321
|
+
const result = impact(dbB3, node.id, 2000);
|
|
2322
|
+
affectedTests += new Set(result.items.filter((item) => item.level === "tests").map((item) => item.file)).size;
|
|
2323
|
+
truncated ||= result.truncated;
|
|
2324
|
+
blindSpots += result.blind_spot_count;
|
|
2325
|
+
} catch {}
|
|
2326
|
+
}
|
|
2327
|
+
diffLineCount2 = spawnSync(["git", "diff", "--numstat", baseSha2, headSha2], { cwd: repo3 }).stdout.split(`
|
|
2328
|
+
`).reduce((sum, line) => {
|
|
2329
|
+
const match = line.match(/^(\d+)\t(\d+)\t/);
|
|
2330
|
+
return sum + (match ? Number(match[1]) + Number(match[2]) : 0);
|
|
2331
|
+
}, 0);
|
|
2332
|
+
decision2 = reviewDecision({ diff: diff3, prodNodesAdded: prodNodesAdded2, bodyChanged: bodyChanged2, affectedTests, truncated, blindSpotCount: blindSpots });
|
|
2333
|
+
output = {
|
|
2334
|
+
range: `${baseSha2}..${headSha2}`,
|
|
2335
|
+
decision: decision2.risk === "high" ? "review" : decision2.risk === "medium" ? "targeted-review" : "safe-to-review",
|
|
2336
|
+
risk: decision2.risk,
|
|
2337
|
+
summary: decision2.summary,
|
|
2338
|
+
reasons: decision2.reasons,
|
|
2339
|
+
recommended_actions: decision2.recommendedActions,
|
|
2340
|
+
structural_changes: total3,
|
|
2341
|
+
affected_test_files: affectedTests,
|
|
2342
|
+
blind_spot_count: blindSpots,
|
|
2343
|
+
truncated,
|
|
2344
|
+
signals: {
|
|
2345
|
+
core_named: coreNamedCount(diff3, prodNodesAdded2),
|
|
2346
|
+
body: bodySignalCount(bodyChanged2, diffLineCount2, () => false),
|
|
2347
|
+
aux_only: total3 > 0 && coreNamedCount(diff3, prodNodesAdded2) === 0 && bodyChanged2.length === 0
|
|
2348
|
+
},
|
|
2349
|
+
diff: diff3
|
|
2350
|
+
};
|
|
2351
|
+
console.log(JSON.stringify(output));
|
|
2352
|
+
dbA3.close();
|
|
2353
|
+
dbB3.close();
|
|
2354
|
+
});
|
|
2355
|
+
|
|
2161
2356
|
// src/demo.ts
|
|
2162
2357
|
var exports_demo = {};
|
|
2163
|
-
import
|
|
2358
|
+
import fs9 from "node:fs";
|
|
2164
2359
|
import path3 from "node:path";
|
|
2165
|
-
var
|
|
2360
|
+
var repo4, db6 = "/tmp/codeblast-demo.db", out = "/tmp/codeblast-demo-arch.html", run = (label, args) => {
|
|
2166
2361
|
console.log(`
|
|
2167
2362
|
\x1B[36m▸ ${label}\x1B[0m`);
|
|
2168
2363
|
console.log(` $ codeblast ${args.slice(2).join(" ")}`);
|
|
@@ -2182,12 +2377,12 @@ var repo3, db6 = "/tmp/codeblast-demo.db", out = "/tmp/codeblast-demo-arch.html"
|
|
|
2182
2377
|
var init_demo = __esm(() => {
|
|
2183
2378
|
init_db();
|
|
2184
2379
|
init_proc();
|
|
2185
|
-
|
|
2186
|
-
console.log(`codeblast demo — target: ${
|
|
2380
|
+
repo4 = path3.resolve(process.argv[2] ?? path3.join(import.meta.dirname, ".."));
|
|
2381
|
+
console.log(`codeblast demo — target: ${repo4}`);
|
|
2187
2382
|
for (const s of ["", "-wal", "-shm"])
|
|
2188
|
-
|
|
2189
|
-
run("1/4 build graph", selfCommand("index",
|
|
2190
|
-
run("2/4 incremental rerun (should skip everything)", selfCommand("index",
|
|
2383
|
+
fs9.rmSync(db6 + s, { force: true });
|
|
2384
|
+
run("1/4 build graph", selfCommand("index", repo4, "--db", db6));
|
|
2385
|
+
run("2/4 incremental rerun (should skip everything)", selfCommand("index", repo4, "--db", db6));
|
|
2191
2386
|
conn = openDatabase(db6, { readonly: true });
|
|
2192
2387
|
pick = conn.prepare(`SELECT n.id, COUNT(DISTINCT e.src) c FROM nodes n
|
|
2193
2388
|
JOIN edges e ON e.dst = n.id AND e.kind = 'calls'
|
|
@@ -2212,7 +2407,7 @@ var init_demo = __esm(() => {
|
|
|
2212
2407
|
|
|
2213
2408
|
// src/name-modules.ts
|
|
2214
2409
|
var exports_name_modules = {};
|
|
2215
|
-
import
|
|
2410
|
+
import fs10 from "node:fs";
|
|
2216
2411
|
var dbPath6, overlayFlag2, overlayPath2, db7, TEST_RE4, files3, byModule, symStmt, evidence, prompt, cmd;
|
|
2217
2412
|
var init_name_modules = __esm(async () => {
|
|
2218
2413
|
init_db();
|
|
@@ -2250,7 +2445,7 @@ ${JSON.stringify(evidence, null, 1)}`;
|
|
|
2250
2445
|
if (!cmd) {
|
|
2251
2446
|
const applyFlag = process.argv.indexOf("--apply");
|
|
2252
2447
|
if (applyFlag >= 0) {
|
|
2253
|
-
const namesJson = JSON.parse(
|
|
2448
|
+
const namesJson = JSON.parse(fs10.readFileSync(process.argv[applyFlag + 1], "utf8"));
|
|
2254
2449
|
const overlay = await loadOverlay(overlayPath2);
|
|
2255
2450
|
for (const [mod, v] of Object.entries(namesJson)) {
|
|
2256
2451
|
const existing = overlay.modules[mod];
|
|
@@ -2259,7 +2454,7 @@ ${JSON.stringify(evidence, null, 1)}`;
|
|
|
2259
2454
|
overlay.modules[mod] = { ...existing, name: `${v.name}`, ...v.desc ? {} : {} };
|
|
2260
2455
|
overlay.modules[mod].desc = v.desc;
|
|
2261
2456
|
}
|
|
2262
|
-
|
|
2457
|
+
fs10.writeFileSync(overlayPath2, JSON.stringify(overlay, null, 2));
|
|
2263
2458
|
console.error(`overlay written: ${overlayPath2}`);
|
|
2264
2459
|
} else {
|
|
2265
2460
|
console.log(prompt);
|
|
@@ -2276,7 +2471,7 @@ ${JSON.stringify(evidence, null, 1)}`;
|
|
|
2276
2471
|
overlay.modules[mod] = { ...overlay.modules[mod], name: v.name };
|
|
2277
2472
|
overlay.modules[mod].desc = v.desc;
|
|
2278
2473
|
}
|
|
2279
|
-
|
|
2474
|
+
fs10.writeFileSync(overlayPath2, JSON.stringify(overlay, null, 2));
|
|
2280
2475
|
console.error(`overlay written: ${overlayPath2}`);
|
|
2281
2476
|
}
|
|
2282
2477
|
});
|
|
@@ -2290,6 +2485,7 @@ var ROUTES = {
|
|
|
2290
2485
|
mermaid: () => Promise.resolve().then(() => (init_archmap(), exports_archmap)),
|
|
2291
2486
|
cochange: () => Promise.resolve().then(() => (init_cochange(), exports_cochange)),
|
|
2292
2487
|
"pr-comment": () => init_pr_comment().then(() => exports_pr_comment),
|
|
2488
|
+
"check-change": () => Promise.resolve().then(() => (init_check_change(), exports_check_change)),
|
|
2293
2489
|
demo: () => Promise.resolve().then(() => (init_demo(), exports_demo)),
|
|
2294
2490
|
"name-modules": () => init_name_modules().then(() => exports_name_modules)
|
|
2295
2491
|
};
|
|
@@ -2304,9 +2500,12 @@ usage: codeblast <command> [args]
|
|
|
2304
2500
|
change <repo> <ref-a> <ref-b> [--json] structural diff between two refs
|
|
2305
2501
|
archmap <graph.db> --out arch.html interactive architecture map
|
|
2306
2502
|
[--impact <sym>] [--diff <base.db>] ...with impact / change overlay
|
|
2503
|
+
[--repo-url <url>] names the page and its JSON-LD (already used above)
|
|
2504
|
+
[--site-url <base>] [--og-image <u>] canonical/og for the published page (optional)
|
|
2307
2505
|
mermaid <graph.db> module map as mermaid
|
|
2308
2506
|
cochange <repo> <graph.db> mine git history coupling
|
|
2309
2507
|
pr-comment <repo> <base-sha> <head-sha> PR review comment (silent if no change)
|
|
2508
|
+
check-change <repo> <base-sha> <head-sha> machine-readable merge safety decision
|
|
2310
2509
|
demo [repo] build + query + map in one shot
|
|
2311
2510
|
|
|
2312
2511
|
docs: https://github.com/alloevil/codeblast · demos: https://alloevil.github.io/codeblast/`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeblast",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Know what breaks before you merge — mutation-tested code graph with architecture, change & impact maps. Evidence on every edge. For humans and AI agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"impact-analysis",
|