Files
Claude cd19a73c55 feat(scripts): interactive dependency-graph viewer, with when-to-use guidance
Renders every production file under src/ as a pannable graph in one self-contained
HTML file — no external requests, no runtime dependency, layouts precomputed at
build time so the viewer never runs a physics simulation on a phone.

  pnpm depgraph          # -> .tmp/depgraph/index.html (+ index.json)
  pnpm depgraph:test

It reuses the layering gate's model (`listSourceFiles`, `resolveImportEdges`,
`zoneRank`) rather than extracting its own graph. That matters more than it
sounds: a separate extractor with its own resolution behaviour would draw a graph
nobody enforces. Because the model is shared, its R6 count reproduces
TYPE_INVERSION_BASELINE exactly, which doubles as a self-check.

The README now documents WHEN it is productive, because the honest answer is
"for three questions, and it misleads on a fourth":

- what am I about to break (dependent counts, including the type-only and dynamic
  edges a grep for `from '...'` misses);
- where is the debt concentrated (zone-level counts);
- what is wrong that CI does not enforce — ~1300 transitively redundant value
  edges and 8 type-only/dynamic cycles, both outside the gate by design.

The fourth: a cluster's SIZE IS NOT ITS DIFFICULTY. `commands -> client` looked
like the obvious win at 28 edges into one file; moving that file down took the
gate from 42 to 48, because the vocabulary it holds depends on commands/, metro/,
core/ and remote/. The render shows an edge's weight, not whether it can be
reversed — so the README pairs every visual question with the numeric query that
answers "can this actually move?", verified against the real output rather than
written from memory.

Also states plainly that `pnpm check:layering` is authoritative and nothing here
gates a merge: it is an instrument, not a rule.

scripts/depgraph/** joins scripts/layering/**, scripts/perf/** and
scripts/maestro-conformance/** in Fallow's ignorePatterns, which is how this repo
already treats tooling trees. Worth knowing rather than discovering: that exempts
viewer.js from the complexity gate, and its `draw` function would fail it.

Two exports added to scripts/layering/model.ts: `zoneRank` (the viewer colours
nodes by rank, so an inversion reads as an edge pointing the wrong way down the
ramp) and `targetDagZone`, previously module-private.

`pnpm check` green, 4488 unit tests. Verified against current main: 898 files,
4627 edges, 25 zones, R6 count matching the gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
2026-07-27 06:54:46 +00:00

126 lines
3.8 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<title>agent-device — module dependency graph</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<style>
/*__STYLES__*/
</style>
<div id="stage">
<canvas id="canvas"></canvas>
</div>
<header id="strip">
<h1>agent-device <span id="commit"></span></h1>
<div class="stats" id="stats"></div>
</header>
<button id="rail-toggle" type="button" aria-expanded="false" aria-controls="rail">Controls</button>
<aside id="rail" aria-label="Graph controls">
<div class="sheet-head">
<span>Controls</span>
<button type="button" class="close" id="rail-done" aria-label="Hide controls">×</button>
</div>
<div class="group">
<h2>Find a file</h2>
<input
id="search"
type="search"
placeholder="core/interactors/tap"
autocomplete="off"
spellcheck="false"
aria-label="Filter files by path"
/>
<p class="hint" id="search-hint">
Drag to pan, pinch or scroll to zoom, tap a node for detail.
</p>
</div>
<div class="group">
<h2>Layout</h2>
<div class="segmented" id="layout-mode" role="group" aria-label="Layout">
<button type="button" data-layout="cluster" aria-pressed="true">Clusters</button>
<button type="button" data-layout="layered" aria-pressed="false">Layers</button>
</div>
<p class="hint" id="layout-hint"></p>
</div>
<div class="group">
<h2>Size nodes by</h2>
<div class="segmented" id="size-mode" role="group" aria-label="Node size metric">
<button type="button" data-size="degree" aria-pressed="true">Coupling</button>
<button type="button" data-size="in" aria-pressed="false">Dependents</button>
<button type="button" data-size="loc" aria-pressed="false">Lines</button>
</div>
</div>
<div class="group">
<h2>Overlay</h2>
<div class="checks" id="overlays">
<label class="check"
><input type="checkbox" id="show-back" /><span
class="swatch"
style="background: var(--critical)"
></span
>Spine back-edges (R5)</label
>
<label class="check"
><input type="checkbox" id="show-cycles" /><span
class="swatch"
style="background: var(--critical)"
></span
>Import cycles</label
>
<label class="check"
><input type="checkbox" id="show-type-inversion" /><span
class="swatch"
style="background: var(--warning)"
></span
>Type-only inversions (R6)</label
>
<label class="check"
><input type="checkbox" id="show-redundant" /><span
class="swatch"
style="background: var(--warning)"
></span
>Redundant edges</label
>
<label class="check"><input type="checkbox" id="hide-weak" />Static value edges only</label>
<label class="check"
><input type="checkbox" id="show-labels" checked />Labels when zoomed</label
>
</div>
</div>
<div class="group">
<h2>Zones — rank ◄ colour</h2>
<div class="legend" id="legend"></div>
<p class="hint">
Colour is the layering gate's spine rank: <b>kernel</b> sink through <b>cli</b>. Grey zones
are intentionally unranked — R1R4 still apply, but the gate asserts no ordering over them.
</p>
</div>
<div class="group">
<h2>Hotspots — most depended on</h2>
<div class="hotspots" id="hotspots"></div>
</div>
</aside>
<section id="detail" aria-live="polite"></section>
<div id="zoom">
<button type="button" id="zoom-in" aria-label="Zoom in">+</button>
<button type="button" id="zoom-out" aria-label="Zoom out"></button>
<button type="button" id="zoom-fit" aria-label="Fit graph to screen"></button>
</div>
<div id="tooltip" role="status"></div>
<script id="graph-data" type="application/json">
"__DATA__"
</script>
<script>
/*__SCRIPT__*/
</script>