feat(scripts): interactive dependency-graph viewer, with when-to-use guidance - #1409
feat(scripts): interactive dependency-graph viewer, with when-to-use guidance#1409thymikee wants to merge 1 commit into
Conversation
…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
Size Report
Startup median (7 runs, lower is better):
Top changed chunks: no changes in the largest emitted chunks. |
|
|
Not ready to merge; four actionable findings:
All ordinary checks are green, but |
|
Closing — not merging this. The visual was a one-time aid to get an overview, and it did not earn ongoing maintenance: neither @thymikee nor I drew a conclusion from the render itself. Every finding in the boundary work came from numeric queries against The honest cost/benefit, measured rather than asserted:
So 2200 lines and a Fallow exemption bought a rendering nobody read. Even the analysis-only variant was not load-bearing — the ad-hoc one-liners that produced every actual result imported the gate's model directly and needed none of this package. Nothing is lost from The experiment that produced the table above is on Generated by Claude Code |
The rendered dependency-graph viewer is not being merged (PR #1409 closed). It cost ~2200 lines plus a Fallow exemption for a 920-line canvas renderer, and nobody — human or agent — reached a conclusion from the picture. Every finding in this document came from short queries against the gate's own model. This file pointed at the `claude/depgraph-viewer` branch for the tooling, which would have dangled once that branch is deleted. Replaced with the thing that was actually load-bearing: a throwaway probe script, inlined, that re-derives the numbers from `scripts/layering/model.ts` and nothing else. Verified verbatim — it reproduces TYPE_INVERSION_BASELINE exactly, which is also the check that tells you whether either side has gone stale. Two numbers in the summary table were stale, describing an intermediate state rather than what shipped: R6 said "35 across 4" (actually 18 across 5 after the vocabulary move) and ranked coverage said "729 of 894" (actually 888 of 901). Both corrected, along with the file/edge counts in the header. Also notes the deduplication detail that makes the query agree with the gate: each file pair counts once, so a raw edge count reads higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
The rendered dependency-graph viewer is not being merged (PR #1409 closed). It cost ~2200 lines plus a Fallow exemption for a 920-line canvas renderer, and nobody — human or agent — reached a conclusion from the picture. Every finding in this document came from short queries against the gate's own model. This file pointed at the `claude/depgraph-viewer` branch for the tooling, which would have dangled once that branch is deleted. Replaced with the thing that was actually load-bearing: a throwaway probe script, inlined, that re-derives the numbers from `scripts/layering/model.ts` and nothing else. Verified verbatim — it reproduces TYPE_INVERSION_BASELINE exactly, which is also the check that tells you whether either side has gone stale. Two numbers in the summary table were stale, describing an intermediate state rather than what shipped: R6 said "35 across 4" (actually 18 across 5 after the vocabulary move) and ranked coverage said "729 of 894" (actually 888 of 901). Both corrected, along with the file/edge counts in the header. Also notes the deduplication detail that makes the query agree with the gate: each file pair counts once, so a raw edge count reads higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
…ers (#1428) * docs: map architecture principles to their enforcing gates and ledgers in CONTEXT.md Writes down the rule → gate → ledger mapping (dependency rule → layering R1-R3/spine + R6 ratchet; acyclicity → R4 + depgraph tolerated-cycle report; policy×detail → the two registries + leak guard + transport seam; information hiding → R7; boundaries-are-earned with the #1409/platform-descriptor evidence; component metrics as observatory data per #1423) so contributors and agents get the why behind the guards they hit, in one place. * review: scope principle claims to actual enforcement; mark depgraph as proposed P1: the section referenced scripts/depgraph/ as present and #1410 as kept — that PR is open and this tree has no depgraph directory. Both the acyclicity and component-metrics bullets now name it as proposed (#1410), not landed. P2: enforcement claims scoped to the concrete guards: publicPlatformString gates one serialized-output DTO class, runnerProvider is one adopted seam, R7 covers SessionState only, and boundaries-are-earned plus stable-interface testing are labeled norms (gated only at the test-only-DI-seam slice). The intro now says explicitly that bullets distinguish gate, ledger, and norm.
The rendered dependency-graph viewer is not being merged (PR #1409 closed). It cost ~2200 lines plus a Fallow exemption for a 920-line canvas renderer, and nobody — human or agent — reached a conclusion from the picture. Every finding in this document came from short queries against the gate's own model. This file pointed at the `claude/depgraph-viewer` branch for the tooling, which would have dangled once that branch is deleted. Replaced with the thing that was actually load-bearing: a throwaway probe script, inlined, that re-derives the numbers from `scripts/layering/model.ts` and nothing else. Verified verbatim — it reproduces TYPE_INVERSION_BASELINE exactly, which is also the check that tells you whether either side has gone stale. Two numbers in the summary table were stale, describing an intermediate state rather than what shipped: R6 said "35 across 4" (actually 18 across 5 after the vocabulary move) and ranked coverage said "729 of 894" (actually 888 of 901). Both corrected, along with the file/edge counts in the header. Also notes the deduplication detail that makes the query agree with the gate: each file pair counts once, so a raw edge count reads higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
The rendered dependency-graph viewer is not being merged (PR #1409 closed). It cost ~2200 lines plus a Fallow exemption for a 920-line canvas renderer, and nobody — human or agent — reached a conclusion from the picture. Every finding in this document came from short queries against the gate's own model. This file pointed at the `claude/depgraph-viewer` branch for the tooling, which would have dangled once that branch is deleted. Replaced with the thing that was actually load-bearing: a throwaway probe script, inlined, that re-derives the numbers from `scripts/layering/model.ts` and nothing else. Verified verbatim — it reproduces TYPE_INVERSION_BASELINE exactly, which is also the check that tells you whether either side has gone stale. Two numbers in the summary table were stale, describing an intermediate state rather than what shipped: R6 said "35 across 4" (actually 18 across 5 after the vocabulary move) and ranked coverage said "729 of 894" (actually 888 of 901). Both corrected, along with the file/edge counts in the header. Also notes the deduplication detail that makes the query agree with the gate: each file pair counts once, so a raw edge count reads higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
…atchet what remains (#1435) * refactor(contracts): declare the public API vocabulary below its consumers The layering gate's largest remaining cluster was 28 type-only inversions from a single edge: `commands/` declaring itself in terms of `client/client-types.ts`. R2 forbids the reverse import, so a shape both surfaces need has to sit below both. The command/device vocabulary — connection config, the device and session views, and every per-command Options/Result — now lives in `contracts/client-api.ts`; `client/client-types.ts` keeps the `AgentDeviceClient` facade and re-exports the rest through one wildcard. R6 total: 42 -> 18. No new inversion in any pair. The published surface is unchanged, and that is verified rather than asserted: the built `index.d.ts` exports the same 216 type names as main, byte-identical. Eight shapes deliberately did NOT move, because each is stated in terms of a HIGHER-ranked zone: `ScrollOptions` (ScrollInputDirection, commands/), the four navigation Options plus `AgentDeviceCommandClient` (navigation-projection, commands/), and the two Metro result aliases (metro/). Declaring those in contracts/ would trade 28 commands->client edges for contracts->commands and contracts->metro ones — the foundation depending on the layers above it, worse in kind even though fewer in number. This is measured, not assumed: moving the whole file to contracts/ first took the gate from 42 to 48, which is how the floor was found. Two keystone moves made the other 84 movable: - `RemoteConnectionProfileFields` joined its sibling `CloudProviderProfileFields` in contracts/remote-config-fields.ts. It was the root of the base chain (AgentDeviceClientConfig -> AgentDeviceRequestOverrides -> DeviceCommandBaseOptions -> every per-command Options), so one rank-4 declaration was pinning ~80 shapes up with it. - `DaemonBatchStep` moved to contracts/batch-step.ts. Its `runtime` field was written `DaemonRequest['runtime']`, dragging the whole daemon request type in to say `SessionRuntimeHints` — the same type, three zones lower. `CompanionTunnelScope`/`MetroBridgeScope` also moved to contracts/, since the vocabulary needs the scope shape and it sat next to client-local env-var names. Six pass-through re-exports in client-types.ts are suppressed per-name with the reason inline: they exist only to publish contracts/kernel types through the package entrypoint wildcard, every internal consumer imports them from the declaring module, so "no consumer" is correct and not actionable — deleting them would remove names from the public types. `pnpm check` green, 4488 unit tests. Findings doc records the sequencing for the last 5: the upstream declarations have to come down before the shapes that need them can. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur * docs: drop the graph viewer, keep the query that replaces it The rendered dependency-graph viewer is not being merged (PR #1409 closed). It cost ~2200 lines plus a Fallow exemption for a 920-line canvas renderer, and nobody — human or agent — reached a conclusion from the picture. Every finding in this document came from short queries against the gate's own model. This file pointed at the `claude/depgraph-viewer` branch for the tooling, which would have dangled once that branch is deleted. Replaced with the thing that was actually load-bearing: a throwaway probe script, inlined, that re-derives the numbers from `scripts/layering/model.ts` and nothing else. Verified verbatim — it reproduces TYPE_INVERSION_BASELINE exactly, which is also the check that tells you whether either side has gone stale. Two numbers in the summary table were stale, describing an intermediate state rather than what shipped: R6 said "35 across 4" (actually 18 across 5 after the vocabulary move) and ranked coverage said "729 of 894" (actually 888 of 901). Both corrected, along with the file/edge counts in the header. Also notes the deduplication detail that makes the query agree with the gate: each file pair counts once, so a raw edge count reads higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur * refactor(contracts): move the four keystones that pinned the rest of the inversions R6 type-only spine inversions: 18 -> 7, and every one of the 7 that remains is a deliberate architectural position rather than a misplaced declaration. Four keystones moved to contracts/, each of which was pinning a much larger set: - `CommandFlags` (was core/dispatch-context.ts). One rank-2 declaration holding the daemon's request type and every recorded action above it. Its last non-contracts dependency was `DaemonBatchStep`, already moved in 3fdbfe0. - `SessionAction` (was daemon/types.ts). replay/ (6 modules) and compat/maestro/ read and write session scripts; declaring the shape inside the daemon made both depend on the server to describe a file format neither asks it to produce. The daemon still owns the recording — only the shape moved. - `TargetAnnotationV1` shape (was replay/target-identity.ts). ADR 0012 target evidence, written by 8 daemon modules and read by commands/; the parsing and classification logic stays in replay/. - `ScrollInputDirection` and the Metro prepare/reload result payloads, which unblocked `ScrollOptions` and `MetroPrepareResult`/`MetroReloadResult`. `DaemonRequest` also split into the three shapes it had been conflating: the kernel WIRE shape (`flags?: Record<string, unknown>`, because a process boundary cannot enforce a vocabulary), the new `contracts/command-request.ts` `CommandRequest` (wire shape with flags typed — what a command surface needs), and the daemon's own refinement (+ `internal?: DaemonRequestInternal`, carrying SessionState callbacks and the admitted lease). core/command-descriptor/ had been importing the third to read `command`, `positionals` and `flags`. Two things deliberately NOT moved, because moving them would add coupling rather than remove it, and the baseline now argues both: - `DaemonCommandDescriptor`/`DaemonCommandRoute` — the route type is `keyof typeof DAEMON_ROUTE_HANDLERS`, derived from what the server implements. Moving it down means re-declaring route names in contracts plus a gate to prove the handler map still covers them. ADR 0003/0008 own that boundary. - `AgentDeviceClient` — used as an opaque handle by 4 files. The facade is built from commands/'s own NAVIGATION_COMMAND_PROJECTIONS, so this is a genuine zone-level cycle; breaking it is a design call about where that registry belongs. R5 is zero here: nothing imports the client at runtime, only its type. Also records the largest structural finding, which R6 does not measure: cycles by edge kind are 1 (value only), 87 (value + type-only), 1 (value + dynamic), 213 (all). At runtime the graph is a clean DAG; the 87-file type-level cluster means no one of those files' types can be read in isolation. Hubs are runtime-contract.ts, commands/runtime-types.ts, backend.ts, commands/runtime-common.ts. Not attempted here — it is a different and much larger change. `pnpm check` green, 4488 unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur * feat(layering): ratchet type-cycle growth (R9), and rule out a narrower client port R9: the largest strongly-connected component over value + type-only edges may not grow. R4 keeps the VALUE graph acyclic, so every cycle counted here is created by type-only imports - free at runtime, invisible to R5/R6, and the largest single obstacle to reading a subsystem in isolation: inside a component of 102 files, no file has a self-contained slice. Baseline set to 102, which is what THIS branch achieves - main carries 107 and the boundary moves here bring it to 102. An earlier revision baselined 87, measured against an older main; after rebasing onto f19864e the real figure was 102 and the new rule fired on its own stale baseline. Worth stating because the failure looked like a regression and was not: attribution showed main at 107 and this branch reducing it, which is the check working rather than complaining. Growth-only, deliberately unlike R6. Reducing 102 is a real refactor rather than a file move, so a hard equality would turn every unrelated improvement into a baseline edit. A shrunk tree is reported in the success line instead of failing. Verified at the new baseline by adding one type-only import that closes a loop and watching 102 become 108 and the gate reject it. The refactor itself is still not attempted. Hubs by in-component dependents are runtime-contract.ts, commands/runtime-types.ts, backend.ts, commands/runtime-common.ts; a pass starts there. Separately, investigated the narrower-port idea for the 4 remaining -> client inversions and it does not work. Measured first: files NAMING AgentDeviceClient (the inversions) 4 files CALLING client methods 26 distinct facade namespaces reached 13 The narrowness is an artifact of where the type is named, not of what is used. Making those four generic over the client type pushes the concrete type into the 26 implementations, turning 4 inversions into up to 26. A port spanning 13 namespaces is the whole facade, so it would either duplicate the public API shape - a second source of truth for it - or derive from the facade and carry the same dependency. So the four are the minimum number of naming sites rather than an accident: they are the choke point. Recorded as a position with the numbers behind it. The remaining option is the question underneath it - whether NAVIGATION_COMMAND_PROJECTIONS belongs in commands/ - and that is a design decision about the command surface, not a dependency cleanup. pnpm check green, 4535 unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur * fix(layering): test R9, specify its floor, and drop two duplications Adversarial self-review of #1435 found four things worth fixing. R9 shipped with no unit test. Every other rule in this gate has one (R5 back-edges, R6 inversions, R7 session state, R8 zero-dep closures); R9's only verification was a manual injection CI cannot repeat. Added tests for the three distinctions it depends on: a type-only loop counts, a dynamic-only loop does not, a value loop still does. Writing that test immediately found an undocumented edge case, which is the argument for it. largestTypeCycleSize returns 1 for an acyclic graph that has non-dynamic edges but 0 when every edge is dynamic, because only edge-participating files enter the walk. Immaterial to a growth ratchet, but an inconsistent floor nobody had written down. Now specified in the doc comment and pinned by the test, so 0 and 1 cannot later be read as a meaningful difference. largestTypeCycleMembers was exported with no consumer - speculative API, and scripts/layering is in Fallow's ignorePatterns so nothing would have flagged it. Same pattern review caught on the previous head with MaestroRuntimeFlags and TargetRect. Made module-private. ResolvedMetroKind was declared twice after the Metro payload move: exported from contracts/metro.ts and still private in metro/client-metro.ts. client-metro.ts now imports it. The gate computed the SCC twice per run, once in the rule and once for the success line. Computed once and threaded, so the two can no longer disagree. Also re-verified the claim this PR rests on, with a stronger check than the one in the body: comparing DECLARATION names in index.d.ts counts inlined internals, and by that measure this branch appears to lose five names (PrepareMetroRuntimeResult, ReloadMetroResult, ResolvedMetroKind, SCROLL_INPUT_DIRECTIONS, ScrollInputDirection). All five are declared-but-not-exported helpers. The real surface - exported names across all eleven published entrypoints - is 69 on both sides, identical. Also proved DaemonRequest structurally equal to its pre-split shape with a type-level assertion rather than by reasoning, and confirmed SessionAction, CommandFlags and TargetAnnotationV1 moved byte-identically. pnpm check green, 4535 unit tests, 24 layering tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur * refactor(contracts): one file per command family, one name per request Addresses review on #1435. contracts/client-api.ts was 1,064 LOC and grouped session, app, interaction, replay, observability and recording contracts together, so it answered no one question and crossed the >1,000-LOC architecture-debt tripwire in AGENTS.md:124. Split it into 14 domain-family files by the command families that already exist (client-connection, client-device-view, client-session, client-lease, client-app, client-capture, client-target, client-gesture, client-selector-read, client-replay, client-observability, client-settings, client-system, client-request); the four Metro client shapes went into the existing contracts/metro.ts so one file answers the Metro question. Largest resulting file is 137 LOC. client/client-types.ts re-exports one wildcard per family, so the published import path is unchanged. Published surface verified unchanged against main two ways: the exported-name set of all 11 published entrypoints is identical (70 names), and every declaration in the built index.d.ts is byte-identical after normalization -- 0 names added, 0 shapes changed. index.d.ts got smaller (1,726 -> 1,682 lines): 10 declarations main duplicated into it now resolve through a shared chunk. Also, from re-examining the two findings the review flagged as blind spots: - CommandRequest was a third name for "a request" that no consumer needed. Every core/command-descriptor/ use read only command/positionals/flags, in two spellings (the full type and a Pick of it). Replaced by contracts/dispatched-command.ts DispatchedCommand -- those three fields and nothing else, with command/positionals Picked from the wire type so they cannot drift. daemon/types.ts DaemonRequest now extends the wire shape directly. Two request shapes again, at two ranks. - The 7 remaining R6 inversions each get a mechanical reason rather than an appeal to an ADR: the 4 AgentDeviceClient edges are a real zone-level cycle (client-types.ts imports ProjectedNavigationCommandClient from commands/), and no narrower port exists (26 call sites across 13 namespaces); the 2 DaemonCommandDescriptor edges are unavoidable because that shape is stated in terms of the server-private DaemonRequest; the 1 DaemonCommandRoute edge is unavoidable because the type is computed from the daemon's handler table. Cleanups found on the way: three doc comments this branch had orphaned from their declarations (SettleCommandOptions, RecordControlOptions, ReloadMetroResult -- the last had drifted onto an unrelated type it misdescribed) are reattached; intra-contracts imports normalized from '../contracts/x.ts' to './x.ts', which is what the duplicate-import lint caught; and stale references to the deleted file removed from the docs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
An interactive, pannable graph of every production file under
src/, rendered into oneself-contained HTML file — no external requests, no build step, no runtime dependency. Layouts are
precomputed at build time and shipped as coordinates, so the viewer never runs a physics simulation
on the reader's device and a given commit always renders identically.
pnpm depgraph # -> .tmp/depgraph/index.html (+ index.json) pnpm depgraph --out /tmp/graph.html pnpm depgraph:testTooling only. Nothing here runs in CI and nothing here gates a merge.
It reuses the layering gate's model
The graph is extracted with
scripts/layering/model.ts— the same modulecheck.tsuses in CI — sothe file set, zone partition, edge kinds (value / type-only / dynamic) and cycle definition are
identical to the rules actually enforced. A separate extractor with its own resolution behaviour
would draw a graph nobody is enforcing.
That shared model gives a free self-check: the viewer's R6 count reproduces
TYPE_INVERSION_BASELINEexactly. Verified against currentmain— 898 files, 4627 edges, 25 zones,42 type-only inversions.
When it is productive — and when it misleads
This is the part the README now spells out, because the honest answer is "three questions, and it
misleads on a fourth."
Worth reaching for:
nodes[].in— dependent count. Counts type-only and dynamic edges a grep forfrom '...'misses.zoneEdges— which boundary carries the most traffic, in one query.A "redundant" edge means the target is still reachable at distance ≥ 2 over value edges, so the
direct import changes nothing about what the module can see. That makes it a candidate, not a
defect — a direct import is often clearer than a re-export chain. At ~1300 it is a place to look,
never a work list.
Where it misleads — a cluster's size is not its difficulty. Stated plainly in the README because
it already cost real time:
commands -> clientlooked like the obvious win at 28 type-onlyinversions all pointing at one file. Moving that file down took the gate from 42 to 48, because
the vocabulary it holds depends on
commands/,metro/,core/andremote/— declaring it incontracts/made the foundation depend on the layers above it. The render shows you an edge'sweight, not whether it can be reversed.
So the README pairs the visual question with the numeric one that answers "can this actually move?",
including a worked query over
index.jsonthat reproduces the gate's R6 breakdown. Both exampleswere run against real output rather than written from memory — the first draft's example filtered on
zoneEdges[].backEdgeand returned an empty list, which is the R5 gate passing rather than a usefulresult. That footgun is now documented next to the query.
What is authoritative
pnpm check:layering. The viewer reads the same model so the numbers should agree, but if they everdiverge the gate is right and the graph is stale.
Two things to know before merging
scripts/depgraph/**joins Fallow'signorePatterns, alongsidescripts/layering/**,scripts/perf/**andscripts/maestro-conformance/**— how this repo already treats tooling trees(not baselined, not inline-suppressed). Worth knowing rather than discovering: that exempts
viewer.jsfrom the complexity gate, and itsdrawfunction would fail it. If you'd rather thiscode met the same bar as
src/, say so and I'll split it instead of exempting 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. Both are additive; the gate's behaviour is unchanged.
Validation
pnpm checkgreen — lint, typecheck, R1–R8, production exports, MCP metadata, build, bundle ownerfiles, Fallow, 4488 unit tests.
pnpm depgraph:testcovers the graph model (5 tests).This branch was rebuilt from current
mainrather than rebased: the earlier version was cut from apre-#1405 base and carried stray
oxfmtoutput across 69 unrelated files. Only the viewer's ownfiles are in this diff.
Generated by Claude Code