# 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. ```toml # 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 ```console $ DOCSITE_GITEA_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: ```console $ 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) ```console $ curl -s http://192.168.178.103:8080/aura/design/INDEX | head
docsite / aura / Design Ledger
…rendered Markdown: prose, syntect-highlighted code, tables…
``` ### Implementation shapes (secondary) The config model and the core functions — shapes only; exact bytes are the planner's: ```rust // config/registry.rs — serde, NO deny_unknown_fields (forward-compatible) struct Registry { bind: String, gitea: String, project: Vec } struct Project { slug: String, repo: String, branch: String, docs: String, section: Vec
} struct Section { title: String, pages: Vec } // 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; fn fetch_raw(repo: &str, branch: &str, path: &str) -> Result; // 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 + parse `registry.toml`; build the slug→project and (slug, url-path)→PageRef lookup. A url path not present in any section is not a `PageRef` — it cannot be served (whitelist). - **`gitea`** — `branch_sha` (one call per repo per request cycle, itself briefly cacheable) and `fetch_raw` at that revision; read-only token in the `Authorization` header. `NotFound` is 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) and `page_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 new `sha`. - **`check`** — `check_all` drives `docsite check` and 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 `docs` root (`..`, absolute) → **refused** before any fetch. - `fetch_raw` → `NotFound` (source renamed/removed) → a **visible in-page error block** naming the missing `repo main:path`, never silent emptiness; logged. The page returns 200 with the error block (the broken reference is content, not a server fault) — but `docsite check` reports 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.toml` malformed → 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_all` over a registry whose fixture fetcher reports one `NotFound` → report lists it broken, exit code 1; all-ok → exit 0. - **Determinism / cache:** same `(repo,path,sha)` → byte-identical output; a changed `sha` → 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 serve` renders `aura/glossary`, `aura/design/INDEX`, and `data-server/README` live from Gitea `main`, each in the theme shell with nav from `registry.toml`. - [ ] An unlisted path (a `docs/specs` file) → 404; never served. - [ ] A `..`-bearing path → refused. - [ ] `docsite check` validates 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).