Files
DocSite/docs/specs/0001-walking-skeleton.md
T
Brummel 1fb3e0883a spec: 0001 walking-skeleton
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
2026-06-28 14:31:56 +02:00

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 + 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).
  • giteabranch_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.
  • renderrender_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.
  • checkcheck_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_rawNotFound (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).