Writing Research Docs
How to write a research catalog under docs/research/<topic>/ that matches the existing corpus. The reference implementations are docs/research/async-io/ and docs/research/monorepo-tooling/ — between them they show every shape a tree takes: a breadth-first .md per subject, a subject deepened into its own directory with companion files (async-io's io-uring/ and its examples/ of runnable programs), and a topic that is per-subject-subdirectory throughout (monorepo-tooling, for its co-located samples). When in doubt, open one and imitate it — a new tree should be stylistically indistinguishable.
NOTE
This guide is about the shape and conventions of research docs, not their content. It complements DDoc (for in-source docs) and the Code Style guide (for code). For where each kind of doc lives, see AGENTS § Where docs live.
IMPORTANT
Research here is grounded twice over: every claim is tied to a primary source (a cited file path or URL, usually with a verbatim quote), and wherever a behaviour can be demonstrated it is backed by a runnable example that CI compiles and runs. Prose drifts; a CI-tested example cannot — if the API or behaviour a deep-dive describes stops working, the build goes red. Lean on runnable, CI-verified examples heavily (see Runnable samples and examples).
When to use docs/research/
docs/research/ holds background surveys that inform a design — breadth-first maps of how other languages/libraries/tools solve a problem Sparkles is about to tackle. A research tree is the evidence base; the design decision it feeds usually lands later in docs/specs/ or in a library's docs/libs/<name>/ tree.
Use it when you are surveying prior art (e.g. "how do other ecosystems do workspaces", "TUI rendering models", "async I/O runtimes"). Do not use it for how-to/reference/tutorial material for a Sparkles library (that is Diátaxis material under docs/libs/<name>/) or for cross-cutting agent/style guides (those live here in docs/guidelines/).
Catalog anatomy
A research catalog is one directory per topic under docs/research/<topic>/, tied together by an index.md umbrella and registered in the VitePress sidebar. Every topic gets its own directory — research never lives as loose files directly in docs/research/.
Within a topic, each subject (a surveyed system, sub-area, or theme) starts as a single breadth-first file and graduates to its own subdirectory as the research deepens:
docs/research/<topic>/
├── index.md # umbrella (always present)
├── concepts.md # shared vocabulary (optional)
├── <subject>.md # starting form: one breadth-first file per subject
├── <subject>/ # graduated form: the subject now carries companion files
│ ├── index.md # the deep-dive
│ ├── sample/ … # a runnable workspace, and/or
│ ├── examples/ … # standalone runnable programs, and/or
│ └── <sub-topic>.md # its own cluster of sub-deep-dives
└── comparison.md # cross-subject synthesis (the capstone)<topic>/<subject>.md— the default starting point while the survey is still breadth-first. Most ofasync-io/stays here (glommio.md,tokio.md, …).<topic>/<subject>/index.md+ companion files — once a subject needs to carry more than prose: a runnablesample/workspace (every subject inmonorepo-tooling/), anexamples/directory of standalone programs (async-io'sio-uring/, whoseexamples/holds ~40 runnable.dprograms the deep-dive links to), or its own sub-deep-dives (io-uring/{features,timeline,opcodes-reference}.md;coroutines/stackless/+stackful/). The prose conventions are unchanged; the file just moves from<subject>.mdto<subject>/index.md, deepening relative-link depth by one.
The rule of thumb is start flat, deepen on demand: reach for a subject directory the moment the subject acquires companion files or its own sub-tree — not before. A topic freely mixes the two forms (async-io has flat glommio.md beside the deepened io-uring/); monorepo-tooling/ went all-subdirectory from the start only because every subject was getting a sample/.
Express a subject's category as a column in the master catalog and a grouping in the sidebar (see VitePress integration), not as an extra directory level — keep the topic→subject nesting shallow.
The index.md umbrella
Model on async-io/index.md and coroutines/index.md. It contains, in order:
- A framing paragraph, then an explicit "this survey answers N questions" list linking to the docs that answer each.
- A
**Last reviewed:** <Month Day, Year>line (absolute date — convert any relative date). Every umbrella and synthesis doc carries one. - A Master Catalog table — one row per subject, with the columns that matter for the topic (always include a Link column pointing at the deep-dive). Mark forward-dated or uncertain entries explicitly.
- Taxonomy tables that re-cut the same set by one axis each (e.g. by I/O model, by dependency-isolation model), every row linking back to deep-dives.
- A Milestones timeline of when key capabilities landed across the field.
- Quick navigation / suggested reading paths (including a path for "I'm designing the Sparkles feature this informs").
- A Sources section + the reference-link block.
The per-subject deep-dive
Model every deep-dive on async-io/glommio.md. Fixed skeleton:
# <Subject> (<Language/Ecosystem>)title + a one-sentence positioning line.- A metadata table near the top (Language, License, Repository, Documentation, Category, plus topic-specific rows like Workspace model, First/Latest release).
## Overview—### What it solvesand### Design philosophy, with at least one verbatim quote from the source tree or official docs, cited to a real file path or URL.## How it works— the real mechanics, identifiers in backticks, short fenced config/code excerpts (label the fence:toml,json,yaml,bash,sdl, …).- A fixed analysis spine — the same set of subsections in every deep-dive of the tree, so the catalog is comparable. (In
monorepo-tooling/that spine is five dimensions: workspace declaration & topology, dependency handling & isolation, task orchestration & scheduling, caching & remote execution, CLI/UX ergonomics. Pick the spine that fits your topic and apply it uniformly.) Where a dimension doesn't apply to a subject, say so and explain why — the absence of a feature is itself a finding. ## Strengths/## Weaknesses(bulleted).## Key design decisions and trade-offs— a three-columnDecision | Rationale | Trade-offtable.## Sources— bulleted primary sources, then the reference-link block.
Be primary-source-driven: read the real source tree or official docs and cite real paths/URLs, not general impressions. Match the density and declarative, citation-heavy tone of the exemplars — never hand-wavy.
Concepts, synthesis & proposal docs
Beyond the deep-dives, a mature tree usually has:
concepts.md— the shared vocabulary the deep-dives reference, each term defined once and grounded in real examples (cf.async-io/primitives.md+techniques.md).comparison.md— the capstone synthesis: an at-a-glance master table, a per-dimension comparison, the consensus standard, the architectural trade-offs, and — when the survey targets a specific Sparkles gap — an explicit delta table mapping each modern capability to where Sparkles stands today. This bridges into any proposal.- A baseline / proposal pair (optional, when the survey drives a concrete feature) — e.g.
monorepo-tooling/'sdub-baseline.md(the system under improvement) anddub-proposal.md(a milestoned plan, each milestone cross-linking the prior art it borrows from).
House style
Enforced partly by the pre-commit hooks and the VitePress build, partly by convention:
- Reference-style links, collected under an HTML comment
<!-- References -->at the very bottom of the file (copy the pattern from any existing deep-dive). Mind the depth: from a<subject>.mda sibling is./<other>.md; from a deepened<subject>/index.mda sibling subject is../<other>/, the umbrella is../, and another tree is../../<tree>/<file>.md. Every link must resolve. - Backtick every identifier — filenames, flags, config keys, type/command names (
dub.sdl,--filter,[workspace],pnpm-workspace.yaml). The whole corpus does this religiously; it also sidesteps Prettier's underscore-in-emphasis mangling. - Link every term to its definition. A reader should be able to click any identifier, operation, or proper noun and land on the authoritative source — a section in the same doc, another page under
docs/, or the canonical external reference (official API docs, a spec, or the upstream source). Theexpectedcheat sheet is the exemplar: every operation links to the section that explains it, and every D/Rust API name links to its official docs. In a research tree this is why the master catalog and comparison tables link each subject to its deep-dive, and why each deep-dive'sSourcesblock carries the external references behind its claims. - Use GitHub alerts (
> [!NOTE],> [!IMPORTANT],> [!WARNING]) for scope notes and caveats. - A
**Last reviewed:**date on every umbrella/synthesis doc; absolute dates everywhere; mark forward-dated/uncertain timeline entries. - Column-aligned tables; dense, declarative, source-grounded prose.
VitePress integration
The site build is the gate for link and markup correctness — npm run docs:build must be green before you commit a tree, and it is the fastest way to catch the gotchas below.
Register the tree in
docs/.vitepress/config.mtsunder theResearchsidebar section: a top-level entry linking the umbrella, with subjects grouped by category into nested collapseditems(see howui-layoutandmonorepo-toolingare grouped). A<slug>/link resolves to itsindex.md.Markdown is compiled as a Vue template, so a few constructs bite:
text• Bare <word> in prose parses as an HTML tag → backtick it or rephrase. Single-line inline code like `<member>` is fine (escaped); a code span that BREAKS ACROSS A LINE while containing <...> is NOT — keep such spans on one line. • {{ ... }} is a Vue interpolation, even inside inline code. To show it literally, put it in a fenced code block, or wrap the span in <span v-pre>...</span>. • Unknown code-fence languages (ninja, just, meson, …) fall back to plain text with a harmless warning. languageAlias them ONLY to a real bundled grammar (e.g. starlark → python); aliasing to a non-grammar (text/make) turns the warning into a hard build error.ignoreDeadLinks— links to source artifacts that aren't built pages (.dfiles, asample/directory) must be added to theignoreDeadLinkspatterns inconfig.mts, the same way the existing/\.d$/rule works.
Co-located runnable samples and examples
A subject that has graduated to its own directory can co-locate runnable code — the strongest grounding the corpus has. The engine is the repository's ci helper (apps/ci), which makes example code self-testing in two ways, both run in CI:
- Markdown-embedded examples. A fenced
dblock written as a single-filedubprogram, immediately followed by an[Output]block.ci --verifyextracts the snippet from the.md, compiles and runs it, and diffs its stdout/stderr against the[Output]block — so the numbers and text quoted in the prose are provably what the code actually prints. For dynamic output, a<!-- md-example-expected -->comment carries a wildcard pattern while the[Output]block keeps reader-friendly literals. Full convention: AGENTS § Runnable README examples. - Standalone
examples/programs. A directory of single-filedubprograms (#!/usr/bin/env dub+ an embeddeddub.sdl), each demonstrating one claim from the deep-dive.ci --example-files(-x) compiles and runs them, and the directory is registered in the helper's defaults (alongsidelibs/core-cli/examples/*.d) so it runs on every pass — no example is "documented but dead". async-io'sio-uring/ships ~40 (nop.d,tcp-echo.d,multishot-accept.d, …).
The discipline is the same either way: if the API or behaviour a deep-dive describes stops working, the example fails to compile/run (or its output stops matching) and CI goes red. To keep that signal honest:
- Cross-link example ↔ prose — each standalone example's header comment points at the deep-dive section it backs, and the deep-dive links to the example. Links to the
.dfiles pass the link checker via theignoreDeadLinks/\.d$/rule. - Keep examples portable-green — gate on
platformsin thedub.sdlheader, and where the host may lack a capability, print aSKIP:line and exit0instead of failing (theio_uringexamples skip on too-old kernels). A red CI from a missing host capability would defeat the purpose. - A shebang example file must be executable (
git add --chmod=+x …) or thecheck-shebang-scripts-are-executablehook blocks the commit.
sample/ workspace fixtures
A sample/ is the non-executed cousin: a minimal, idiomatic workspace co-located with a deep-dive to make its config concrete (every monorepo-tooling/ subject; see cargo/sample/). It would run with the toolchain installed, but CI does not execute it — its job is to be correct and illustrative. Rules:
- Minimal and real: a root manifest + two members with a genuine local cross-reference + one task.
- Source only: never commit build artifacts or dependency stores (
node_modules/,target/,dist/,.dub/, …); add them to.gitignore. - No
.mdundersample/— keep all prose in the deep-dive, so the link checker and VitePress don't try to build fixture files. (If a sample must contain.md, add asrcExcludeglob inconfig.mts.) - Match each ecosystem's native formatting so the hooks pass: per-file
.editorconfigindent (4-space Rust/D, tabs for Go/Makefile, 2-space JSON/YAML/JS), a final newline, no trailing whitespace. - JSON formatter tension: Prettier and the
pretty-format-jsonhook disagree on array style. Keep sample JSON inpretty-format-json's canonical form (sorted keys, expanded arrays) and add those files to.prettierignore(negatingpackage.jsonback in) so the two stop fighting.
Citations & link-checking
- Cite primary sources — real repository file paths and official-doc URLs — and quote verbatim where it carries weight. Each deep-dive's
Sourcessection is its provenance. - Pin flaky external links to
web.archive.org. Some hosts rate-limit or reject the link checker; replace such a URL with a verified Wayback snapshot (https://web.archive.org/web/<timestamp>/<url>— confirm the snapshot is HTTP 200, e.g. via the CDX index). For hosts that should be ignored everywhere, add a pattern to the sharedlychee.excludefile at the repo root. - The
lycheelink checker is slow and flaky on large external sets; it is reasonable toSKIP=lycheeat commit time and let CI run it (see below).
Hooks & committing
The pre-commit hooks (AGENTS § Pre-commit hooks) shape every commit:
- prettier re-flows markdown and re-aligns tables; re-check literal-data tables afterward (it has corrupted underscores). It owns markdown/JSON/YAML formatting.
- editorconfig-checker checks indentation. Markdown indent is exempt, but a fenced block with required tabs (e.g. a Makefile recipe) needs a per-file
indent_style = unsetoverride in.editorconfig. - fix-markdown-reference-links de-duplicates reference URLs; pretty-format-json and the
check-*validators police the sample configs. - lychee (external links) and verify-md-examples (OOM-prone; a no-op when a doc has no
[Output]examples) are the two it's reasonable to bypass for a large docs drop:SKIP=lychee,verify-md-examples git commit ….
Commit the catalog as a coherent unit (the deep-dives and synthesis docs cross-link densely, so they only build green together), with the VitePress/.editorconfig/ .gitignore prep in a separate preparation commit ahead of it. Follow the repo's commit-message and git-hygiene conventions — scope research-doc commits docs or research.
Authoring checklist
- [ ] Tree registered in the VitePress sidebar, grouped by category.
- [ ]
npm run docs:buildis green (no dead links, no Vue/mustache compile errors). - [ ] Every deep-dive follows the skeleton and the tree's fixed analysis spine.
- [ ] At least one verbatim, cited primary-source quote per deep-dive.
- [ ] Umbrella + synthesis docs carry a
**Last reviewed:**date. - [ ] Reference-style links, all resolving; identifiers backticked.
- [ ] Every term links to its definition — a same-doc section, another
docs/page, or the canonical external reference (theexpectedcheat sheet is the model). - [ ] Demonstrable claims are backed by a runnable example the
cihelper compiles and runs — a markdown[Output]example (ci --verify) or a standaloneexamples/*.d(ci --example-files); examples stay green cross-platform (SKIP:/platforms). - [ ] Samples are source-only illustrative fixtures; artifacts git-ignored; hooks pass.
- [ ] Flaky external links pinned to
web.archive.orgor excluded inlychee.exclude.