@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.
Files changed (58) hide show
  1. package/README.md +361 -0
  2. package/dist/bin/archmap.d.ts +3 -0
  3. package/dist/bin/archmap.d.ts.map +1 -0
  4. package/dist/bin/archmap.js +72 -0
  5. package/dist/bin/archmap.js.map +1 -0
  6. package/dist/src/cache.d.ts +30 -0
  7. package/dist/src/cache.d.ts.map +1 -0
  8. package/dist/src/cache.js +78 -0
  9. package/dist/src/cache.js.map +1 -0
  10. package/dist/src/churn.d.ts +6 -0
  11. package/dist/src/churn.d.ts.map +1 -0
  12. package/dist/src/churn.js +32 -0
  13. package/dist/src/churn.js.map +1 -0
  14. package/dist/src/classify.d.ts +16 -0
  15. package/dist/src/classify.d.ts.map +1 -0
  16. package/dist/src/classify.js +60 -0
  17. package/dist/src/classify.js.map +1 -0
  18. package/dist/src/commands/check.d.ts +5 -0
  19. package/dist/src/commands/check.d.ts.map +1 -0
  20. package/dist/src/commands/check.js +33 -0
  21. package/dist/src/commands/check.js.map +1 -0
  22. package/dist/src/commands/classify.d.ts +5 -0
  23. package/dist/src/commands/classify.d.ts.map +1 -0
  24. package/dist/src/commands/classify.js +31 -0
  25. package/dist/src/commands/classify.js.map +1 -0
  26. package/dist/src/commands/explain.d.ts +5 -0
  27. package/dist/src/commands/explain.d.ts.map +1 -0
  28. package/dist/src/commands/explain.js +34 -0
  29. package/dist/src/commands/explain.js.map +1 -0
  30. package/dist/src/commands/export.d.ts +48 -0
  31. package/dist/src/commands/export.d.ts.map +1 -0
  32. package/dist/src/commands/export.js +82 -0
  33. package/dist/src/commands/export.js.map +1 -0
  34. package/dist/src/commands/risk.d.ts +6 -0
  35. package/dist/src/commands/risk.d.ts.map +1 -0
  36. package/dist/src/commands/risk.js +27 -0
  37. package/dist/src/commands/risk.js.map +1 -0
  38. package/dist/src/commands/scan.d.ts +4 -0
  39. package/dist/src/commands/scan.d.ts.map +1 -0
  40. package/dist/src/commands/scan.js +37 -0
  41. package/dist/src/commands/scan.js.map +1 -0
  42. package/dist/src/config.d.ts +43 -0
  43. package/dist/src/config.d.ts.map +1 -0
  44. package/dist/src/config.js +87 -0
  45. package/dist/src/config.js.map +1 -0
  46. package/dist/src/files.d.ts +2 -0
  47. package/dist/src/files.d.ts.map +1 -0
  48. package/dist/src/files.js +31 -0
  49. package/dist/src/files.js.map +1 -0
  50. package/dist/src/graph.d.ts +11 -0
  51. package/dist/src/graph.d.ts.map +1 -0
  52. package/dist/src/graph.js +40 -0
  53. package/dist/src/graph.js.map +1 -0
  54. package/dist/src/risk.d.ts +11 -0
  55. package/dist/src/risk.d.ts.map +1 -0
  56. package/dist/src/risk.js +37 -0
  57. package/dist/src/risk.js.map +1 -0
  58. 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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=archmap.d.ts.map
@@ -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,6 @@
1
+ export interface ChurnData {
2
+ commits: number;
3
+ windowDays: number;
4
+ }
5
+ export declare function buildChurnMap(windowDays?: number): Map<string, ChurnData>;
6
+ //# sourceMappingURL=churn.d.ts.map
@@ -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"}