ADR 0015: discern map — an in-binary project-map browser with a hand-rolled terminal Markdown renderer
Superseded by ADR 0287. The in-binary browse, search, raw, export, and non-interactive decisions remain; the hand-rolled terminal parser and its approximate syntax contract do not.
Vocabulary amendment: Current pointers use
finish→done,docs→mapwhere it names the command, config, or tree, the retired product-category wording →discern, the gate, or the bar; the decision and reasoning are unchanged. Project Script vocabulary amendment: Current pointers use Project Script for the former project Recipe surface; the decision and reasoning are unchanged.
Presentation and interaction amendment (2026-08-13; ADR 0279): The browse → select → page product flow survives. Selection now routes through Discern's package-backed prompt choke point, and terminal tables render through the package
./cligraph.@cliffy/commandremains the parser; Discern has no direct Cliffy Table or prompt dependency. Command's own transitive Table lock node is not a presentation authority for the map browser.
Status: superseded by ADR 0287
Context
§discern scaffolds a map/ tree into every install (orientation, a glossary, a system map, design principles, ADRs) and grows it subtree-by-subtree with the document-subsystem skill. As that tree fills up it becomes the thing it was meant to be — the canonical map of the system — but reading it means knowing the numbering scheme and opening files by hand. There was no way to browse it: list what exists, search a growing set by name, and read a doc with its Markdown actually rendered.
Two constraints shaped the design. First, the audience is split: a human wants to explore interactively, while the coding agents that ground their work in these docs want to pull a specific doc or an index programmatically, without a prompt ever blocking them. Second, the installed discern engine is pure POSIX shell — an install has no Deno and no Node (that is the whole point of compiling src/ to a binary). Rich terminal rendering — colour, wrapping, OSC-8 links, boxed tables — is not something to attempt in the agent shell engine.
A viewer also has to render Markdown that is dense with snake_case identifiers (main_branch, schema_version). A naïve _emphasis_ parser mangles those, so "just pull in a Markdown library" is not obviously cheaper than owning the small subset we actually need.
Decision
§The map browser is discern map, a subcommand of the binary, with a hand-rolled, dependency-light Markdown→terminal renderer, serving one command to two audiences decided by the TTY.
- It lives in the binary, not the shell engine.
discern mapbrowses the install's ownmap/(found by walking up to the nearestdiscern.toml, the same anchorbin/agentuses). Thediscernbinary is already on PATH wherever it installed a project, so every install gets the viewer at no extra cost — with no new file undertemplates/and nothing added to the managed set. - The renderer is ours (
src/lib/markdown.ts): a focused subset — headings, paragraphs, inline emphasis/code/strikethrough/links, ordered/unordered/task lists, fenced code, blockquotes, GFM tables, HR — rendered to ANSI. It is a pure functionrenderMarkdown(md, {width, color}), unit-tested without a TTY. - Wrapping is computed on plain text; styling is applied after. Inline parsing yields styleless segments that flatten to a styled-character stream, which is word-wrapped on visible width and only then painted. So coloured and
--no-coloroutput wrap identically, and a link's escape codes never throw off a line length. - Underscores are conservative.
_/__only open and close on word boundaries (CommonMark flanking), and code spans are parsed first, so identifiers and anything in backticks survive verbatim. - One command, two surfaces. On a TTY with no target, an interactive, searchable picker (Cliffy
Select, type-to-filter) leads to a paged, rendered view ($PAGER, defaultless -R). Off a TTY, or when given a target/flag, it is non-interactive: a target renders to stdout,--rawprints the pristine source,--jsonemits a machine index (or a single doc's record with content),--listprints a plain table of contents. It never prompts when stdin or stdout is not a terminal. - Scoped to the user-facing tree when browsing. Internal/reference subtrees in
_-prefixed directories (_adr,_internal) are excluded from browsing, indexing, and single-page resolution, so the normal command stays focused on what an end user should read.--dir map/_adrtargets one explicitly when needed. The explicit concatenation surface is the exception:--export publickeeps the filter, while--export allincludes the entire tree and--export select --output <path>offers top-level sections in a terminal picker.
Explicit nos:
- Not a Project Script. An install has no Deno; reimplementing this in POSIX shell would be a worse renderer and a large managed-surface cost, for a command that is a developer convenience, not part of the gate.
- Not a Markdown dependency, and not a full CommonMark parser. The viewer renders the subset these docs use;
--rawis the exact-source escape hatch. - Not a bespoke full-screen TUI. Cliffy
Selectfor navigation and the user's$PAGERfor scrolling are robust, idiomatic, and far less terminal plumbing to own. (@cliffy/tableis the one new dependency, from the Cliffy family already in use, for table borders.) - Not auto-paging in scripts. The pager is gated on an interactive terminal, so piped and redirected output stays clean.
Consequences
§- Every install carrying the binary gains a docs viewer with zero template or managed-file footprint — it ships entirely inside
src/. - Agents get a stable, scriptable contract (
--json,--raw, a resolvable target, deterministic exit codes for not-found/ambiguous) that cannot hang on a prompt — the same human/agent split the gate already draws withdone --json. - Internal tooling can produce a deterministic, comment-delimited Markdown bundle on stdout (
--export public|all) or in an explicit output file. The MCP tool remains index/page-oriented so an accidental call cannot return the whole documentation corpus. - We now own a Markdown renderer. Exotic input (deeply nested blockquotes, setext headings, raw HTML blocks, reference-style links) renders approximately rather than perfectly. That is acceptable for a viewer of our own docs, and bounded: the renderer is a pure function with focused tests, and
--rawalways yields the true bytes. - Rendering and discovery are independently testable pure functions, so the interactive shell (the one part that needs a TTY) is the only thin, untested seam — covered manually and by the non-interactive paths it shares.