Skip to content

Latest commit

 

History

History
470 lines (421 loc) · 31.4 KB

File metadata and controls

470 lines (421 loc) · 31.4 KB

code-atlas — roadmap

Progress tracker for PLAN.md. A phase is done when every box under it is checked — the gate is the definition of done, not a suggestion.

Status: Phases 0, 2, 2.5, 2.6, 3, 4, 5 and 6 complete · 8 of 13. Phase 1 holds one gate a human has to measure. Phase 7's server and reader are in; what is left there is --embed-source and --gzip-source, which share a gate.

# Milestone Unblocks Status
0 Repo skeleton, lift-and-shift, payload contract everything ● done
1 Renderer: perf, rotation, decoupling 7, 8, 9 ◐ one gate open
2 Config, detection, graceful degradation 3, 4 ● done
2.5 Visual system: palette, selection, chrome ● done
2.6 Visual pass against a reference design ● done
3 Language adapters + conformance fixtures 4, 6 ● done
4 Endpoint extraction v2 5, 6, 8 ● done
5 Findings engine ● done
6 Path derivation + calibration 8 ● done
7 atlas serve + code viewer 8, 9 ◐ built; one ratio deferred
8 Request composer UI 9
9 Live proxy mode
10 Open-source packaging

Legend: ○ not started · ◐ in progress · ● done · [~] deliberately deferred, with the reason


Phase 0 — Skeleton and lift-and-shift

