Design: documentation site for the-loop
Phase 2 of 3 (requirements → design → tasks). Derives from the approved requirements.
Retroactive note: authored during PR #71 review. The design's central decision — VitePress reading
docs/in place rather than a synceddocs-site/mirror — was settled by owner feedback on the PR, and is captured here (and indecision-033) as the record.
Overview
Stand up VitePress with its source directory set to the existing docs/ folder. VitePress is a Markdown-first static-site generator (satisfies "all content MUST be Markdown" and "good static site like VitePress"), ships a clean default theme with light/dark and local search (satisfies "bare minimum good looking, no fancy CSS"), and its build is a self-contained bun project scoped to docs only — bun is the-loop's declared TS package manager (tooling.packageManager.ts) and its scripts are TypeScript (.mts), so this adds no new toolchain (satisfies "don't maintain two toolchains" — the CLI stays Python per decision-030; the docs site is orthogonal tooling on the repo's own TS stack, not a second authoring stack for the same content).
The key design tension is Requirement 3 (reuse, don't duplicate). VitePress treats its srcDir as the site; by pointing srcDir at docs/, the files already in docs/architecture/, docs/capabilities/, docs/decisions/, docs/specs/ and docs/reports/ are the site's pages — no copy exists. Only content that cannot live under docs/ for a functional reason is synced in at build time.
flowchart TD
subgraph repo["repository (single source of truth)"]
A["docs/architecture, docs/capabilities,\ndocs/decisions, docs/specs, docs/reports"]
G["docs/guide, docs/reference,\ndocs/index.md, docs/contributing.md\n(new hand-written site pages)"]
C["cli/README.md\n(also PyPI package readme)"]
R["skills/the-loop/reference/*.md\n(read at runtime by the harness)"]
end
C -- "build-time sync (git-ignored)" --> X["docs/cli.md"]
R -- "build-time sync (git-ignored)" --> Y["docs/operating-model/reference/**"]
A -- "rendered in place" --> V["VitePress build (srcDir = docs/)"]
G -- "rendered in place" --> V
X --> V
Y --> V
V --> D["docs/.vitepress/dist"]
D --> P["GitHub Pages (Actions/OIDC)"]Architecture
Site root = docs/. VitePress config at docs/.vitepress/config.mts:
base: "/the-loop/"— project Pages URL ishttps://<owner>.github.io/the-loop/.docs/specs/anddocs/reports/are included (PR #71 review). Thedocs/specs/sidebar is generated from the filesystem inconfig.mts(specSidebarGroups()): eachissue-<n>/becomes a collapsed group whose items are its artifacts ordered by the loop's phase order (brainstorm → requirements → bugfix → design → tasks → execution-log), sorted by issue number. New work items appear with no manual nav upkeep.docs/reports/gets a small static group.ignoreDeadLinks: true— the canonicaldocs/decisionsanddocs/capabilitiescontent links out tocli/README.mdandskills/the-loop/SKILL.md(outside the site's srcDir). Rather than rewrite canonical docs to route around the site, dead-link checking is off; those files remain readable in the repo regardless. (Trade-off discussed below.)- Default theme;
search: { provider: "local" }. No custom CSS/animations.
New hand-written site pages (committed under docs/, Markdown):
docs/index.md— VitePresslayout: homehero + feature grid.docs/guide/—what-is-the-loop,installation,quickstart,how-it-works.docs/reference/—commands,configuration.docs/contributing.md— contributor + docs-site instructions.docs/operating-model/index.md— landing page that indexes the synced reference set.docs/specs/index.md,docs/reports/index.md— overview landing pages for those sections.
Build-time sync (docs/scripts/sync-content.mts, TypeScript run by bun), the only copying, limited to files that structurally cannot be authored under docs/:
cli/README.md→docs/cli.md.cli/README.mdis declared as the CLI package's readme incli/pyproject.toml(readme = "README.md") and rendered on PyPI, so it must remain atcli/README.md.skills/the-loop/reference/*.md→docs/operating-model/reference/. These are read at runtime by the harness fromskills/the-loop/reference/(progressive disclosure); they cannot move.
Both synced destinations are git-ignored and markdownlint-ignored (lint/version the source, not the copy — Requirement 5.2). The script rewrites the handful of relative links in the reference docs (../../../docs/decisions/ → /decisions/) so they resolve in the site.
Deployment (.github/workflows/docs.yml): on push to main touching docs sources (or workflow_dispatch), oven-sh/setup-bun + bun install --frozen-lockfile + bun run docs:build, then the first-party Pages actions with pages: write + id-token: write and single-flight concurrency.
Components & interfaces
| Component | Responsibility | Interface |
|---|---|---|
docs/package.json | Site toolchain + scripts | docs:sync, docs:dev, docs:build, docs:preview |
docs/.vitepress/config.mts | Nav, sidebar (incl. filesystem-generated specs sidebar), base | VitePress defineConfig |
docs/scripts/sync-content.mts | Build-time sync of the 2 external sources | TypeScript, run by bun via docs:sync (prepended to dev/build) |
.github/workflows/docs.yml | Build + deploy to Pages | GitHub Actions |
Data models
No data models — a static site. The only "schema" is VitePress's config object and the sidebar/nav structure, both typed by vitepress's defineConfig.
Error handling
- A build failure (bad link to a site page, invalid config) fails
docs:build→ fails the workflow → nothing is deployed; the previously published site stays live. - Missing GitHub Pages enablement: the
deployjob errors until the owner enables Pages (Settings → Pages → Source: GitHub Actions). Flagged as an open item on the PR.
Security design
Requirement's Security considerations enforced here.
- AuthN/AuthZ: none needed — public static site, no input, no server. The privileged action is the Pages deploy, gated by the
github-pagesenvironment and GitHub Actions OIDC (id-token: write) — no stored secret. - Input validation / injection surfaces: none at runtime (no forms, no server). Build consumes only in-repo Markdown.
- Secrets handling: the workflow reads no secrets; the minimum scopes are
contents: read,pages: write,id-token: write(least privilege). - Fail-closed: a failed build publishes nothing.
- No remote fetch at build: pages are self-contained; the build does not pull untrusted remote content.
Testing strategy
This is a docs/site work item; "tests" are build-and-render verifications rather than a unit suite (no product code changes):
bun run docs:buildcompletes with 0 errors and renders the expected page set (guide, reference, cli, architecture, capabilities, decisions, operating-model, specs, reports, contributing).- Every
docs/specs/<id>/artifact anddocs/reports/gh-queries.mdrender as pages; the generated specs sidebar lists every work item. bun run docs:previewserves the built site; key routes return HTTP 200.pre-commit run --all-filesstays green (ruff, pyright, pytest, markdownlint, schema).
No Gherkin integration scenarios apply (no runtime behaviour to script); recorded here so the absence is deliberate, not an omission.
Trade-offs & decisions
docs/as srcDir vs. a separatedocs-site/mirror (decided:docs/). The first PR #71 revision used adocs-site/folder that synced all ofdocs/into itself. The owner rejected the duplication. Readingdocs/in place removes the copy entirely; the residual sync is only the two files that genuinely cannot live underdocs/. Recorded asdecision-033.ignoreDeadLinks: truevs. rewriting canonical links. Rewritingdocs/decisionsanddocs/capabilitiesto avoid links that leave the site would mutate the canonical historical record purely for the site's benefit, and would re-break every time upstream content changes. Disabling dead-link checking keeps the canonical docs authoritative; the cost is the build won't catch a genuinely broken site link. Mitigated by keeping hand-written pages' internal links consistent and preview-checked.- Include specs/reports, with a generated sidebar (PR #71 review). An earlier cut excluded them via
srcExcludeto keep the vite.dev/guide-style IA clean; the owner wanted them kept ("why are we excluding docs and reports … we should keep it"). They're now first-class site content. To avoid hand-maintaining nav for ~20 work items × ~5 artifacts, the specs sidebar is generated from the filesystem, so it self-updates as specs are added — the same low-maintenance principle as readingdocs/in place. - bun + TypeScript, not npm + JS (PR #71 review). The owner asked for bun as the package manager/runner and TypeScript-only scripts ("NO JS. Only TS."). This aligns the docs site with the-loop's own declared TS tooling (
tooling.packageManager.ts: bun): the single sync script issync-content.mts, run bybun run(Node 22+ / bun both strip types natively, so no build step), the lockfile isbun.lock, and CI usesoven-sh/setup-bun. Nopackage-lock.json, no.mjs.
Open questions
- GitHub Pages must be enabled once by the owner (repository setting) — carried to the PR as the single blocking action outside automation's control.