DDIA: Ch 2, still ~halfway (no reading today) Plan items: none directly — this is groundwork for #1
Added four skill packages scoped to this repo (.claude/skills/, gitignored,
restorable from the committed skills-lock.json): Leonxlnx/taste-skill (13
skills), vercel-labs/agent-skills → web-design-guidelines, a curated 10 of
bergside/awesome-design-skills, and @playwright/cli whose SKILL.md ships
inside playwright-core.
Then rebuilt site/ from scratch against the taste skill. Four decisions were
made deliberately and should not be re-litigated:
| Decision | Choice |
|---|---|
| Visual language | Instrument. Mono for everything structural and numeric, sans for prose only |
| Homepage | Live figure first, index below |
| Theme | Both, tokenised now |
| Scope | Per-visualization write-ups, published journal, DDIA progress, about page |
The site is now: token layer → base layer → layout layer, no CSS framework,
no component library. Homepage, visualization page, journal index, journal
entry, about, 404. Journal entries are read from journal/*.md at the repo
root via import.meta.glob and inlined at build time, so publishing an entry
is just writing it.
The categorical palette is a hard constraint on the ring, and the number is three. This is the finding that actually matters, because it changes what visualization #1 has to draw. Ran the dataviz validator against this site's own surfaces:
| Slots in play | Pairs checked | Result |
|---|---|---|
| 1-8 | adjacent | PASS — worst CVD ΔE 9.1 light / 8.4 dark |
| 1-3 | all pairs | PASS — worst CVD ΔE 9.4, normal-vision ΔE 20.9 |
| 1-4 | all pairs | FAIL — yellow↔orange, normal-vision ΔE 10.6 (floor is 15) |
| 1-5 | all pairs | FAIL — magenta↔aqua, deutan ΔE 1.6 |
A ring places nodes by hash, so any two nodes can end up visually adjacent. That is the all-pairs case, not the adjacent case. So a four-node ring already has a pair a full-colour reader struggles with, and a five-node ring has a pair that is effectively identical to a red-green colourblind viewer (ΔE 1.6 on a scale where 15 is the floor).
Consequence for #1: every arc gets a direct label. Colour is a secondary
cue past three nodes, never the identity. This is encoded in
theme/vizTokens.ts as COLOR_ALONE_SERIES_LIMIT with a
requiresDirectLabels() helper, so the renderer cannot quietly forget it.
Command, so it does not need re-deriving:
node .../dataviz/scripts/validate_palette.js \
"#3987e5,#d95926,#199e70,#c98500" --mode dark --surface "#131417" --pairs all
Canvas renderers must read colours from CSS, and the bridge is written.
A canvas has no cascade, so the obvious thing is ctx.fillStyle = '#7cb7ff'.
Do that once per visualization and theming is dead: eleven private palettes,
none responding to the toggle. theme/vizTokens.ts reads the tokens via
getComputedStyle and re-reads on theme change. Verified at runtime that
--series-1 resolves to #2a78d6 in light and #3987e5 in dark.
This is the same shape as the 2026-08-15 lesson about writing browser-bound Go I/O-free from the start: near-free at visualization #1, five renderers plus their tests at #5.
Every colour in the palette was measured, not chosen. Contrast against
--page, both modes:
| Role | Light | Dark |
|---|---|---|
| ink-primary | 17.20:1 | 17.62:1 |
| ink-secondary | 7.26:1 | 8.52:1 |
| ink-muted | 4.63:1 | 6.04:1 |
| accent | 6.76:1 | 7.32:1 |
| accent hover | 8.79:1 | 9.00:1 |
| label on accent button | 7.19:1 | 7.32:1 |
First pass had light ink-faint at 2.97:1 against a 3:1 requirement. Caught by
the script, not by eye — the difference is invisible and the failure is real.
Hover is higher contrast than rest in both modes, which is the actual rule
(interactive states get more prominent, not just different).
Build output. 79.2 kB gzip JS, 4.7 kB gzip CSS, 374ms build. Fonts are
self-hosted Geist and Geist Mono via @fontsource-variable, subset by
unicode-range, so a latin reader fetches ~53 kB of the 12 emitted files.
Typecheck clean, eslint zero problems.
Tests are red, and that is the correct state. 23 failing in
ring.test.ts, all bottoming out in createRng, hash and Ring.* throwing
their TODO(human). Nothing was implemented on that side today. The homepage
figure frame catches the throw and renders the file path instead of blanking,
so the unimplemented state is visible rather than broken.
- Sized the display type for a sans face while specifying a mono one.
clamp(..., 3.5rem)gave a three-line headline against a two-line cap. Mono runs roughly 25% wider per character; the cap had to come down to 2.625rem. Reading the CSS would never have shown this. Screenshotting did, immediately. - Set
aspect-ratioinline and then tried to override it from a stylesheet. Inline styles beat every selector regardless of specificity, so the:has(.figurePending)rule silently did nothing and the empty frame stayed 585px tall. Passing--figure-aspectas a custom property instead fixed it. Silent no-ops are the expensive kind of CSS bug — nothing errors, the rule just never applies. - Shipped a graph-paper grid on an empty frame and called it plot furniture. It is furniture when there is a plot. On an empty frame it is decoration, which the brief rules out. Now gated on the frame having real content.
- Printed "PLANNED" on ten consecutive index rows. A column whose value never varies carries no information. Only the exceptions are badged now.
- Used em-dashes in eleven rendered chapter labels with the zero-em-dash rule loaded at the time. Found by grepping, not by reading.
- The general failure behind four of those five: I reviewed the code and not the page. Every one was caught in the first screenshot pass and none were caught by reading the source. Look at the rendered thing earlier.
- No journal entry exists for the session that scaffolded
lab/consistent-hashing/andsite/src/visualizations/consistentHashing/— roughly a week ago, between the 16th and today. The design doc reviews survive inlab/SKILLS.md, but what was decided aboutrun.ts, the seek model, and the test list is not written down anywhere. lab/SKILLS.mdwas not updated, deliberately. It tracks the human's HLD and implementation skill, and today involved neither a design doc nor an implementation by hand. Adding an entry would have been padding.
- Resume #1: implement
rng.ts,hash.ts,ring.tsby hand. 23 tests are waiting. Watch the wrap case inlookup— there is a test aimed at it. - Once those pass, the renderer gets written against
RingandreadVizTokens(), with direct labels on every arc per the finding above. - Ch 2 is due tomorrow (Aug 24) and has not moved today.