codeblast 0.3.1 → 0.3.3
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 +127 -23
- package/README.zh-CN.md +1 -7
- package/SKILL.md +77 -5
- package/dist/bin.js +322 -43
- package/package.json +16 -5
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
**codeblast** is
|
|
1
|
+
**codeblast** is the pre-merge blast-radius check for TypeScript monorepos and AI coding agents. It tells you what to review, which tests to run, and when the graph is incomplete — with a source line for every reported edge.
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="assets/readme/hero.svg" width="100%" alt="codeblast — deterministic code graph: know what breaks before you merge"/>
|
|
@@ -15,6 +15,29 @@
|
|
|
15
15
|
<img src="https://img.shields.io/badge/license-MIT-8b949e?style=flat-square" alt="MIT"/>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
+
## Get value in one pull request
|
|
19
|
+
|
|
20
|
+
Copy this workflow into `.github/workflows/codeblast.yml`:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
name: codeblast
|
|
24
|
+
on: pull_request
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
pull-requests: write
|
|
28
|
+
jobs:
|
|
29
|
+
codeblast:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
with: { fetch-depth: 0 }
|
|
34
|
+
- uses: alloevil/codeblast@v0.3.3
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
On a structural PR, codeblast posts a bounded review decision, affected tests, `file:line` evidence,
|
|
38
|
+
and explicit blind-spot warnings. On a docs-only or otherwise irrelevant PR, it stays silent. It runs
|
|
39
|
+
locally in the runner; source is not uploaded to a codeblast service.
|
|
40
|
+
|
|
18
41
|
## What it is
|
|
19
42
|
|
|
20
43
|
**codeblast parses your repository into a deterministic code graph and answers the three most expensive questions around any code change:**
|
|
@@ -49,7 +72,7 @@ Built for humans (CLI / interactive HTML / PR comments) and for AI agents ([SKIL
|
|
|
49
72
|
```bash
|
|
50
73
|
npx codeblast demo # build a graph of the current repo, run one impact query, emit the map
|
|
51
74
|
npm i -g codeblast # or install globally; needs Node ≥ 22.13 (built-in sqlite) or Bun
|
|
52
|
-
# npm serves 0.3.
|
|
75
|
+
# npm serves 0.3.3
|
|
53
76
|
|
|
54
77
|
# Install as an agent skill (Claude Code, Codex, Cursor, and 14 more harnesses)
|
|
55
78
|
npx skills add alloevil/codeblast
|
|
@@ -57,28 +80,28 @@ npx skills add alloevil/codeblast
|
|
|
57
80
|
|
|
58
81
|
### As a GitHub Action (one line)
|
|
59
82
|
|
|
83
|
+
The smallest useful installation is a PR workflow. Pin the release tag, or pin the commit when your
|
|
84
|
+
repository requires immutable third-party actions:
|
|
85
|
+
|
|
60
86
|
```yaml
|
|
61
|
-
# .github/workflows/codeblast.yml
|
|
62
87
|
name: codeblast
|
|
63
88
|
on: pull_request
|
|
64
89
|
permissions:
|
|
65
90
|
contents: read
|
|
66
91
|
pull-requests: write
|
|
67
92
|
jobs:
|
|
68
|
-
|
|
93
|
+
codeblast:
|
|
69
94
|
runs-on: ubuntu-latest
|
|
70
95
|
steps:
|
|
71
96
|
- uses: actions/checkout@v4
|
|
72
|
-
with: { fetch-depth: 0 }
|
|
73
|
-
- uses: alloevil/codeblast@v0.3.
|
|
97
|
+
with: { fetch-depth: 0 }
|
|
98
|
+
- uses: alloevil/codeblast@v0.3.3
|
|
74
99
|
```
|
|
75
100
|
|
|
76
|
-
The action builds the analyzer from the ref you pinned
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
the commenting step, copy [`.github/workflows-template/codeblast.yml`](.github/workflows-template/codeblast.yml)
|
|
81
|
-
instead — it runs the same command with `npx`.
|
|
101
|
+
The action builds the analyzer from the ref you pinned, posts one sticky comment per PR, and stays
|
|
102
|
+
silent when the diff has no structural change. For the full input/output contract, use the
|
|
103
|
+
[workflow template](.github/workflows-template/codeblast.yml).
|
|
104
|
+
|
|
82
105
|
|
|
83
106
|
## Why not yet another LLM diagram tool
|
|
84
107
|
|
|
@@ -113,7 +136,8 @@ codeblast change <repo> main~5 main --json
|
|
|
113
136
|
|
|
114
137
|
# ③ Architecture Map — interactive HTML: module → file → symbol drill-down,
|
|
115
138
|
# symbols link to source lines
|
|
116
|
-
codeblast archmap graph.db --out arch.html --repo-url <github-url>
|
|
139
|
+
codeblast archmap graph.db --out arch.html --repo-url <github-url> \
|
|
140
|
+
--site-url https://example.github.io/repo --og-image <share-image-url>
|
|
117
141
|
|
|
118
142
|
# Optional: mine git co-change coupling (protocol pairs, config + consumers —
|
|
119
143
|
# edges static analysis can't see)
|
|
@@ -121,6 +145,19 @@ codeblast cochange <repo> graph.db
|
|
|
121
145
|
```
|
|
122
146
|
|
|
123
147
|
### PR bot (runs in CI, stays quiet by default)
|
|
148
|
+
### Reproducible PR cases
|
|
149
|
+
|
|
150
|
+
These are not synthetic diagrams; each case is a committed replay or pilot artifact:
|
|
151
|
+
|
|
152
|
+
| Case | Run it | Reviewer takeaway |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| Function-body behavior change | `codeblast pr-comment <repo> <base> <head>` | A symbol can keep the same shape while its callers still need review. |
|
|
155
|
+
| Exported signature change | `codeblast check-change <repo> <base> <head> --json` | API contraction or signature changes route to targeted review or review. |
|
|
156
|
+
| Incomplete static graph | `codeblast impact <db> <symbol> --json` | `warnings` and `blind_spot_count` prevent an apparently complete answer. |
|
|
157
|
+
|
|
158
|
+
The self-pilot record is [`eval/pilot-2026-09-24.json`](eval/pilot-2026-09-24.json): the published
|
|
159
|
+
package indexed this repository with 0 extraction failures and returned separate review-first and test
|
|
160
|
+
guidance. It is evidence that the workflow runs, not a claim of universal accuracy.
|
|
124
161
|
|
|
125
162
|
Copy [`.github/workflows-template/codeblast.yml`](.github/workflows-template/codeblast.yml) into your repo (it runs `npx codeblast pr-comment`, no other setup):
|
|
126
163
|
every PR gets an automatic comment with structural changes + blast radius + new symbols with no test coverage; **PRs with no structural change get zero comments**.
|
|
@@ -128,6 +165,48 @@ Replayed against 50 real commits: 42 correctly stayed silent. Comment usefulness
|
|
|
128
165
|
four review rounds — rounds 1–3 independent blind review, round 4 by the current model — scored 25% / 75% / 57% / 20% useful, against 7/8 = 87.5% when the
|
|
129
166
|
authoring agent rated its own comments; both numbers and the fixes that followed each round are logged in
|
|
130
167
|
[intent.md](intent.md).
|
|
168
|
+
## Evidence you can rerun
|
|
169
|
+
|
|
170
|
+
The headline promise is bounded: TypeScript, within the statically analyzable scope, and measured by
|
|
171
|
+
mutation testing against the real test suite. The committed runs are inspectable under [`eval/`](eval/)
|
|
172
|
+
and every published figure has a machine-readable receipt in [`docs/claims.json`](docs/claims.json).
|
|
173
|
+
|
|
174
|
+
| Scenario | Evidence | What it proves |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| tRPC, 30 injected mutations | [`mutation-2026-08-28-trpc-n30.json`](eval/mutation-2026-08-28-trpc-n30.json) | 28/28 killed mutants recalled; 2 were not killed by the suite |
|
|
177
|
+
| graphql-tools, Jest | [`mutation-2026-09-07-graphql-tools-n10.json`](eval/mutation-2026-09-07-graphql-tools-n10.json) | 10/10 killed mutants recalled across a second workspace layout |
|
|
178
|
+
| Real package pilot | [`pilot-2026-09-24.json`](eval/pilot-2026-09-24.json) | 32 files indexed, 0 extraction failures, guidance separated from repository-wide blind spots |
|
|
179
|
+
|
|
180
|
+
The pilot is not a benchmark and does not establish a universal accuracy rate. It is a reproducible
|
|
181
|
+
smoke run of the published package against this repository.
|
|
182
|
+
|
|
183
|
+
### A reviewer's decision, not a diagram
|
|
184
|
+
|
|
185
|
+
For agents and CI, `check-change --json` returns a routing decision plus evidence:
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"decision": "targeted-review",
|
|
190
|
+
"risk": "medium",
|
|
191
|
+
"affected_test_files": 3,
|
|
192
|
+
"blind_spot_count": 0,
|
|
193
|
+
"graph_health": {"warnings": []}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### What the numbers do and do not mean
|
|
198
|
+
|
|
199
|
+
The 28/28 and 10/10 figures are mutation-testing recall on two pinned repositories. They mean every
|
|
200
|
+
test file that failed for each killed mutation was present in the predicted set **within the measured
|
|
201
|
+
static-analysis boundary**. They do not mean codeblast catches every production regression, understands
|
|
202
|
+
dynamic runtime behavior, or provides function-level guarantees for Python.
|
|
203
|
+
|
|
204
|
+
The conservative import/file channel is deliberately retained: a controlled call-only ablation reached
|
|
205
|
+
better precision but recalled only 2/14 killed mutations. Treat `call` items as the first reading list,
|
|
206
|
+
the complete result as the test safety net, and every blind spot as an explicit limit.
|
|
207
|
+
`review` means inspect before merge; it does not mean the tool has proven the change unsafe. A nonzero
|
|
208
|
+
graph failure or an incomplete impact result is a reason to stop and inspect, not a reason to hide the
|
|
209
|
+
uncertainty.
|
|
131
210
|
|
|
132
211
|
## The precision promise (bounded, and evidence-backed)
|
|
133
212
|
|
|
@@ -157,15 +236,38 @@ authoring agent rated its own comments; both numbers and the fixes that followed
|
|
|
157
236
|
- **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
237
|
- **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
238
|
|
|
160
|
-
## For AI agents
|
|
161
239
|
|
|
240
|
+
### Unified change safety check
|
|
241
|
+
|
|
242
|
+
For agents and CI that need one decision instead of composing `change` and `impact`, run:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
codeblast check-change <repo> <base-sha> <head-sha> --json
|
|
162
246
|
```
|
|
247
|
+
|
|
248
|
+
The output contains `decision` (`safe-to-review`, `targeted-review`, or `review`), `risk`, reasons,
|
|
249
|
+
recommended actions, structural facts, predicted affected-test count, blind-spot count, truncation,
|
|
250
|
+
and the complete graph diff. It is a conservative routing decision, not a claim that the change is
|
|
251
|
+
safe to merge. `review` is required when API surface contracts, removed symbols, or incomplete impact
|
|
252
|
+
results are detected.
|
|
253
|
+
## For AI agents
|
|
254
|
+
|
|
255
|
+
```text
|
|
163
256
|
before editing: impact "symbol" --json → callsite list into context, so nothing gets missed
|
|
164
|
-
after editing: change
|
|
257
|
+
after editing: check-change repo base head --json → risk, graph health, affected tests and warnings
|
|
165
258
|
```
|
|
166
259
|
|
|
167
|
-
The full contract and interpretation discipline (including
|
|
168
|
-
Agent conventions: [AGENTS.md](AGENTS.md).
|
|
260
|
+
The full contract and interpretation discipline (including “never pretend the impact list is complete”)
|
|
261
|
+
is in [SKILL.md](SKILL.md). Agent conventions: [AGENTS.md](AGENTS.md).
|
|
262
|
+
|
|
263
|
+
## Help improve the reviewer
|
|
264
|
+
|
|
265
|
+
Found a false positive, missed impact, noisy comment, or wrong silence decision? Open a
|
|
266
|
+
[privacy-safe bot feedback issue](https://github.com/alloevil/codeblast/issues/new?template=bot-feedback.yml).
|
|
267
|
+
Share the public PR URL and a redacted explanation; never paste source code, secrets, private diffs,
|
|
268
|
+
or full repository contents. Reproducible benchmark results and pilot evidence belong in [`eval/`](eval/),
|
|
269
|
+
not in issue comments.
|
|
270
|
+
|
|
169
271
|
|
|
170
272
|
## FAQ
|
|
171
273
|
|
|
@@ -179,14 +281,16 @@ Agent conventions: [AGENTS.md](AGENTS.md).
|
|
|
179
281
|
|
|
180
282
|
**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
283
|
|
|
284
|
+
### PR bot feedback
|
|
285
|
+
|
|
286
|
+
Feedback is intentionally issue-based and privacy-safe. Report only the public PR URL, the affected
|
|
287
|
+
decision category, and a redacted explanation. Do not include source code, secrets, proprietary diffs,
|
|
288
|
+
or full repository contents. The useful labels are false positive, missed impact, noise, incorrect
|
|
289
|
+
silence, or incorrect recommendation. Feedback is qualitative calibration data; it is not telemetry
|
|
290
|
+
and codeblast does not require access to customer repositories.
|
|
291
|
+
|
|
182
292
|
## Status & roadmap
|
|
183
293
|
|
|
184
294
|
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
295
|
|
|
186
296
|
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.3,与本仓库一致
|
|
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
|
@@ -9,18 +9,79 @@ Three deterministic queries over a graph built by `tsc` (TypeScript, function-le
|
|
|
9
9
|
Python AST (file-level with typed-call upgrades). Every result carries the `file:line` where the
|
|
10
10
|
dependency actually occurs. The graph comes from the code, not from a model's reading of it.
|
|
11
11
|
|
|
12
|
-
## When to run it
|
|
13
|
-
|
|
14
12
|
| Situation | Command | What you get back |
|
|
15
13
|
|---|---|---|
|
|
16
|
-
| About to edit an exported symbol | `codeblast impact <db> "<symbol>" --json` |
|
|
14
|
+
| About to edit an exported symbol | `codeblast impact <db> "<symbol>" --json` | `guidance.review_first`, `guidance.run_tests`, conservative items and warnings |
|
|
17
15
|
| Finished a multi-file change; verifying scope | `codeblast change <repo> HEAD~1 HEAD --json` | Symbols and dependency edges added / removed / renamed |
|
|
16
|
+
| Need one merge-safety decision | `codeblast check-change <repo> <base> <head> --json` | Risk, decision, graph health, affected tests and recommended actions |
|
|
18
17
|
| Need to understand an unfamiliar repo | `codeblast archmap <db> --out arch.html` | Module → file → symbol map with cycle detection |
|
|
19
18
|
| Reviewing a PR | `codeblast pr-comment <repo> <base> <head>` | Markdown review comment; empty output when nothing structural changed |
|
|
20
19
|
|
|
21
20
|
Prerequisites: Node ≥ 22.13 or Bun ≥ 1.0 (`npx codeblast` works with no install); `python3` for Python
|
|
22
21
|
repos; the target repo's dependencies installed (missing `node_modules` turns external calls into blind spots).
|
|
23
22
|
|
|
23
|
+
|
|
24
|
+
Recommended agent loop:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
1. index the repository and check `failures == 0`
|
|
28
|
+
2. before editing: read `impact --json` and start with `guidance.review_first`
|
|
29
|
+
3. make the change
|
|
30
|
+
4. run `check-change --json` against base and head
|
|
31
|
+
5. run distinct files from `guidance.run_tests` / `affected_test_files`
|
|
32
|
+
6. report `warnings`, blind spots, and every `via_file:via_line` evidence location
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Never turn a `safe-to-review` routing result into “safe to merge”; the command routes attention and
|
|
36
|
+
|
|
37
|
+
`check-change --json` has `schema_version: "1"` and `engine_version`. Its exit codes are stable:
|
|
38
|
+
|
|
39
|
+
- `0`: analysis completed with no high-risk or graph-health warning;
|
|
40
|
+
- `1`: analysis completed but routes the change to review (`high` risk or incomplete graph);
|
|
41
|
+
- `2`: command, repository, worktree, or graph construction error.
|
|
42
|
+
|
|
43
|
+
`impact --json` also has `schema_version: "1"`; every guidance array is de-duplicated and sorted for
|
|
44
|
+
stable agent diffs. Treat the schema version as a compatibility boundary, not as a prose suggestion.
|
|
45
|
+
|
|
46
|
+
Machine-readable compatibility contract:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
impact --json: schema_version=1, guidance arrays sorted and de-duplicated
|
|
50
|
+
check-change --json: schema_version=1, engine_version, decision, risk, graph_health
|
|
51
|
+
exit 0: result complete and no high-risk routing
|
|
52
|
+
exit 1: result complete but review routing is required
|
|
53
|
+
exit 2: analysis or repository error; do not consume the result as evidence
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When a future schema version appears, stop and read its contract before making decisions from fields
|
|
57
|
+
that are not explicitly understood. Do not silently fall back to prose parsing.
|
|
58
|
+
|
|
59
|
+
The formal schemas are committed at [`eval/check-change.schema.json`](eval/check-change.schema.json)
|
|
60
|
+
and [`eval/impact.schema.json`](eval/impact.schema.json). Version policy: within schema version `1`,
|
|
61
|
+
new optional fields may be added, but existing field meanings, enum values, array ordering, and exit
|
|
62
|
+
codes remain stable. A breaking field removal, type change, or enum change requires a new schema
|
|
63
|
+
version and an explicit migration note. Consumers must reject unknown schema versions rather than
|
|
64
|
+
guessing.
|
|
65
|
+
|
|
66
|
+
The supported agent-facing artifacts are the two version-one JSON contracts and the smoke command:
|
|
67
|
+
`bun run agent-smoke` (or `node eval/agent-workflow-smoke.mjs` after building). A consumer integration
|
|
68
|
+
should fail closed when the schema version is unknown, and should preserve the complete JSON artifact
|
|
69
|
+
alongside its human summary for later review.
|
|
70
|
+
|
|
71
|
+
Continuous evolution outputs:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
bun run release-smoke # package install and executable surface
|
|
75
|
+
bun run agent-smoke # end-to-end safety JSON contract
|
|
76
|
+
bun run pilot-summary # committed pilot graph/impact summary
|
|
77
|
+
bun run guidance-stability # deterministic guidance sample
|
|
78
|
+
bun run validate-compatibility # compatibility sample boundary check
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Each command should either produce a small machine-readable result or fail. Keep the result with the
|
|
82
|
+
release or compatibility evidence; a green test suite without a current evidence sample is not a
|
|
83
|
+
complete evolution cycle.
|
|
84
|
+
tests. The graph health and warning fields are part of the contract.
|
|
24
85
|
## Interpretation rules — read before running
|
|
25
86
|
|
|
26
87
|
These are the mistakes an agent makes with this tool. Each one has produced a wrong answer in practice.
|
|
@@ -53,8 +114,7 @@ codeblast index <repo-root> --db /tmp/graph.db
|
|
|
53
114
|
|
|
54
115
|
Auto-discovers every package `tsconfig.json` in a monorepo and ingests Python via AST. Re-running only
|
|
55
116
|
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):
|
|
117
|
+
`codeblast index` against [tRPC](https://github.com/trpc/trpc) at commit `66d0544` with the historical codeblast 0.3.0 benchmark
|
|
58
118
|
|
|
59
119
|
```json
|
|
60
120
|
{ "db": "/tmp/graph.db", "seconds": 5.2, "tsconfigs": 34, "files_indexed": 957, "files_skipped": 0,
|
|
@@ -93,9 +153,21 @@ Output (`--json`):
|
|
|
93
153
|
via_file: string; via_line: number; // where the dependency occurs (rule 6)
|
|
94
154
|
}>;
|
|
95
155
|
co_change_hints: Array<{ file: string; co_commits: number; evidence: string }>; // rule 5
|
|
156
|
+
guidance: {
|
|
157
|
+
review_first: string[]; // call-channel non-test nodes
|
|
158
|
+
run_tests: string[]; // test file paths, de-duplicate before running
|
|
159
|
+
conservative: string[]; // non-test file-channel nodes; never discard
|
|
160
|
+
warnings: string[]; // truncation and blind-spot warnings
|
|
161
|
+
};
|
|
96
162
|
}
|
|
97
163
|
```
|
|
98
164
|
|
|
165
|
+
The `guidance` object is a convenience projection of `items`; it does not add analysis results.
|
|
166
|
+
Use `review_first` for the initial callsite checklist, `run_tests` for the affected test files,
|
|
167
|
+
and `conservative` as the import/re-export safety net. `warnings` is non-empty when the result is
|
|
168
|
+
incomplete or the target file contains unresolved analysis. Always retain `via_file` and `via_line`
|
|
169
|
+
when reporting a dependency.
|
|
170
|
+
|
|
99
171
|
How to use it: `items.filter(level === "direct")` is the callsite checklist. `items.filter(level ===
|
|
100
172
|
"tests")` de-duplicated by `file` is the test set to run. Test-directory fixtures are included
|
|
101
173
|
conservatively; estimate test cost by distinct files, not item count.
|
package/dist/bin.js
CHANGED
|
@@ -238,15 +238,30 @@ class Extractor {
|
|
|
238
238
|
rel(fileName) {
|
|
239
239
|
return path.relative(this.rootDir, fileName);
|
|
240
240
|
}
|
|
241
|
+
safeSymbolAt(node) {
|
|
242
|
+
try {
|
|
243
|
+
return this.checker.getSymbolAtLocation(node);
|
|
244
|
+
} catch {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
safeAliased(symbol) {
|
|
249
|
+
if (!(symbol.flags & ts.SymbolFlags.Alias))
|
|
250
|
+
return symbol;
|
|
251
|
+
try {
|
|
252
|
+
return this.checker.getAliasedSymbol(symbol);
|
|
253
|
+
} catch {
|
|
254
|
+
return symbol;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
241
257
|
collectImplementers() {
|
|
242
258
|
for (const sf of this.sourceFiles()) {
|
|
243
259
|
const visit = (node) => {
|
|
244
260
|
if (ts.isClassDeclaration(node) && node.heritageClauses) {
|
|
245
261
|
for (const clause of node.heritageClauses) {
|
|
246
262
|
for (const typeNode of clause.types) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
sym = this.checker.getAliasedSymbol(sym);
|
|
263
|
+
const raw = this.safeSymbolAt(typeNode.expression);
|
|
264
|
+
const sym = raw ? this.safeAliased(raw) : undefined;
|
|
250
265
|
const decl = sym?.declarations?.[0];
|
|
251
266
|
if (!decl)
|
|
252
267
|
continue;
|
|
@@ -289,18 +304,27 @@ class Extractor {
|
|
|
289
304
|
if (ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt)) {
|
|
290
305
|
const spec = stmt.moduleSpecifier;
|
|
291
306
|
if (spec && ts.isStringLiteral(spec)) {
|
|
292
|
-
|
|
307
|
+
let resolved;
|
|
308
|
+
try {
|
|
309
|
+
resolved = this.resolveModule(spec.text, sf.fileName);
|
|
310
|
+
} catch {
|
|
311
|
+
blindSpots.push({ file: relPath, line: lineOf(stmt), reason: `module resolution failed: ${spec.text}`, src_file: relPath });
|
|
312
|
+
}
|
|
293
313
|
if (!resolved) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
314
|
+
try {
|
|
315
|
+
for (const entry of this.externalReentry(spec.text, sf.fileName)) {
|
|
316
|
+
edges.push({
|
|
317
|
+
src: relPath,
|
|
318
|
+
dst: this.rel(entry),
|
|
319
|
+
kind: "imports",
|
|
320
|
+
file: relPath,
|
|
321
|
+
line: lineOf(stmt),
|
|
322
|
+
confidence: "conservative",
|
|
323
|
+
src_file: relPath
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
blindSpots.push({ file: relPath, line: lineOf(stmt), reason: `external reentry resolution failed: ${spec.text}`, src_file: relPath });
|
|
304
328
|
}
|
|
305
329
|
} else {
|
|
306
330
|
edges.push({
|
|
@@ -429,9 +453,8 @@ class Extractor {
|
|
|
429
453
|
for (const clause of node.heritageClauses) {
|
|
430
454
|
const ek = clause.token === ts.SyntaxKind.ImplementsKeyword ? "implements" : "extends";
|
|
431
455
|
for (const t of clause.types) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
sym = this.checker.getAliasedSymbol(sym);
|
|
456
|
+
const raw = this.safeSymbolAt(t.expression);
|
|
457
|
+
const sym = raw ? this.safeAliased(raw) : undefined;
|
|
435
458
|
const decl = sym?.declarations?.[0];
|
|
436
459
|
const dst = decl ? this.nodeIdOfDecl(decl) : undefined;
|
|
437
460
|
if (dst)
|
|
@@ -489,9 +512,8 @@ class Extractor {
|
|
|
489
512
|
}
|
|
490
513
|
resolveCall(call, caller, relPath, line, edges, blindSpots) {
|
|
491
514
|
const expr = call.expression;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
sym = this.checker.getAliasedSymbol(sym);
|
|
515
|
+
const raw = this.safeSymbolAt(expr);
|
|
516
|
+
const sym = raw ? this.safeAliased(raw) : undefined;
|
|
495
517
|
const decl = sym?.valueDeclaration ?? sym?.declarations?.[0];
|
|
496
518
|
if (!decl) {
|
|
497
519
|
const structural = ts.isElementAccessExpression(expr) || ts.isPropertyAccessExpression(expr) && ["call", "apply", "bind"].includes(expr.name.text);
|
|
@@ -709,7 +731,11 @@ function indexProgram(extractor) {
|
|
|
709
731
|
} catch (err) {
|
|
710
732
|
failures++;
|
|
711
733
|
seenFiles.delete(relPath);
|
|
712
|
-
|
|
734
|
+
const detail = err instanceof Error ? `${err.name}: ${err.message}${err.stack ? `
|
|
735
|
+
${err.stack.split(`
|
|
736
|
+
`).slice(1, 12).join(`
|
|
737
|
+
`)}` : ""}` : String(err);
|
|
738
|
+
console.error(`EXTRACT FAILED ${relPath}: ${detail}`);
|
|
713
739
|
}
|
|
714
740
|
}
|
|
715
741
|
}
|
|
@@ -1131,7 +1157,17 @@ var init_impact_cli = __esm(() => {
|
|
|
1131
1157
|
result = impact(db2, targetId, maxNodes);
|
|
1132
1158
|
ms = (performance.now() - t02).toFixed(0);
|
|
1133
1159
|
if (process.argv.includes("--json")) {
|
|
1134
|
-
|
|
1160
|
+
const uniqueSorted = (values) => [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
1161
|
+
const guidance = {
|
|
1162
|
+
review_first: uniqueSorted(result.items.filter((it) => it.channel === "call" && it.level !== "tests").map((it) => it.id)),
|
|
1163
|
+
run_tests: uniqueSorted(result.items.filter((it) => it.level === "tests").map((it) => it.file)),
|
|
1164
|
+
conservative: uniqueSorted(result.items.filter((it) => it.channel === "file" && it.level !== "tests").map((it) => it.id)),
|
|
1165
|
+
warnings: [
|
|
1166
|
+
...result.truncated ? ["impact_truncated_run_full_test_suite"] : [],
|
|
1167
|
+
...result.blind_spot_count > 0 ? ["blind_spots_may_underestimate_impact"] : []
|
|
1168
|
+
]
|
|
1169
|
+
};
|
|
1170
|
+
process.stdout.write(JSON.stringify({ schema_version: "1", ...result, guidance }) + `
|
|
1135
1171
|
`);
|
|
1136
1172
|
db2.close();
|
|
1137
1173
|
process.exitCode = 0;
|
|
@@ -1436,7 +1472,7 @@ async function layoutGraph(nodes, edges) {
|
|
|
1436
1472
|
}))
|
|
1437
1473
|
};
|
|
1438
1474
|
}
|
|
1439
|
-
var CLIENT_JS, dbPath3, outFlag2, outPath2, overlayFlag, overlayPath, repoFlag, repoUrl, impactFlag, impactTarget, diffFlag, diffBase, db3, overlay, TEST_RE, moduleOf = (file) => {
|
|
1475
|
+
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
1476
|
if (TEST_RE.test(file))
|
|
1441
1477
|
return "tests";
|
|
1442
1478
|
const ix = file.indexOf("/");
|
|
@@ -1460,6 +1496,51 @@ var init_archmap_html = __esm(async () => {
|
|
|
1460
1496
|
impactTarget = impactFlag >= 0 ? process.argv[impactFlag + 1] : undefined;
|
|
1461
1497
|
diffFlag = process.argv.indexOf("--diff");
|
|
1462
1498
|
diffBase = diffFlag >= 0 ? process.argv[diffFlag + 1] : undefined;
|
|
1499
|
+
siteFlag = process.argv.indexOf("--site-url");
|
|
1500
|
+
siteUrl = siteFlag >= 0 ? process.argv[siteFlag + 1].replace(/\/+$/, "") : undefined;
|
|
1501
|
+
ogImageFlag = process.argv.indexOf("--og-image");
|
|
1502
|
+
ogImage = ogImageFlag >= 0 ? process.argv[ogImageFlag + 1] : undefined;
|
|
1503
|
+
repoLabel = (() => {
|
|
1504
|
+
const m = repoUrl?.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
1505
|
+
if (m)
|
|
1506
|
+
return m[1].replace(/\.git$/, "");
|
|
1507
|
+
const base = outPath2.split("/").pop() ?? outPath2;
|
|
1508
|
+
return base.replace(/\.html$/, "");
|
|
1509
|
+
})();
|
|
1510
|
+
pageUrl = siteUrl ? `${siteUrl}/${outPath2.split("/").pop()}` : undefined;
|
|
1511
|
+
pageKind = impactTarget ? "blast-radius map" : diffBase ? "change map" : "architecture map";
|
|
1512
|
+
pageTitle = `${repoLabel} — ${pageKind}`;
|
|
1513
|
+
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." : "");
|
|
1514
|
+
headMeta = [
|
|
1515
|
+
`<title>${escAttr(pageTitle)}</title>`,
|
|
1516
|
+
`<meta name="description" content="${escAttr(pageDesc)}">`,
|
|
1517
|
+
...pageUrl ? [`<link rel="canonical" href="${escAttr(pageUrl)}">`] : [],
|
|
1518
|
+
`<meta property="og:type" content="website">`,
|
|
1519
|
+
`<meta property="og:title" content="${escAttr(pageTitle)}">`,
|
|
1520
|
+
`<meta property="og:description" content="${escAttr(pageDesc)}">`,
|
|
1521
|
+
...pageUrl ? [`<meta property="og:url" content="${escAttr(pageUrl)}">`] : [],
|
|
1522
|
+
`<meta property="og:site_name" content="codeblast">`,
|
|
1523
|
+
...ogImage ? [`<meta property="og:image" content="${escAttr(ogImage)}">`] : [],
|
|
1524
|
+
`<meta name="twitter:card" content="${ogImage ? "summary_large_image" : "summary"}">`,
|
|
1525
|
+
`<meta name="twitter:title" content="${escAttr(pageTitle)}">`,
|
|
1526
|
+
`<meta name="twitter:description" content="${escAttr(pageDesc)}">`,
|
|
1527
|
+
...ogImage ? [`<meta name="twitter:image" content="${escAttr(ogImage)}">`] : [],
|
|
1528
|
+
`<script type="application/ld+json">`,
|
|
1529
|
+
JSON.stringify({
|
|
1530
|
+
"@context": "https://schema.org",
|
|
1531
|
+
"@type": "WebPage",
|
|
1532
|
+
name: pageTitle,
|
|
1533
|
+
description: pageDesc,
|
|
1534
|
+
inLanguage: "zh-CN",
|
|
1535
|
+
...pageUrl ? { url: pageUrl } : {},
|
|
1536
|
+
...repoUrl ? {
|
|
1537
|
+
isBasedOn: repoUrl,
|
|
1538
|
+
about: { "@type": "SoftwareSourceCode", name: repoLabel, codeRepository: repoUrl }
|
|
1539
|
+
} : {}
|
|
1540
|
+
}, null, 2),
|
|
1541
|
+
`</script>`
|
|
1542
|
+
].join(`
|
|
1543
|
+
`);
|
|
1463
1544
|
if (!dbPath3) {
|
|
1464
1545
|
console.error("usage: codeblast archmap <graph.db> --out arch.html");
|
|
1465
1546
|
process.exit(1);
|
|
@@ -1629,10 +1710,10 @@ var init_archmap_html = __esm(async () => {
|
|
|
1629
1710
|
fileEdges: importRows
|
|
1630
1711
|
};
|
|
1631
1712
|
html = `<!DOCTYPE html>
|
|
1632
|
-
<html lang="zh">
|
|
1713
|
+
<html lang="zh-CN">
|
|
1633
1714
|
<head>
|
|
1634
1715
|
<meta charset="utf-8">
|
|
1635
|
-
|
|
1716
|
+
${headMeta}
|
|
1636
1717
|
<style>
|
|
1637
1718
|
:root { --bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#e6edf3; --dim:#8b949e;
|
|
1638
1719
|
--accent:#58a6ff; --warn:#f85149; --ok:#3fb950; }
|
|
@@ -1671,6 +1752,11 @@ var init_archmap_html = __esm(async () => {
|
|
|
1671
1752
|
.edge.cyclic { stroke: var(--warn); stroke-dasharray: 5 3; stroke-width: 2;
|
|
1672
1753
|
animation: cycflow 1.2s linear infinite; }
|
|
1673
1754
|
@keyframes cycflow { to { stroke-dashoffset: -16; } }
|
|
1755
|
+
/* 循环依赖用虚线静止表示即可 —— 虚线与颜色承载信息,流动只是让它更显眼。
|
|
1756
|
+
reduced-motion 下停掉动画,而不是去掉虚线。 */
|
|
1757
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1758
|
+
.edge.cyclic { animation: none; }
|
|
1759
|
+
}
|
|
1674
1760
|
.edge.hi { stroke: #d29922 !important; stroke-width: 2.6; stroke-opacity: 1; }
|
|
1675
1761
|
.edge.dim { stroke-opacity: 0.1; }
|
|
1676
1762
|
.edge.faint { stroke-opacity: 0.06; }
|
|
@@ -1947,7 +2033,7 @@ function bodySignalCount(bodyChanged, diffLineCount, hasCallImpact) {
|
|
|
1947
2033
|
return n;
|
|
1948
2034
|
}
|
|
1949
2035
|
function coreNamedCount(diff, prodNodesAdded) {
|
|
1950
|
-
return diff.edgesAdded.filter((e) => !AUX_RE.test(e.file)).length + prodNodesAdded.filter((n) => !AUX_RE.test(n.file)).length + diff.renamed.filter((r) => !AUX_RE.test(r.file)).length + diff.visibilityChanged.filter((v) => !AUX_RE.test(v.file)).length + diff.signatureChanged.filter((s) => !AUX_RE.test(s.file)).length;
|
|
2036
|
+
return diff.edgesAdded.filter((e) => !AUX_RE.test(e.file)).length + diff.edgesRemoved.filter((e) => !AUX_RE.test(e.file)).length + prodNodesAdded.filter((n) => !AUX_RE.test(n.file)).length + diff.nodesRemoved.filter((n) => !AUX_RE.test(n.file) && !TEST_RE3.test(n.file)).length + diff.renamed.filter((r) => !AUX_RE.test(r.file)).length + diff.visibilityChanged.filter((v) => !AUX_RE.test(v.file)).length + diff.signatureChanged.filter((s) => !AUX_RE.test(s.file)).length;
|
|
1951
2037
|
}
|
|
1952
2038
|
var TEST_RE3, AUX_RE, BIG_DIFF_LINES = 40;
|
|
1953
2039
|
var init_pr_silence = __esm(() => {
|
|
@@ -1955,6 +2041,54 @@ var init_pr_silence = __esm(() => {
|
|
|
1955
2041
|
AUX_RE = /^(www|docs|examples)\//;
|
|
1956
2042
|
});
|
|
1957
2043
|
|
|
2044
|
+
// src/pr-decision.ts
|
|
2045
|
+
function reviewDecision(input) {
|
|
2046
|
+
const { diff, prodNodesAdded, bodyChanged, affectedTests, truncated, blindSpotCount } = input;
|
|
2047
|
+
const apiContractions = diff.visibilityChanged.filter((v) => !v.nowExported).length;
|
|
2048
|
+
const apiChanges = apiContractions + diff.signatureChanged.length;
|
|
2049
|
+
const removals = diff.nodesRemoved.length;
|
|
2050
|
+
const behaviorChanges = bodyChanged.length;
|
|
2051
|
+
const reasons = [];
|
|
2052
|
+
const recommendedActions = [];
|
|
2053
|
+
let risk = "low";
|
|
2054
|
+
if (apiContractions > 0 || removals > 0 || truncated)
|
|
2055
|
+
risk = "high";
|
|
2056
|
+
else if (apiChanges > 0 || behaviorChanges > 0 || affectedTests > 0 || blindSpotCount > 0)
|
|
2057
|
+
risk = "medium";
|
|
2058
|
+
if (apiContractions > 0)
|
|
2059
|
+
reasons.push(`${apiContractions} exported symbol${apiContractions === 1 ? " is" : "s are"} no longer public`);
|
|
2060
|
+
if (removals > 0)
|
|
2061
|
+
reasons.push(`${removals} symbol${removals === 1 ? " was" : "s were"} removed`);
|
|
2062
|
+
if (diff.signatureChanged.length > 0)
|
|
2063
|
+
reasons.push(`${diff.signatureChanged.length} exported signature${diff.signatureChanged.length === 1 ? " changed" : "s changed"}`);
|
|
2064
|
+
if (behaviorChanges > 0)
|
|
2065
|
+
reasons.push(`${behaviorChanges} function bod${behaviorChanges === 1 ? "y" : "ies"} changed`);
|
|
2066
|
+
if (affectedTests > 0)
|
|
2067
|
+
reasons.push(`${affectedTests} test file${affectedTests === 1 ? " is" : "s are"} in the predicted impact set`);
|
|
2068
|
+
if (truncated)
|
|
2069
|
+
reasons.push("the impact set exceeded the reporting limit");
|
|
2070
|
+
if (blindSpotCount > 0)
|
|
2071
|
+
reasons.push(`${blindSpotCount} static-analysis blind spot${blindSpotCount === 1 ? "" : "s"} may hide impact`);
|
|
2072
|
+
if (reasons.length === 0 && prodNodesAdded.length > 0)
|
|
2073
|
+
reasons.push(`${prodNodesAdded.length} production symbol${prodNodesAdded.length === 1 ? " was" : "s were"} added`);
|
|
2074
|
+
if (reasons.length === 0)
|
|
2075
|
+
reasons.push("only low-risk structural additions were found");
|
|
2076
|
+
if (apiChanges > 0 || removals > 0)
|
|
2077
|
+
recommendedActions.push("Review the public API changes and migration impact.");
|
|
2078
|
+
if (truncated)
|
|
2079
|
+
recommendedActions.push("Run the full test suite; the reported impact list is truncated.");
|
|
2080
|
+
else if (affectedTests > 0)
|
|
2081
|
+
recommendedActions.push(`Run the ${affectedTests} predicted affected test file${affectedTests === 1 ? "" : "s"}.`);
|
|
2082
|
+
if (blindSpotCount > 0)
|
|
2083
|
+
recommendedActions.push("Inspect the reported blind spots before treating the impact set as complete.");
|
|
2084
|
+
if (behaviorChanges > 0)
|
|
2085
|
+
recommendedActions.push("Review the changed function bodies even where the exported shape is unchanged.");
|
|
2086
|
+
if (recommendedActions.length === 0)
|
|
2087
|
+
recommendedActions.push("Review the named structural additions; no existing API contraction was detected.");
|
|
2088
|
+
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.";
|
|
2089
|
+
return { risk, summary, reasons, recommendedActions };
|
|
2090
|
+
}
|
|
2091
|
+
|
|
1958
2092
|
// src/pr-comment.ts
|
|
1959
2093
|
var exports_pr_comment = {};
|
|
1960
2094
|
import fs7 from "node:fs";
|
|
@@ -1993,7 +2127,7 @@ var args3, repo2, baseSha, headSha, urlFlag, repoUrl2, dbPathA = "/tmp/codeblast
|
|
|
1993
2127
|
const wrap = s.kind === "interface" || s.kind === "const" ? (t) => t : (t) => `(${t})`;
|
|
1994
2128
|
const what = s.kind === "interface" ? "成员变化" : s.kind === "const" ? "类型变化" : "";
|
|
1995
2129
|
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;
|
|
2130
|
+
}, apiSig, testSig, uncovered, impactRows, affectedTestCount = 0, anyImpactTruncated = false, totalBlindSpots = 0, prodNodesAdded, diffLineCount, bodySignal, coreNamed, changedFiles, changedTests, testedBodySignal, decision;
|
|
1997
2131
|
var init_pr_comment = __esm(async () => {
|
|
1998
2132
|
init_db();
|
|
1999
2133
|
init_proc();
|
|
@@ -2106,6 +2240,9 @@ var init_pr_comment = __esm(async () => {
|
|
|
2106
2240
|
const r = impact(dbB2, n.id, 2000);
|
|
2107
2241
|
const callItems = r.items.filter((i) => i.channel === "call");
|
|
2108
2242
|
const tests = callItems.filter((i) => i.level === "tests").length;
|
|
2243
|
+
affectedTestCount += tests;
|
|
2244
|
+
anyImpactTruncated ||= r.truncated;
|
|
2245
|
+
totalBlindSpots += r.blind_spot_count;
|
|
2109
2246
|
impactRows.push(`| \`${n.name}\` | ${n.kind} | ${callItems.length} | ${tests} | ${link(n.file, n.line)} |`);
|
|
2110
2247
|
const blind = r.blind_spot_count > 0;
|
|
2111
2248
|
if (tests === 0 && n.kind !== "interface" && n.kind !== "const") {
|
|
@@ -2132,7 +2269,11 @@ var init_pr_comment = __esm(async () => {
|
|
|
2132
2269
|
}
|
|
2133
2270
|
});
|
|
2134
2271
|
coreNamed = coreNamedCount(diff2, prodNodesAdded);
|
|
2135
|
-
|
|
2272
|
+
changedFiles = spawnSync(["git", "diff", "--name-only", baseSha, headSha], { cwd: repo2 }).stdout.split(`
|
|
2273
|
+
`).filter(Boolean);
|
|
2274
|
+
changedTests = changedFiles.some((file) => TEST_RE3.test(file));
|
|
2275
|
+
testedBodySignal = bodyChanged.length > 0 && changedTests ? 1 : 0;
|
|
2276
|
+
if (coreNamed + bodySignal + testedBodySignal === 0)
|
|
2136
2277
|
process.exit(0);
|
|
2137
2278
|
if (bodyChanged.length > 0) {
|
|
2138
2279
|
const rows = [];
|
|
@@ -2153,16 +2294,153 @@ var init_pr_comment = __esm(async () => {
|
|
|
2153
2294
|
if (total2 === 0 && bodyChanged.length > 0) {
|
|
2154
2295
|
lines3[2] = `**无结构变更**,但有 ${bodyChanged.length} 个函数体内改动(见下)`;
|
|
2155
2296
|
}
|
|
2297
|
+
decision = reviewDecision({
|
|
2298
|
+
diff: diff2,
|
|
2299
|
+
prodNodesAdded,
|
|
2300
|
+
bodyChanged,
|
|
2301
|
+
affectedTests: affectedTestCount,
|
|
2302
|
+
truncated: anyImpactTruncated,
|
|
2303
|
+
blindSpotCount: totalBlindSpots
|
|
2304
|
+
});
|
|
2305
|
+
lines3.splice(3, 0, `> **Review decision · ${decision.risk.toUpperCase()}** — ${decision.summary}`, `> ${decision.reasons.join("; ")}.`, ``, `### Recommended checks`, ``, ...decision.recommendedActions.map((action) => `- ${action}`), ``);
|
|
2156
2306
|
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
2307
|
console.log(lines3.join(`
|
|
2158
2308
|
`));
|
|
2159
2309
|
});
|
|
2160
2310
|
|
|
2161
|
-
// src/
|
|
2162
|
-
var
|
|
2311
|
+
// src/check-change.ts
|
|
2312
|
+
var exports_check_change = {};
|
|
2163
2313
|
import fs8 from "node:fs";
|
|
2164
2314
|
import path3 from "node:path";
|
|
2165
|
-
|
|
2315
|
+
function buildGraphAt3(ref, db) {
|
|
2316
|
+
const wt = `/tmp/codeblast-check-${ref.replace(/[^\w]/g, "_")}`;
|
|
2317
|
+
spawnSync(["git", "worktree", "remove", "--force", wt], { cwd: repo3 });
|
|
2318
|
+
const add = spawnSync(["git", "worktree", "add", "--detach", wt, ref], { cwd: repo3 });
|
|
2319
|
+
if (add.exitCode !== 0)
|
|
2320
|
+
throw new Error(add.stderr.slice(0, 300));
|
|
2321
|
+
try {
|
|
2322
|
+
const built = spawnSync(selfCommand("index", wt, "--db", db));
|
|
2323
|
+
if (built.exitCode !== 0)
|
|
2324
|
+
throw new Error(built.stderr.slice(0, 500));
|
|
2325
|
+
} finally {
|
|
2326
|
+
spawnSync(["git", "worktree", "remove", "--force", wt], { cwd: repo3 });
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
var SCHEMA_VERSION = "1", EXIT_OK = 0, EXIT_REVIEW = 1, EXIT_ERROR = 2, readVersion = () => {
|
|
2330
|
+
try {
|
|
2331
|
+
const parsed = JSON.parse(fs8.readFileSync(path3.join(import.meta.dirname, "..", "package.json"), "utf8"));
|
|
2332
|
+
if (parsed && typeof parsed === "object" && "version" in parsed && typeof parsed.version === "string")
|
|
2333
|
+
return parsed.version;
|
|
2334
|
+
} catch {}
|
|
2335
|
+
return "unknown";
|
|
2336
|
+
}, ENGINE_VERSION, repo3, baseSha2, headSha2, dbAPath = "/tmp/codeblast-check-base.db", dbBPath = "/tmp/codeblast-check-head.db", dbA3, dbB3, graphHealth = (db) => ({
|
|
2337
|
+
files: Number(db.prepare("SELECT COUNT(*) c FROM files").get().c),
|
|
2338
|
+
nodes: Number(db.prepare("SELECT COUNT(*) c FROM nodes").get().c),
|
|
2339
|
+
edges: Number(db.prepare("SELECT COUNT(*) c FROM edges").get().c),
|
|
2340
|
+
blind_spots: Number(db.prepare("SELECT COUNT(*) c FROM blind_spots").get().c)
|
|
2341
|
+
}), healthBase, healthHead, diff3, healthWarnings, prodNodesAdded2, total3, affectedTests = 0, truncated = false, blindSpots = 0, bodyChanged2, diffText, currentFile = "", diffLineCount2, decision2, output;
|
|
2342
|
+
var init_check_change = __esm(() => {
|
|
2343
|
+
init_db();
|
|
2344
|
+
init_proc();
|
|
2345
|
+
init_impact();
|
|
2346
|
+
init_pr_silence();
|
|
2347
|
+
ENGINE_VERSION = readVersion();
|
|
2348
|
+
process.on("uncaughtException", (error) => {
|
|
2349
|
+
console.error(`check-change analysis error: ${error instanceof Error ? error.message : error}`);
|
|
2350
|
+
process.exitCode = EXIT_ERROR;
|
|
2351
|
+
});
|
|
2352
|
+
process.on("unhandledRejection", (reason) => {
|
|
2353
|
+
console.error(`check-change analysis error: ${reason instanceof Error ? reason.message : reason}`);
|
|
2354
|
+
process.exitCode = EXIT_ERROR;
|
|
2355
|
+
});
|
|
2356
|
+
[repo3, baseSha2, headSha2] = process.argv.slice(2);
|
|
2357
|
+
if (!repo3 || !baseSha2 || !headSha2) {
|
|
2358
|
+
console.error("usage: codeblast check-change <repo> <base-sha> <head-sha> --json");
|
|
2359
|
+
process.exit(1);
|
|
2360
|
+
}
|
|
2361
|
+
for (const f of [dbAPath, dbBPath])
|
|
2362
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
2363
|
+
fs8.rmSync(f + suffix, { force: true });
|
|
2364
|
+
buildGraphAt3(baseSha2, dbAPath);
|
|
2365
|
+
buildGraphAt3(headSha2, dbBPath);
|
|
2366
|
+
dbA3 = openDatabase(dbAPath, { readonly: true });
|
|
2367
|
+
dbB3 = openDatabase(dbBPath, { readonly: true });
|
|
2368
|
+
healthBase = graphHealth(dbA3);
|
|
2369
|
+
healthHead = graphHealth(dbB3);
|
|
2370
|
+
diff3 = graphDiff(dbA3, dbB3);
|
|
2371
|
+
healthWarnings = [
|
|
2372
|
+
...healthHead.files === 0 || healthHead.nodes === 0 ? ["graph_empty"] : [],
|
|
2373
|
+
...healthHead.nodes < healthBase.nodes / 2 ? ["graph_node_count_dropped_sharply"] : []
|
|
2374
|
+
];
|
|
2375
|
+
prodNodesAdded2 = diff3.nodesAdded.filter((n) => !TEST_RE3.test(n.file));
|
|
2376
|
+
total3 = structuralTotal(diff3);
|
|
2377
|
+
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)) {
|
|
2378
|
+
try {
|
|
2379
|
+
const result = impact(dbB3, node.id, 2000);
|
|
2380
|
+
affectedTests += new Set(result.items.filter((item) => item.level === "tests").map((item) => item.file)).size;
|
|
2381
|
+
truncated ||= result.truncated;
|
|
2382
|
+
blindSpots += result.blind_spot_count;
|
|
2383
|
+
} catch {}
|
|
2384
|
+
}
|
|
2385
|
+
bodyChanged2 = [];
|
|
2386
|
+
diffText = spawnSync(["git", "diff", "--unified=0", baseSha2, headSha2, "--", "*.ts", "*.tsx"], { cwd: repo3 }).stdout;
|
|
2387
|
+
for (const line of diffText.split(`
|
|
2388
|
+
`)) {
|
|
2389
|
+
const file = line.match(/^\+\+\+ b\/(.+)$/);
|
|
2390
|
+
if (file) {
|
|
2391
|
+
currentFile = file[1];
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)/);
|
|
2395
|
+
if (!hunk || !currentFile || TEST_RE3.test(currentFile))
|
|
2396
|
+
continue;
|
|
2397
|
+
const row = dbB3.prepare("SELECT id, name, kind, file, line FROM nodes WHERE file = ? AND kind IN ('function','method') AND line <= ? AND end_line >= ? ORDER BY (end_line - line) ASC LIMIT 1").get(currentFile, Number(hunk[1]), Number(hunk[1]));
|
|
2398
|
+
if (row && !bodyChanged2.some((item) => item.id === row.id))
|
|
2399
|
+
bodyChanged2.push(row);
|
|
2400
|
+
}
|
|
2401
|
+
diffLineCount2 = spawnSync(["git", "diff", "--numstat", baseSha2, headSha2], { cwd: repo3 }).stdout.split(`
|
|
2402
|
+
`).reduce((sum, line) => {
|
|
2403
|
+
const match = line.match(/^(\d+)\t(\d+)\t/);
|
|
2404
|
+
return sum + (match ? Number(match[1]) + Number(match[2]) : 0);
|
|
2405
|
+
}, 0);
|
|
2406
|
+
decision2 = reviewDecision({ diff: diff3, prodNodesAdded: prodNodesAdded2, bodyChanged: bodyChanged2, affectedTests, truncated, blindSpotCount: blindSpots });
|
|
2407
|
+
if (healthWarnings.length > 0) {
|
|
2408
|
+
decision2.risk = "high";
|
|
2409
|
+
decision2.reasons.push(...healthWarnings);
|
|
2410
|
+
decision2.recommendedActions.unshift("Rebuild or inspect the graph before relying on this decision.");
|
|
2411
|
+
}
|
|
2412
|
+
output = {
|
|
2413
|
+
schema_version: SCHEMA_VERSION,
|
|
2414
|
+
engine_version: ENGINE_VERSION,
|
|
2415
|
+
range: `${baseSha2}..${headSha2}`,
|
|
2416
|
+
decision: decision2.risk === "high" ? "review" : decision2.risk === "medium" ? "targeted-review" : "safe-to-review",
|
|
2417
|
+
risk: decision2.risk,
|
|
2418
|
+
summary: decision2.summary,
|
|
2419
|
+
reasons: decision2.reasons,
|
|
2420
|
+
recommended_actions: decision2.recommendedActions,
|
|
2421
|
+
structural_changes: total3,
|
|
2422
|
+
affected_test_files: affectedTests,
|
|
2423
|
+
blind_spot_count: blindSpots,
|
|
2424
|
+
graph_health: { base: healthBase, head: healthHead, warnings: healthWarnings },
|
|
2425
|
+
truncated,
|
|
2426
|
+
signals: {
|
|
2427
|
+
core_named: coreNamedCount(diff3, prodNodesAdded2),
|
|
2428
|
+
body: bodySignalCount(bodyChanged2, diffLineCount2, () => false),
|
|
2429
|
+
aux_only: total3 > 0 && coreNamedCount(diff3, prodNodesAdded2) === 0 && bodyChanged2.length === 0
|
|
2430
|
+
},
|
|
2431
|
+
diff: diff3
|
|
2432
|
+
};
|
|
2433
|
+
console.log(JSON.stringify(output));
|
|
2434
|
+
dbA3.close();
|
|
2435
|
+
dbB3.close();
|
|
2436
|
+
process.exitCode = healthWarnings.length > 0 || decision2.risk === "high" ? EXIT_REVIEW : EXIT_OK;
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
// src/demo.ts
|
|
2440
|
+
var exports_demo = {};
|
|
2441
|
+
import fs9 from "node:fs";
|
|
2442
|
+
import path4 from "node:path";
|
|
2443
|
+
var repo4, db6 = "/tmp/codeblast-demo.db", out = "/tmp/codeblast-demo-arch.html", run = (label, args) => {
|
|
2166
2444
|
console.log(`
|
|
2167
2445
|
\x1B[36m▸ ${label}\x1B[0m`);
|
|
2168
2446
|
console.log(` $ codeblast ${args.slice(2).join(" ")}`);
|
|
@@ -2182,12 +2460,12 @@ var repo3, db6 = "/tmp/codeblast-demo.db", out = "/tmp/codeblast-demo-arch.html"
|
|
|
2182
2460
|
var init_demo = __esm(() => {
|
|
2183
2461
|
init_db();
|
|
2184
2462
|
init_proc();
|
|
2185
|
-
|
|
2186
|
-
console.log(`codeblast demo — target: ${
|
|
2463
|
+
repo4 = path4.resolve(process.argv[2] ?? path4.join(import.meta.dirname, ".."));
|
|
2464
|
+
console.log(`codeblast demo — target: ${repo4}`);
|
|
2187
2465
|
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",
|
|
2466
|
+
fs9.rmSync(db6 + s, { force: true });
|
|
2467
|
+
run("1/4 build graph", selfCommand("index", repo4, "--db", db6));
|
|
2468
|
+
run("2/4 incremental rerun (should skip everything)", selfCommand("index", repo4, "--db", db6));
|
|
2191
2469
|
conn = openDatabase(db6, { readonly: true });
|
|
2192
2470
|
pick = conn.prepare(`SELECT n.id, COUNT(DISTINCT e.src) c FROM nodes n
|
|
2193
2471
|
JOIN edges e ON e.dst = n.id AND e.kind = 'calls'
|
|
@@ -2212,7 +2490,7 @@ var init_demo = __esm(() => {
|
|
|
2212
2490
|
|
|
2213
2491
|
// src/name-modules.ts
|
|
2214
2492
|
var exports_name_modules = {};
|
|
2215
|
-
import
|
|
2493
|
+
import fs10 from "node:fs";
|
|
2216
2494
|
var dbPath6, overlayFlag2, overlayPath2, db7, TEST_RE4, files3, byModule, symStmt, evidence, prompt, cmd;
|
|
2217
2495
|
var init_name_modules = __esm(async () => {
|
|
2218
2496
|
init_db();
|
|
@@ -2250,7 +2528,7 @@ ${JSON.stringify(evidence, null, 1)}`;
|
|
|
2250
2528
|
if (!cmd) {
|
|
2251
2529
|
const applyFlag = process.argv.indexOf("--apply");
|
|
2252
2530
|
if (applyFlag >= 0) {
|
|
2253
|
-
const namesJson = JSON.parse(
|
|
2531
|
+
const namesJson = JSON.parse(fs10.readFileSync(process.argv[applyFlag + 1], "utf8"));
|
|
2254
2532
|
const overlay = await loadOverlay(overlayPath2);
|
|
2255
2533
|
for (const [mod, v] of Object.entries(namesJson)) {
|
|
2256
2534
|
const existing = overlay.modules[mod];
|
|
@@ -2259,7 +2537,7 @@ ${JSON.stringify(evidence, null, 1)}`;
|
|
|
2259
2537
|
overlay.modules[mod] = { ...existing, name: `${v.name}`, ...v.desc ? {} : {} };
|
|
2260
2538
|
overlay.modules[mod].desc = v.desc;
|
|
2261
2539
|
}
|
|
2262
|
-
|
|
2540
|
+
fs10.writeFileSync(overlayPath2, JSON.stringify(overlay, null, 2));
|
|
2263
2541
|
console.error(`overlay written: ${overlayPath2}`);
|
|
2264
2542
|
} else {
|
|
2265
2543
|
console.log(prompt);
|
|
@@ -2276,7 +2554,7 @@ ${JSON.stringify(evidence, null, 1)}`;
|
|
|
2276
2554
|
overlay.modules[mod] = { ...overlay.modules[mod], name: v.name };
|
|
2277
2555
|
overlay.modules[mod].desc = v.desc;
|
|
2278
2556
|
}
|
|
2279
|
-
|
|
2557
|
+
fs10.writeFileSync(overlayPath2, JSON.stringify(overlay, null, 2));
|
|
2280
2558
|
console.error(`overlay written: ${overlayPath2}`);
|
|
2281
2559
|
}
|
|
2282
2560
|
});
|
|
@@ -2290,6 +2568,7 @@ var ROUTES = {
|
|
|
2290
2568
|
mermaid: () => Promise.resolve().then(() => (init_archmap(), exports_archmap)),
|
|
2291
2569
|
cochange: () => Promise.resolve().then(() => (init_cochange(), exports_cochange)),
|
|
2292
2570
|
"pr-comment": () => init_pr_comment().then(() => exports_pr_comment),
|
|
2571
|
+
"check-change": () => Promise.resolve().then(() => (init_check_change(), exports_check_change)),
|
|
2293
2572
|
demo: () => Promise.resolve().then(() => (init_demo(), exports_demo)),
|
|
2294
2573
|
"name-modules": () => init_name_modules().then(() => exports_name_modules)
|
|
2295
2574
|
};
|
|
@@ -2304,13 +2583,13 @@ usage: codeblast <command> [args]
|
|
|
2304
2583
|
change <repo> <ref-a> <ref-b> [--json] structural diff between two refs
|
|
2305
2584
|
archmap <graph.db> --out arch.html interactive architecture map
|
|
2306
2585
|
[--impact <sym>] [--diff <base.db>] ...with impact / change overlay
|
|
2307
|
-
|
|
2308
|
-
cochange <repo> <graph.db> mine git history coupling
|
|
2586
|
+
[--repo-url <url>] names the page and its JSON-LD (already used above)
|
|
2309
2587
|
pr-comment <repo> <base-sha> <head-sha> PR review comment (silent if no change)
|
|
2588
|
+
check-change <repo> <base-sha> <head-sha> machine-readable merge safety decision
|
|
2310
2589
|
demo [repo] build + query + map in one shot
|
|
2311
2590
|
|
|
2312
2591
|
docs: https://github.com/alloevil/codeblast · demos: https://alloevil.github.io/codeblast/`);
|
|
2313
|
-
process.exit(cmd2
|
|
2592
|
+
process.exit(cmd2 === "--help" || cmd2 === "-h" || !cmd2 ? 0 : 1);
|
|
2314
2593
|
}
|
|
2315
2594
|
process.argv.splice(2, 1);
|
|
2316
2595
|
await ROUTES[cmd2]();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeblast",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"type": "module",
|
|
28
28
|
"bin": {
|
|
29
|
-
"codeblast": "
|
|
29
|
+
"codeblast": "dist/bin.js"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
@@ -38,8 +38,13 @@
|
|
|
38
38
|
"build": "bun build src/bin.ts --target=node --outdir=dist --entry-naming=bin.js --external typescript --external @dagrejs/dagre && cp src/archmap-client.js src/py_extract.py dist/",
|
|
39
39
|
"prepack": "bun run build",
|
|
40
40
|
"typecheck": "tsc -p .",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
41
|
+
"agent-smoke": "bun run build && node eval/agent-workflow-smoke.mjs",
|
|
42
|
+
"guidance-stability": "node eval/guidance-stability.mjs",
|
|
43
|
+
"offline-replay": "bun run build && node eval/offline-replay.mjs",
|
|
44
|
+
"pilot-summary": "node eval/pilot-summary.mjs",
|
|
45
|
+
"release-smoke": "node eval/release-smoke.mjs",
|
|
46
|
+
"validate-compatibility": "node eval/validate-compatibility.mjs",
|
|
47
|
+
"validate-safety": "node eval/validate-safety.mjs",
|
|
43
48
|
"verify": "python3 eval/mutation_check.py"
|
|
44
49
|
},
|
|
45
50
|
"engines": {
|
|
@@ -52,5 +57,11 @@
|
|
|
52
57
|
"devDependencies": {
|
|
53
58
|
"@types/node": "^26.4.0",
|
|
54
59
|
"bun-types": "^1.4.0"
|
|
55
|
-
}
|
|
60
|
+
},
|
|
61
|
+
"main": "index.js",
|
|
62
|
+
"directories": {
|
|
63
|
+
"doc": "docs",
|
|
64
|
+
"test": "test"
|
|
65
|
+
},
|
|
66
|
+
"author": ""
|
|
56
67
|
}
|