@ivlad003/archdiff
v0.6.0
Published
Live diagrams of a TypeScript, JavaScript, Python, Rust or PHP repository: structure, dependencies, call flows, and the architectural diff of a pull request — algorithmic, fast, no model
Maintainers
Readme
archdiff
Live diagrams of a TypeScript, JavaScript, Python, Rust or PHP repository, computed from the code: the layers and the imports between them, every module with what it imports and what imports it, the call tree from every entry point, the map of what the analysis cannot see, and the architectural diff of a change for reviewing pull requests. Algorithmic (tree-sitter and graph algorithms, no model), fast (a fact cache keyed by file content, parallel parsing), one npm package.
npx @ivlad003/archdiff web # the diagrams of the current repository in a browser
npx @ivlad003/archdiff review # the architectural diff against the main branch, as Markdown + MermaidThe package is @ivlad003/archdiff on npm (npm refused the bare name as too close to arr-diff); the command it installs is archdiff, so after npm i -g @ivlad003/archdiff every example below reads as written.
Commands
| Command | What it does | Exit |
| --- | --- | --- |
| archdiff new <dir> [--lang ts\|js] [--template blank\|lib\|cli\|api\|web] [--agent claude\|cursor\|codex\|opencode] [--no-git] | A project from scratch: the skeleton, archdiff.json, ARCHITECTURE.md with the template's intent, the first commit (--no-git: no repository) | 0 |
| archdiff import <file> [dir] [--force] | A document's Mermaid flowchart becomes ARCHITECTURE.md (the contract); one that exists is kept unless --force | 0 |
| archdiff intent [dir] [--init [--force] [--layers]] [--view V] [--json] | Print the contract; --init seeds it from the code (--force over an existing one; --layers the layers with a few modules each, which a repository over 60 modules gets by itself); --view folds a saved drawing into it | 0 |
| archdiff check [dir] [--json] | The code against the contract: imports between layers it does not allow, what is planned and not built, what is built beyond it | 0 / 1 |
| archdiff analyze [dir] [--json] [--no-cache] | Parse the repository and print its size: files, functions, imports, calls followed and gaps | 0 |
| archdiff views [dir] | List every view: layers, layer:<name>, dir:<path>, module:<path>, calls:<path>#<fn>, machine:<path>#<name>, coverage… | 0 |
| archdiff diagram <view> [dir] [--format F] [--out FILE] [--base REF] [--head REF] [--heads A,B] [picture options] | Draw one view as md (Markdown with Mermaid, default), mermaid, json, drawio, excalidraw or archimate; the picture options (--depth, --limit, --weight, --no-hubs, --packages, --expand, --hide-lanes, --overlay, --changes, --only-changes, --changed, --mermaid, --lanes, --types) are one table that the API's query and the page's link read the same way; an unknown flag is an error with a suggestion; --base, --head and --heads are the revisions of review, compare, timeline and --changes | 0 |
| archdiff metrics [dir] [--layer NAME] [--types] [--json] | Coupling and cohesion by module and by layer: Ca, Ce, instability, abstractness, the distance from the main sequence, the cohesion of a module's functions | 0 |
| archdiff run <calls:view> [dir] [--input JSON] [--storages JSON] [--timeout MS] [--json] | Run an entry for real, the storages mocked: every function traced, the storage calls recorded, the result or the error set against the predicted flow; 1 when the run threw; an entry that cannot run (a method, another language, an unknown function) is a wrong call | 0 / 1 |
| archdiff coverage [dir] [--json] | The map of what the diagrams cannot show: blind calls, unresolved imports, syntax errors, by module | 0 |
| archdiff apis [dir] [--json] [--out FILE] | The SCAPI operations the code calls, by family, each with its contract as used: who calls it, what the code sends (the first argument key by key, with the literals, names and declared types written) and reads of the result — directly or through the functions that return it; the custom attributes (c_…) read | 0 |
| archdiff postman [dir] [--family NAME] [--out FILE] | A Postman collection (v2.1) of those operations: a folder per family, a request per operation with the method and path the runtime declares (read from node_modules, else archdiff's table of the runtime), the query parameters and the body as the code sends them, the contract in the description; the page's export menu downloads it from an apis picture | 0 |
| archdiff flow <calls:view> [dir] [--depth N] [--json] | The data flow from an entry point without running it: the steps in call order with parameter and return types, the storages on the way, the saved mock values carried by name | 0 |
| archdiff review [dir] [--base REF] [--head REF] [--format F] | Modules, functions, imports and calls added, removed or changed between the base (default: the merge-base with develop, main or master) and the head (default: the working tree); 1 when a new dependency between layers appeared that ARCHITECTURE.md does not allow (any new one, without a contract) | 0 / 1 |
| archdiff branches [dir] | The branches with the default base (develop, main or master) and the current one | 0 |
| archdiff compare [dir] [--base REF] [--heads A,B,C] [--format F] | Several heads (branches, tags, commits, WORKTREE) against one base: a table per head and one picture of what every head touched | 0 / 1 |
| archdiff history [dir] [--base REF] [--head REF] [--json] | The drift commit by commit: modules, functions, imports, layer couplings and packages each commit added or removed | 0 |
| archdiff drawings [dir] [--prune] | The saved drawings (layouts is the old name of the command); --prune deletes those whose view the code no longer has | 0 |
| archdiff plan <view> [dir] [--json] [--out FILE] | The task drawn on the saved drawing of a view: new shapes as modules or functions, arrows as imports or calls, deleted shapes as removals, notes — Markdown for an agent | 0 |
| archdiff spec [dir] [--json] [--prompt V\|all\|update] [--validate] [--tests [DIR]] | The specification: list it, print the agent's task, validate it, or write a test skeleton per Gherkin scenario (into tests/, or DIR) | 0 / 1 |
| archdiff web [dir] [--port N] [--host H] [--base REF] [--agent claude\|cursor\|codex\|opencode] [--no-watch] | The page: views on the left, one Excalidraw canvas (reading, and drawing on demand), and the Markdown, code, coverage and data flow of what is picked; the Chat tab runs the coding agent picked there (--agent: the one picked first); without --port it takes 7080 or the next free port above it; --host binds another interface than localhost; --no-watch keeps the page as it is when a file changes | 0 |
Exit code 2 is a wrong invocation or an I/O error. Every command that writes a report takes --out FILE: a new file, or one archdiff wrote before (its generated mark, or a command's JSON of the same shape); never a directory or a file of someone else's, though it mentions archdiff.
Views and detail
- layers — one shape per layer with its module count, one line per import direction with the count.
- modules — the whole map: every module in its layer's lane and every import between them. Big by nature: hubs become badges, the tail folds by directory, lines of weight ≥ N thins it; the path, focus and find tools work on it like on any picture. Above six lanes the lanes lie as columns (blocks across the picture, in the order the lines flow) rather than as rows;
--lanes rows|columnsand the toolbar's lanes ↔ choose. - layer:name — the modules of a layer;
--depth 1(default) adds the modules of other layers they import or are imported by. - dir:path — the modules of a directory (and below) with their neighbours: the rung between a layer and a module.
- module:path — what the module imports (files and packages) and what imports it;
--depth 2goes one step further. - inside:path — the functions, classes and methods of a module and the calls among them, each module a lane;
--depth 1(default) adds the functions of other modules they call. What a file does, as a map. - sequence:path#fn — the calls from a function as a sequence diagram: the modules on the way across the top (at most 12 lifelines; the rest share one), every call a numbered message in the order the code makes them, a call repeated in a row as one message "×N", and the calls under a branch or a loop framed as fragments — an
ifwith itselse, aswitch's cases and atrywith itscatchas onealtwith its arms, a loop asloop, a lone branch asopt— nested up to three deep; an operation of an API (SCAPI'sgetBasket) is a message to its family's lifeline; a call into a hub module (a logger most of the repository imports, unless its functions reach an operation within three calls) is drawn and not followed, so the business calls stay in view (--no-hubsfollows it); past the limit the operations and the calls leading to them stay and the deepest of the rest leave first, and past twelve lifelines the modules fold before the API families;--depth 1reads as the function's own logic — its branches as fragments, its calls and operations as messages (MermaidsequenceDiagramin the Markdown). - path:path#fn — from an entry point to the storages only: every branch that leads to no store, database, cache or operation of an API is pruned.
- type:path#Type — where a type flows: the functions that produce it, consume it or transform it, storages marked.
- usage:path — the exports of a module and who uses each from outside; an export nothing uses says so (
contract:is the old name: the contract is ARCHITECTURE.md). - classes, classes:path, classes:dir — the class diagram: every class, interface and object type as a box with its fields (
+,-,#) and methods,extendsandimplementsas lines,hasfor a field typed with another class andusesfor the calls a class's methods make to another class, modules as lanes; MermaidclassDiagramin the Markdown. A box's second tap opens the module's inside. - cycles / cycle:path / cycles:fns / cycle:path#fn — modules importing each other (or, with
fns, functions calling each other across modules), one lane per strongly connected component, with the fewest and lightest lines whose removal breaks the circle drawn red and dashed ("cut: 1 import"); layers importing each other get a lane of their own;--typescounts type-only imports too, dashed. - metrics / metrics:layer — coupling and cohesion as numbers: per module Ca (who imports it), Ce (what it imports), instability I = Ce/(Ca+Ce), abstractness A, the distance from the main sequence D = |A+I−1| (orange when far and coupled: the zone of pain, concrete and depended upon, or of uselessness, abstract and depending on everything) and the cohesion of its functions (1 when they form one group by calls, shared callees and shared types); Mermaid: a
quadrantChartof I against A;--overlay instabilityshades any picture by I and marks the unstable modules others depend on. - machines / machine:path#name — state machines: the explicit ones a library declares (XState's
createMachine, javascript-state-machine, Python'stransitions) and the implicit ones written with literals — a field set to two or more literal states and compared or switched on; its transitions are the assignments under acaseor anifon the same field (case "new": order.status = "paid"is new → paid, inpay), an assignment under no such guard comes from any state, and a union type naming the states declares the ones never set (dashed). Mermaid:stateDiagram-v2; Markdown: the states and a table of transitions withfile:line. - matrix, matrix:dir — the dependency structure matrix of the layers, or of a directory's modules: rows import columns, counts in the cells, imports against the order in red. A matrix scales where a graph of the same size tangles.
- events — emitters → event names → listeners (
emit/publish/dispatchandon/subscribe/addEventListenerwith a literal name), and messages:postMessage/sendMessage/sendof an object with atypeis an event of that type, handled by every function that compares or switches on atypewith that value — a browser extension's side panel, service worker and host, or a worker and its page, joined by what they say to each other. - messages, messages:path — what the parts say to each other as a sequence diagram: the modules as participants, every typed message an arrow from its sender's module to the module of each function that handles it (one per sender, handler and type, counted when sent from several places); a module as the scope keeps the messages it sends or handles. Mermaid
sequenceDiagram. - apis, apis:Family, apis:path — the operations of an external API the code calls (SCAPI, the Salesforce Commerce API:
clients.shopperBaskets.getBasket()), by family and by module; a family opens to its operations and the functions calling each; the OCAPI hooks of a cartridge (hooks.json) are joined to the resource they modify — a GET hook to theget…operations. - postman, postman:file — a Postman collection of the repository (every
*.postman_collection.jsongit lists, and the filesarchdiff.json'spostmannames) against the code: each request in its folder's lane, joined to the modules calling the operation it names (matched by method and path, a variable of either —{{basketId}},:basketId,{basketId}— standing for any value, the route that agrees most taken); a request the code never calls is marked removed (red), an operation the code calls that the collection lacks is marked added (green) in the API's lane. A collectionarchdiff postmanwrote reads back whole. - tests — which modules the test files import; a module no test imports is marked.
- hotspots — every module as a box sized by its lines and shaded by the calls into it from other modules.
- packages — the package.json files of a monorepo as a system: declared dependencies, imports between packages, and imports no package.json admits.
- timeline — the drift of the architecture commit by commit as bars (
--base,--head), red when a commit coupled two layers; a commit opens its own change. - branches — the branches as a picture: a lane per branch, the base on top (develop, main or master, or
--base), time from left to right; each branch leaves the commit it forked from — on the base, or on the branch it was started on — with how far it is ahead and behind; a branch with nothing of its own is named on the commit it points at. On the page, a tap on a commit shows its message and files, a double tap opens its change, and its menu gives the code at that commit, the code of its branch and the branch's review. - commit:sha — one commit's own change against the commit before it (a first commit against nothing), as a review: the modules, functions, imports and calls it added, removed or changed, and the layer couplings it introduced.
- calls:path#fn — the call tree from a function,
--depth 3by default; a hexagon is a call archdiff could not follow, with the reason. Double-click a function on the page to open its own tree. - focus:path#fn, focus:path — everything joined to one shape, across the modules: for a function, what calls it and what it calls (
--depthsteps each way, calls and references alike), each module a lane; for a module, its functions with their callers and callees in other modules, and the modules joined to it by an import alone. The page opens it from a shape's right-click menu or the picture select. - logic, logic:path#fn — the business logic of a function as a flowchart: a diamond per branch or loop with the condition as the code writes it (
if,switch/matchwith one arrow per case,for/whilewitheachanddone,trywithokanderror), a box per run of plain statements, an ellipse per exit (return,throwin orange, the end of the body); the Markdown lists every path from the start to an exit as a rule — the conditions taken, the calls made, how it ends — so the rules can be read and checked.logiclists the functions with branches, the most branching first. The body is read from the file when the view is asked for (TypeScript, JavaScript, Python); a step that calls one function of the repository opens that function's logic on a second tap. - coverage — the modules with blind spots, biggest first.
Entry points come from the code and the frameworks: exported functions under bin/, a browser extension's listeners (the functions a script hands to chrome.*.on*.addListener or a port's onMessage.addListener, labelled with the role manifest.json gives the script: background, content script, side panel, popup, options, devtools), functions named main, React Router route modules (the loader, action, clientLoader, clientAction, default and ErrorBoundary exports of files under a routes/ directory), SFCC cartridge hooks (cartridges/*/hooks.json), the files named by entries in archdiff.json, and exported functions nothing in the repository calls.
The page
One canvas, Excalidraw, in its hand-drawn style. Reading: tap a shape for its code and Markdown (a module's declarations fold open to their calls and code), Shift+tap or Ctrl+tap more shapes to select several (two light the shortest chain of lines between them; more list themselves with the lines among them in a strip under the tabs, with ask about them, dim others and clear), hold Alt and press a shape or a line for a peek — its lines alone, lit, the shapes at their ends kept and the rest faded, until the button or Alt is released — right-click a shape, a line or the empty canvas for its menu (open, focus, logic, the other pictures of the subject, add to the selection, ask the agent, dim, code, copy the name), tap a line to light it with its two ends and read where it leads — the imports and calls behind it, each with its file:line; where several lines run together the panel lists them all to pick from, and follow (or Shift+tap) lights the whole chain the arrows lead on to; the line under the pointer lights as you hover, and a tap on nothing puts every light out — tap a shape again to open its own view (a layer → its modules, a module → inside it, a function → its calls, the function a call tree starts from → its logic; a shape that opens nothing says so); a picture with nothing to draw says why in the middle of the canvas, and one that cannot be drawn clears the canvas and says why; the select beside the title switches between the pictures of one subject (dependencies or inside; call tree or sequence), drag to pan, wheel to zoom, 0 fits, Backspace goes back. Big views fold their tail by directory into "… N in src/x/" shapes (a tap opens that directory) so the picture stays readable — above 40 shapes by default, since a picture reads best well under that (the whole map, modules, above 200: it is there to show every module; the field fold above shows the view's own number); the Markdown lists everything. A module much of a picture imports — a quarter of its modules, at least six and at most forty of them (a logger, the shared types) — is drawn as a badge "+ log.ts" on its importers rather than as a fan of lines across everything; a picture of fewer than eight modules keeps every module, a shape a review marked stays drawn, archdiff.json "hubs" sets the numbers, and --no-hubs or the hubs as badges box draws them all; lines of weight ≥ N leaves out lines standing for fewer than N imports or calls. Every picture carries the one question it answers (the title's tooltip, the first line of its Markdown) and the commit it was drawn from (in every export). Arrows leave a shape on the right, run in the gap before the next column and enter on the left; a backward arrow goes around below; a line's label sits on the line, clear of the shapes and the other labels. Breadcrumbs in the title (layers › layer › directory › module › function) walk back up; a picture is fitted whole below the toolbar and clear of the minimap, and fitted again when the window or a panel changes the canvas's size (a picture too big even at the smallest zoom starts from its beginning, and the line under it says so); a view the reader zoomed or panned comes back where it was left (remembered in the browser; 0 fits it again and forgets); the page reloads when a source file changes (--no-watch turns that off; when the system has no file watchers left, the server's first line and the stats in the header say the page does not follow the code); ◐ switches the dark theme.
The canvas toolbar (top left of the picture) is the quick way: a picture select switches between the pictures of the current subject and the overviews; minimal · normal · full set how much shows at once (depth, fold, line weight, hubs together; full goes one step deeper than the view by itself) and depth −/+ step the depth from the view's own (3 for a call tree; off on a view that goes no depth, like the layers); mix (m) opens a tapped module in place — it becomes a lane of its own functions, and the lines into it land on the functions the sources actually call, the rest on a "rest of the module" shape (--expand a.ts,b.ts in the terminal; close opened folds them back); dim (d) fades everything but the tapped shape and its neighbours (the menu's Focus is the other way round: a picture of everything joined to the shape, across the modules). Also there: find (/) lights the shapes matching a name and Enter centres the first (a name the picture does not show opens the view whose subject it is); lanes ticks lanes in and out of the picture (--hide-lanes in the terminal); split (s) opens a second, independent picture beside the first — another view, another branch, the same page embedded (one above the other below 1520 pixels, too narrow for two canvases); lanes ↔ (l) flips the lanes between rows and columns; minimap hides or shows the picture in miniature at the bottom right (on by default, remembered; the shapes and lines of the whole picture, the viewport as a box — tap or drag it to move); project (p) opens the project map over the canvas: the whole repository as dots coloured by layer and sized by lines, every import a line, laid out by forces like a vault in Obsidian and fitted whole (a dot pushes only the dots near it and every dot is pulled to the middle, so a script nothing imports stays beside the rest); hover names a dot and lights its neighbours, the dots of the current picture wear a ring, a click opens the module, Shift+click everything joined to it, a double click its inside, the layer chips hide layers, the search box finds a module (a repository over 400 modules is folded to directories); a Shift+tap on a second shape lights the shortest chain of lines from the shape tapped before and lists it. Under a shape's corners, small counts say what lies beyond the picture's depth — ◂ 3: three modules (or functions) import or call it from outside the picture, 5 ▸: it imports or calls five there; a tap on a count picks the shape. A strip under the tabs shows a picked shape's neighbours in this picture as chips (← what imports or calls it, → what it imports or calls), then the ones beyond, dashed (a click opens the neighbour's own picture, Shift+click its focus): a click picks the neighbour here, Shift+click opens the neighbour's own view; the arrow keys walk the same way (→ along an arrow, ← against it, ↑ ↓ along the lane) and Enter goes into the picked shape — so the diagram is walked sideways as well as deeper. In the options, only the changed and their neighbours (--only-changes with --changes) cuts a review picture down to what the change touched, and only the changed files (--changed, changed=1 in the link) keeps the shapes of the files changed since the base alone, with the marks — a layer by the files in it, a folded tail by the files it stands for, a package or an event when a line joins it to one — while the views list and the folder tree list those files alone (archdiff views --changed [--base REF] [--head REF] in the terminal, every view with a note on stderr outside git; the header says how many files changed since which base, or why git could not say). The view's own subject stays whatever changed; a grid, a flowchart, a timeline or a comparison stays whole and its reason says the cut does not apply there.
show as (the toolbar's second select, as= in the link) is how the centre shows the view: the canvas; the same picture in 3D — each lane a terrace raised by its order (the top lane highest), each shape a block with its footprint of the canvas in its colours, each line a tube arched from block to block with an arrowhead, every label floating above its block; drag to orbit, wheel to zoom, right-drag to pan, tap a block for its code on the right, double-tap it to open its own view, 0 fits — its Markdown page, or the code of its subject (the file the view names, a function's lines marked; an overview shows its page instead). The 3D view is loaded when first asked for. Drawing, mix, dim, split and the project map need the canvas.
The left panel lists the views in groups, or (tree, t, remembered) shows the repository as a folder tree: the layers, their directories as folders, the modules as files, and a module's functions once its caret is opened (read from its inside picture); a tap on an entry opens its view with the centre as it is — in the Markdown presentation the pages read like a book with the tree as its table of contents — the entry of the current view is lit with the folders above it opened, which folders are open is remembered, and the filter finds entries with their folders open. In the lists and the tree alike, the files the picture shown now is of carry a dot — the navigation says where the diagram is — and ● N in picture over the filter cuts the panel to those files (remembered).
The keys, outside the fields and the editor and without Ctrl, Alt or Cmd: / find a shape, v filter the views, t the views as lists or as the folder tree, o the options, p the project map, 0 fit, Backspace back, ← → ↑ ↓ walk, Enter go in, s split, m mix, d dim, l the lanes, [ ] the previous and the next branch, b the branches, e edit, Escape clear (or close the menu and the project map first); Ctrl+B and Ctrl+] fold the side panels (remembered), Ctrl+S saves a drawing. A picture read from git (a review, a comparison, a commit, the branches) is not drawn on: its keys of the tools are off. The options menu also holds the presets of who reads — reviewer (the changes and the tests overlay), newcomer (one step deep, thirty shapes), architect (the heat, sixty shapes, lines of weight two) — and remember as defaults keeps the options for every link that leaves them out, in this browser.
⚙ options (or o) holds what the terminal flags hold: the depth (no ceiling: past 12 the field turns orange and the picture says first that it may be big and slow to draw; --depth 20 says so on stderr too), the fold limit, the weight of the lines, hubs as badges, the packages, the overlays and the Mermaid type of the Markdown and the export (the types that draw one kind of view, a machine's state diagram and the timeline's gitGraph, are greyed out elsewhere); a preset sets them for a reader at once — reviewer (changes since base, untested modules), newcomer (depth 1, fold above 30), architect (heat, lines of weight 2 and more, fold above 60). Overlays lay one more fact over any view, in any mix — changes since base (the review's marks, --changes), untested modules (what no test file imports, in orange), storages (⛁ with what they hold), heat (every shape shaded by the calls into it, the count in its label), instability (every module shaded by Martin's I, the unstable ones others depend on in orange); --overlay tests,storages,heat,instability in the terminal. Every option travels in the link, so a link shared in a pull request opens the same picture; a value the options table refuses (limit=1.5, overlay=nope, an old link's) is left out alone, the rest of the link kept, and the line under the picture names it. remember as defaults keeps the current options in this browser for every link that names none, reset forgets them and sets every option of the picture back, the toolbar's and the lanes menu's too (opened modules, hidden lanes); both say so once the picture is drawn again. changes since base needs git: elsewhere the picture is drawn without the marks and says why.
Drawing (✎ edit, or e): move shapes, draw with the pencil, write notes, take a PNG or SVG of the drawing or of the selected shapes. save writes .archdiff/layouts/<view>.json; the live diagram and every export then follow the saved places — the lines are routed around every shape on a grid of their sides when shapes left the layout's columns, and a computed shape never sits on one placed by hand — and re-layout merges a fresh picture of the code into the drawing — shapes that still exist keep their place, your strokes stay, stale shapes go. Commit the layouts to share them in a pull request, or ignore .archdiff/ to keep them local.
The Markdown panel renders (a raw toggle shows the text to copy); file:line in it opens the code. A page is a navigation too: its sections fold by their headings (a fold is remembered across pages with that heading), a module or a function named in bold opens its view with the centre as it is, and a caret on a module's bullet unfolds its functions, each opening its view or its code. The Flow tab, on a call-flow view, lists the steps in call order with their parameter and return types and the storages on the way (modules named db, store, repository, cache…, or importing pg, prisma, redis, mongoose…). Attach a mock input to the entry and mock contents to the storages — typed by hand, or generate mocks from the types, or from a description of the scenario when an agent is configured — and values are carried to parameters and properties of the same name, so the flow reads with data in it. ▶ play lights the predicted steps on the diagram one after another. ▶ run runs the entry for real: the repository's own TypeScript or JavaScript runs in a child Node process under Node's permission model (reads only, no child processes), every function traced at the start of its body, the storage packages (a database client, node:fs, fetch) replaced by recorders that answer from the mocks; the functions entered light up green, the predicted ones never reached dim, and the report under the steps lists the storage calls and the result or the error (archdiff run calls:… --input '{…}' --storages '{"node:fs": {…}}' in the terminal). What the predicted flow claims is what the types and the names say; what the run shows is what happened.
In the Chat tab, the first row asks a question: about the picture on the canvas (its Markdown and Mermaid go along, with the options as shown) or about the shapes picked on it — the selection strip's ask about them and the menu's ask the agent about it put the shapes in the row as chips — and the agent is told to answer without changing a file; without an agent the question with its context lands in the task box, to be copied to any assistant. The transcript is compact: a question, its tool calls and thoughts folded under one line (click to unfold), the answer rendered, the checks and the exit under it. @ names what the diagrams know — @module:src/lib/orders.ts, @fn:src/lib/orders.ts#placeOrder, @layer:db, @dir:src/lib, @flow:src/routes/checkout.tsx#action, @type:src/lib/orders.ts#Order, @logic:src/lib/orders.ts#placeOrder (the flowchart and its rules), @view:<key> (any picture) — with suggestions as you type; each tag is sent to the agent with its context (the shape's Markdown: declarations, calls, callers, code). While the agent works, the shapes of the files it reads or edits light up on the diagram. When it finishes, archdiff runs the contract check and the commands archdiff.json names in "checks" (npm test, npm run typecheck, npx -y @fission-ai/openspec validate --specs…) and streams their results into the tab (✔ / ✘ with the output's tail); run checks does it on request. Claude Code is allowed to run exactly those commands, archdiff itself and the OpenSpec CLI (--allowedTools), so it can verify its own work before it answers.
Reviewing branches
The header's head field is the code the page shows: any branch, tag or commit (the branches are offered as you type, [ and ] step through them), read from git without a checkout — every picture, the views list, the Code tab, the coverage and the project map follow it, and the title ends with "@ branch"; empty is the working tree (diagram <view> --head REF in the terminal). b shows the branches as a picture, to see where each one stands and open its code, its review or one of its commits. review on the page opens the branch bar: a base (develop, main or master by default, or any branch) and the heads — the working tree, the current branch, any other local or remote branch. One head is the review; tick two or more and compare draws every module any head touched — each head from where it left the base (their merge-base, named in its row), as its pull request would show it, so what the base gained since is not read as the head's doing — labelled with what each head did to it (+ feature-a ~ feature-b, or "every head" when they all did the same; a module's page lists what each head did to it), the imports each head added or removed labelled with the head, and a table per head with the layer couplings it introduced. A bar below lists the commits of the range; pick one, or step with ‹ ›, to see that commit's own change against the one before it. A tap on a shape in a review shows the commits that touched its file. In the terminal: archdiff branches, archdiff compare --base develop --heads feature-a,feature-b,WORKTREE, archdiff history; docs/ci.md shows the GitHub Action that posts the review on every pull request.
A project from scratch: the intent
archdiff new my-app --lang ts --template api (templates: blank, lib, cli, api, web) writes the skeleton that ties nothing to a framework — package.json, tsconfig, archdiff.json with the template's layers, ARCHITECTURE.md with the template's intent, the first commit on main — and nothing else: the libraries are the agent's to add from the drawing. ARCHITECTURE.md is the contract: a Markdown file with one Mermaid flowchart where a subgraph is a layer, a node is a module (its label the file path, then what it does), an arrow is an import. Agents read and write that file as it is; archdiff import note.md takes any document that holds such a flowchart (a design note, an ISD, another tool's export) as the contract; archdiff intent --init seeds one from the code of a repository that has code and no contract yet.
The intent view draws the contract against the code: planned and not built yet dashed blue, built plain, the code's modules and imports beyond the contract orange. Draw on it in ✎ edit and press code this in the Chat tab: the drawing goes into ARCHITECTURE.md first (so it is the contract before it is code), then the agent builds what the contract names and the code lacks. improve the diagram has the agent review the contract — names, missing links, layers, what the prose leaves open — and edit the file; from document turns a pasted design note into it (a note that already holds a flowchart needs no agent). The agent may ask you a question with options (a fenced archdiff-question block in its answer): the Chat tab shows the options as buttons, and the answer resumes its session. archdiff check (exit 1 on an import between two of the contract's layers that no arrow of the contract allows) keeps the code to it in CI; it also lists what is planned and not built and what was built beyond the contract.
From a drawing to code: the Chat tab
Draw the change on a view in ✎ edit: a shape with a name is a new module (src/lib/refunds.ts) or a new function (refund, placed in the nearest module, or path#name), an arrow between shapes is an import or a call, a deleted shape is a removal, free text is a note. Save, open Chat, and read the task: archdiff diffs the drawing against the code graph and writes a Markdown task with the files and lines it touches, the layers it crosses (a new dependency between layers is flagged) and the rules to keep. Edit the task, then run it: the agent runs in this repository and its output streams into the tab; a follow-up message continues the same session. When it finishes the diagram reloads, and review shows what the agent changed against what was drawn.
The agent is picked at the top of the Chat tab, among those installed on this computer: Claude Code (claude -p … --permission-mode acceptEdits), Cursor (cursor-agent -p --trust …), Codex (codex exec --json in its workspace-write sandbox) and OpenCode (opencode run --format json --auto); the page remembers the pick, and a follow-up goes on with the agent that holds the session. archdiff web --agent codex (or "agent" in archdiff.json) is the one picked first, else the first installed; "agent" may also be { "command": ["…"], "resume": "--resume", "protocol": "stream-json" } for another CLI that prints Claude's stream-json (or "codex", "opencode": their events). With "none" picked, or none installed, the tab only writes the task (archdiff plan does the same from the terminal). Nothing runs by itself: every run starts from the button with the prompt as shown, every prompt is kept in .archdiff/tasks/, one run goes at a time, and the server refuses a request from a page of another site.
Layers
Without configuration a module's layer is its top directory, or src/<dir> under src/. archdiff.json at the root names them explicitly:
{
"layers": { "routes": ["src/routes/**"], "lib": ["src/lib/**"], "db": ["src/db/**"] },
"other": "shared",
"exclude": ["**/*.stories.tsx"],
"entries": ["src/jobs/**"],
"hubs": { "min": 6, "share": 0.25, "max": 40 },
"storages": { "src/cache/**": "the cache" },
"storagePackages": { "my-orm": "MyORM" },
"checks": ["npm test", "npm run typecheck"]
}Without layers, a repository with an ARCHITECTURE.md takes the contract's layers. A file no layer's globs match falls in the layer other names (other by default). entries adds entry points the analysis cannot see (a job runner's files); postman names Postman collections to read against the code besides the ones named like an export; hubs sets when a module is a hub (a badge, not lines); storages and storagePackages name stores the code's names, packages and calls (the file system, fetch, a database client's query, Python's open, Rust's std::fs, PHP's PDO) do not reveal; checks are the commands run after every agent run. archdiff keeps itself in the same contract: its own archdiff.json and ARCHITECTURE.md are in the repository and archdiff check is green on it.
A file a module names by a URL — new Worker(new URL("./worker.ts", import.meta.url)), a child process given fileURLToPath(new URL(…)), a hook module.register loads — is a dependency too, drawn as a dashed line labelled loads, and the module's page says loaded by URL. Imports resolve per language: TypeScript/JavaScript like the compiler (relative paths with or without extensions, .js written for a .ts file, index files, the paths, baseUrl and rootDirs of the nearest tsconfig.json, extends into node_modules; a file on disk that is no module — a JSON, a stylesheet, an image, public/ behind an absolute path, a source under a hidden directory — is an asset, nothing missing), Python by dotted path from the repository root or src/ (the standard library known by name), Rust by crate::/self::/super:: paths over the targets of each Cargo.toml (workspace members and dependencies known; tests/, benches/ and examples/ files are crates of their own), PHP by qualified name against the declarations of the repository and the namespaces of the composer lock. Everything else is a package or a built-in. One repository may mix them; the layers and the diagrams are the same.
A call out of the repository that names an operation of an API archdiff knows is not a blind spot. SCAPI, the Salesforce Commerce API as the storefront-next runtime and commerce-sdk-isomorphic expose it, is known: clients.shopperBaskets.getBasket(), a destructured shopperProducts.getProducts(), a client typed ShopperOrders — the operation ShopperBaskets.getBasket is a shape of its own on a call tree, a message to the family's lifeline in a sequence, a storage the path and the flow end at, the reason api in the coverage report, and the apis view counts them by family and module. The OCAPI hooks a cartridge declares in hooks.json (dw.ocapi.shop.basket.modifyGETResponse, whose entry is the export the name ends with) are joined to the family serving the resource they modify. The contract of an operation as the code uses it — what the call sites send, key by key, and what the code reads of the result (const { data } = await op(); data.productItems, also through a function that returns the result, four calls deep, and through the callback an array of it is iterated with) — is on the operation's page, under an apis picture, in archdiff apis and in the Postman collection archdiff postman writes.
What the diagrams cannot show
A call is followed when its callee is a declaration of the same file, an import of a repository file (re-exports, a namespace, a glob, a require or an import() in a function), a member of a class the file declares or imports (its bases and PHP traits too), this.m inside a class, or a method of a value whose class the code names: a parameter, a local, a field or a constant typed with it or made with new, or the result of a call whose target declares it (const s = makeStore(), await load() of a Promise<Store>). Through an interface it reaches every class implementing it. Everything else is a gap with a reason: dynamic (a computed callee, or a call through a parameter), unknown-member (a known class without that member) interface (an interface no class implements: an object literal's function, a value injected from outside) and unresolved (a name an import binds that resolves to no file) are blind spots inside the repository; member-of-value (list.map, req.on, expect(x).toBe, a constant new Map()), global and external (a package, a built-in, the resolve of a new Promise) are calls out of it; local is a call of a function of the caller's own body (a nested function, a local arrow), whose calls are the caller's already. A parameter or a local spelled like a function of the module is never taken for it. archdiff coverage and the Coverage tab of the page list them by module, with the constructs read but not followed (an eval, a computed import), so an empty diagram is never read as proof.
Which Mermaid type where
Every view exports as a Mermaid flowchart; some views read better as another Mermaid type, chosen with --mermaid (or the export menu on the page). --mermaid flowchart draws any picture as a flowchart (a sequence as its modules and numbered messages); a type that does not fit the view — a gitGraph off the timeline and the branches, a state diagram of no machine, a quadrant off the hotspots and the metrics — is not drawn empty: the picture keeps its own type, with a comment saying why. A marked line (added, removed, changed) keeps its colour in a flowchart (linkStyle), and a folded shape's Markdown lists every shape it stands for.
| View | Mermaid type | Why |
| --- | --- | --- |
| sequence: | sequenceDiagram (automatic) | the order of calls across modules; calls: is the same calls as a tree, a flowchart |
| classes | classDiagram (automatic) | fields, methods, inheritance in UML's own notation |
| layers, packages | C4Context (--mermaid c4) | boundaries and containers, the architect's vocabulary |
| hotspots | quadrantChart (--mermaid quadrant) | size against calls in, the big and central modules top right |
| layers, any lanes | pie (--mermaid pie) | the share of modules per layer |
| timeline | gitGraph (automatic) | commits in order, tagged where a coupling appeared |
| branches | gitGraph (automatic) | each branch opened where it left the base or the branch it was started on |
| dir:, layer: | mindmap (--mermaid mindmap) | a tree of what lives where |
| matrix, metrics | a Markdown table (metrics also a quadrantChart) | a matrix is a table; I against A is a quadrant |
| machine: | stateDiagram-v2 (automatic) | states and transitions in their own notation |
| logic: | flowchart TB with diamonds (automatic) | a function's branches top down, the way a flowchart is read |
Not used, and why: an ER diagram needs a schema (a later step with Prisma or SQL files); Gantt, Kanban, journey, Sankey, XY and the rest describe work or data, not code.
Exports
Every view (and the review) exports to draw.io / diagrams.net (.drawio, uncompressed, each cell carrying the archdiff ID and the code position), Excalidraw (.excalidraw, shapes with bound text and arrows), Archi (ArchiMate Open Exchange XML: components, functions, relationships and one view at the laid-out places) and Mermaid.
Speed, cache and CI
Facts are cached by file content in .archdiff/facts.json.gz (add .archdiff/ to .gitignore), under a version that is a hash of the extractors' own sources, so a changed extractor empties it by itself; a second run parses only what changed. The files of other revisions — a review's base, a history's commits, a comparison's heads — are cached apart, in .archdiff/revisions.json.gz (the 4000 most recently read versions), so a second history reads git and nothing more. Symbolic links inside the repository are read once, under their own path, when git does not list the files. Cold start parses in worker threads: about 6 s for 500 files, 9 s for 2300. In CI, persist .archdiff/ between runs and fetch enough history for the merge-base (fetch-depth: 0), or pass --base origin/main. Files over 1.5 MB are skipped and listed by coverage. Directories whose name starts with a dot (.git, .claude, .agents) are not analysed, nor node_modules, dist, build, coverage, out, .next and .cache; "exclude" in archdiff.json leaves out more. --out only writes a new file or one archdiff wrote before. web binds to localhost and refuses requests whose Host header is not its own; --host 0.0.0.0 exposes the sources to the network. The analysis of what can go wrong on real repositories is in docs/production.md; the test pass over eight kinds of user and four real repositories (requests, ripgrep, Slim, express) is in docs/qa-2026-10-16.md.
Specification
The Spec tab of the page lists the specification and has the agent write it: spec this view writes the OpenSpec spec and the Gherkin feature of the view's subject (a layer, a directory, a module; every layer from an overview), review all specs checks every spec against the code, validate runs the OpenSpec CLI; the task goes through the Chat tab like any other, and the agent asks when a behaviour is a real choice. In the terminal, archdiff spec lists them, archdiff spec --prompt <view>|all|update prints the task (pipe it to any agent), archdiff spec --validate validates, and archdiff spec --tests writes a test skeleton per Gherkin scenario (tests/<feature>.test.ts, each test marked to do with its steps as comments; a file archdiff did not write is kept), so a scenario without a test is visible. The copy button beside the tabs copies what the active tab shows: the Markdown, the file, the coverage, the flow, the transcript or the spec.
The behaviour is specified twice, for tools and for people: openspec/specs/<capability>/spec.md in the OpenSpec format (requirements with SHALL, scenarios with WHEN and THEN; npx @fission-ai/openspec validate --specs checks them, openspec view browses them) and features/*.feature in Gherkin (the scenarios the end-to-end tests run, readable by Cucumber and IDE plugins). openspec/project.md holds the context: the stack, the pipelines, the conventions. When a behaviour changes, its requirement and its scenario change with it.
Development
npm install
npm run typecheck
npm test # the CLI and the HTTP API, through the real binary
npm run test:web # the page in headless Chrome (playwright-core; skipped without Chrome)
node bin/archdiff.js web .npm pack compiles src/ to dist/, copies the tree-sitter grammars to dist/wasm and bundles the page (Excalidraw, React, marked) to dist/web, about 6 MB with the fonts. The published package has two runtime dependencies, web-tree-sitter (the parser, WebAssembly) and smol-toml (the Cargo.toml files of a Rust repository); no native code is compiled on install.
License
Apache-2.0. The extractor and the layout come from keylang.
