diff --git a/docs/specs/0001-walking-skeleton.md b/docs/specs/0001-walking-skeleton.md new file mode 100644 index 0000000..8c987b1 --- /dev/null +++ b/docs/specs/0001-walking-skeleton.md @@ -0,0 +1,258 @@ +# 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).