The walking-skeleton design spec: live-render aura + data-server docs from Gitea main, dark-themed, navigation from a DocSite-owned whitelist registry, with the code + table render directives. Source stays once on the Gitea host; DocSite holds only a revision-keyed cache and never writes to a source. grounding-check returned BLOCK (greenfield: a first spec has no existing tests to ground against); user overrode after the one substantial external assumption — the source files present on the foreign repos' main — was verified (aura a517899, data-server 694f96f). Decision log on the issue. refs #1
10 KiB
Walking Skeleton — Design Spec
Date: 2026-06-28
Status: Draft — awaiting sign-off (under /boss: the Step-5 grounding-check PASS)
Authors: orchestrator + Claude
Goal
The first runnable DocSite server: render the published docs of aura and
data-server live from their canonical Gitea repositories (pushed main),
wrapped in one consistent dark theme, navigated from a DocSite-owned
whitelist. This is the minimal end-to-end proof of the single-source
live-render pipeline — content stays once on the Gitea host, DocSite holds
only a revision-keyed cache, and it never writes to a source repo.
Two render directives are in scope: code (syntax-highlighted fences) and
table (native Markdown). code-include, mermaid, and chart/graph
embedding are explicitly out of scope (later iterations; embedding is gated
on Brummel/Aura#150).
Architecture
A single Rust binary docsite with two subcommands:
docsite serve— runs the axum HTTP server.docsite check— validates every configured navigation reference against the current Gitea sources and exits non-zero if any is broken.
Layers:
- Config (
registry.toml, DocSite-owned) — lists the published projects, each with its Gitea coordinates (repo,branch,docs) and an ordered set of nav sections (the publish whitelist). - Gitea source client — read-only. Resolves a branch's current commit SHA, then fetches a file's raw bytes at that revision. Authenticated with a read-only token from config/env for private repos.
- Render — comrak (GitHub-flavored Markdown, tables on) → syntect highlighting of code fences (dark theme) → HTML body.
- Theme — one HTML shell (header breadcrumb · nav from registry ·
content) plus a static stylesheet served at
/assets/theme.css. - Cache — keyed on
(repo, path, commit_sha); a rendered page is a pure function of the source bytes at a revision, so a cache hit is byte-identical and revision-exact.
The render pipeline exposes a directive-expansion seam (an AST walk step)
that is a no-op in this iteration — code and table are native Markdown
— so the closed-vocabulary handler registry (invariant 4) attaches here
when code-include lands, without reshaping the pipeline.
Concrete code shapes
The user-facing artifact: registry.toml
This is the configuration the maintainer (the AI) writes; it is the acceptance evidence — adding a project to DocSite is editing this file, no code change.
# DocSite/registry.toml
bind = "0.0.0.0:8080"
gitea = "http://192.168.178.103:3000" # Gitea base URL
# read-only token resolved from $DOCSITE_GITEA_TOKEN (never stored here)
[[project]]
slug = "aura"
repo = "Brummel/aura"
branch = "main"
docs = "docs" # doc-root within the repo
[[project.section]]
title = "Concepts"
pages = ["glossary.md"]
[[project.section]]
title = "Design & Architecture"
pages = ["design/INDEX.md"] # project-layout.md is obsolete — omitted
[[project]]
slug = "data-server"
repo = "Brummel/data-server"
branch = "main"
docs = "." # README at repo root
[[project.section]]
title = "Reference"
pages = ["README.md"]
CLI invocations
$ DOCSITE_GITEA_TOKEN=<ro-token> docsite serve
docsite: serving 2 projects on 0.0.0.0:8080
docsite: validated 3 nav references against Gitea main
$ docsite check
aura/glossary.md ok (main @ a1b2c3d)
aura/design/INDEX.md ok (main @ a1b2c3d)
data-server/README.md ok (main @ e4f5a6b)
3 references ok, 0 broken
$ echo $?
0
A broken reference (a source file renamed/removed upstream) is the fail-loud case:
$ docsite check
aura/glossary.md ok (main @ a1b2c3d)
aura/design/INDEX.md BROKEN — not found at Brummel/aura main:docs/design/INDEX.md
data-server/README.md ok (main @ e4f5a6b)
2 references ok, 1 broken
$ echo $?
1
Request → response (the served page)
$ curl -s http://192.168.178.103:8080/aura/design/INDEX | head
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><link rel="stylesheet" href="/assets/theme.css">…
<body>
<header class="crumb"><a href="/">docsite</a> / <a href="/aura/">aura</a> / Design Ledger</header>
<nav>…sections from registry…</nav>
<main class="doc">…rendered Markdown: prose, syntect-highlighted code, tables…</main>
Implementation shapes (secondary)
The config model and the core functions — shapes only; exact bytes are the planner's:
// config/registry.rs — serde, NO deny_unknown_fields (forward-compatible)
struct Registry { bind: String, gitea: String, project: Vec<Project> }
struct Project { slug: String, repo: String, branch: String, docs: String,
section: Vec<Section> }
struct Section { title: String, pages: Vec<String> }
// A page reference resolved to its Gitea location.
struct PageRef { slug: String, rel: String } // rel is relative to project.docs
// gitea/client.rs — read-only
fn branch_sha(repo: &str, branch: &str) -> Result<String, GiteaError>;
fn fetch_raw(repo: &str, branch: &str, path: &str) -> Result<String, GiteaError>;
// GiteaError::NotFound is the broken-reference signal (distinct from transport errors)
// render/mod.rs
fn render_doc(markdown: &str) -> String; // comrak(GFM) + syntect; pure
fn page_html(reg: &Registry, page: &PageRef, body: &str) -> String; // theme shell + nav
// check.rs — the reference validator behind `docsite check`
fn check_all(reg: &Registry) -> CheckReport; // exit code = (broken == 0) ? 0 : 1
Components
config— load + parseregistry.toml; build the slug→project and (slug, url-path)→PageRef lookup. A url path not present in any section is not aPageRef— it cannot be served (whitelist).gitea—branch_sha(one call per repo per request cycle, itself briefly cacheable) andfetch_rawat that revision; read-only token in theAuthorizationheader.NotFoundis modelled distinctly from a transport failure so a broken reference and an unreachable Gitea render differently.render—render_doc(pure: comrak GFM with tables, syntect dark theme on fences, unknown language → plain fenced fallback) andpage_html(wrap in the theme shell, inject nav from the registry).theme— the shell template +theme.css(dark background, Catppuccin accents, prose in a reading face, code/tables monospace), served statically.server— axum router:GET /(project index from registry),GET /:slug/*page,GET /assets/theme.css.cache—(repo, path, sha)→ rendered HTML; invalidated implicitly by a newsha.check—check_alldrivesdocsite checkand the startup validation pass.
Data flow
GET /aura/design/INDEX
-> config: slug=aura -> project; url-path "design/INDEX" in a section?
no -> 404 (whitelist)
yes -> PageRef{ slug:"aura", rel:"design/INDEX.md" }
-> gitea.branch_sha("Brummel/aura","main") -> sha
-> cache.get((repo, "docs/design/INDEX.md", sha))
hit -> serve
miss -> gitea.fetch_raw(repo,"main","docs/design/INDEX.md")
NotFound -> visible error block (fail-loud), log
ok -> render_doc -> page_html(nav) -> cache.put -> serve
GET /assets/theme.css -> static
GET / -> project index from registry
Startup (serve) runs check_all once and logs any broken reference
without refusing to boot (a single broken page must not down the whole
site); docsite check is the same validation as a standalone exit-coded
command for a hook/cron.
Error handling
- Unknown slug, or a url-path absent from every section → 404
(whitelist;
docs/specs/docs/plans/postmortems are unreachable by construction). - A page path that escapes the project's
docsroot (.., absolute) → refused before any fetch. fetch_raw→NotFound(source renamed/removed) → a visible in-page error block naming the missingrepo main:path, never silent emptiness; logged. The page returns 200 with the error block (the broken reference is content, not a server fault) — butdocsite checkreports it as broken with a non-zero exit.- Gitea unreachable / transport error → 502 with a clear message
(distinct from
NotFound). - syntect unknown language → plain fenced fallback, no failure.
registry.tomlmalformed → refuse to start with a precise parse error.
Testing strategy
- Hermetic by default. The Gitea client is fed by an injected fetcher (a trait or fn pointer) so tests run against in-repo fixtures, not the live Gitea or the foreign repos — tests stay deterministic and offline.
- E2E render: a fixture project + fixture Markdown (prose + a code fence + a table) → assert the served HTML carries the theme shell, the nav from the registry, syntect-classed code, and table markup.
- Whitelist negative: a path not in any section (e.g. a
docs/specs/…name) → 404; never leaked. - Traversal: a
..-bearing path → refused (no fetch attempted). - Reference check:
check_allover a registry whose fixture fetcher reports oneNotFound→ report lists it broken, exit code 1; all-ok → exit 0. - Determinism / cache: same
(repo,path,sha)→ byte-identical output; a changedsha→ re-render. - Gated live smoke: one test hitting the real Gitea for
aura/glossary.md, skipped cleanly when Gitea is unreachable (the project's skip-on-no-dependency convention).
Acceptance criteria
Per DocSite's default feature-acceptance criterion (solves a real
user-facing problem; contradicts no stated design commitment): the
maintainer publishes a project's docs by editing registry.toml alone
(shown above) — no code change — and the served pages honor all five domain
invariants. Concretely:
docsite serverendersaura/glossary,aura/design/INDEX, anddata-server/READMElive from Giteamain, each in the theme shell with nav fromregistry.toml.- An unlisted path (a
docs/specsfile) → 404; never served. - A
..-bearing path → refused. docsite checkvalidates the configured nav references against the current Gitea sources, reporting broken ones with a non-zero exit.- Same source revision → byte-identical rendered output.
- DocSite performs no write against any source repo (read-only Gitea access only).