Guidance for Claude Code when working in this repository.
agda-deps is an Agda compiler backend (not a standalone tool) that
emits a graph of lemma / definition dependencies for an Agda program.
Registered as a Backend via Agda.Main.runAgda, so it runs inside
Agda's type-checking pipeline and is invoked through the Agda CLI
surface that runAgda provides.
Outputs:
- DOT (Graphviz)
- HTML (self-contained or
--lazysplit-files; one of several views —module-dag-podsdefault,cytoscape,sigma,big-module-dag-pods, dashboards, hierarchical, etc.) - JSON (v2
graph.jsonschema,packedorexpanded)
Each node is coloured by state — Defined / Postulate / Hole /
Failed (see State semantics).
The graph.json it emits is a stable wire artifact for downstream
tooling; the v2 schema (see v2 graph.json schema)
is the contract.
For end-user documentation see README.md. For shipped work see Changelog.md. For forward-looking work see TODO.md. For deferred / refused ideas see Backlog.md.
cabal build
cabal run agda-deps -- -i test/ -o test/ test/Test.agda
-- separates cabal flags from arguments forwarded to the backend.
After --, flags are Agda CLI flags (-i include path; trailing
positional is the Agda module). Backend flags are documented in
commandLineFlags in src/AgdaDeps/Backend.hs and listed in
README.md.
For a stable on-PATH binary (so callers don't hunt under
dist-newstyle/): cabal install exe:agda-deps --overwrite-policy=always. agda-deps --version reports the full
build fingerprint (version + git rev + date + GHC) — the same string
stamped into graph.json as "producer" — so "which build is this?"
needs no mtime/git log forensics. --numeric-version stays the bare
number for parsing.
No test suite, no lint config. Exercise the backend against test/ —
entry point test/Test.agda. CI (.github/workflows/ci.yml) builds
and runs the binary across dot / html / html --lazy / json
against this corpus.
Two side-channels skip parts of the normal pipeline:
--keep-going— catches type-check errors and proceeds with whatever modules Agda did load (AgdaDeps.ModuleExplorer; failing module taggedFin the graph).--skip-agda— doesn't invoke Agda at all; renders a module-level graph straight from the source-file scan (AgdaDeps.SkipAgda+AgdaDeps.Precompute).
src/Main.hs argv pre-processing, .agda-lib
discovery, dispatch to runSkipAgda /
runAgdaArgsKeepGoing / runAgdaArgs.
src/BuildInfo.hs Compile-time build identity for
agda-deps: package version + git
revision + compile date + GHC.
Surfaced in --version and stamped into
graph.json as "producer".
src/BuildInfoTH.hs TH helper behind BuildInfo (separate
module for the stage restriction):
captures the git revision at build time.
src/AgdaDeps/
Backend.hs Backend' record + hooks
(moduleSetup, compileDefAD,
postModuleAD, postCompileAD); CLI
flag list; post-compile dispatcher;
dropExternalDefs, collectReExports,
classifyExternalModules.
Driver.hs Thin shim around ModuleExplorer
exposing runAgdaArgsKeepGoing.
ModuleExplorer.hs --keep-going glue: drop-in for
backendInteraction that catches
check failures (TCErr AND other GHC
exceptions via catchAllTCM). On the
failure branch re-drives preModule /
compileDef / postModule over every
decoded interface (rebuilding the
import state — signature, builtins,
metas, pattern syns, display forms —
via mergeIfaceState first) so
postCompile receives def-level
data for every module that did
elaborate. Every stage guarded:
postCompile always runs.
FragmentCache.hs --incremental: per-module fragment
cache (EmbPrj-serialised ADDefs +
side-channel slices), keyed on
iFullHash + option fingerprint +
nodeKeyVersion. Agda >= 2.9 only;
no-op fallback on 2.8. Also
gcFragments (prune stale .frag).
SerialiseCache.hs --incremental serialise (P2): a
plain-text manifest of content
epochs in the cache dir, letting a
rebuild skip re-emitting unchanged
output (monolithic no-op skip +
per-module lazy-file skip). No CPP.
LibResolve.hs --resolve-deps machinery: parses
the project's .agda-lib and
Agda's libraries registry, walks
the depend: closure, returns the
'--no-libraries -i <dir> ...'
argv expansion. Wired in Main.hs
before Agda's CLI parser runs.
SkipAgda.hs --skip-agda entry point.
Precompute.hs Cheap line-scan for module / import
declarations.
Help.hs --help / --version intercepts.
Logging.hs quietRef + info :: MonadIO m => …
Options.hs Options record, defaultOptions,
parsers, View ADT, JsonMode.
Config.hs YAML config loader for
.agda-deps.yml (kebab-case schema
mirroring CLI flags); discovery
(--config / $AGDA_DEPS_CONFIG /
dotfile / walk-up to *.agda-lib);
applyConfig :: A.Object -> Options
-> Either String Options. Theme
presets live here.
Deps.hs ADDef, compileDefAD, classifyDef,
ignoreDef, ignoredEdgesRef,
contractIgnoredEdges.
Layout.hs (x, y) positions per definition.
Csr.hs CSR adjacency + base64 serialisation.
Source.hs --with-source machinery.
TermCanon.hs --with-term-hashes: canonicalises
each elaborated subterm (de-Bruijn
indices, source positions stripped,
MetaV identities wildcarded, Hiding
preserved) to a Word64 fingerprint.
Util.hs isWithFun, jsString, dedupOrd,
hex-colour parsing, argv inspection.
Backend/
Wire.hs Single source of truth for the v2
*expanded* wire shape: field tables
(wire name + SchemaDoc + byte encoder)
+ SchemaDoc ADT. Generates the JSON
Schema (`--emit-schema`, CI-diffed vs
the committed schema/ oracle by
schema/check_schema.py) AND encodes the
output (`encodeExpanded`). Owns the
expanded wire tags (wireState/Kind/…).
GraphJson.hs v2 graph.json emitter. Packed + --lazy
are hand-rolled here; expanded's
buildExpandedJson builds an ExpandedGraph
and defers encoding to Wire.encodeExpanded,
so its field set IS Wire's by construction.
Html.hs renderHtml / renderLazyHtml +
renderHtmlFromInput (used by
SkipAgda). templateRawFor.
Dot.hs Graphviz DOT renderer.
Json.hs --format=json output.
templates/
*.html.tmpl Legacy cytoscape template.
views/*.html.tmpl One per view variant.
Embedded at compile time via
Data.FileEmbed.embedStringFile.
moduleSetup captures the module's in-scope QNames (ModuleEnv).
compileDefAD per Definition: filter via ignoreDef, walk defType +
theDef via Agda.Syntax.Internal.Names.namesIn for
dep QNames, classifyDef tags state, classifyKind tags
kind. Records raw out-edges of every ignored def
in ignoredEdgesRef.
postModuleAD diffs getSignature (pre-prune) vs visited QNames to
recover dead-end private defs that
eliminateDeadCode pruned from iSignature; feeds them
through compileDefAD. Captures the entry module on
the IsMain pass.
postCompileAD aggregate ADDefs; contractIgnoredEdges (topsort DP)
rewrites kept defs' deps by expanding hidden refs
into their transitive non-hidden targets; dispatch
on optFormat to renderDot / renderHtml{,Lazy} /
renderJson.
-
ignoreDefis the single source of truth for what counts as a real definition vs. compiler-generated noise. Changes to the set of nodes go here; rendering-only changes (colour, snippet, label) go incomputeDefAD/postCompileAD. -
ignoreDefshort-circuits ondefCopyat the top level (before inspectingtheDef). Module-instantiation copies exist forRecord/Datatype/Constructor/Projection, not justFunction— without the short-circuit, ghost defs surface in the importing module. -
ignoreDef'sfunInlinedrop is deliberate — don't remove it. Agda inlines every call to an{-# INLINE #-}function into the caller during type-checking, so by the time the backend sees the elaboratedDefnthe function has zero incoming edges. Keeping it as a node adds a permanent zero-caller orphan that dead-code passes flag as a falsedead. The lost call edges are unrecoverable from post-elaboration syntax (fixture:test/InlineGap.agda); a consumer-side source scan is the right fix. -
Scope is captured per-module in
moduleSetup(getCurrentScope), not per-definition incompileDefAD. Don't move it. -
Filter
ignoreDependencyonce incontractIgnoredEdges(on the contracted set), not per-def incomputeDefAD: raw hidden refs must survive for the contraction pass to expand them, else edges through with-/where-helpers are lost. -
Node identity is
nodeKey, never bareprettyShowand never derivedshow.hashQName = hashString . nodeKey; the wire"name"(expanded + packed) and edge endpoints all derive from it, and the expanded-edge filter resolves by the canonical string, notQNameOrd(which distinguishes same-nodeKeyhelpers and would re-drop their edges). Why not the alternatives:- Derived
ShowcarriesNameIdmetadata that differs between QNames sourced fromiSignaturevsstSignature— can't key nodes. prettyShowalone over-collapses: same-namedwhere/anonymous-module helpers render identically (Mod._.name) and merge onto one node.nodeKeydisambiguates._.helpers with a@<binding-line>suffix (the one per-helper coordinate stable across signature sources).nodeKeyalso lifts anonymous-module segments — the_Agda emits for bothwhereblocks andmodule _ (…) wheresections — viaUtil.liftAnonSegments:Mod._.helper@15↦Mod.helper@15(nodeKeyVersion 3). The edges were already correct (Agda lifts enclosing vars intodefType); the blind spot was naming only. Don't try to distinguishwherefrom section — post-scope-check they're identical.- Every QName→module-string site must route through
moduleKey(= liftAnonSegments . prettyShow . qnameModule): externals,--exclude, module DAG, manifests, DOT clusters — else set/index membership drifts and phantomMod._module nodes + false cycles appear. Regressions:test/Collision.agda,test/AnonSection.agda(both imported bytest/Test.agda).
- Derived
-
nodeKeyVersiontracks the node-naming convention. Stamped intograph.jsonso a consumer can detect a stale-format cache and rebuild. Bump it whenevernodeKeychanges shape. Currently 3 (1 = bareprettyShow; 2 =@<line>on._.helpers; 3 = anon-module segments lifted). The consumer repoagda-graph-explorerreads the same constant — a bump is a cross-repo coordination point. -
BuildInfois split across two modules for the TH stage restriction. The git-revision splice generator lives inBuildInfoTH(GHC forbids a top-level splice using a generator from the same module);BuildInfodoes the splice + CPP date + version. Both are inother-modulesof theagda-depsstanza. Thebuiltdate freezes across an uncommitted rebuild that doesn't recompileBuildInfo, so the reliable discriminator between two dirty builds is the binary's mtime, not the fingerprint. -
Output routing. DOT / JSON →
<outDir>/deps.dot|.json, else stdout. HTML →<outDir>/deps.htmland errors ifoptOutDiris unset.postCompileADcreatesoptOutDir. -
Agda 2.8 / 2.9 version coupling.
Agda >= 2.8 && < 3. The defaultcabal.projectpins a 2.9 upstream commit;cabal.project.agda28(GHC 9.6 + HackageAgda ==2.8.0) builds the 2.8 variant:cabal build --project-file=cabal.project.agda28 --builddir=dist-agda28 agda-deps. The solver picks which Agda — no flag. API deltas are bridged with CPP onMIN_VERSION_Agda(2,9,0)in five modules:Util.hs—funWithisMaybe QName(2.8) vsIsWithFunction QName(2.9).Help.hs—usageInfogained a leading column-width arg in 2.9.Main.hs— 2.8 has norunAgdaArgs; shimmed viawithArgs+runAgda'.Backend.hs—anameNameinScope.Base(2.8) vsAbstract.Name(2.9).ModuleExplorer.hs—stCurrentModulelazyMaybe (_,_)(2.8) vs strict:!:(2.9);setTCLens' stBackends(2.8) vssetSession lensBackends(2.9). CPP gotcha: these five modules must not use backslash string gaps (cpp collapses\-newline) — use++concatenation. Note: Agda 2.9'sfunProjectionisEither ProjectionLikenessMissing Projection— matchRight{projProper = Just _}. Both builds produce byte-identical graphs (modulo theproducerfingerprint).
-
Main.hsargv pre-processing canonicalizes path-bearing flags (-o,-i,--include-path,--out-dir,.agda/.lagda*positionals) to absolute paths, thensetCurrentDirectoryto the nearest ancestor containing a*.agda-lib. Skipped under--no-libraries/--library/-l/--library-file. Lets users run from outside the source dir without losing library deps, and makesclassifyExternalModules's cwd-prefix check work. -
Adding a flag. Extend
Options+commandLineFlagsandConfig.hs'sapplyConfig(kebab-case YAML mirror) andNFData Options(forces every field). Merge order: defaults → config → CLI. Don't add a second config mechanism. -
Adding a view. Extend
View; addviewSlug+viewOptcases; add the template toextra-source-filesinagda-deps.cabal; add thetemplateRawForline. The placeholder contract is documented in the templates (copysigma.html.tmpl). Don't add a--<slug>shortcut flag — the surface is--view=NAME/ YAMLview: NAMEonly. -
Config discovery order:
--config=PATH>$AGDA_DEPS_CONFIG>./.agda-deps.yml(or.yaml) > walk up to the nearest*.agda-liband use its dotfile. Merge: defaults → config → CLI. Top-level keys are kebab-case CLI flag names. Bad type / unknown key errors with file + key and exits 1. A stderr breadcrumb fires when a config applies (unless--quiet). -
--themeis a colour preset. It resolves to the four state colours (color-defined/-postulate/-hole/-failed) inOptions; explicit--color-*CLI flags layer on top and win. Adding a theme is a tuple inConfig.hs— don't growVieworOptions. -
Auto-format from
-o. When--formatis not set andoptOutDirends in.html/.json/.dot, the format is inferred from the extension (explicit--formatwins; directories/extensionless keep the defaultdot). Inference lives inMain.hsargv pre-processing — don't duplicate it in the backend. -
Scaling. Prefer strict
Map/IntMap/IntSet/Setfolds; avoid(++)queues (useData.Sequencefor BFS);BangPatternsonfoldl'accumulators. -
agda-depsis single-threaded on purpose. Agda'sTCMand itsIORef-backed state aren't thread-safe and theBackend'hooks are called serially. Don't addparMap/forkIO/Asyncundersrc/AgdaDeps/. (Post-Agda IO inpostCompileADwould be safe to parallelise — deferred.) -
Edge provenance (producer side).
computeDefAD(AgdaDeps.Deps.tagOne) walksdefTypeandtheDefseparately:defTypenames →Signature;theDefnames →Body, refined toWithwhen the parent'sfunWithpoints at the target andModuleLocal(wire tagmodule-local) when the qname'sprettyShowcontains the._.marker.module-localis a property of the target (an anonymous-module-local helper), not the (src, dst) pair. Precedence:Signature > With > ModuleLocal > Body > Unknown. Contracted edges inherit the source side's provenance toward the hidden helper;addInstanceMethodEdgesaddsUnknownwith a left-biasedM.union. Invariant: every kept edge has exactly one tag andM.keysSet _depsProv == _depsat everyADDefsite. Emitted asdefinitionEdgesProvenance. -
Don't count authored vs. synthesised clauses via
clauseLHSRange. Some pass between type-checking and the backend hook runsKillRangeonClause, so every clause we receive hasnoRange— even clearly authored multi-clause defs. A--with-clause-originfield was prototyped and removed (every entry was(0, total)— a clause counter, not a signal). UsefunCompiled(CompiledClauses) or work upstream of the killRange pass instead. -
catchErrorcannot catch__IMPOSSIBLE__(exit 120); the partial pass guards withcatchAllTCM. Agda'sImpossibleis a GHC exception, not aTCErr.catchAllTCM(TCErr + GHC exceptions; re-throwsExitCode+ async) guards every stage — per definition, per module, per interface merge;preCompile/postCompileadd named diagnostics + rethrow. Companion: TCM'scatchErrorrolls back the wholeTCStateon failure, sopartialCompilerMainmust rebuild import state from the decoded interfaces viamergeIfaceState(signature alone isn't enough — missing builtins kill--with-signaturesreification ininfallibleSortKit); it merges builtins with per-prim rebinds, remote metas, pattern synonyms, display forms. The partial pass passesNotMainto every module's hooks (passingIsMainmadeentryModulerecord whichever module ran last). Don't simplify back tocatchError. Locked bytest-keepgoing/+ CI. -
Regenerate the golden from a COLD run (
rm -f test/*.agdaifirst — CI does). A warm-loaded main module skips thegetSignaturedead-private recovery and emits a poorer graph (the warm-.agdaiedge loss:Test.Int-0-class pattern helpers lose edges/kind/type). The loss is MAIN-MODULE-ONLY:stSignatureholds only the entry module's defs at backend time, so imported modules' emission is cache-state independent. -
--incrementalfragment invariants. Fragments must carry the module'signoredEdgesRef+methodProvidersRefcontributions — aSkipped module never runscompileDef, so without themcontractIgnoredEdgessilently loses every edge through its with-/where-helpers. The slices must be before/after deltas (ModuleEnvsnapshots), not name-prefix filters: Agda homes module-instantiation copies at bare_.…/prefixless QNames that no interface name prefixes (prefix slicing lost ~12k contracted edges on Jolteon). -
--packed-analyticalparity is by construction, andaccessis 3-valued. The analytical packed arrays (kinds/lines/access/types/subterm CSR) must agree node-for-node with expanded (the consumer treats them as interchangeable). Parity is guaranteed by sharing the per-QName lookups (mkDefKind/mkDefLine/… over the samedefsList) betweentoExpandedGraphandbuildGraphJson— don't inline them per-form.accessmust stay 3-valued (encodeDefAccess:0=unknown/absent,1=public,2=private): expanded omitsaccess/line/typefor QNames with no localADDef, and0round-trips to that omission; a 2-valued enum breaks the byte-identical gate on every external node. Gate:schema/packed_analytical_check.py(run both sides cold). -
--incrementalserialise skip (SerialiseCache). The monolithic no-op skip needs BOTH guards:not anyRecompiled(perrecompiledRef— guards per-definition content) AND a matchingoutputToken(module set + output-affecting options + build identity — guards everything else). Dropping either serves stale output, sooutputTokenmust list every output-affecting option (same discipline asNFData Options). The lazy per-module skip is content-epoch based (mdjEpoch, from the exact renderOne inputs without base64) and self-corrects on a global-index shift. Serialisation rides Agda'sEmbPrj(exactQName/NameIdround-trip); the byte layer (Agda.Utils.Serialize) is 2.9-only — on 2.8 the flag warns + no-ops. BumpfragmentFormatVersionwhen the payload shape changes;optionsFingerprintmust list every flag that changes fragment content. Disabled under--keep-going. The warm-rebuild floor is Agda's interface load, not serialise — this helps signature-/source- heavy output and no-op rebuilds, not the floor.
Every definition in the graph carries one of four states:
DDefined. Elaborated normally: aFunctionwith clauses, aDatatype,Record,Constructor.PPostulate. Body matchesAxiom{}— user-writtenpostulateor an Agda primitive. No operational content; type-level promise.HHole. Definition contains an unsolved meta. Three signals: the QName itself starts withunsolved#meta.(Agda'sopenMetasToPostulatesrewrites?into a synthetic postulate of that name under--allow-unsolved-metas), the def references such a name, or a syntactic walk ofdefType/theDeffinds an openMetaV(vialookupMetaInstantiation+isOpenMeta). The synthetic-name path is the one that actually fires in practice — by the time the backend runs, Agda has already rewritten user?s.FFailed. Module-level, not def-level. Synthesised by--keep-goingwhen a module's type-check raisedTCErr: marker node carrying the module name. Read as "we tried; the module's contents are not available" — neither "absent" nor "trustworthy".
Wire encoding: packed JSON → Int8 byte (0/1/2/3); expanded JSON +
the v2 graph.json consumed by HTML → letter "D" / "P" / "H" /
"F".
All HTML views consume the v2 schema; --format=json emits it
directly. The expanded form has a machine-readable JSON Schema at
schema/graph-v2-expanded.schema.json
(draft 2020-12). required covers the fields emitted since v2
inception; additive fields (nodeKeyVersion, producer,
definitionEdgesProvenance, definitionSubterm*, externals_summary,
per-def line/access/type) are optional, and additionalProperties
is open, so it validates older and forward-compatible output too. The
packed form and --lazy layout are not (yet) schematised.
Schema single-source-of-truth + drift check (do not hand-edit the
committed schema casually). The expanded wire shape is described once
in AgdaDeps.Backend.Wire (field tables + a small SchemaDoc ADT).
agda-deps --emit-schema regenerates the JSON Schema from it, and CI
(schema/check_schema.py) fails if that regenerated schema diverges
structurally (ignoring description/$id/$schema/title, sorting
required/enum) from the committed
schema/graph-v2-expanded.schema.json. So the committed file is a
frozen oracle: to change the wire shape you change Wire.hs, the
check fails, and you update the committed schema deliberately in the
same change — no silent schema drift. CI still also runs
check-jsonschema of freshly-generated expanded output against the
committed schema (conformance). Emission goes through the same field
tables: buildExpandedJson builds an ExpandedGraph and calls
encodeExpanded (= encodeObject expandedFields), which emits only
fields present in the tables. So a field cannot be emitted without being
in Wire.hs (hence in the generated schema), and the generated schema
cannot diverge from the committed oracle without CI failing:
emitted-bytes ≡ Wire.hs ≡ committed, by construction. Wire.hs is
also the single source of the expanded wire tags (wireState /
wireKind / wireAccess; GraphJson no longer carries its own
stateLetter/kindTag/etc.). The packed form and --lazy layout are
still hand-rolled in GraphJson and not covered. Two further guards:
Wire.validateExpanded (run by buildExpandedJson, which errors on
violation) asserts the cross-array length + edge-endpoint invariants the
schema can't express; and a committed golden
(test/golden/expanded.golden.json + schema/golden_check.py, CI-diffed)
catches content regressions (states/kinds/edges/…) after normalising
out build/layout/path-volatile fields. Three conventions for downstream
consumers:
-
Schema version. Every payload starts with
"v": 2. Expanded form additionally emits"schemaVersion": 2and"mode": "expanded". Refuse avyou don't recognise — silent decoding of a future v3 produces wrong-looking snapshots. -
Build provenance. Both forms also emit
"producer"(theBuildInfo.buildFingerprintof the emittingagda-deps) and"nodeKeyVersion"(theAgdaDeps.Deps.nodeKeyVersionnode-key convention version). Both additive/optional: absentnodeKeyVersionparses as1(pre-E1 collapsed-helper format). The schema version (v/schemaVersion) is for wire-shape compatibility;nodeKeyVersionis orthogonal — it tracks the node naming convention so a consumer can spot a stale-format cache whose wire shape is still v2 (the E1 fix changed helper names without changing the shape) and rebuild rather than serve results keyed by an older convention. -
JSON mode (
--json-mode=packed|expanded).- packed — adjacency in CSR form; per-def state in base64 typed
arrays. Best for 100k+ node projects. See
AgdaDeps.Backend.GraphJson.buildGraphJson. - expanded — arrays of records keyed by qname / module name. No
base64. Carries explicit
schemaVersion/mode. Includeskindper definition and thereexports[]array. Round 4 added an optionaldefinitionEdgesProvenance :: [Provenance]array parallel todefinitionEdges, whereProvenanceissignature | body | module-local | with | unknown(themodule-localvalue waswherebefore nodeKeyVersion 3 — see the Edge-provenance gotcha); older JSON without it parses cleanly (consumer treats the absence as "every edge isunknown").agda-depsnow emits this array on every expanded-mode output, so consumers can rely on its presence in fresh JSON; only legacy fixtures still hit the absence path. SeebuildExpandedJsonfor the emission,AgdaDeps.Deps.tagOnefor the per-edge classification. Under--with-signatureseach definition object additionally carries an optional"type"string (the reified type, not normalised, Agda's default printing — no--show-implicit; rendered inAgdaDeps.Deps.computeDefADviaprettyTCM, emitted bybuildExpandedJson). Absent without the flag, so default output is byte-identical.
- packed — adjacency in CSR form; per-def state in base64 typed
arrays. Best for 100k+ node projects. See
-
Lazy ingest path (
--lazy). Wire format split across files:graph.json— module-level skeleton:modules :: [String]moduleEdges :: [[Int, Int]]— pairs of indices intomodules.moduleFiles :: Map String FilePath— name →modules/<Module>.json. This is the manifest — walk it; never derive a URL from a module name directly (the Haskell side falls back todetail-<hash>.jsonfor non-safe names).bundleFiles :: Map String FilePath— name →snippets/<Module>.json, present only when--with-sourcewas set.
modules/<Module>.json— that module's defs (names,states,x,y) plus outgoing leaf edges withtargetModuleannotations. (The on-disk file is<Module>.json, not<Module>.detail.json; the manifest above carries the real name either way.graph.jsonalso emits the richer module-level fields the views consume —moduleStates,moduleDepth,modulePodLayout,fileTree/moduleTree,transitiveModuleEdges, CSRmoduleToFile/fileToModules— none of which the lazy schema is (yet) formally documented for.)
Lazy output requires HTTP serving (browsers block
fetch()onfile://).
- Inspiration / prior art for the HTML view: https://unimath.github.io/agda-unimath/VISUALIZATION.html.