Ranger architecture

6 — What may import what

Written: 2026-08-15 · Established by: extracting every cross-directory from '../…' import across src/ outside src/__tests__/ at commit 6052e10, then reading each edge that ran against the layering.

This describes the dependency direction that exists, not one that is enforced. No lint rule constrains imports between directories — eslint.config.js restricts only node:* builtins. So this diagram is a measurement, and the four edges called out below are the ones the measurement found going the wrong way.

Three layers, and one dotted arrow that should not be there.

issue 80

src/App.tsx

React layer

src/viewer/utils.ts

src/store

Engine layer

The solid arrows are the layering: React reads the store, the store drives the engine, and the engine reaches neither back. The dotted pair is the exception, and it is the whole finding — four engine-layer directories reach up into the render layer for geometry that was never render-specific.

Directory Depends on Notes
src/parser src/types The bottom of the graph
src/types src/parser Type-only; see below
src/checks src/parser, src/transforms, src/utils, src/types
src/transforms src/parser, src/utils, src/types Pure core; its dialogs are React
src/sim src/parser, src/utils, src/types, src/viewer
src/clearance src/parser, src/sim, src/utils, src/viewer, src/types Engine layer, and stateless by construction — the fixture-clearance check (issue 35). src/sim is placedTriangles and stockBodies, the placement and role helpers it shares with the workpiece rather than any part of the field
src/utils src/parser, src/types, src/viewer
src/workers src/sim, src/parser, src/utils, src/types
src/corpus src/parser, src/utils, src/types
src/store src/parser, src/sim, src/transforms, src/utils, src/viewer, src/scenarios
src/viewer src/store, src/sim, src/transforms, src/parser, src/utils
src/scenarios src/store, src/types, src/utils, src/parser Same shape as store/viewer: the bake types and fetch helpers are pure and store/scenario-slice.ts imports them back, while ScenarioBar.tsx reads the store. src/parser is Units, type-only (issue #408)
src/sidebar src/store and most of the rest The widest importer
src/codemirror src/sidebar, src/types One symbol; see below
src/export src/store, src/transforms, src/codemirror
src/playback src/store, src/viewer, src/parser
src/commands src/viewer, src/utils
src/compare src/store, src/export, src/transforms
src/bug-report src/store, src/parser, src/utils
src/control-profile src/store, src/parser, src/components, src/utils, src/types Added by #1108, not by the extraction this file was written from — see the note below
src/about src/components, src/utils No store; a static credits dialog (#322)
src/components src/utils Leaf
src/editions nothing Leaf; not yet imported by the app — see below
src/dev src/store, src/sim, src/viewer, src/scenarios, src/bench Dev-only; src/bench through a dynamic import inside the DEV block, so no edition ships it
src/bench src/store, src/scenarios, src/viewer, src/components, src/utils, src/editions The in-app benchmark runner and its dialog (issue #2104). It drives the store through the same actions a click does — opens a scenario, runs the simulation, drives playback — reaches the pipeline only through src/viewer/picture-tally.ts's sample subscription and the drawn batch through src/viewer/picture-stats.ts, and asks src/editions which build it is measuring. Present in the verification edition alone, behind ScenarioBar.tsx's edition-gated dynamic import. src/scenarios imports its BenchmarkSpec type back, type-only, the way src/types and src/parser already do
src/track src/store, src/utils, src/types The program on one time axis under the backplot (issue #2226). track-model.ts is pure over utils/cycle-time and utils/playback; Track.tsx reads the store directly, because it owns selection and the seek — the carve-out .claude/rules/ui.md names
src/harvest nothing Offline; see below
src/reference — Leaf; test/tooling-only, see below
src/__tests__ everything pure, dom, scene, scripts

Four rows this file did not measure

src/clearance, src/control-profile, src/bench and src/track were added by #1294, #1108, #2104 and #2226, each of which created its directory. Their dependencies are read off those changes' own imports rather than off the 6052e10 extraction in the header, so the four rows are accurate for their directories and say nothing about whether the rest of the table has moved since.

The rate is the finding, not the rows. pure/architecture-diagrams.test.ts fails a top-level directory under src/ that no diagram names, so every new subsystem arrives as a red gate that the smallest honest fix turns green — a hand-written row. That is the right response to the guard and the wrong shape for the table, because the rows accumulate faster than anyone re-runs the extraction. Refreshing the whole set is #1302.

The four edges that run against the layering

src/sim, src/utils, src/store and src/transforms all import src/viewer/utils.ts. Nine files outside src/viewer/ import it, and between them they take ten symbols: arcGeometry, arcLength, arcSegmentCount, arcExtremePoints, chordTolerance, computeBounds, unionBounds, buildAllSegmentPositions, buildTransitionMarkers and the TransitionMarker type. The first seven are arc geometry and bounds with nothing to do with rendering, sitting in the render layer because that is where they were first needed. Issue #80 is open against exactly this and proposes one geometry layer below the viewer.

src/types/gcode.ts imports five types from src/parser. So src/parser is the bottom of the graph, not src/types: the shared vocabulary is defined where each construct is detected — Units, SubprogramInfo, CannedCycleInfo, ToolTableEntry, G10Offset — and gcode.ts re-exports them. These are type-only imports and cost nothing at runtime.

src/checks/fixes/ imports src/transforms. Two symbols only, applyEdits and the Edit type. A guided fix produces the same edit shape a cleanup tool does, so this is reuse of the splice machinery rather than a check reaching into the transform UI.

src/codemirror/check-marks.ts imports src/sidebar/severity.ts. One symbol, SEVERITY_LABEL. The editor's gutter marks and the checks panel have to name a severity the same way.

src/editions has no importer yet, and that is temporary rather than the shape of the graph. Edition, Capability, the manifest table and entitled() (issue #261, ADR-0055) exist to be imported widely — every future gated call site in store/, viewer/ and App.tsx reads it — but #261 built the mechanism and deliberately carved nothing out of the app itself; that is #262 (free, basic) and #263 (demo). So today it sits below every layer with zero edges in either direction, unlike src/harvest/src/reference below, which are permanently outside the graph by design.

The directories outside the app entirely

src/harvest is outside the app's dependency graph entirely. Not the engine layer's floor, and not omitted from the flowchart by an oversight: it imports nothing under src/, and nothing under src/ imports it back. bin/harvest-user-stories (issue #65 / ticket 267) is a CLI script run by hand or on a schedule, never by the app in a browser — it reads docs/user-stories/lexicon.txt and sources.txt and writes .user-stories-cache/hits.jsonl, and sits under src/ only so src/__tests__/pure/harvest/ gets the fast, DOM-free project the same way every other pure module does.

src/reference imports nothing outside itself, and nothing outside its own tests imports it. It is the reference-interpreter trace schema and adapters (issue #272) — bin/capture-reference-traces (outside src/) shells out to two externally-built interpreters and feeds their output through it to write the committed JSONL under test-fixtures/reference/, and src/__tests__/pure/reference/ reads both the adapters and those committed captures. No runtime code path reaches it; it exists for the test suite and for a developer regenerating captures out-of-band, never for the built app.

Neither is the engine layer's floor: the flowchart above is the three-layer app (React, store, engine), and a box with no edge into any of them would say nothing the table rows above don't already say.

Where the store is genuinely absent

The engine subgraph reaches no React and no store, and that is what makes src/__tests__/pure/ the project most of the suite lives in — it runs in a node environment with no jsdom setup, in roughly a quarter of the whole suite's time. src/transforms straddles the line: its .ts core is pure, and its *Dialog.tsx components import the store. Only useDeferredTransform.ts does so among the .ts files, and it is a React hook.

What this map does not tell you

It is directory-level, so it hides the shape inside each box. src/viewer alone holds 33 plain .ts modules next to its scene components. It also draws every edge at equal weight, which is why the load path, the write path and the simulation pipeline get their own diagrams rather than being read off this one.

Drag to pan · scroll to zoom · Esc to close