Design document for Requirement #21 · obot.roadmap · supersedes the examples-site deferral in the #1 design · interim artifact for #15
STATUS: Signed off 2026-07-08 · drafted 2026-07-08, updated same day (dual-purpose screenshot baselines; dev/main branch model — review Q&A) · drafted by Claude Code using Fable 5, reviewed by @jwildfireRequirement #21 (filed 2026-07-08) makes documentation the delivery bar for the renderer migration: a GitHub Pages site for safety.viz with three pillars per renderer — a live example-gallery demo, a test-evidence page (requirements → tests → screenshots), and an API reference — plus the process rule that a renderer is not done, and its migration requirement not Released, until all three are published.
Inputs already in place: the merged library scaffold (safety.viz
PR #3), the histogram module
extraction (PR #4) with its
requirement-keyed test suite (32 Vitest + 15 Playwright), the coverage table
docs/histogram-coverage.md, the JSON data contract
src/data/schema/histogram.json, and the fixture demo page the Playwright
harness already uses.
Four scoping decisions were made with @jwildfire on 2026-07-08 and are treated as settled inputs to this design:
| Decision | Choice |
|---|---|
| Site tooling | Plain Node build script — no static-site framework; mirrors the hub's hand-rolled-HTML precedent and the library's esbuild-minimal stack. |
| Evidence screenshots | Committed to the repo — reviewable in PR diffs, stable, and available offline; regeneration is deliberate (see Evidence pipeline). Dual-purpose (review Q&A, 2026-07-08): the committed screenshots are simultaneously Playwright visual-regression baselines. |
| API reference | Generated from JSDoc in source — docs cannot drift from code; the JSON schema feeds the data-contract section directly. |
| Publish cadence | Every merge to main, plus PR-specific preview builds published under {site}/pr/{N}/. |
| Branch model (added in review) | Protected main + dev integration branch: feature PRs target dev; release PRs go dev → main; the site builds a third tier from dev at {site}/dev/. |
src/ (JSDoc) docs/<module>-coverage.md docs/evidence/<module>/ site/ (templates,
│ jsdoc -X │ (committed json + png) config, css)
▼ ▼ ▼ ▼
└────────────────────► scripts/site.mjs ◄──────────────────────────────────────────┘
(plain Node, no framework)
│ emits relative-URL static pages
▼
_site/ ──► gh-pages branch ──► https://jwildfire.github.io/safety.viz/
├── / (deployed from main — releases)
├── /dev/ (deployed from dev — integration)
└── /pr/{N}/ (deployed per open PR, removed on close)
safety.viz/
├── site/
│ ├── shell.html # shared page shell: header, nav, footer, styles
│ ├── config.json # per-renderer metadata: title, blurb, status
│ │ # (available / in-migration / planned), hero evidence ID
│ └── site.css # one stylesheet for the whole site
├── scripts/
│ ├── site.mjs # assembles _site/ from templates + generated data
│ └── evidence.mjs # runs the suites with JSON reporters + screenshot
│ # capture and (re)writes docs/evidence/<module>/
├── docs/evidence/<module>/ # committed: evidence.json + <REQ-ID>[_slug].png
├── _site/ # build output — gitignored, never committed
└── .github/workflows/pages.yml # build → verify → publish (main + PR previews)
The site pages per renderer: index.html (gallery),
<module>/index.html (live demo), <module>/evidence.html,
<module>/api.html. All internal links and asset paths are relative,
so the same build works at the site root and under /pr/{N}/.
site/config.json with status badges (available /
in-migration / planned), so the gallery doubles as the migration
scoreboard. Card thumbnail is the renderer's designated hero screenshot from its
committed evidence set.dist/ bundle (the same IIFE asset gsm.safety
vendors) against the committed fixture data (e.g. adbds.csv), with the
full control panel active. The existing Playwright harness page is the seed; harness
pages stay test-only, demo pages get the site shell plus a short "what you're
looking at" intro and links to the evidence + API pages.npm run evidence)scripts/evidence.mjs runs Vitest and Playwright with JSON reporters and
normalizes the results to docs/evidence/<module>/evidence.json:
one record per test — test name, parsed requirement IDs, issue refs
((#N)), suite (unit/browser), pass/fail, screenshot filenames.await expect(page).toHaveScreenshot('SH-FUNC-004C_pulse-hides-control.png'),
with snapshotPathTemplate pointing baselines at
docs/evidence/<module>/. One committed PNG is therefore evidence
artifact, regression baseline, and site image at once — and site images can never go
stale, because a rendering change fails CI until baselines are deliberately
refreshed. Capture runs under fixed conditions: 1280×800 viewport, device scale 1,
animations disabled, committed fixture data only, with per-assertion
maxDiffPixelRatio thresholds to absorb antialiasing noise.Pixel comparison is only stable within one rendering stack (fonts and canvas
antialiasing differ across OSes), so baselines are canonical to the Playwright Linux
container CI already uses. npm run evidence:update regenerates baselines
inside that container (locally via Docker, or as a manually-triggered workflow that
commits the refresh); a plain local npm run evidence still runs the suites
and captures screenshots for eyeballing, but never writes canonical baselines. Playwright
browser bumps that shift pixels are absorbed the same way — one deliberate refresh, which
updates the site images in the same commit.
Two layers, both in the regular CI workflow: (1) the browser suite's
toHaveScreenshot assertions enforce the pixel baselines natively; (2) a step
compares test IDs and pass/fail statuses against the committed
evidence.json — a mismatch fails the build with "evidence stale — run
npm run evidence:update and commit".
Generated per renderer by joining the coverage table
(docs/<module>-coverage.md) with evidence.json: requirement
ID → source matrix rows (linked to the safety.agent matrix) → evidencing test(s) → status
chip → screenshot(s). Unrouted/manual matrix rows render in the same routing-status
sections the coverage table already carries, so the page tells the whole story — covered,
superseded, deferred, and pending rows alike.
init, setData, setSettings,
render, resize, destroy) and every
DEFAULT_SETTINGS key (type, default, behavior).jsdoc -X (the only new dev-dependency) emits doclet
JSON; site.mjs renders it into the per-renderer API page — no JSDoc
HTML templates or themes.src/data/schema/<module>.json directly (field, type, required,
description), keeping one source of truth for the contract.safety.viz adopts the gsm-style two-branch model (added in review, 2026-07-08 — already applied to the repo):
dev is the integration branch — feature-branch PRs target
dev (histogram PR #4 retargeted accordingly).dev → main; the site root
republishes on merge, so the public site always reflects the latest release.main is protected: PR required (no direct pushes), the
"Build, format, and test" check must pass, force pushes and deletion blocked;
required approvals set to 0 so @jwildfire can merge his own release PRs (the
never-merge-without-Jeremy gate stays a process rule).main and dev.The single-artifact deploy-pages flow replaces the whole site on every
deploy, which cannot host persistent PR previews — so Pages serves from a
gh-pages branch written incrementally by pages.yml,
with three tiers:
| Trigger | Action |
|---|---|
push to main (release) | build _site/ → sync to the gh-pages root, preserving dev/ and pr/. |
push to dev (integration) | build → sync to gh-pages /dev/ — the always-current integration preview. |
pull_request (same-repo) | build → sync to gh-pages /pr/{N}/; a sticky bot comment on the PR links the preview. Review of a renderer's site entry happens here, per the deployed-review workflow. |
pull_request closed | remove /pr/{N}/ from gh-pages. |
The site build also runs as a check in the regular CI workflow (build must succeed,
internal links and referenced screenshots must resolve), alongside the evidence freshness
guard. Repo settings change: Pages source = gh-pages branch (one-time
manual step).
CONTRIBUTING.md as the renderer
definition-of-done checklist, and in each renderer requirement's acceptance
criteria from here on.| Item | Disposition |
|---|---|
| #1 design — "examples-site scope deferred until 2–3 modules exist" | Superseded by this design (decision 2026-07-08): the site ships with the histogram as its first entry. |
| #15 — qcthat-compatible JS reporting | Interim artifact: the evidence pages are the human-readable issue-linked qualification view; evidence.json (test ↔ requirement ↔ issue ↔ status) is designed to be trivially translatable to a qcthat matrix when #15 leaves the backlog. #15's scope is unchanged. |
| #2 — Safety Histogram migration | Adopts the done-gate retroactively: Released only after the histogram site entry deploys (see Done-gate mechanics). |
| gsm.safety#30 — Widget_Histogram | Sequenced after this requirement's histogram entry (agreed 2026-07-08); the widget's docs will link to the module's site pages. |
| Visual regression + accessibility hardening | Visual regression: now partially in scope (review Q&A 2026-07-08) — the committed evidence screenshots double as Playwright baselines, giving regression protection at every documented evidence point (not exhaustive visual coverage). Accessibility hardening remains out of scope (per the #1/#2 designs). |
Sub-issues to be filed via requirement-tasks after sign-off (all in
safety.viz unless noted):
evidence.mjs, toHaveScreenshot
baselines in docs/evidence/, canonical-container
evidence:update flow, JSON reporters, committed histogram evidence set,
CI freshness guard.jsdoc -X →
page generation in site.mjs.site.mjs, shell/templates/config, gallery + histogram demo
+ evidence page assembly, link/asset validation, CONTRIBUTING done-gate checklist.pages.yml (main root + /dev/ tier + PR
previews + close cleanup), Pages settings, preview-link bot comment.site/config.json.