Weekly architecture snapshots of a TypeScript/JavaScript repo, for everyone on the team, PMs included.
archsnap run walks git history, takes one snapshot per ISO week, and writes a self-contained
.archsnap/index.html with:
- The week in plain words: new or removed areas, areas that grew or shrank, new dependencies between areas, packages started or stopped.
- How the codebase got here: lines of code per week, split by the five largest areas. Select a week (or use ← →) to read what changed in it. Weeks without commits show as gaps.
- System map, Poster view: the system drawn top-down like a design diagram, with brand logos
(Simple Icons, CC0) and moving dashed arrows coloured by kind: requests, data, queued work,
schedules. Each service shows its features, named after its folders (
apps/api/src/prices→ "Prices"), and each outside call is drawn from the feature whose files make it. - System map, Explore view: what runs, what stores data and what it talks to, in five lanes (who and what starts
it, apps and services, shared code, data and queues, outside services). Read from
package.jsonfiles, Cloudflarewranglerconfig (Workers, D1, KV, R2, Durable Objects, queues, crons, served assets, routes), well-known SDKs (Stripe, Postgres, Redis, …), Xcode and XcodeGen projects, and the URLs the code actually calls. The week's summary says what changed, e.g. "Added queue emails." - How the app works, Sequence: a sequence diagram for each thing that starts work (someone
opening an app, a cron, a queue): one lifeline per part, numbered arrows top to bottom, and the
same steps listed in words underneath. A service "reads and writes" its database when the handling
file uses an area or package named like one (
db,prisma,store, …). More than four outside services fold into one "Outside services" lifeline. - How the app works, Steps: numbered steps in plain words, each with pseudocode underneath: who opens
what, which endpoints each app requests, what every endpoint (
on GET /api/fx-rates) uses and calls, what scheduled and queue handlers do, and where data lives. Endpoints come from Hono, Express-stylerouter.get("/…")calls with theirapp.route(prefix, …)mounts, and Next.jsroute.ts/pages/apifiles; requests come fromfetch-style calls and"/api/…"strings in TS/JS and Swift, matched to those endpoints. Type-only imports are left out of these views. - How the code fits together, In words: every area and what it uses from the others.
- How the code fits together, Matrix: an import matrix of every area (rows import from columns), grouped by workspace package. Select a cell, or pick two areas, to see how they talk: which names cross the boundary, which file defines them and which files use them.
- An areas table with the same numbers, readable without colour.
It reads only committed code (never your working tree), makes no network or LLM calls, and a 12-week run on a ~200-file repo takes about a second.
cargo install --path .
archsnap run # in any git repo
archsnap run --repo ../app --out /tmp/app-report
archsnap run --depth 4 # finer areas, e.g. apps/web/src/components
archsnap run --weeks 26 --strict| Flag | Default | Meaning |
|---|---|---|
--repo <path> |
current dir | Repository to scan |
--out <dir> |
<repo>/.archsnap |
Where snapshots and index.html go |
--weeks <n> |
12 | How many weeks that had commits to snapshot (at least 1) |
--depth <n> |
1 | Folder levels per area below each workspace package, after src/ (apps/web/src/billing/api/x.ts → apps/web/src/billing at 1) |
--strict |
off | Exit 1 when there are warnings (the report is still written) |
--verbose |
off | Print every git call |
Snapshots are cached in <out>/snapshots/<week>.json and reused on the next run. The newest week
is always rescanned.
on:
schedule: [{ cron: "0 6 * * 1" }] # Mondays
workflow_dispatch:
jobs:
archsnap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # full history; a shallow clone only gets this week
- run: cargo install --git https://github.com/cpeaustriajc/archsnap
- run: archsnap run
- uses: actions/upload-artifact@v4
with: { name: archsnap-report, path: .archsnap/index.html }Problems that only affect part of the snapshot are warnings: archsnap points at the exact spot, keeps going, and lists them in the report.
⚠ archsnap(unresolved_import): can't find `./missing` imported from src/cart/cart.ts
╭─[src/cart/cart.ts:2:22]
1 │ import { pay } from '../pay/index.js';
2 │ import { gone } from './missing';
· ─────┬─────
· ╰── imported here
╰────
help: no committed file matches this path; it is shown as unresolved in the report
| Code | What happened | What archsnap does |
|---|---|---|
parse_skipped |
A file has a syntax error | Leaves it out of the snapshot |
unresolved_import |
A relative or @/ import matches no committed file |
Counts it as unresolved |
shallow_clone |
The clone has no history | Snapshots the current week only |
snapshot_corrupt |
A cached snapshot can't be read | Rebuilds it from git |
Printed warnings come from the newest week only; older weeks just record counts.
| Exit code | Meaning |
|---|---|
| 0 | Report written (warnings allowed) |
| 1 | --strict and there were warnings |
| 2 | Bad flags, not a git repo, no commits, or git missing |
| 3 | Can't write the output folder, a git command failed, or a git object is missing |
| 70 | Bug in archsnap |
- Files:
.ts .tsx .mts .cts .js .jsx .mjs .cjs(JSX allowed in.js), except.d.ts,*.min.*, files over 512 KB, symlinks, and anything undernode_modules,dist,build,out,coverage,vendor,.next. Imports of those skipped files still count as resolved. - Imports: static
import/export … from, andimport("…")with a plain string. Query suffixes like?raware dropped.require()is not read yet. @/xand~/xresolve tosrc/xorxunder the importing file's folder or any parent, so monorepo packages likeapps/web/srcwork. Other tsconfigpathsaliases show up as packages.- Imports of stylesheets, images, JSON and other assets are ignored.
- Workspace package imports (
@shop/core) resolve to the package's files, so they link areas instead of showing up as packages. - An outside URL counts as a service the code calls when it is passed to
fetch,axios,ky,new URLand similar, stored in an endpoint-named constant (API_BASE,baseURL), or sits in a file that makes network calls. URLs in comments, JSX attributes and link fields (href,image, …) do not count, and placeholder domains (example.com,acme.com, …) are ignored. Hosts group by domain (api.stripe.com→ Stripe); the map shows the 12 most-connected and folds the rest. - File-by-file import detail is kept for the newest week only, to keep the report small on big repos.
End-to-end tests drive the real binary against throwaway git repos:
cargo test