Zero behaviour change. The point of this phase is that the next ten can be trusted.

  • git init, directory scaffold
  • PLAN.md, ROADMAP.md
  • package.jsonbin:{atlas}, engines>=20, no dependencies field
  • bin/atlas.mjsparseArgs subcommand dispatch (build/serve/init/scan/findings)
  • Scanner split into src/scan/* + src/model/*; the walk never follows symlinks (Phase 7's security posture needs it, and a symlinked checkout would otherwise be double-counted)
  • Viewer split into src/viewer/* + concatenation in src/build/assemble.mjs
  • TaxVault taxonomy frozen into examples/taxvault.config.mjs
  • meta.schemaVersion = 1
  • meta.acquisition (worktree / ref / fs, + ref, commit, dirty) — the fs rung landed early: the in-repo fixtures are not git repos and the test suite must not require git. The rungs above it and the fallback logic that chooses between them are still Phase 2.
  • test/ harness: helpers.mjs, generic.test.mjs, payload.test.mjs, golden.test.mjs, viewer.test.mjs
  • fixtures/mini-monorepo/ — the CI-enforced target, plus docs/payload-schema.md
  • Gate: payload byte-identical to the prototype except generatedAt
    • baseline captured from the prototype pinned at taxvault 22595f3a and committed as test/golden/taxvault.prototype.json — a file, not a hash, so a failure prints a diff
    • 197 nodes / 467 edges / 18 endpoints / 9 flows / 7 services / 14 layers / 43 groups
    • (the sha256 previously recorded here was unreproducible against any normalization of the prototype's output; the counts were correct)
  • Gate: test/generic.test.mjs — zero target-specific strings in src/ or bin/, minus an explicit accepted list holding the viewer strings Phase 1 removes

Phase 1 — Renderer: perf, rotation, decoupling

Moved ahead of the scanner work: the RangeError is a crash, and every later view builds on this renderer.

  • RangeError fix — accumulate instead of Math.min(...pts.map()). Also fixed in focusOn and the service plates, which had the same spread
  • World-space static cache + mip re-render (was invalidated by every pan frame, buying nothing)
  • Cached label widths; edges bucketed by style instead of save()/restore() per edge
  • Set for endpoint dedupe (O(E²) → O(E)); Map for group lookup (O(N·G) → O(N))
  • heightOf8 + 130·log1p(loc)/log1p(p95), bounded so one generated file cannot flatten the map
  • Theme single-sourced into JS tokens; style.css contains zero hex literals outside :root (test enforces)
  • VIEWS, EDGE_STYLE, PACKET_COLOR, COVER_TINT, hint copy → moved into the payload; views are derived per distinct flows[].view and the viewer branches on kind, not on names
  • meta.suiteCount (killed the hardcoded ["SUITES","4"])
  • Camera rotation: yaw-parameterized projection, generalized depth sort, conditional face visibility, reproject() split from relayout()
  • Rotation controls: Q/E 15° steps, Shift+drag free, R snap to 45°, yaw in the overlay
  • Gate: 60 fps sustained drag on the largest target — the mechanism is tested (render.test.mjs: 120 pans → 1 rasterisation, 121 blits) and the atlas was checked by hand in Chrome, but the frame-time number itself has not been measured: requestAnimationFrame is suspended in a backgrounded tab, so the harness cannot sample it. Needs a human with the window in front.
  • Gate: synthetic 5,000-node / 12,000-edge payload renders, no RangeError — plus 20,000, which is past the argument limit on any engine (5,000 alone would not have caught it: this Node build tolerates ~125k arguments)
  • Gate: generic.test.mjs green with an empty accepted list — every taxvault-coupled viewer string is gone
  • Gate: yaw 45° is bit-identical to Phase 0; 360° sweep never mis-occludes; click selects correctly at every angle (verified in Chrome at 105° yaw). Both rotation gates are regression-checked: reverting the depth sort to gx+gy fails the sweep, pinning a face plane fails visibility at 100°

Phase 2 — Config, detection, graceful degradation

  • src/config/{defaults,detect,load}.mjs; precedence: defaults < detected < config file < CLI flags — one normalized shape downstream, so nothing in src/scan/ or src/model/ knows where a value came from. Both example configs keep their classify() function; the loader accepts that shape and the rule-array shape alike
  • Service auto-detection from manifests, run over the filtered file list (build artifacts like .next/package.json must not register as services) — plus: a manifest directory with no code under it is an umbrella, not a service
  • Total serviceOf — an explicit fallback service always exists (kills the blank-screen failure). Enforced at the payload too: a service id a config used but never declared is added, rather than its files being moved or dropped
  • Directory-derived layer fallback; unsorted share reported — unsorted is its own column, because calling unplaceable application code "tooling" is a claim the tool cannot support. The default taxonomy also gained ui and util, without which every component in a client-side repo lands in the fallback
  • No tests → coverage: null everywhere; suite count from meta
  • Acquisition ladder: worktree → git ref → plain fs; default ref HEAD (origin/develop exists only on taxvault), and handle a git repo with an unborn HEAD — sonder has zero commits, so rev-parse --git-dir succeeds while rev-parse HEAD fails. The walk no longer descends into excluded directories, which a worktree scan needs and a git archive scan never did
  • Curated-flow validation demoted to a warning; --strict restores the hard fail
  • Classification provenance — every node records the rule that placed it, shown in INSPECT
  • atlas scan diagnostics report — the build summary, plus unresolved specifiers grouped by specifier and a layer/service histogram. One missing alias is a hundred identical lines otherwise, which hides the cause rather than showing it. Its report goes to stdout, because it is the command's output rather than its commentary
  • Streamed progress to stderr, throttled, suppressed when not a TTY — walk · parse n/total · resolve · endpoints · derive on one rewritten line. Entering a phase always draws; only the per-file counter is throttled, or the line reports a phase the scan has already left. A non-TTY stream writes zero bytes rather than bytes a consumer is expected to filter, which is what keeps --json clean
  • atlas init writes a starter config (the only command that writes to a target repo) — it emits what detection found, spelled out, because you cannot correct a list you have never seen; every other key is a commented one-liner rather than the defaults restated, which would drift the first time the defaults improve. It refuses to overwrite: an existing config was written by a person, and no amount of detection outweighs that
  • Gate: atlas build with no config yields a legible atlas for taxvault, Shuttrr, terra, sonder (git repo, zero commits), llmbench (pyproject only)
  • Gate: for each — nodeCount > 0, services.length ≥ 1, every node's service ∈ services
    • test/corpus.test.mjs, skipping per repo so a fresh clone and CI stay green. Also asserts ≥3 layers in use and unsorted under 60%: the tool is allowed not to recognise a layout, not allowed to be silently useless on one
    • two bugs this found, both invisible on the configured targets: a file's language was decided with markdown as the else branch, so every unrecognised extension counted as prose and dropped out of fileCount; and subjectOf still called the config's classify() directly, which crashed on any repo without one

Phase 2.5 — Visual system

Inserted, not renumbered: phases 3–10 and every gate reference are untouched. The renderer is fast and honest and reads as a diagram; this is the phase that makes it read as a tool. Selection and palette first — both are small, and both are visible on every single interaction.

  • meta.unsortedCount / unresolvedCount / derivedCount — the map's own coverage in the payload rather than only on stderr. A curated hop counts as derived: curation and derivation alike model an ordering imports cannot express, and the honesty contract does not distinguish them
  • groups[].code — a stable 2-char district name, and groups[].parentId
  • nodes[].travelledBy — the flow index inverted, absent when empty
  • Selection and hover drawn in the live pass, not baked into the world cache; full-silhouette stroke + footprint ring, hover distinct from select
  • Neutral light/dark palette replacing cream; every colour literal in 50-render.js promoted into the theme
  • Two colour channels: identity (fill) vs state (stroke/glow/badge); colorMode identity/mono, state channel identical in both
  • Isometric ground grid; district plate tabs anchored with a leader line
  • Tiered flow dimming with a short ease; numbered step badges; caption bar
  • TRAVELLED BY chips that enter a flow at this node's step; sidebar accent edge-bar for the reverse index — hover-to-preview is not done: switching flows relayouts, so a preview is not the free thing the design assumed
  • Chrome: DERIVED n · UNMAPPED n in the top strip, persistent key hints in the bottom strip, hover readout, prose type register
  • Gate: select and hover cause zero re-rasterisations; 120 pans still cause exactly one
  • Gate: zero colour literals of any form (#, rgb(, hsl() outside :root and the payload theme
  • Gate: COVER_TINT and the derived dotted stroke survive every colour mode — mono disables identity, never a honesty channel
  • Gate: mini-monorepo (no flows, no curation) renders no empty panel section and no empty chip row — checked by hand: no TRAVELLED BY heading without flows, no EXTERNAL PACKAGES heading without packages, and DERIVED · UNMAPPED reads 0 · 0 because both are true of it

Phase 2.6 — Visual pass against a reference design

Inserted like 2.5 and for the same reason: numbering stays put. Driven by five reference screenshots the user supplied, plus four things they named directly.

  • White ground replacing the cream; panels share it and are separated by a hairline rather than a fill. Dark is derived, not copied — every reference shot is light, so that palette is ours
  • Contrast hierarchy made explicit, lightest first: grid ≪ plate < block fill < block stroke. The grid kept the alpha it had on cream, where it read as texture; on white it read as a second set of edges and the blocks it stood under became hard to find
  • Isolated flow view restored, and it is the default. The old behaviour was a re-layout and not a filter, which is why dimming never felt like it: the blocks re-pack, so an 8-step flow draws 9 buildings instead of 200. IN CONTEXT gives the dimmed-in-place reading. One answers what is this path, the other where does it sit
  • Block shape is a list of prisms — block, tower, slab, stepped, round — read by the renderer, the selection hull and hit testing from one face list. Face visibility is decided by projected winding rather than by yaw quadrant, so a stepped or eight-sided block is not a special case
  • Packing within a district (grid / wide / tall). The district grid itself is deliberately not on offer: service down, layer across is the information design, not a preference
  • Services are disclosures holding their own districts, sorted by the order the map lays them out in; flows are real buttons; districts carry their two-character code, tinted by layer and legible in mono
  • Panel type scale from the references: dim eyebrow → large sans title → quiet meta → detail
  • Three claims that were not true of every repo: prose naming a view label a config can rename, a coverage note asserting the repo has no coverage tooling, and a checkbox saying "show docs/" while filtering markdown
  • Gate: every shape's drawn faces are the faces hit testing uses, at four yaws — checked by test, and by hand: picking a node at its own roof centre returns that node for 102 of 105, the rest being correct occlusion
  • Gate: select and hover still cause zero re-rasterisations; 120 pans still cause exactly one
  • Gate: the empty fixture renders no heading with an empty body, in both themes and both colour modes

Phase 3 — Language adapters + conformance

  • Adapter interface documented in docs/adapters.md; resolve() returns an array of ids (Go packages / Java wildcards are one specifier → many files)
  • fixtures/hostile-ts/ — barrel chains, export * from, aliased re-exports, circular imports, @/ alias, extensionless, side-effect, require(), dynamic import(), .tsx
  • fixtures/hostile-py/ — relative-dot imports at several depths, __init__.py re-export barrels, from . import x
  • Conformance test asserting exact expected resolution for both fixtures — including the rows that must come back unresolved, which is the honesty path and the one a counting test cannot protect
  • src/adapters/ts.mjs — tsconfig paths/baseUrl scoped per tsconfig (Shuttrr's web/tsconfig.json maps @/*./* root-relative while server/tsconfig.json has no paths at all), extension swap, all four import forms, and the six extensions the ecosystem ships rather than .ts alone — a .jsx repo was extracting nothing at all. baseUrl is null unless declared: defaulting it to the tsconfig's own directory fabricated an internal edge for import "lodash" beside a local lodash.ts, which is a phantom edge and is not permitted
  • src/adapters/py.mjs — module roots inferred from the layout, generalized barrels (every __init__.py, not a hand-listed array), and relative-dot imports, which the old letter-anchored pattern could not match at all. Config still wins outright wherever it is given, so a configured repository's payload cannot move underneath it
  • src/adapters/generic.mjs — no edges, still renders; now also the documented skeleton a new language is copied from
  • Import line numbers recorded; comment/string blanking before extraction — held on the extraction result until Phase 7 needed them, then promoted to edges[].line as an additive field, so no schemaVersion bump
  • Gate: both fixtures resolve exactly as asserted
  • Gate: Shuttrr unresolved === 0, zero internal specifier classified external (174 @/… alias imports total, of which @/lib/utils/cn ×24)
    • 161 → 423 resolved, 9 → 3 unresolved. Not zero: the three are .css and .wgsl asset imports whose files are outside keep, so the node they would point at does not exist. Reported rather than special-cased away — widening keep to draw stylesheets is a defaults decision, not an adapter one
  • Gate: llmbench — all 172 relative-dot imports resolve (was 0 of 172), across 50 files at 1, 2 and 3 dots plus bare from . import x
    • verified as 172 extracted / 172 internal, at depths 109 · 58 · 5, and the only absolute specifier resolving in-repo is the package's own name
  • Gate: TaxVault 350 resolved / 0 unresolved / 319 external, and the prototype payload otherwise unchanged
    • the gate said 320. One of them was a phantom: a module docstring whose prose wraps onto a line beginning import time:, which the prototype read as an import of the stdlib time. Blanking comments and strings removes it, so 319 is the corrected number and the golden records the divergence the same way Phase 2's undoJsonLangFix does — one bug named, one bug undone

Phase 4 — Endpoint extraction v2

  • Rule schema as data; never match a non-literal path; count and report skips — two shapes are counted: a call whose path is not a literal, and a literal path handed to a helper inside a file mounted as a router, where the method lives in code this tool does not follow. The skip list rides on the returned array as a non-index property, so JSON.stringify keeps it out of the payload
  • Symbol → file mount resolution, run to a fixpoint — a test harness mounting a router at / is excluded, or every route is reported at a second path nobody can call; app.use("/*", handler) is middleware, not a mount
  • File-based routing: (groups) stripped, [param]:param, route.ts vs page.tsx — a route.ts emits one endpoint per exported HTTP method, at that export's own line; a component file inside a route directory is not a route
  • Path normalization so :id and {id} collapse to one logical node — in the identity key only. Rewriting the displayed path broke validateFlows()'s match against curated flow steps and silently dropped 5 edges (467 → 462); the count gate did not catch it, the golden did
  • Route line numbers — additive, so no schema bump
  • Gate: Shuttrr yields POST /api/photos/upload, GET /api/photos/gallery, GET /health, ≥12 /api/ai/* via the two-level mount, /sign-in, /auth/callback
    • 24 → 52 with the mount chain → 71 with file-based routing; /api/ai/* = 23
  • Gate: (auth) / (dashboard) absent from every path; zero endpoints from web/app/(dashboard)/studio/components/*
  • Gate: ≥8 skipped non-literal registrations reported, naming presets.ts
    • 9: eight in presets.ts, one in the _shared.ts helper that registers them
  • Gate: TaxVault's 18 endpoints unchanged; every curated flow still validates
    • 18 endpoints, 467 edges, 350/0/319 imports; the only warning is the pre-existing service reconciliation one, and no flow warns

Phase 5 — Findings engine

atlas scan diagnoses the tool. This diagnoses the code.

  • Import cycles (Tarjan SCC), smallest-first
  • Layering violations — an edge whose target rank is lower than its source, spine layers only: tooling/test/docs/unsorted carry ranks for the layout, not for the spine, and unsorted ranks above every real layer — so judging it turned "no rule matched" into "every import runs backwards". Shared with derive.mjs as OFF_SPINE_LAYERS rather than restated
  • Oversized files, ranked against the repo's own p95
  • Endpoints no test reaches
  • Orphans (zero in and out edges, excluding entrypoints and configured roots)
  • Unreachable from any entrypoint (BFS from entry-layer files)
  • God nodes (in-degree percentile, with a floor so a small repo's low p95 does not flag half of it)
  • Cross-service coupling that bypasses declared boundaries
  • Each finding carries severity, evidence (exact nodes/edges), and a one-line "why this matters"
  • FINDINGS view highlights implicated blocks in place on the map — the rest of the city dims rather than disappearing, so a cycle reads AS a cycle. The view is unconditional: a repo with nothing to report has a result worth showing, and dropping the view would make "eight checks ran and matched nothing" look like "this tool does not check"
  • atlas findings --json; configurable thresholds; per-finding mute with a reason — a muted finding stays in the payload marked, never removed: silencing one should be a visible diff, not a silent subtraction
  • Gate: on TaxVault reports the core-case-service orphans and server.ts unreachable-from-tests (both known-true) — run against the corpus, passes
  • Gate: zero false layering violations on a repo that enforces layering by policy — plus a second gate over fixtures that DO have unplaceable files, because the first one passed throughout the period the off-spine bug was live
  • Gate: finds a known cycle in a synthetic fixture — fixtures/import-cycle, built so each of the eight findings has one isolated known-true instance

Phase 6 — Path derivation + calibration

Calibration is a script and it runs before any composer UI exists. One endpoint is an anecdote.

  • src/model/derive.mjs — mount-chain seed, handler-slice BFS seed, non-decreasing rank, neutral io terminals, response leg
  • Per-hop certainty: wired / imported / inferred — computed, shipped, and drawn. Weight and dash carry it; colour stays with the step's kind, so a proven hop and an admitted guess no longer read the same
  • Derived at scan time; steps stored as integer node indices
  • test/calibrate.mjs — diff derived vs curated across all 9 flows (moved out of tools/: it imports test/helpers.mjs and the regression test imports it)
  • Per-flow and aggregate precision/recall; names hops invented, missed, and mis-ordered
  • Calibration wired as a regression test (test/calibrate.test.mjs), with floors rather than a committed artifact — plus fixture-driven invariants in test/derive.test.mjs that run on a fresh clone, because the calibration gate skips without the corpus
  • Gate: every endpoint across all targets produces a ≥2-hop path with no crash — asserted in test/derive.test.mjs
  • Gate: calibration numbers published in the README — measured at taxvault 22595f3a: precision 17%, recall 12% (tp=11 of 93 curated, 64 derived; 52 invented, 81 missed, 1 mis-ordered), under Path derivation, calibrated. Stated plainly rather than softened: expectations are set before first use, or the number is decoration

Phase 7 — atlas serve + code viewer

  • src/serve/server.mjslisten(port, "127.0.0.1"), bind host hardcoded, not a flag
  • File allowlist from the scanned set (membership is the defense), lstat symlink refusal, size cap, always text/plain
  • Host header check + Sec-Fetch-Site rejection (DNS rebinding); CSP; no-store; nosniff
  • INFO | SOURCE tabs; wide right-docked overlay; line gutter; target line centered — min(760px,55vw), Esc closes, canvas renders behind, and the map's own overlays shift out from under it so the DERIVED caveat is never covered. Full width below 900px, where a 420px code pane would be unreadable
  • Jump-to-line from endpoint, import edge, test subject, and derived hop — an inferred hop is offered no button rather than one that lands somewhere plausible: there is no import to open, and saying so is the point
  • Highlighter — vendored Prism 1.29.0 instead of the planned ~60-line regex (~27 KB, committed not installed; reasoning in PLAN.md). Tokenizer only, DOM built by hand from text nodes; test/source.test.mjs runs the real paint against a DOM whose innerHTML setter throws, so escape-as-you-emit is enforced rather than reviewed
  • --embed-source [glob] + permanent SOURCE EMBEDDED badge + CLI size warning — the badge names the file count and says GZIP when compressed, and is the only legend entry styled as a warning: sending this file sends the code
  • --gzip-source via node:zlib + DecompressionStream("gzip") — ONE shared blob, not one stream per file: 143 independent streams cannot share a dictionary, and per-file cost 16% over compressing the map together
  • Gate: all 404 — ../../../etc/passwd, /etc/passwd, .env, node_modules/x, in-repo symlink pointing outside, ..%2f..%2f, Host: evil.exampletest/serve.test.mjs covers the list verbatim, plus a null byte and a post-scan symlink swap
  • Gate: clicking an endpoint opens its file at the declaring line — verified in Chrome against this repo rather than the Shuttrr endpoint the gate names, which needs a corpus checkout: GET /admin/stats opens fixtures/express-js/src/routes/admin.mjs at line 7, which is the router.get call. Re-run on Shuttrr when the corpus is to hand
  • [~] Gate: gzip round-trips; embedded size cut ≥3× — round-trip met, ratio DEFERRED. Lossless is verified through a real DecompressionStream and by hand against a built HTML. The ≥3× is not met and the reason is arithmetic, not implementation: gzip gets ~3.1× on this repo's source, and base64 then multiplies by 4/3 to survive JSON, landing at ~2.3× on the artifact: measured 696,953 B → 301,282 B, 2.31×. Raw gzip with no text-safe wrapping at all is 3.10× — barely over the bar before paying any encoding tax — so the ceiling is this repo's own redundancy, not the wrapper. ascii85 (5/4) would reach ~2.48× and still miss, so nothing was spent chasing it. The number ≥3× came from PLAN.md's Shuttrr estimate, which quoted the gzip size and omitted the base64 the file has to carry. Deferred deliberately so development continues; re-open it with a measured number if a corpus repo compresses better, or retire it.

Phase 8 — Request composer UI

  • kind:"request" view; endpoint list; composer (path params, query, headers, JSON body + validity)
  • Composer state to sessionStorage, authorization value excluded
  • SEND (MODELED) animates the path with substituted values
  • Curated-vs-derived badge on the canvas, not only the panel; per-hop solid/dotted certainty
  • Audit: every solid hop opens the import line that justifies it
  • [+ CURATE THIS] emits a paste-ready config entry
  • Gate: the emitted entry pastes into a config and validates

Phase 9 — Live proxy mode

  • src/serve/proxy.mjs — accepts {method, path, headers, body} only; no host, no URL
  • Origin assertion; --allow-live required at the process level; loopback/private target restriction
  • Method + header allowlists; timeout; 256 KB cap; redirect:"manual"; rate bucket; one stderr line per request
  • --auth-env keeps the token out of the browser; sessionStorage fallback with explicit clear
  • Status ring + latency on the endpoint node only; halt-on-non-2xx at hop 1; persistent real-vs-modeled banner
  • No per-hop timings, ever
  • [ COPY AS cURL ]
  • Gate: real 200 + latency from a running Shuttrr; 401 halts at hop 1 and says so
  • Gate: proxy refuses path:"http://example.com/", refuses a non-loopback target, 403s without --allow-live
  • Gate: no token in stderr or in document.documentElement.outerHTML

Phase 10 — Open-source packaging

  • README.md — why it exists · real-vs-modeled table placed before the feature list · quick start with no config · running the servers + security posture · configuration with two worked examples · features · tech stack and why · limitations incl. the Phase 6 calibration numbers
  • LICENSE, CONTRIBUTING.md
  • docs/payload-schema.md — every field, with a stability tier
  • docs/adapters.md — "add a language in 30 lines", against a real fixture
  • docs/config.md
  • Gate: a reader who has never seen the repo goes from git clone to a rendered atlas of their own project using only the README, on a repo with no config

Open findings

Raised by the quality sweep, verified against the code, and deliberately not fixed in it. None is a crash; each is something the map currently claims or omits without saying so.

Modelled hops inside curated flows carry no badge. build.mjs counts them (meta.derivedCount) and its own comment says curation and derivation alike model an ordering imports cannot express — but only tool-derived flows raise the badge. The caveat for a curated flow's unobserved hops appears in the sidebar strip and nowhere on the canvas.

Language-specific code in src/model/. The adapters↔model seam says language knowledge lives in an adapter. Four places breach it: derive.mjs's importBindings branches on ts/py with seven regexes; mounts.mjs's specifierFor understands ES import only; endpoints.mjs's ROUTE_FILE and METHOD_EXPORT are JS-only; tests.mjs's subjectOf treats any non-.ts file as Python. The fix is to widen the adapter contract, not to add branches.

Mount resolution does not follow require(). specifierFor reads import syntax, so a CommonJS router still yields its endpoints but at the path it declares rather than the one it is served at. fixtures/express-js uses ESM in its server for exactly this reason and says so.

Endpoint and datastore nodes carry no provenance. File nodes record the rule that placed them and INSPECT reads those fields; graph.mjs builds endpoint and datastore nodes without them, so INSPECT on an endpoint shows a bare label. endpoints.mjs's add() also discards which rule matched and where the prefix came from — the two facts a user needs to correct a wrong path.


Resolved questions

  • Desktop app — parked until more of the tool exists. Revisit after v1.0, as a separate package rather than a constraint on this one.
  • Dependencies — zero-dep is the default for the CLI and scanner, not a law. The viewer may take a rendering dependency (three.js, paper-shaders are wanted) when a phase justifies one, weighed against the single-self-contained-file promise. WebGL is deferred until v1.0, so Phase 1 keeps a renderer seam.

Release checkpoints

  • v0.1 after Phase 4 — the tool renders any repo. Pull a trimmed README and LICENSE forward from Phase 10; everything after is additive.
  • v1.0 after Phase 10.