nav — static navigationThe cheapest, highest-value piece, needing neither LSP nor a REPL. Verified:
babashka bundles rewrite-clj with position metadata, so this is a zero-new-dependency starting point.
outline — one entry per top-level form. Turns a 900-line namespace into 40 lines and forces a decision about
what to read. Include (comment …)
blocks; that is where the REPL experiments live.
The runtime half built 2026-08-05 (outline.clj, surfaced as __r/outline): everything the glance needs —
arglists, docstrings, :line, the macro/private/dynamic flags — already sits on the loaded vars, with no file
plumbing and no parse. {:ns … :doc … :vars […]} in source order, privates included and marked (the gist of a
namespace, not just its API), the ns docstring's first sentence as orientation, :include-docstrings carrying
every docstring whole. The default flipped twice on 2026-08-05: docstrings-on when this call was the initialize
instructions' bootstrap and both fresh tenants stripped the option; back to the signatures-only glance the same
day, when the rendered manual moved into the instructions themselves and outline returned to being purely a
reading tool. :match (string case-insensitive, or regex) filters by name or docstring; in the glance mode
docstring hits quote their matching lines as :matched — the answer shows its evidence, grep-style.
Namespace designators: symbol, string, the namespace itself, or an alias resolved against the eval's *ns* —
(outline '__r) works wherever the eval has navigated, which is what lets the initialize instructions bootstrap
("start with (__r/outline '__r)") stay a one-liner. defmethod defines no var, so a var holding a MultiFn lists
its :dispatches — without it, mcp's whole handle surface would be invisible. :line survives AOT (measured:
this JVM's compiled clojure.core reports map at 2744), though following it needs the source on the classpath.
The view is the running system's, not the file's — measured immediately: the live registry still interns
announcements!, a var renamed told! on disk long ago, because :reload never unmaps. Honest for a runtime tool,
and the reason the source outline planned above (unloaded files, comment blocks, the file as written) remains
separate, unbuilt work. Two options only, both instances of the one knob §7 permits — how much crosses into
context; :match searches text bare mode deliberately doesn't ship.
nses and source built 2026-08-05, completing the drilldown gradient: nses → outline → doc/source,
system → namespace → var, every step the same two moves (glance, then match). nses (in outline.clj, reusing its
matcher; surfaced as __r/nses) is the missing entry point: every loaded namespace as a sorted vector of bare
symbols — plain composable data, deliberately names-only. Measured: 94 namespaces in this JVM, ~2.1k chars — the
no-arg form is safe unfiltered. Loaded namespaces, not the classpath; classpath enumeration is the source half's.
source (an __r macro over clojure.repl/source, the doc sibling — printed, not returned) is the first
written-half reader, needing no rewrite-clj because verbatim text is a resource read: source-fn follows
:file/:line to the classpath, which works for anything shipped as source — verified on remontoire's own vars
(privates included, consistent with outline), on q/timeout (a dependency, full source with comments), and
prints Source not found for AOT-only vars and unresolvable names alike. The live/written distinction stays
visible at the call site: outline/doc say what is live, source says what is written, and after a hot reload
they can differ — the pair, not a switch. Motivating context: pattern namespaces (real, loadable, testable code
under something like co.multiply.remontoire.patterns.*) become indexable by (nses \"patterns\") and readable by
the same tools as everything else — the single-source doctrine a third time, since a pattern that stops compiling
breaks the load instead of quietly rotting. Multi-eval recipes want the whole-file grain ((comment …) blocks are
invisible to var-based source) — that reader arrives with the pattern book.
file built the same day — the whole-file grain: a namespace's source file, verbatim, as a string. Feasibility
measured first: Clojure has no semi-compiled intermediate — libraries ship the actual .clj/.cljc files as jar
resources (even AOT'd clojure.core carries clojure/core.clj verbatim; only a deliberately source-stripped jar
loses it), and a var's :file metadata is the classpath-relative resource path, straight from the loader. Lookup
order: munged namespace path first (.clj then .cljc), so a namespace assembled from several files — clojure.core
loads five — answers with its defining file; var :file metadata is the fallback for unconventional layouts; no
hit throws a plain explanation carrying :tried. Returns the string rather than printing, unlike doc and
source — settled on the channels' actual semantics: the value renderer's caps (*print-length*/*print-level*)
bound collections, not string length, so a returned string never truncates but arrives pr-str'd with escaped
newlines; the output bank is the mirror image — verbatim but capped at 32k, announced. So composition (the
primitive: split-lines, drop-while to a section) gets the return value, and reading is the one-wrap incantation
(println (file 'some.ns)) through the capped channel. The exposed footgun, named in the docstring: a bare
(file 'clojure.core) returns 276k chars (measured) with nothing to bound it until shape — "mind the size,
count is cheap, slice before returning". Verified: remontoire .clj, quiescent .cljc via munge, clojure.core's
defining file, string and alias designators, the no-file throw, and the compose path landing drop-while on a
named defn in one expression. Locally io/resource resolves through the directory classpath at call time, so
file reads the current disk bytes — verified with a marker comment seen mid-session with no reload — which
gives the live/written pair diagnostic teeth: file vs outline disagreement is the edited-but-not-reloaded
state. Deployed from a jar, frozen at build. nses also grew symbol leeway the same day (a symbol is matched as
its name), making (nses 'co.multiply.quiescent) the is-it-really-loaded check — it answers with the loaded
family, [] for not loaded.
The pattern book built 2026-08-05 — co.multiply.remontoire.patterns.*, six recipe namespaces (cleanup,
bounding, partial-results, integrations, outliving, parallelism) under an umbrella ns that requires them all;
mcp requires the umbrella so the drilldown finds them — being loaded is what cashes "breaks the load instead of
quietly rotting," and it is also the discipline: defns and (comment …) blocks only, nothing effectful at top
level, because these load inside the host JVM at boot. Each recipe is real code — with-resource (the loan
pattern), bounded (q/timeout, and the ns docstring carries the await-vs-timeout distinction: await bounds
the response, timeout bounds the work), reporting (progress via the output rails), from-cf (callable proof
a CompletableFuture grounds bare), detached (compel-around-construction as a macro, making the inline-ness
syntactic), gated (bounded fan-out) — with walkthroughs in comment blocks written as eval code, assuming the
scratch refers and __r, readable through file where var-grain tools cannot see them. All verified live on
build day: the loan released on the cancellation path (the reason finally is the recipe), both bounded arms,
a CF grounding to its value through the entry, 8 results in order through a 3-wide gate. Only quiescent and the
JDK appear in compiled positions — core.async stays inside a comment block, honest about being host-conditional.
The ambient pointer is one instructions sentence: (__r/nses \"patterns\") is the index.
vars built 2026-08-07 — the global entry point the first in-situ smoke test found missing: "where does that
function live?" required guessing namespaces, the one call the tenant reached for and didn't find (ranked first on
its wishlist by felt friction). outline's :match machinery mapped across all-ns: hits are outline entries with
:ns attached, ordered by namespace then source line, docstring hits quoting their :matched evidence as ever. A
match is required — the JVM's whole var population is no glance — and it takes what nses takes: string
(case-insensitive substring), regex, or a symbol matched as its name, so (vars 'normalize-str) is the natural ask.
Hits are scored into three tiers — exact name match, name hit, docstring-only hit — namespace-then-source-line
order within each tier: the definition asked for leads, the mentions follow, and the shape stays one flat vector
(the ordering is the ranking; no score field, no second result shape). Build day shipped grep-style flat order,
unranked — a call that sounded principled and was falsified by first use the same day (see the logbook). No index,
nothing to go stale: linear
meta reads at call time, measured fast enough not to matter (see the logbook). The composability invariant holds by
construction — a hit's :ns designates for outline and file, and (symbol (str ns) (str name)) feeds
doc/source.
form — exact source, and critically including the preceding comment block and any metadata or >defn spec.
Cutting those off is precisely the damage line-guessing does today.
changed-forms — git diff -U0 → changed line numbers → enclosing top-level forms, emitted whole. This is the
direct answer to "I updated function F, have a look", and it is roughly 30 lines of code. Run kondo on the result and
inline the diagnostics; it is already parsing those forms.
Use the clj-kondo analysis database rather than text search: it resolves aliases, separates definition from usage, and disambiguates same-named vars across modules.
Verified against fluent-platform: its .clj-kondo/config.edn:31-46 already
lint-ases e/defn, e/def, defdepot, defpstate, defresource,
defservice, plus real hooks for Rama, Electric hooks, guardrails and uix. Both
($ nav2/Routes) (alias-qualified) and ($ Reroute) (unqualified, same-ns)
resolve correctly in var-usages. So the Electric $ macro is not the blind spot it appears to be.
The real blind spot is components and handlers held as values.
src/co/multiply/client.cljc:126 does
(if-let [InitState (get-in +route-match [:data :nav/state])] ($ InitState)), and
:128 the same with Handler. Those are locals, correctly absent from
var-usages. Any registry or route table storing a component is invisible. Therefore:
Once usages returns re-feedable symbols, graph queries become caller code rather than library features:
;; everything that transitively reaches execute-tool
(loop [frontier #{'co.multiply.module.driver.impl/execute-tool} seen #{}]
(let [next (into #{} (comp (mapcat nav/usages) (keep :sym) (remove seen)) frontier)]
(if (empty? next) seen (recur next (into seen next)))))
Run inward it is wedge diagnosis; run outward from entry points it is tree-shake reachability, which previously had to be hand-verified.
:sym can be nil — usages at top level have no enclosing var (a def
referencing another var, (comment …) bodies, defmethod dispatch); kondo omits
:from-var there. Design it in: mark such sites :top-level true so the reason
is visible, and keep is the idiom.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |