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
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.json—bin:{atlas},engines>=20, nodependenciesfield -
bin/atlas.mjs—parseArgssubcommand 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 insrc/build/assemble.mjs - TaxVault taxonomy frozen into
examples/taxvault.config.mjs -
meta.schemaVersion = 1 -
meta.acquisition(worktree/ref/fs, +ref,commit,dirty) — thefsrung 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, plusdocs/payload-schema.md - Gate: payload byte-identical to the prototype except
generatedAt- baseline captured from the prototype pinned at taxvault
22595f3aand committed astest/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)
- baseline captured from the prototype pinned at taxvault
- Gate:
test/generic.test.mjs— zero target-specific strings insrc/orbin/, minus an explicit accepted list holding the viewer strings Phase 1 removes
Moved ahead of the scanner work: the RangeError is a crash, and every later view builds on this renderer.
-
RangeErrorfix — accumulate instead ofMath.min(...pts.map()). Also fixed infocusOnand 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 -
Setfor endpoint dedupe (O(E²) → O(E));Mapfor group lookup (O(N·G) → O(N)) -
heightOf→8 + 130·log1p(loc)/log1p(p95), bounded so one generated file cannot flatten the map - Theme single-sourced into JS tokens;
style.csscontains zero hex literals outside:root(test enforces) -
VIEWS,EDGE_STYLE,PACKET_COLOR,COVER_TINT, hint copy → moved into the payload; views are derived per distinctflows[].viewand the viewer branches onkind, not on names -
meta.suiteCount(killed the hardcoded["SUITES","4"]) - Camera rotation: yaw-parameterized projection, generalized depth sort, conditional face visibility,
reproject()split fromrelayout() - Rotation controls:
Q/E15° steps, Shift+drag free,Rsnap 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:requestAnimationFrameis 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.mjsgreen 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+gyfails the sweep, pinning a face plane fails visibility at 100°
-
src/config/{defaults,detect,load}.mjs; precedence: defaults < detected < config file < CLI flags — one normalized shape downstream, so nothing insrc/scan/orsrc/model/knows where a value came from. Both example configs keep theirclassify()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.jsonmust 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;
unsortedshare reported —unsortedis its own column, because calling unplaceable application code "tooling" is a claim the tool cannot support. The default taxonomy also gaineduiandutil, without which every component in a client-side repo lands in the fallback - No tests →
coverage: nulleverywhere; suite count frommeta - Acquisition ladder: worktree → git ref → plain fs; default ref
HEAD(origin/developexists only on taxvault), and handle a git repo with an unborn HEAD — sonder has zero commits, sorev-parse --git-dirsucceeds whilerev-parse HEADfails. The walk no longer descends into excluded directories, which a worktree scan needs and agit archivescan never did - Curated-flow validation demoted to a warning;
--strictrestores the hard fail - Classification provenance — every node records the rule that placed it, shown in INSPECT
-
atlas scandiagnostics 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 · deriveon 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--jsonclean -
atlas initwrites 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 buildwith 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 ∈ servicestest/corpus.test.mjs, skipping per repo so a fresh clone and CI stay green. Also asserts ≥3 layers in use andunsortedunder 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; andsubjectOfstill called the config'sclassify()directly, which crashed on any repo without one
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, andgroups[].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.jspromoted into the theme - Two colour channels: identity (fill) vs state (stroke/glow/badge);
colorModeidentity/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 nin 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:rootand the payload theme - Gate:
COVER_TINTand the derived dotted stroke survive every colour mode —monodisables 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 · UNMAPPEDreads0 · 0because both are true of it
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 CONTEXTgives 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
- 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(), dynamicimport(),.tsx -
fixtures/hostile-py/— relative-dot imports at several depths,__init__.pyre-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— tsconfigpaths/baseUrlscoped per tsconfig (Shuttrr'sweb/tsconfig.jsonmaps@/*→./*root-relative whileserver/tsconfig.jsonhas nopathsat all), extension swap, all four import forms, and the six extensions the ecosystem ships rather than.tsalone — a.jsxrepo was extracting nothing at all.baseUrlis null unless declared: defaulting it to the tsconfig's own directory fabricated an internal edge forimport "lodash"beside a locallodash.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[].lineas an additive field, so noschemaVersionbump - 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
.cssand.wgslasset imports whose files are outsidekeep, so the node they would point at does not exist. Reported rather than special-cased away — wideningkeepto draw stylesheets is a defaults decision, not an adapter one
- 161 → 423 resolved, 9 → 3 unresolved. Not zero: the three are
- 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 stdlibtime. Blanking comments and strings removes it, so 319 is the corrected number and the golden records the divergence the same way Phase 2'sundoJsonLangFixdoes — one bug named, one bug undone
- the gate said 320. One of them was a phantom: a module docstring whose prose
wraps onto a line beginning
- 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.stringifykeeps 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.tsvspage.tsx— aroute.tsemits 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
:idand{id}collapse to one logical node — in the identity key only. Rewriting the displayed path brokevalidateFlows()'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
- 24 → 52 with the mount chain → 71 with file-based routing;
- Gate:
(auth)/(dashboard)absent from every path; zero endpoints fromweb/app/(dashboard)/studio/components/* - Gate: ≥8 skipped non-literal registrations reported, naming
presets.ts- 9: eight in
presets.ts, one in the_shared.tshelper that registers them
- 9: eight in
- 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
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/unsortedcarry ranks for the layout, not for the spine, andunsortedranks above every real layer — so judging it turned "no rule matched" into "every import runs backwards". Shared withderive.mjsasOFF_SPINE_LAYERSrather 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-serviceorphans andserver.tsunreachable-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
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, neutralioterminals, 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 oftools/: it importstest/helpers.mjsand 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 intest/derive.test.mjsthat 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
-
src/serve/server.mjs—listen(port, "127.0.0.1"), bind host hardcoded, not a flag - File allowlist from the scanned set (membership is the defense),
lstatsymlink refusal, size cap, alwaystext/plain -
Hostheader check +Sec-Fetch-Siterejection (DNS rebinding); CSP;no-store;nosniff -
INFO | SOURCEtabs; wide right-docked overlay; line gutter; target line centered —min(760px,55vw),Esccloses, 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.mjsruns the real paint against a DOM whoseinnerHTMLsetter throws, so escape-as-you-emit is enforced rather than reviewed -
--embed-source [glob]+ permanentSOURCE EMBEDDEDbadge + 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-sourcevianode: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.example—test/serve.test.mjscovers 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/statsopensfixtures/express-js/src/routes/admin.mjsat line 7, which is therouter.getcall. 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
DecompressionStreamand 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.
-
kind:"request"view; endpoint list; composer (path params, query, headers, JSON body + validity) - Composer state to
sessionStorage,authorizationvalue 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
-
src/serve/proxy.mjs— accepts{method, path, headers, body}only; no host, no URL - Origin assertion;
--allow-liverequired 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-envkeeps the token out of the browser;sessionStoragefallback 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
-
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 cloneto a rendered atlas of their own project using only the README, on a repo with no config
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.
- 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.
- 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.