audit: cycle 0001 (walking skeleton) close

Architect drift review (0060fea..HEAD) against the five domain invariants and
the cycle spec. What holds: read-only Gitea access (the Fetcher exposes no
write path); whitelist + traversal refusal (config::resolve, e2e-pinned); the
single docs-root join shared by serve and check, so check validates exactly
what serve fetches.

Drift resolved in iter 0001.tidy (preceding commit): the missing registry.toml,
the cache-before-fetch data-flow, and the unescaped registry slug/page.

Ratified (carry-on, not fixed this cycle, each tracked as a follow-up issue):
- The render directive-expansion seam is conceptual (a comment), not an AST
  walk step. For the walking skeleton (code + table are native Markdown) a
  no-op seam needs no code; it materialises when code-include lands.
- GiteaClient's 404->NotFound status mapping is covered by the gated live
  smoke's happy path, not a unit test of the 404 branch.

No regression scripts are configured (CLAUDE.md project facts), so the
architect is the sole drift gate; its What holds is non-empty (review
confirmed). Retires the cycle's spec and plan per the lifecycle convention.

closes #1
This commit is contained in:
2026-06-28 16:55:23 +02:00
parent 55801cf819
commit 056db1ce08
2 changed files with 0 additions and 1375 deletions
File diff suppressed because it is too large Load Diff
-258
View File
@@ -1,258 +0,0 @@
# 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=<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:
```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
<!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:
```rust
// 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).
- **`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).