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:
File diff suppressed because it is too large
Load Diff
@@ -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).
|
||||
Reference in New Issue
Block a user