@esbenwiberg/archmap 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 +361 -0
- package/dist/bin/archmap.d.ts +3 -0
- package/dist/bin/archmap.d.ts.map +1 -0
- package/dist/bin/archmap.js +72 -0
- package/dist/bin/archmap.js.map +1 -0
- package/dist/src/cache.d.ts +30 -0
- package/dist/src/cache.d.ts.map +1 -0
- package/dist/src/cache.js +78 -0
- package/dist/src/cache.js.map +1 -0
- package/dist/src/churn.d.ts +6 -0
- package/dist/src/churn.d.ts.map +1 -0
- package/dist/src/churn.js +32 -0
- package/dist/src/churn.js.map +1 -0
- package/dist/src/classify.d.ts +16 -0
- package/dist/src/classify.d.ts.map +1 -0
- package/dist/src/classify.js +60 -0
- package/dist/src/classify.js.map +1 -0
- package/dist/src/commands/check.d.ts +5 -0
- package/dist/src/commands/check.d.ts.map +1 -0
- package/dist/src/commands/check.js +33 -0
- package/dist/src/commands/check.js.map +1 -0
- package/dist/src/commands/classify.d.ts +5 -0
- package/dist/src/commands/classify.d.ts.map +1 -0
- package/dist/src/commands/classify.js +31 -0
- package/dist/src/commands/classify.js.map +1 -0
- package/dist/src/commands/explain.d.ts +5 -0
- package/dist/src/commands/explain.d.ts.map +1 -0
- package/dist/src/commands/explain.js +34 -0
- package/dist/src/commands/explain.js.map +1 -0
- package/dist/src/commands/export.d.ts +48 -0
- package/dist/src/commands/export.d.ts.map +1 -0
- package/dist/src/commands/export.js +82 -0
- package/dist/src/commands/export.js.map +1 -0
- package/dist/src/commands/risk.d.ts +6 -0
- package/dist/src/commands/risk.d.ts.map +1 -0
- package/dist/src/commands/risk.js +27 -0
- package/dist/src/commands/risk.js.map +1 -0
- package/dist/src/commands/scan.d.ts +4 -0
- package/dist/src/commands/scan.d.ts.map +1 -0
- package/dist/src/commands/scan.js +37 -0
- package/dist/src/commands/scan.js.map +1 -0
- package/dist/src/config.d.ts +43 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +87 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/files.d.ts +2 -0
- package/dist/src/files.d.ts.map +1 -0
- package/dist/src/files.js +31 -0
- package/dist/src/files.js.map +1 -0
- package/dist/src/graph.d.ts +11 -0
- package/dist/src/graph.d.ts.map +1 -0
- package/dist/src/graph.js +40 -0
- package/dist/src/graph.js.map +1 -0
- package/dist/src/risk.d.ts +11 -0
- package/dist/src/risk.d.ts.map +1 -0
- package/dist/src/risk.js +37 -0
- package/dist/src/risk.js.map +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# archmap
|
|
2
|
+
|
|
3
|
+
Classify every file in a Node+TypeScript repo as `leaf | branch | hub` based on dependency topology. AI builder and reviewer agents shell out to the same binary and get the same answer — one shared artifact, opposite behaviours.
|
|
4
|
+
|
|
5
|
+
## The model
|
|
6
|
+
|
|
7
|
+
Classification is based on **afferent coupling (Ca)** — how many files import a given file.
|
|
8
|
+
|
|
9
|
+
| Class | Ca | Meaning for an agent |
|
|
10
|
+
|----------|----------------------------|---------------------------------------------------|
|
|
11
|
+
| `leaf` | ≤ threshold.leaf (2) | Nothing depends on it. Change freely. |
|
|
12
|
+
| `branch` | > leaf, ≤ junction (3–10) | Moderate blast radius. Warn, soft-flag. |
|
|
13
|
+
| `hub` | > threshold.junction (10) | Many things depend on it. Mandatory human review. |
|
|
14
|
+
|
|
15
|
+
Also computed: **instability** `I = Ce / (Ca + Ce)` (Robert Martin's metric). Reported for context; the primary classification signal is Ca.
|
|
16
|
+
|
|
17
|
+
`.archmap.yaml` carries `overrides` that always win over computed class. This is where Ca can't see risk (security boundaries, published contracts, legacy code scheduled for deletion).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @esbenwiberg/archmap
|
|
23
|
+
# or run locally:
|
|
24
|
+
npx tsx bin/archmap.ts <command>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
archmap scan # rebuild topology → .archmap/topology.json
|
|
31
|
+
archmap classify <file> # leaf | branch | hub + Ca, instability, risk, reason
|
|
32
|
+
archmap check <file>... # exit 1 if ANY input is a hub (CI gate)
|
|
33
|
+
archmap explain <file> # list dependents + classification rationale
|
|
34
|
+
archmap risk [--top N] # rank files by combined topology + churn risk
|
|
35
|
+
archmap export [--scope <file>] # self-contained classified artifact (JSON); --scope narrows to a path list
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
All commands accept `--json` for machine-readable output (`export` is always JSON). All commands accept `--config <path>` to point at an alternate `.archmap.yaml`.
|
|
39
|
+
|
|
40
|
+
**Config discovery.** When `--config` is omitted, `archmap` walks up from the
|
|
41
|
+
current directory to the nearest `.archmap.yaml` and treats that directory as
|
|
42
|
+
the project root (chdir'ing into it), so you can run any command from a
|
|
43
|
+
subdirectory and still analyse the whole tree. `entry` paths in a discovered
|
|
44
|
+
config resolve relative to the config file's directory. (With an explicit
|
|
45
|
+
`--config`, paths resolve relative to your current directory.)
|
|
46
|
+
|
|
47
|
+
### scan
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
archmap scan
|
|
51
|
+
# Scanned 47 files → .archmap/topology.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### classify
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
archmap classify src/auth/TokenValidator.ts --json
|
|
58
|
+
# {
|
|
59
|
+
# "file": "src/auth/TokenValidator.ts",
|
|
60
|
+
# "class": "hub",
|
|
61
|
+
# "ca": 14,
|
|
62
|
+
# "tca": 63,
|
|
63
|
+
# "instability": 0.06,
|
|
64
|
+
# "risk": { "risk": 100, "structural": 8.41, "churn": 3.14, "tca": 63, "commits": 22 },
|
|
65
|
+
# "reason": "Ca=14 (14 direct, 63 transitive)",
|
|
66
|
+
# "overridden": false
|
|
67
|
+
# }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### check
|
|
71
|
+
|
|
72
|
+
Exit code `0` = no hubs in changeset. Exit code `1` = at least one hub touched.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
archmap check $(git diff --name-only main)
|
|
76
|
+
# load-bearing (hub) files detected:
|
|
77
|
+
# src/auth/TokenValidator.ts Ca=14 (63 transitive) risk=100/100
|
|
78
|
+
# [exits 1]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### explain
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
archmap explain src/utils/format.ts
|
|
85
|
+
# src/utils/format.ts
|
|
86
|
+
# class: hub
|
|
87
|
+
# reason: Ca=12 (12 dependents)
|
|
88
|
+
# ca: 12 (12 files depend on this)
|
|
89
|
+
# dependents:
|
|
90
|
+
# src/components/DatePicker.ts
|
|
91
|
+
# src/services/ReportService.ts
|
|
92
|
+
# ...
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### risk
|
|
96
|
+
|
|
97
|
+
`classify`/`explain`/`check` answer "how connected is this file?" — a pure
|
|
98
|
+
topology question. `risk` answers "where is fragility concentrated *right
|
|
99
|
+
now*?" by folding in **git churn** (how often a file has changed recently).
|
|
100
|
+
A hub nobody touches is stable; a hub under constant churn is where incidents
|
|
101
|
+
come from.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
archmap risk --top 5
|
|
105
|
+
# Top 5 riskiest files:
|
|
106
|
+
#
|
|
107
|
+
# 100/100 src/auth/TokenValidator.ts
|
|
108
|
+
# ca=14 tca=63 churn=22c/90d structural=8.41
|
|
109
|
+
# 91/100 src/utils/format.ts
|
|
110
|
+
# ca=12 tca=40 churn=9c/90d structural=6.77
|
|
111
|
+
# ...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
archmap risk --top 1 --json
|
|
116
|
+
# [
|
|
117
|
+
# {
|
|
118
|
+
# "file": "src/auth/TokenValidator.ts",
|
|
119
|
+
# "risk": 100, # percentile rank across the repo (0–100)
|
|
120
|
+
# "structural": 8.41, # log1p(Ca) + 1.5·log1p(tCa)
|
|
121
|
+
# "churn": 3.14, # log1p(commits in window)
|
|
122
|
+
# "tca": 63, # transitive afferent coupling
|
|
123
|
+
# "commits": 22 # commits touching this file in the last 90 days
|
|
124
|
+
# }
|
|
125
|
+
# ]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Risk model.** Each file gets a raw score `0.6·structural + 0.4·churn`, then
|
|
129
|
+
risk is the **percentile rank** of that raw score across the repo (0–100, ties
|
|
130
|
+
share a rank, a lone file scores 50). Components:
|
|
131
|
+
|
|
132
|
+
- **structural** = `log1p(Ca) + 1.5·log1p(tCa)` — direct *and* transitive
|
|
133
|
+
fan-in, log-damped so a 200-dependent file isn't 10× a 20-dependent one.
|
|
134
|
+
Transitive coupling (`tCa`) is weighted higher: indirect blast radius is
|
|
135
|
+
what makes a change scary.
|
|
136
|
+
- **churn** = `log1p(commits in the last 90 days)` — recency-bounded edit
|
|
137
|
+
frequency from `git log`.
|
|
138
|
+
|
|
139
|
+
Risk is **advisory context, not a gate** — `check` still keys off `class`
|
|
140
|
+
alone. Use `risk` to prioritise review attention and refactoring, not to block
|
|
141
|
+
merges. The same `risk` block is attached to every `classify` result, so a
|
|
142
|
+
builder agent gets it for free on its pre-edit probe.
|
|
143
|
+
|
|
144
|
+
### export
|
|
145
|
+
|
|
146
|
+
The other commands answer one file at a time and need the source tree present
|
|
147
|
+
to build the graph. `export` answers the whole repo **once** and bakes the
|
|
148
|
+
verdicts into a single self-contained JSON — so a consumer with *no source
|
|
149
|
+
tree* (a hosted review bot that only ever sees a diff) can classify changed
|
|
150
|
+
files by pure lookup.
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
archmap export > archmap.json # whole repo — every file (good for a global map)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Scope it to a PR.** The graph still has to be built whole-repo (you can't know
|
|
157
|
+
a file's `dependents` without seeing every importer), but the *output* doesn't
|
|
158
|
+
have to be. `--scope` narrows the emitted `files` to a path list — typically the
|
|
159
|
+
PR's changed files — while keeping each entry's full `dependents` blast radius:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git diff --name-only BASE HEAD | archmap export --scope - # "-" = stdin; or a file
|
|
163
|
+
# {
|
|
164
|
+
# "version": 1,
|
|
165
|
+
# "commit": "36d9aef…", # consumer verifies this == PR head SHA
|
|
166
|
+
# "generatedAt": "2026-05-29T19:37:36.047Z",
|
|
167
|
+
# "scope": {
|
|
168
|
+
# "requested": ["src/auth/TokenValidator.ts", "src/old.ts"],
|
|
169
|
+
# "missing": ["src/old.ts"] # requested but not in the graph — loud, not silent
|
|
170
|
+
# },
|
|
171
|
+
# "files": { # only the in-scope (changed) files
|
|
172
|
+
# "src/auth/TokenValidator.ts": {
|
|
173
|
+
# "class": "hub", "ca": 14, "tca": 63,
|
|
174
|
+
# "instability": 0.06, "risk": 100,
|
|
175
|
+
# "overridden": false,
|
|
176
|
+
# "reason": "Ca=14 (14 direct, 63 transitive)",
|
|
177
|
+
# "dependents": ["src/api/login.ts", "…"] // the off-diff blast radius
|
|
178
|
+
# }
|
|
179
|
+
# }
|
|
180
|
+
# }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Scoping makes the artifact's size track the **diff**, not the repo (a one-line PR
|
|
184
|
+
→ a few entries, not 10k), and archmap normalizes the incoming paths against its
|
|
185
|
+
own graph keys — so a path that doesn't resolve lands in `scope.missing` rather
|
|
186
|
+
than silently vanishing.
|
|
187
|
+
|
|
188
|
+
**The artifact is derived data — never commit it.** Committing a generated file
|
|
189
|
+
means churn on every PR and a guaranteed merge conflict between any two
|
|
190
|
+
concurrent PRs. Instead, build it in CI (which has the tree) and ship it *out of
|
|
191
|
+
band*, keyed by commit SHA.
|
|
192
|
+
|
|
193
|
+
#### Consuming the artifact (diff-only review bot)
|
|
194
|
+
|
|
195
|
+
The integration is: **CI builds and scopes the artifact, the bot just reacts.**
|
|
196
|
+
|
|
197
|
+
**Race condition** — the `pull_request` webhook and the CI run fire at the same
|
|
198
|
+
instant. Any consumer triggered by `pull_request` will arrive before the
|
|
199
|
+
artifact exists and get a 404. Trigger downstream work on `workflow_run`
|
|
200
|
+
instead:
|
|
201
|
+
|
|
202
|
+
```yaml
|
|
203
|
+
on:
|
|
204
|
+
workflow_run:
|
|
205
|
+
workflows: ["archmap"]
|
|
206
|
+
types: [completed]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
That event fires only after the archmap job finishes — the artifact is
|
|
210
|
+
guaranteed to be present. No polling, no retries, no sleep loops.
|
|
211
|
+
|
|
212
|
+
**As a GitHub Actions workflow** — see
|
|
213
|
+
[`.github/workflows/archmap-review.yml`](.github/workflows/archmap-review.yml)
|
|
214
|
+
for a complete, copy-paste ready example. Key points:
|
|
215
|
+
- `workflow_run` jobs run in the base-branch context and have write access to
|
|
216
|
+
`pull-requests` even for fork PRs (unlike `pull_request` jobs).
|
|
217
|
+
- Download the artifact from the *triggering* run via `run-id:
|
|
218
|
+
${{ github.event.workflow_run.id }}`.
|
|
219
|
+
- `github.event.workflow_run.pull_requests` is empty for fork PRs; embed the PR
|
|
220
|
+
number inside the artifact or via a sidecar artifact if you need fork support.
|
|
221
|
+
|
|
222
|
+
**As an external webhook bot** — subscribe to `workflow_run` events (not
|
|
223
|
+
`pull_request`). Filter for `workflow_run.name === "archmap"` and
|
|
224
|
+
`workflow_run.conclusion === "success"`, then fetch the artifact by
|
|
225
|
+
`workflow_run.head_sha`:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
// webhook handler for workflow_run events
|
|
229
|
+
if (payload.workflow_run.name !== "archmap") return;
|
|
230
|
+
if (payload.workflow_run.conclusion !== "success") return;
|
|
231
|
+
|
|
232
|
+
const sha = payload.workflow_run.head_sha;
|
|
233
|
+
const art = await fetchArtifact(`archmap-${sha}`); // GH Actions API
|
|
234
|
+
|
|
235
|
+
if (art.scope.missing.length) // paths CI couldn't resolve —
|
|
236
|
+
warn(`unresolved: ${art.scope.missing}`); // surface, don't swallow
|
|
237
|
+
|
|
238
|
+
const hubs = Object.entries(art.files)
|
|
239
|
+
.filter(([, v]) => v.class === "hub");
|
|
240
|
+
|
|
241
|
+
if (hubs.length) {
|
|
242
|
+
comment(
|
|
243
|
+
"⚠️ load-bearing files in this diff:\n" +
|
|
244
|
+
hubs.map(([p, h]) => `- \`${p}\` — ${h.dependents.length} dependents (risk ${h.risk}/100)`).join("\n")
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The `dependents` array is the payload the bot can't get from a diff: the
|
|
250
|
+
files that import the changed one but aren't in the changeset.
|
|
251
|
+
|
|
252
|
+
If you need the artifact to outlive Actions' retention or to be reachable
|
|
253
|
+
outside GitHub, swap the upload step for a `PUT` to a blob store keyed by SHA
|
|
254
|
+
(`…/<sha>/archmap.json`); the bot's fetch URL changes, nothing else does.
|
|
255
|
+
|
|
256
|
+
## Configuration: `.archmap.yaml`
|
|
257
|
+
|
|
258
|
+
Commit this file. It is the human source of truth.
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
version: 1
|
|
262
|
+
|
|
263
|
+
thresholds:
|
|
264
|
+
leaf: 2 # Ca <= 2 → leaf
|
|
265
|
+
junction: 10 # Ca > 10 → hub; between is branch
|
|
266
|
+
|
|
267
|
+
# Overrides always win over computed class.
|
|
268
|
+
overrides:
|
|
269
|
+
- path: "packages/formbuilder/src/schema/**"
|
|
270
|
+
classification: hub
|
|
271
|
+
reason: "Schema contract shipped to partners — external blast radius"
|
|
272
|
+
- path: "src/auth/TokenValidator.ts"
|
|
273
|
+
classification: hub
|
|
274
|
+
reason: "Security boundary, low internal fan-in but high risk"
|
|
275
|
+
- path: "src/legacy/**"
|
|
276
|
+
classification: leaf
|
|
277
|
+
reason: "Frozen, scheduled for deletion — don't gate"
|
|
278
|
+
|
|
279
|
+
analyzers:
|
|
280
|
+
- lang: typescript
|
|
281
|
+
entry: "src/"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
If `.archmap.yaml` is absent, defaults apply: thresholds 2/10, no overrides, entry `src/`.
|
|
285
|
+
|
|
286
|
+
## Agent recipes
|
|
287
|
+
|
|
288
|
+
### Builder agent — pre-edit probe
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
FILE="src/auth/TokenValidator.ts"
|
|
292
|
+
RESULT=$(archmap classify "$FILE" --json)
|
|
293
|
+
CLASS=$(echo "$RESULT" | jq -r '.class')
|
|
294
|
+
|
|
295
|
+
if [ "$CLASS" = "hub" ]; then
|
|
296
|
+
echo "⚠ Hub file detected. Make smallest possible diff, preserve public interface."
|
|
297
|
+
echo "Note contract impact in PR description."
|
|
298
|
+
else
|
|
299
|
+
echo "Leaf/branch — proceed freely."
|
|
300
|
+
fi
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Or as a preamble snippet for your agent:
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
Before editing any file, run: archmap classify <file> --json
|
|
307
|
+
If class == "hub":
|
|
308
|
+
- Make the smallest possible diff
|
|
309
|
+
- Preserve the public interface exactly
|
|
310
|
+
- Note contract impact in the PR description
|
|
311
|
+
If class == "leaf" or "branch": proceed normally
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Reviewer agent — PR gate
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
HUB_CHECK=$(archmap check $(git diff --name-only main) --json)
|
|
318
|
+
HAS_HUBS=$(echo "$HUB_CHECK" | jq -r '.hasHubs')
|
|
319
|
+
|
|
320
|
+
if [ "$HAS_HUBS" = "true" ]; then
|
|
321
|
+
echo "$HUB_CHECK" | jq -r '.hubs[] | "Hub: \(.file) Ca=\(.ca)"'
|
|
322
|
+
gh pr edit --add-label "requires-human-review"
|
|
323
|
+
fi
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Pre-commit hook
|
|
327
|
+
|
|
328
|
+
Add to `.git/hooks/pre-commit` (or via husky/lint-staged):
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
#!/bin/sh
|
|
332
|
+
archmap check $(git diff --cached --name-only)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Caching
|
|
336
|
+
|
|
337
|
+
`.archmap/` is a derived cache — **add it to `.gitignore`**:
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
.archmap/
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`.archmap.yaml` is config — **commit it**.
|
|
344
|
+
|
|
345
|
+
The cache key hashes only import/export structure (not function bodies), so editing a leaf function body doesn't invalidate the graph. `classify` and `check` are near-instant when structure is unchanged.
|
|
346
|
+
|
|
347
|
+
**Churn cache.** Risk scoring also needs git history, which is independent of
|
|
348
|
+
import structure. The churn map is cached separately, keyed on the current git
|
|
349
|
+
`HEAD` and the window length — so a body edit doesn't bust it, but a new commit
|
|
350
|
+
does. Repeated `classify`/`risk`/`check` calls at the same `HEAD` (e.g. a
|
|
351
|
+
pre-commit run touching many files) reuse one `git log` instead of shelling out
|
|
352
|
+
per file.
|
|
353
|
+
|
|
354
|
+
## Deferred features (v2+)
|
|
355
|
+
|
|
356
|
+
These are intentionally absent from v1:
|
|
357
|
+
|
|
358
|
+
- **C#/Roslyn analyzer** — config schema allows `analyzers[].lang: csharp` but only `typescript` is implemented. Doing it properly needs an LSP/compiler to resolve dependency injection and generics, so it's a large task deferred until there's demand. Node+TypeScript is the focus.
|
|
359
|
+
- **Dirty-subgraph partial rebuild** — full rescan on miss; the cache makes it rare
|
|
360
|
+
- **Cycle / SCC handling** — `dependency-cruiser` can report cycles; classifying them is v2
|
|
361
|
+
- **A review bot** — `archmap` stays just the CLI. `export` + the sample workflow give a consumer everything it needs (see [export](#export)), but the bot that fetches the artifact and comments on PRs lives downstream.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archmap.d.ts","sourceRoot":"","sources":["../../bin/archmap.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { scanCommand } from "../src/commands/scan.js";
|
|
4
|
+
import { classifyCommand } from "../src/commands/classify.js";
|
|
5
|
+
import { checkCommand } from "../src/commands/check.js";
|
|
6
|
+
import { explainCommand } from "../src/commands/explain.js";
|
|
7
|
+
import { riskCommand } from "../src/commands/risk.js";
|
|
8
|
+
import { exportCommand } from "../src/commands/export.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("archmap")
|
|
12
|
+
.description("Classify repo files as leaf | branch | hub by dependency topology")
|
|
13
|
+
.version("0.1.0")
|
|
14
|
+
.option("--config <path>", "Path to .archmap.yaml (default: .archmap.yaml)");
|
|
15
|
+
program
|
|
16
|
+
.command("scan")
|
|
17
|
+
.description("Rebuild topology and write .archmap/topology.json")
|
|
18
|
+
.option("--entry <path>", "Entry path override (overrides config)")
|
|
19
|
+
.option("--json", "JSON output")
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
const { resolveProject } = await import("../src/config.js");
|
|
22
|
+
const globalOpts = program.opts();
|
|
23
|
+
const { entry: cfgEntry } = resolveProject(globalOpts.config);
|
|
24
|
+
const entry = opts.entry ?? cfgEntry;
|
|
25
|
+
await scanCommand(entry, opts);
|
|
26
|
+
});
|
|
27
|
+
program
|
|
28
|
+
.command("classify <file>")
|
|
29
|
+
.description("Classify a single file (leaf | branch | hub)")
|
|
30
|
+
.option("--json", "JSON output")
|
|
31
|
+
.action(async (file, opts) => {
|
|
32
|
+
const globalOpts = program.opts();
|
|
33
|
+
await classifyCommand(file, { ...opts, config: globalOpts.config });
|
|
34
|
+
});
|
|
35
|
+
program
|
|
36
|
+
.command("check <files...>")
|
|
37
|
+
.description("Exit non-zero if any input file is a hub (CI/hook gate)")
|
|
38
|
+
.option("--json", "JSON output")
|
|
39
|
+
.action(async (files, opts) => {
|
|
40
|
+
const globalOpts = program.opts();
|
|
41
|
+
await checkCommand(files, { ...opts, config: globalOpts.config });
|
|
42
|
+
});
|
|
43
|
+
program
|
|
44
|
+
.command("explain <file>")
|
|
45
|
+
.description("List dependents and classification rationale")
|
|
46
|
+
.option("--json", "JSON output")
|
|
47
|
+
.action(async (file, opts) => {
|
|
48
|
+
const globalOpts = program.opts();
|
|
49
|
+
await explainCommand(file, { ...opts, config: globalOpts.config });
|
|
50
|
+
});
|
|
51
|
+
program
|
|
52
|
+
.command("risk")
|
|
53
|
+
.description("List riskiest files by combined topology + churn score")
|
|
54
|
+
.option("--top <n>", "Number of files to show (default: 10)")
|
|
55
|
+
.option("--json", "JSON output")
|
|
56
|
+
.action(async (opts) => {
|
|
57
|
+
const globalOpts = program.opts();
|
|
58
|
+
await riskCommand({ ...opts, config: globalOpts.config });
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command("export")
|
|
62
|
+
.description("Emit a self-contained classified topology artifact (JSON) for external consumers")
|
|
63
|
+
.option("--scope <file>", "Narrow output to a newline-delimited path list (\"-\" for stdin)")
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
const globalOpts = program.opts();
|
|
66
|
+
await exportCommand({ ...opts, config: globalOpts.config });
|
|
67
|
+
});
|
|
68
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
69
|
+
console.error(err.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=archmap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archmap.js","sourceRoot":"","sources":["../../bin/archmap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Topology } from "./graph.js";
|
|
2
|
+
import type { ChurnData } from "./churn.js";
|
|
3
|
+
interface ChurnCache {
|
|
4
|
+
key: string;
|
|
5
|
+
entries: Array<[string, ChurnData]>;
|
|
6
|
+
}
|
|
7
|
+
interface CacheEntry {
|
|
8
|
+
hash: string;
|
|
9
|
+
topology: Topology;
|
|
10
|
+
churn?: ChurnCache;
|
|
11
|
+
}
|
|
12
|
+
export declare function readCache(): CacheEntry | null;
|
|
13
|
+
/** Persist topology under a structure hash, preserving any cached churn. */
|
|
14
|
+
export declare function writeCache(hash: string, topology: Topology): void;
|
|
15
|
+
export declare function getFreshTopology(entry: string, buildFn: (entry: string) => Promise<Topology>): Promise<{
|
|
16
|
+
topology: Topology;
|
|
17
|
+
cacheHit: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Return the churn map, reusing the cached one when git HEAD and the window
|
|
21
|
+
* are unchanged. Churn is keyed on HEAD (not the structure hash) because it
|
|
22
|
+
* derives from commit history, not import structure — so editing a file body
|
|
23
|
+
* doesn't bust it, but a new commit does.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getFreshChurn(windowDays: number, buildFn: (windowDays: number) => Map<string, ChurnData>): {
|
|
26
|
+
churn: Map<string, ChurnData>;
|
|
27
|
+
cacheHit: boolean;
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;CACrC;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAkBD,wBAAgB,SAAS,IAAI,UAAU,GAAG,IAAI,CAM7C;AAOD,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAGjE;AAED,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,GAC5C,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CASpD;AAaD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GACtD;IAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAUtD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
4
|
+
import { listTypeScriptFiles } from "./files.js";
|
|
5
|
+
const CACHE_DIR = ".archmap";
|
|
6
|
+
const CACHE_FILE = `${CACHE_DIR}/cache.json`;
|
|
7
|
+
function computeStructureHash(entry) {
|
|
8
|
+
const files = listTypeScriptFiles(entry);
|
|
9
|
+
const hasher = createHash("sha256");
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
hasher.update(file + "\n");
|
|
12
|
+
try {
|
|
13
|
+
const src = readFileSync(file, "utf8");
|
|
14
|
+
const imports = src.match(/^(import|export).*from\s+['"].*['"]/gm) ?? [];
|
|
15
|
+
hasher.update(imports.join("\n") + "\n");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// skip unreadable files
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return hasher.digest("hex");
|
|
22
|
+
}
|
|
23
|
+
export function readCache() {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(CACHE_FILE, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function writeCacheEntry(entry) {
|
|
32
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
33
|
+
writeFileSync(CACHE_FILE, JSON.stringify(entry, null, 2));
|
|
34
|
+
}
|
|
35
|
+
/** Persist topology under a structure hash, preserving any cached churn. */
|
|
36
|
+
export function writeCache(hash, topology) {
|
|
37
|
+
const existing = readCache();
|
|
38
|
+
writeCacheEntry({ hash, topology, churn: existing?.churn });
|
|
39
|
+
}
|
|
40
|
+
export async function getFreshTopology(entry, buildFn) {
|
|
41
|
+
const hash = computeStructureHash(entry);
|
|
42
|
+
const cached = readCache();
|
|
43
|
+
if (cached && cached.hash === hash) {
|
|
44
|
+
return { topology: cached.topology, cacheHit: true };
|
|
45
|
+
}
|
|
46
|
+
const topology = await buildFn(entry);
|
|
47
|
+
writeCache(hash, topology);
|
|
48
|
+
return { topology, cacheHit: false };
|
|
49
|
+
}
|
|
50
|
+
function gitHead() {
|
|
51
|
+
try {
|
|
52
|
+
return execSync("git rev-parse HEAD", {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
55
|
+
}).trim();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return "nogit";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Return the churn map, reusing the cached one when git HEAD and the window
|
|
63
|
+
* are unchanged. Churn is keyed on HEAD (not the structure hash) because it
|
|
64
|
+
* derives from commit history, not import structure — so editing a file body
|
|
65
|
+
* doesn't bust it, but a new commit does.
|
|
66
|
+
*/
|
|
67
|
+
export function getFreshChurn(windowDays, buildFn) {
|
|
68
|
+
const key = `${gitHead()}:${windowDays}`;
|
|
69
|
+
const cached = readCache();
|
|
70
|
+
if (cached?.churn && cached.churn.key === key) {
|
|
71
|
+
return { churn: new Map(cached.churn.entries), cacheHit: true };
|
|
72
|
+
}
|
|
73
|
+
const churn = buildFn(windowDays);
|
|
74
|
+
const base = cached ?? { hash: "", topology: { files: {} } };
|
|
75
|
+
writeCacheEntry({ ...base, churn: { key, entries: [...churn] } });
|
|
76
|
+
return { churn, cacheHit: false };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"churn.d.ts","sourceRoot":"","sources":["../../src/churn.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,UAAU,SAAK,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAkCrE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
export function buildChurnMap(windowDays = 90) {
|
|
3
|
+
const churn = new Map();
|
|
4
|
+
let raw;
|
|
5
|
+
try {
|
|
6
|
+
raw = execSync(`git log --name-only --pretty=format:"COMMIT" --after="${windowDays} days ago"`, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] });
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return churn;
|
|
10
|
+
}
|
|
11
|
+
let currentFile = false;
|
|
12
|
+
for (const line of raw.split("\n")) {
|
|
13
|
+
const trimmed = line.trim();
|
|
14
|
+
if (trimmed === "COMMIT") {
|
|
15
|
+
currentFile = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (trimmed === "") {
|
|
19
|
+
currentFile = false;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (currentFile && trimmed) {
|
|
23
|
+
const existing = churn.get(trimmed);
|
|
24
|
+
churn.set(trimmed, {
|
|
25
|
+
commits: (existing?.commits ?? 0) + 1,
|
|
26
|
+
windowDays,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return churn;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=churn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"churn.js","sourceRoot":"","sources":["../../src/churn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAOzC,MAAM,UAAU,aAAa,CAAC,UAAU,GAAG,EAAE;IAC3C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IAE3C,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,QAAQ,CACZ,yDAAyD,UAAU,YAAY,EAC/E,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CACxD,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,WAAW,GAAG,IAAI,CAAC;YACnB,SAAS;QACX,CAAC;QACD,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;YACnB,WAAW,GAAG,KAAK,CAAC;YACpB,SAAS;QACX,CAAC;QACD,IAAI,WAAW,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACpC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE;gBACjB,OAAO,EAAE,CAAC,QAAQ,EAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC;gBACrC,UAAU;aACX,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ArchmapConfig } from "./config.js";
|
|
2
|
+
import type { Topology } from "./graph.js";
|
|
3
|
+
import type { RiskScore } from "./risk.js";
|
|
4
|
+
export type Klass = "leaf" | "branch" | "hub";
|
|
5
|
+
export interface Classification {
|
|
6
|
+
file: string;
|
|
7
|
+
class: Klass;
|
|
8
|
+
ca: number;
|
|
9
|
+
tca: number;
|
|
10
|
+
instability: number;
|
|
11
|
+
risk: RiskScore | null;
|
|
12
|
+
reason: string;
|
|
13
|
+
overridden: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function classifyFile(file: string, topology: Topology, config: ArchmapConfig, riskScores?: Map<string, RiskScore>): Classification;
|
|
16
|
+
//# sourceMappingURL=classify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/classify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,aAAa,EACrB,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GAClC,cAAc,CA8DhB"}
|