docs: final review fixes — deferred backlog, doc accuracy, sample-data note

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-23 19:13:41 +02:00
co-authored by Claude Opus 5.5
parent dd106c5d0e
commit 2ff58ace46
11 changed files with 103 additions and 37 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ bun dist/server.js # wrong: crashes with 'Bundled file "./index-….js" no
- Assets are bundled, never loaded from a CDN: fonts from `@fontsource/*`, three.js from npm.
YouTube videos are link cards (`createVideoCards`), not embeds.
- Adding a mockup route means editing `src/server.ts`, `src/mockups/registry.ts` and the cards in
`src/index.html` together; `tests/server.test.ts` checks they agree.
`src/index.html` together; `tests/server.test.ts` checks every registry slug is served and that the index shows each entry's name, pitch and tryThis.
## Workflow
@@ -69,7 +69,7 @@ Mockup rounds — any UI change beyond a trivial tweak, while brainstorming and
`src/server.ts` -> `Bun.serve` with HTML-import routes: `/` and `/mockups/NN-slug/`
`src/data/` -> `types.ts` (shapes), `sample.ts` (shared sample data), `query.ts` (helpers)
`src/shared/` -> credits, audio slot (direct MP3, empty state), YouTube link cards
`src/mockups/NN-slug/` -> one design direction each: `index.html`, `main.ts`, `style.css`
`src/mockups/NN-slug/` -> one design direction each: `index.html`, `main.ts`, `style.css`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`)
`tests/` -> data invariants, shared DOM modules (happy-dom), route smoke tests
Full picture: `docs/ARCHITECTURE.md` — read it before your first change in a session.
+1 -1
View File
@@ -14,7 +14,7 @@ shared credits snippet.
| `src/server.ts` | `startServer(port)`: routes `/`, `/mockups/NN-slug/` (+ 302 from the no-slash form), 404 otherwise. Runs when `import.meta.main`; `PORT` env, default 3000. |
| `src/index.html`, `src/index.ts`, `src/index.css` | Index page: static cards linking each mockup, then `mountCredits()`. |
| `src/mockups/registry.ts` | `mockups: MockupInfo[]` — slug, number, name, pitch, what to try. |
| `src/mockups/NN-slug/` | One design direction: `index.html` (links `../../shared/credits.css`, has `<footer data-fan-credits>`), `main.ts`, `style.css`. |
| `src/mockups/NN-slug/` | One design direction: `index.html` (links `../../shared/credits.css`, has `<footer data-fan-credits>`), `main.ts`, `style.css`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`). |
| `src/data/types.ts` | `Saga`, `Song`, `Part`, `Motif`, `StoryTime`, `OdysseyRef`, `Video`. |
| `src/data/sample.ts` | `sagas`, `motifs`, `songs`. Placeholder lyrics; unverified fields `// TODO(verify)`. |
| `src/data/query.ts` | `songsWithMotif`, `motifsInSong`, `gapYears`, `sagaOf`. |
+9 -1
View File
@@ -11,8 +11,16 @@ or `AGENTS.md` Commands.
| Command | Result |
|---|---|
| `git status --short` | worked — exit 0 |
| `git diff --stat` | worked — exit 0 |
| `git log --oneline -1` | worked — exit 0 |
| `git show --stat --oneline HEAD` | worked — exit 0 |
| `git branch --list` | worked — lists `main` |
| `git rev-parse --short HEAD` | worked — exit 0 |
| `git add <paths>` | worked — evidence: the final-review fixes commit that added these rows |
| `git commit -m <msg>` | worked — evidence: the final-review fixes commit that added these rows |
| `bun install --frozen-lockfile` | worked (no changes) |
| `bun test` | worked — 32 pass, 0 fail, 3 files |
| `bun test` | worked — 34 pass, 0 fail, 3 files |
| `bun test tests/data.test.ts` | worked — 10 pass |
| `bun test -t "registry has 7"` | worked — 1 pass |
| `bun run typecheck` | worked (`tsc --noEmit`, exit 0) |
+44 -3
View File
@@ -4,7 +4,7 @@ Durable backlog. Agents append here instead of leaving `TODO` comments in source
Small, specific, independently actionable. Remove only when actually done.
## Now
- [ ] Verify every `// TODO(verify)` field in `src/data/sample.ts` (48 at bootstrap: saga names, singers,
- [ ] Verify every `// TODO(verify)` field in `src/data/sample.ts` (56 after final review: saga names, singers,
story times, Odyssey book/lines, motif descriptions, facts) before any content leaves mockup stage.
## Next
@@ -39,5 +39,46 @@ Small, specific, independently actionable. Remove only when actually done.
- [ ] Two mockups with byte-identical `style.css` fail `bun run build` with "Multiple files share the same
output path" (reproduced by copying `01-editorial/style.css` over `02-vertical-story/style.css`).
Identical `main.ts` files do not trigger it.
- [ ] `docs/plans/001-timeline-mockups.md` tells mockup agents to start `bun dist/server.js`, which hits
the issue above; use `bun run start` instead.
## Deferred from final review (2026-10-23)
### Shared / foundation
- [ ] `src/server.ts`: routes are hand-listed; generate them from `src/mockups/registry.ts` (see Someday).
### Docs / permissions
- [ ] `docs/DECISIONS.md`: entries are oldest-first; the template is newest-first. Pick one.
- [ ] `AGENTS.md`: the architecture map duplicates `docs/ARCHITECTURE.md`; trim one.
- [ ] `docs/plans/README.md`: says "feature branch" while branching is still `TODO(maintainer)`.
### Sample data
- [ ] `src/data/sample.ts`: `odyssey.url` points at Butler's prose translation, so "lines X-Y" will not match the linked text; switch to book/chapter references or a lineated edition when verifying.
### Mockup 01 editorial
- [ ] `src/mockups/01-editorial/style.css`: credit footer clips at about 761-1068px width with the folio open (parked; spec targets desktop 1440x900).
### Mockup 02 vertical story
- [ ] `src/mockups/02-vertical-story/main.ts`: `frame()` interleaves layout reads and writes.
- [ ] `src/mockups/02-vertical-story/style.css`: saga marker padding inflates boundary gaps; `.sea` background snaps between sagas.
### Mockup 03 transit map
- [ ] `src/mockups/03-transit-map/main.ts`: ruler repeat label reads "Year 0" for fall-of-Troy songs.
- [ ] `src/mockups/03-transit-map/main.ts`: `--footer-h` is not re-measured after fonts load.
### Mockup 04 greek frieze
- [ ] `src/mockups/04-greek-frieze/main.ts`: post-drag click-swallow listener can stay armed if no click fires.
### Mockup 05 score
- [ ] `src/mockups/05-score/main.ts`: `.part.is-cued` is not cleared on song change or close.
- [ ] `src/mockups/05-score/main.ts`: chip click re-renders the panel, which would restart the MP3.
- [ ] `src/mockups/05-score/main.ts`: `has-song` class is unused.
### Mockup 06 constellation
- [ ] `src/mockups/06-constellation/`: per-frame `Vector3` allocations; constructor failure leaks the canvas; WebGL probe context is not released.
- [ ] `src/mockups/06-constellation/`: label box cache goes stale on resize; Enter on the credits link opens a song.
- [ ] `src/mockups/06-constellation/`: fallback ruler is inert; legend/panel overflow; Troy saga caption is clipped.
- [ ] `src/mockups/06-constellation/`: check intro flight and easing in a real browser (unverifiable headless).
### Mockup 07 sea voyage
- [ ] `src/mockups/07-sea-voyage/`: per-frame `Color`/`Vector3` allocations; WebGL probe context is not released.
- [ ] `src/mockups/07-sea-voyage/`: no `webglcontextlost` handler; DPR is read once.
- [ ] `src/mockups/07-sea-voyage/`: waypoint tag `innerHTML` is unescaped; drawer clips the "The open sea" label.
+12 -12
View File
@@ -44,7 +44,7 @@ export const songs: Song[] = [
odyssey: null, // TODO(verify)
parts: [
{ name: "Verse 1", singers: ["Odysseus"], lines: lyric("Odysseus", 1), motifIds: ["odysseus", "just-a-man"] }, // TODO(verify)
{ name: "Chorus", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["just-a-man"], startSec: 60 },
{ name: "Chorus", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["just-a-man"], startSec: 60 }, // TODO(verify) singers
],
facts: ["Shows Odysseus as a husband and father before the voyage."], // TODO(verify)
videos: [{ title: "Just a Man — search", url: yt("EPIC The Musical Just a Man") }],
@@ -55,7 +55,7 @@ export const songs: Song[] = [
odyssey: { book: 9, lines: "39-61", url: BUTLER }, // TODO(verify) raid on the Cicones
parts: [
{ name: "Verse 1", singers: ["Eurylochus", "Odysseus"], lines: lyric("crew", 1), motifIds: ["odysseus"] }, // TODO(verify)
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["warrior"] },
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["warrior"] }, // TODO(verify) singers
],
facts: ["The crew's high spirits are about to run into trouble."], // TODO(verify)
videos: [{ title: "Full Speed Ahead — search", url: yt("EPIC The Musical Full Speed Ahead") }],
@@ -65,8 +65,8 @@ export const songs: Song[] = [
storyTime: { yearsAfterTroy: 0, label: "Shortly after leaving Troy" }, // TODO(verify)
odyssey: null, // TODO(verify)
parts: [
{ name: "Verse 1", singers: ["Odysseus"], lines: lyric("Odysseus", 1), motifIds: ["open-arms"] }, // TODO(verify)
{ name: "Verse 2", singers: ["Odysseus", "Crew"], lines: lyric("crew", 2), motifIds: ["open-arms", "odysseus"] },
{ name: "Verse 1", singers: ["Polites", "Odysseus"], lines: lyric("Odysseus", 1), motifIds: ["open-arms"] }, // TODO(verify) Polites leads, with Odysseus
{ name: "Verse 2", singers: ["Polites", "Odysseus", "Crew"], lines: lyric("crew", 2), motifIds: ["open-arms", "odysseus"] }, // TODO(verify) singers
],
facts: ["Introduces a welcoming melody that returns later."], // TODO(verify)
videos: [{ title: "Open Arms — search", url: yt("EPIC The Musical Open Arms") }],
@@ -77,7 +77,7 @@ export const songs: Song[] = [
odyssey: { book: 13, lines: "287-310", url: BUTLER }, // TODO(verify) Athena and Odysseus
parts: [
{ name: "Verse 1", singers: ["Athena"], lines: lyric("Athena", 1), motifIds: ["athena", "warrior"] }, // TODO(verify)
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "warrior"], startSec: 70 },
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "warrior"], startSec: 70 }, // TODO(verify) singers
],
facts: ["Athena teaches Odysseus to fight with wit."], // TODO(verify)
videos: [{ title: "Warrior of the Mind — search", url: yt("EPIC The Musical Warrior of the Mind") }],
@@ -87,8 +87,8 @@ export const songs: Song[] = [
storyTime: { yearsAfterTroy: 1, label: "The island of the Cyclopes" }, // TODO(verify)
odyssey: { book: 9, lines: "105-460", url: BUTLER }, // TODO(verify)
parts: [
{ name: "Verse 1", singers: ["Polyphemus"], lines: lyric("Polyphemus", 1), motifIds: ["polyphemus"] },
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "warrior"], startSec: 50 },
{ name: "Verse 1", singers: ["Polyphemus"], lines: lyric("Polyphemus", 1), motifIds: ["polyphemus"] }, // TODO(verify) singers
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "warrior"], startSec: 50 }, // TODO(verify) singers
],
facts: ["Odysseus meets Polyphemus in his cave."], // TODO(verify)
videos: [{ title: "Polyphemus — search", url: yt("EPIC The Musical Polyphemus") }],
@@ -99,7 +99,7 @@ export const songs: Song[] = [
odyssey: { book: 9, lines: "287-412", url: BUTLER }, // TODO(verify)
parts: [
{ name: "Verse 1", singers: ["Polyphemus", "Crew"], lines: lyric("Polyphemus", 1), motifIds: ["polyphemus"] }, // TODO(verify)
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "just-a-man"] },
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["odysseus", "just-a-man"] }, // TODO(verify) singers
],
facts: ["The crew is trapped and Odysseus must choose a plan."], // TODO(verify)
videos: [{ title: "Survive — search", url: yt("EPIC The Musical Survive") }],
@@ -109,8 +109,8 @@ export const songs: Song[] = [
storyTime: { yearsAfterTroy: 1, label: "Escaping the cave" }, // TODO(verify)
odyssey: { book: 9, lines: "431-460", url: BUTLER }, // TODO(verify)
parts: [
{ name: "Verse 1", singers: ["Telemachus"], lines: lyric("Telemachus", 1), motifIds: ["remember"] }, // TODO(verify)
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["remember", "open-arms"], startSec: 40 },
{ name: "Verse 1", singers: ["Telemachus"], lines: lyric("Telemachus", 1), motifIds: ["remember"] }, // TODO(verify) Telemachus as singer
{ name: "Verse 2", singers: ["Odysseus"], lines: lyric("Odysseus", 2), motifIds: ["remember", "open-arms"], startSec: 40 }, // TODO(verify) singers
],
facts: ["A memorial theme introduced here returns later."], // TODO(verify)
videos: [{ title: "Remember Them — search", url: yt("EPIC The Musical Remember Them") }],
@@ -120,9 +120,9 @@ export const songs: Song[] = [
storyTime: { yearsAfterTroy: 1, label: "Leaving the island" }, // TODO(verify)
odyssey: { book: 9, lines: "473-542", url: BUTLER }, // TODO(verify)
parts: [
{ name: "Verse 1", singers: ["Odysseus", "Polyphemus"], lines: lyric("Odysseus", 1), motifIds: ["polyphemus", "odysseus"] }, // TODO(verify)
{ name: "Verse 1", singers: ["Odysseus", "Athena"], lines: lyric("Odysseus", 1), motifIds: ["odysseus", "athena"] }, // TODO(verify) singers
],
facts: ["The Cyclops saga ends with Odysseus revealing his name."], // TODO(verify)
facts: ["Closes the Cyclops saga as Odysseus leaves the island."], // TODO(verify)
videos: [{ title: "My Goodbye — search", url: yt("EPIC The Musical My Goodbye") }],
},
{
+1 -1
View File
@@ -10,7 +10,7 @@
<body>
<main>
<h1>EPIC: The Musical Timeline</h1>
<p class="lede">Seven design directions for an interactive timeline. Pick one, or mix several.</p>
<p class="lede">Seven design directions for an interactive timeline. Pick one, or mix several. Sample content: placeholder lyrics and unverified details, for comparing designs only.</p>
<ul class="cards">
<li class="card"><a href="/mockups/01-editorial/"><span class="card__num">01</span><h2>Editorial scroll</h2><p>A clean horizontal line in a parchment-and-serif editorial style, with motifs as thin coloured threads underneath.</p><p class="card__try">Try this: Pan sideways, notice gap length tracking in-story time, and open a song.</p></a></li>
<li class="card"><a href="/mockups/02-vertical-story/"><span class="card__num">02</span><h2>Vertical story</h2><p>The voyage scrolls downward; songs reveal one at a time beside a sticky detail panel.</p><p class="card__try">Try this: Scroll and watch the sticky detail panel update.</p></a></li>
+1 -1
View File
@@ -1,4 +1,4 @@
import { mountCredits } from "./shared/credits";
// Cards are static in index.html (kept in sync with src/mockups/registry.ts by tests/server.test.ts).
// Cards are static in index.html (tests/server.test.ts checks each registry entry's name, pitch and tryThis appear here).
mountCredits();
+15 -15
View File
@@ -12,21 +12,21 @@ export function startServer(port: number): Server<undefined> {
return Bun.serve({
port,
routes: {
"/": home,
"/mockups/01-editorial/": m0,
"/mockups/01-editorial": Response.redirect("/mockups/01-editorial/", 302),
"/mockups/02-vertical-story/": m1,
"/mockups/02-vertical-story": Response.redirect("/mockups/02-vertical-story/", 302),
"/mockups/03-transit-map/": m2,
"/mockups/03-transit-map": Response.redirect("/mockups/03-transit-map/", 302),
"/mockups/04-greek-frieze/": m3,
"/mockups/04-greek-frieze": Response.redirect("/mockups/04-greek-frieze/", 302),
"/mockups/05-score/": m4,
"/mockups/05-score": Response.redirect("/mockups/05-score/", 302),
"/mockups/06-constellation/": m5,
"/mockups/06-constellation": Response.redirect("/mockups/06-constellation/", 302),
"/mockups/07-sea-voyage/": m6,
"/mockups/07-sea-voyage": Response.redirect("/mockups/07-sea-voyage/", 302),
"/": home,
"/mockups/01-editorial/": m0,
"/mockups/01-editorial": Response.redirect("/mockups/01-editorial/", 302),
"/mockups/02-vertical-story/": m1,
"/mockups/02-vertical-story": Response.redirect("/mockups/02-vertical-story/", 302),
"/mockups/03-transit-map/": m2,
"/mockups/03-transit-map": Response.redirect("/mockups/03-transit-map/", 302),
"/mockups/04-greek-frieze/": m3,
"/mockups/04-greek-frieze": Response.redirect("/mockups/04-greek-frieze/", 302),
"/mockups/05-score/": m4,
"/mockups/05-score": Response.redirect("/mockups/05-score/", 302),
"/mockups/06-constellation/": m5,
"/mockups/06-constellation": Response.redirect("/mockups/06-constellation/", 302),
"/mockups/07-sea-voyage/": m6,
"/mockups/07-sea-voyage": Response.redirect("/mockups/07-sea-voyage/", 302),
},
development: process.env.NODE_ENV !== "production",
fetch() {
+1 -1
View File
@@ -21,7 +21,7 @@ export function createAudioSlot(song: Song): HTMLElement {
btn.textContent = part.name;
btn.addEventListener("click", () => {
audio.currentTime = part.startSec!;
void audio.play?.();
audio.play?.()?.catch(() => {});
});
root.append(btn);
}
+5
View File
@@ -17,6 +17,11 @@ test("index links every mockup", async () => {
for (const m of mockups) expect(html).toContain(`/mockups/${m.slug}/`);
});
test("index shows every registry name, pitch and tryThis", async () => {
const html = (await (await fetch(base + "/")).text()).replaceAll("&amp;", "&");
for (const m of mockups) for (const text of [m.name, m.pitch, m.tryThis]) expect(html).toContain(text);
});
for (const m of mockups) {
test(`mockup ${m.slug} serves with credits hook`, async () => {
const res = await fetch(`${base}/mockups/${m.slug}/`);
+12
View File
@@ -48,6 +48,18 @@ describe("createAudioSlot", () => {
expect(el.querySelectorAll("audio[src]")).toHaveLength(1);
expect(el.querySelectorAll('button[data-start="30"]')).toHaveLength(1);
});
test("seek button sets currentTime and swallows a rejected play()", async () => {
const { createAudioSlot } = await import("../src/shared/audio-slot");
const song: Song = { ...base, audio: "/a.mp3", parts: [{ ...base.parts[0]!, startSec: 30 }] };
const el = createAudioSlot(song);
const audio = el.querySelector("audio") as HTMLAudioElement;
let played = 0;
audio.play = () => { played++; return Promise.reject(new Error("blocked")); };
(el.querySelector("button.audio-slot__seek") as HTMLButtonElement).click();
await Promise.resolve();
expect(audio.currentTime).toBe(30);
expect(played).toBe(1);
});
});
describe("createVideoCards", () => {