diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/theme-studio-perceptual-color-metrics-spec.org | 576 | ||||
| -rw-r--r-- | docs/design/theme-studio-seeding-engine-spec.org | 350 |
2 files changed, 926 insertions, 0 deletions
diff --git a/docs/design/theme-studio-perceptual-color-metrics-spec.org b/docs/design/theme-studio-perceptual-color-metrics-spec.org new file mode 100644 index 00000000..7e7dedb2 --- /dev/null +++ b/docs/design/theme-studio-perceptual-color-metrics-spec.org @@ -0,0 +1,576 @@ +#+TITLE: theme-studio — perceptual color metrics (OKLCH, APCA, ΔE) +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-08 + +* Status + +Spec / review incorporated (Codex, 2026-06-08, two passes). Adds perceptual color metrics to +the theme-studio (=scripts/theme-studio/=) so it can build deliberately +low-contrast themes (Solarized / Zenburn class) with the same rigor it already +brings to high-contrast WCAG checking. Four additions: an OKLCH color model, a +per-color perceptual-lightness readout, an APCA contrast score alongside the +existing WCAG ratio, and a pairwise ΔE distinguishability check across the +palette. + +Came out of a design conversation comparing the low-contrast school (Solarized, +Zenburn) against Prot's high-contrast Modus themes. The conclusion: a theme has +three independent dials — contrast ratio, overall luminance, and chroma — and +the low-contrast camp turns down the first while Modus leaves it high and turns +down the other two. The current tool only measures the first (WCAG contrast) and +edits color in HSV, whose "lightness" is not perceptually uniform. To build +low-contrast themes by metric rather than by eye, the tool needs +perceptually-uniform lightness and chroma controls plus distinguishability and +polarity-aware contrast measures. + +Rubric: *Ready.* The four v1 product questions are decided in "Agreed decisions +(v1)" below and confirmed by Craig (2026-06-08); the testing strategy was +revised on his direction to a layered pyramid (Node-unit-tested color core + +thin UI hash tests + measured coverage). No remaining blocking ambiguity — the +implementer no longer has to invent product behavior while coding. Implementation +is sequenced into five phases, each independently shippable and tested. Tasks +filed in =todo.org=. + +* Background — the current color model + +The tool today works entirely in sRGB hex, HSV, and WCAG luminance. The relevant +cluster in =generate.py=: + +- =rl(hex)= (line 517) — WCAG relative luminance, via the existing =lin()= + sRGB-linearization helper. +- =contrast(a,b)= (line 519) — the WCAG 2.x ratio =(L1+0.05)/(L2+0.05)=. +- =rating(r)= / =ratingColor(r)= (lines 520-521) — AA (≥4.5) / AAA (≥7) verdict + and its display color. +- =hsv2rgb=, =rgb2hsv=, =hex2rgb=, =rgb2hex=, =normHex= (lines 604-609). +- The picker holds state as =pkH, pkS, pkV= (HSV) and renders an SV box (=#sv=), + a hue strip (=#hue=), crosshairs (=#svcur=, =#huecur=), a hex+contrast readout + (=#pkhex= / =#pkcon=, line 451), the contrast-mask buttons (=.pmode=, state + =pkMode=, values =any= / =aa= / =aaa=), and palette chips (=#pkchips=). +- =drawMask()= (line 613) greys SV-box regions whose contrast against the + background falls below the selected mask threshold (=pkThresh()=). +- Per-face contrast readouts appear across *three* tables — syntax (line 548), + UI (line 1064), and package faces (line 752) — each via =contrast()= + + =rating()=. The package-face tier has grown large since the tool's early + versions (51 packages in the current inventory), so any "add a column to the + table readouts" change now touches that whole surface, not just the two + original tables. + +Two limitations this spec addresses: + +1. *HSV lightness lies.* Two colors at the same HSV =V= can differ markedly in + perceived brightness, so the SV box cannot hold perceived lightness constant + while hue changes — exactly the operation a calm, even palette needs. +2. *WCAG 2.x is a known-flawed contrast model in the low-contrast / dark band.* + Its ratio misjudges contrast most where this work operates, and it is not + polarity-aware: it scores light-on-dark and dark-on-light identically, which + perception does not. WCAG 3 is reworking contrast but is years out — still a + Working Draft in 2026, with the final Recommendation not expected until + roughly 2028–2030 — and its contrast algorithm is undetermined: APCA was moved + *out* of the WCAG 3 draft in 2023 for further evaluation. So APCA enters here + as a well-regarded independent perceptual model used as an additional + diagnostic, not as a coming standard. WCAG 2.x stays the baseline precisely + because nothing has replaced it yet. + +* Goal + +Add four metrics, each a discrete increment: + +1. *OKLCH color model* — perceptually-uniform Lightness / Chroma / Hue, so the + editor can move one axis without disturbing the others, plus a gamut clamp + for OKLCH values outside sRGB. +2. *Perceptual-lightness readout* — show each color's OKLCH L (and C, H) in the + picker, so "low, even lightness steps" becomes a number rather than a guess. +3. *APCA score* — the Accessible Perceptual Contrast Algorithm Lc value + displayed next to the WCAG ratio, as the more trustworthy contrast metric in + the low-contrast band. +4. *Pairwise ΔE check* — perceptual color-difference between every pair of + palette entries, flagging pairs too similar to tell apart, which is the + constraint that keeps a low-chroma / low-lightness-spread palette from + collapsing into mush. + +Non-goals: replacing WCAG (it stays as the compatibility baseline, shown +alongside APCA, which is an additional perceptual diagnostic, not a +replacement); replacing the HSV picker outright (OKLCH is added as a parallel +color model, HSV remains the default); CIEDE2000 in v1 (ΔE-OK is the v1 +difference metric — see vNext). + +* Agreed decisions (v1) + +Settled on author + reviewer alignment and confirmed by Craig (2026-06-08). + +1. *ΔE metric and threshold.* Use ΔE-OK (Euclidean distance in OKLab) on its + native scale (OKLab L is 0..1). Default "too similar" warning threshold is + *0.02* — the just-noticeable-difference floor, so the warning fires only when + two palette colors are genuinely hard to tell apart. The threshold is a named + constant, calibratable in one place. CIEDE2000 (the CIE's 2000 perceptual + color-difference standard — more accurate than plain Euclidean distance, but + ~40 lines of trigonometric lightness/chroma/hue corrections plus a blue-region + rotation term) is deferred to vNext: ΔE-OK is accurate enough to flag + indistinguishable pairs, which is all this check needs, and it is five lines. +2. *Low-contrast preset.* v1 ships *readouts only* (OKLCH, APCA, ΔE). No named + low-contrast preset / mask mode yet. No such preset exists anywhere today — it + would be a new feature: a saved low-contrast target (e.g. an APCA Lc band, or + a contrast ceiling as well as a floor) that masks the palette to a comfortable + range in one click, the way the current any/AA+/AAA buttons mask by a contrast + floor. It is deferred until the raw readouts are in use, because only then is + it clear which band is worth presetting. v1 gives the numbers; the preset + would automate a judgment the numbers first have to inform. +3. *APCA placement.* v1 shows APCA *only in the picker* readout, not in the + syntax/UI/package table contrast cells. Adding it to the tables is + low-complexity once =apca()= exists — the same pattern as the existing + =contrast()= + =rating()= cells, repeated across the three tables — so the + deferral is about table *density*, not difficulty: the package table alone is + 51 packages wide, and a second contrast number per row risks clutter before + it is clear anyone reads it there. Table-wide APCA is a vNext candidate if + picker-only proves too hidden. +4. *Picker default model.* HSV stays the *default* picker model; OKLCH is + opt-in via a color-model control. The reason: HSV is the familiar 2D SV-box + the picker already has, and OKLCH is slider-only until the C×L plane (Phase + 4b) lands — so defaulting to OKLCH before 4b would hand users a worse default + editing surface than they have now. Once 4b ships the C×L plane, making OKLCH + the default becomes a real option worth revisiting; until then, HSV default + keeps the current editing experience intact and makes OKLCH an additive + choice, not a regression. + +* Color-math foundation (Phase 1, prerequisite) + +The pure color math is *extracted into its own importable module* rather than +inlined as loose functions in the page. This is the core architectural change +this spec makes to the test surface: the math is logic, so it gets tested as +logic — directly, in Node, with exhaustive fixtures — and the picker becomes a +thin UI layer over a tested core, not the only way to exercise the math. + +- New file: =scripts/theme-studio/colormath.js= — the pure, side-effect-free + conversion + metric functions, written as an ES module (each =export=-ed), + with a small guard so the same source loads both ways: =import=-ed by the Node + tests and spliced into the page by the generator. +- =generate.py= inlines =colormath.js= into the page's =<script>= the same way + it already inlines =samples.py='s data, so there is *one source of truth* — the + exact code the browser runs is the code the tests import. An inline-integrity + check (see Verification strategy) asserts the page contains the module verbatim + so the two can never drift. +- The existing inline helpers it supersedes (=lin=, =rl=, =contrast=, =rating=, + =hsv2rgb=, =rgb2hsv=, =hex2rgb=, =rgb2hex=) move into =colormath.js= too, so the + whole color core lives and is tested in one place. =normHex= stays at the UI + boundary; module functions assume a normalized =#rrggbb= and the Node tests + cover their edges directly. + +The functions (standard published algorithms): + +- =srgb2oklab(hex)= / =oklab2srgb(L,a,b)= — Björn Ottosson's OKLab matrices + (2020). sRGB → linear (reuse =lin()=) → LMS → cube-root → OKLab, and the + inverse. ~20 lines. +- =oklab2oklch(L,a,b)= / =oklch2oklab(L,C,H)= — Cartesian↔polar: =C=√(a²+b²)=, + =H=atan2(b,a)=. Trivial. +- =oklch2hex(L,C,H)= with the *gamut clamp* (see below). Returns + ={hex, clamped}= — the in-gamut hex plus a boolean flag. +- =apca(textHex, bgHex)= — the APCA-W3 algorithm. Returns a signed Lc (positive + for dark-text-on-light, negative for light-text-on-dark; magnitude ~0–107). +- =deltaE(aHex, bHex)= — ΔE-OK: Euclidean distance in OKLab, + =√((ΔL)²+(Δa)²+(Δb)²)=. Five lines. + +** Gamut clamp policy (v1, fixed) + +OKLCH can express colors sRGB cannot show (high C at some L/H). The v1 policy is +*binary-search chroma reduction*: hold L and H fixed, reduce C until the color +is in sRGB gamut. This preserves the two perceptual axes the user is reasoning +about and only sacrifices saturation. Component clipping (which can shift all +three axes and make a slider feel broken) is explicitly *not* used. + +=oklch2hex= returns ={hex, clamped}= where =clamped= is true when chroma was +reduced. The picker keeps its sliders and readouts on the *actual clamped color* +after conversion, and shows a short status ("chroma clamped to sRGB") when +=clamped= is true — so the user never sees an axis silently move. + +** APCA source (pinned) + +Implement against *APCA-W3 0.1.9* (Myndex), transcribing the constants verbatim: + +- Source: =https://github.com/Myndex/apca-w3= (the =apca-w3= package, version + 0.1.9). The implementation puts this URL + version in a code comment beside + =apca()=. +- Screen luminance per color uses the *exact* APCA-W3 0.1.9 =colorSpace= + constants, not rounded values: =Ys = 0.2126729·R^2.4 + 0.7151522·G^2.4 + + 0.0721750·B^2.4= on the 0..1 sRGB channels (straight 2.4 power, not the WCAG + piecewise). All remaining APCA constants — the black soft-clamp + (=blkThrs=/=blkClmp=), the polarity-specific text/background exponents + (=normBG=/=normTXT=/=revTXT=/=revBG=), the low-contrast roll-off + (=loBoThresh=/=loBoFactor=/=loClip=), =deltaYmin=, and =scaleBoW= — are + likewise transcribed verbatim from the pinned source. The spec does not restate + those numbers, to avoid becoming a second, drift-prone source: the pinned + =apca-w3= 0.1.9 is the single authority. +- Fixture values asserted by the Node unit tests: =apca('#000000','#ffffff')= + Lc ≈ *106.0* (dark on light, positive); =apca('#ffffff','#000000')= Lc ≈ + *-107.9* (light on dark, negative); plus at least one *chromatic* APCA fixture + (e.g. =apca('#67809c','#ffffff')=) computed from the pinned reference — + black/white alone cannot reveal rounded-coefficient drift, since the rounding + error is near zero at the channel extremes. + +The tool ships as a single self-contained generated HTML file with no runtime +build step or package manager, so the APCA algorithm is transcribed into +=colormath.js= (inlined into the page) rather than vendored as an npm dependency. +The Node test harness is dev-only — it imports =colormath.js= to assert against +fixtures — and does not make the shipped artifact depend on Node or any package. + +** Verification — Node unit tests (=test-colormath.mjs=) + +The math is tested *directly*, not through the browser: =scripts/theme-studio/test-colormath.mjs= +imports =colormath.js= and asserts against fixtures under =node --test=. No DOM, +no Chrome, sub-second, and not capped by what the UI happens to exercise — this +is where the bulk of the feature's test value lives, and it can be far more +exhaustive than a hash test. It must include *chromatic* fixtures and properties, +because many incorrect matrix/sign implementations still pass black, white, and +round-trip: + +- =srgb2oklab('#ffffff')= L ≈ 1.0, a ≈ 0, b ≈ 0; =srgb2oklab('#000000')= L ≈ 0. +- chromatic fixture 1 — saturated red =#ff0000=: OKLab/OKLCH within epsilon of + the reference (L ≈ 0.628, C ≈ 0.258, H ≈ 29.2°). +- chromatic fixture 2 — the dupre blue =#67809c=: OKLCH ≈ (L 0.591, C 0.052, + H 252°), epsilon ~0.005 on L/C and ~1° on H. Computed from the Ottosson + reference; the implementation verifies against the same reference it + transcribes. +- round-trip *property*: for a generated sample of hexes, + =oklch2hex(oklab2oklch(srgb2oklab(h))).hex= ≈ =h= within epsilon. A property + test over random inputs, not a fixed list — it explores corners a hand-written + list would miss. +- =apca= both polarities against the pinned fixtures above (assert sign and + magnitude), plus the chromatic APCA fixture. +- =deltaE(h,h)= = 0; =deltaE('#000000','#ffffff')= > 0; ordering: a near pair + scores below the 0.02 threshold, a well-separated pair above it. +- gamut clamp: a known out-of-gamut OKLCH (very high C) returns a valid + =#rrggbb= with L and H preserved within epsilon, C reduced, and =clamped= + true; an in-gamut input returns =clamped= false unchanged. + +Pure-function TDD with no rendering dependency: write the failing Node test, +confirm it FAILs (e.g. with a deliberately wrong constant), then make it pass. +There is no =#mathtest= browser hash — the math is not a UI concern, so it is not +tested through the UI. + +* Phase 2 — perceptual L and APCA readouts + +Smallest visible change; validates Phase 1 by eye. + +- Extend =pkReadout(hex)= (line 615) to populate new spans for OKLCH L / C / H + and APCA Lc, alongside the existing WCAG ratio in the =.pinfo= bar (line 451). + Add the spans to the picker DOM (lines 448-451) and minimal CSS. +- The APCA span carries a compact polarity-aware label (e.g. =APCA Lc -58=); the + sign convention (positive = dark-on-light, negative = light-on-dark) is + documented in its tooltip and in the README. +- WCAG remains exactly as-is in the picker and in all three table contrast cells. + Per "Agreed decisions" #3, no APCA in the tables for v1. + +Pure additions; no behavior changes. Headless guard: =#readouttest= loads a +known hex and asserts the OKLCH L/C/H and APCA Lc spans carry the expected +values and the WCAG readout is unchanged. + +* Phase 3 — pairwise ΔE across the palette + +Self-contained, high value for low-contrast work. + +- On =renderPalette()=, compute =deltaE= for every unordered pair of =PALETTE= + entries. Flag any pair below the threshold (0.02, the named constant). +- Warning copy and ordering: sort failing pairs ascending by ΔE (closest first), + show the first *5*, and append "and N more" when capped — so a noisy palette + never silently hides the count. Format: "blue / steel — ΔE 0.014, hard to + distinguish". +- Each palette chip's =title= gains its nearest-neighbor ΔE. +- Reuses the chip rendering already in =renderPalette= / =buildPkChips= (line + 619). No new rendering surface. + +Headless guard: =#deltatest= seeds two near-identical palette colors and asserts +the warning fires (and names the pair); seeds a well-spread palette and asserts +it does not; if the cap triggers, asserts the "and N more" suffix and ascending +order. + +* Phase 4 — the OKLCH editor + +The largest piece, and the one that delivers "hold lightness while changing +chroma." Two shippable sub-phases, in order. + +** Phase 4a — OKLCH sliders + color-model control + +- Add a *separate color-model control* — a segmented =HSV= / =OKLCH= toggle with + its *own* state variable =pkModel= — distinct from the existing contrast-mask + control (=.pmode= / =pkMode=, values =any= / =aa= / =aaa=). The two are + orthogonal concepts: =pkModel= is "how I edit the color," =pkMode= is "what + constraint I mask." They must not share state. +- In OKLCH mode, show L / C / H as numeric + range inputs that drive the color + through =oklch2hex=, updating =#newhexstr=, the swatch, and the readouts. On + clamp, the sliders snap to the clamped color and the status text appears. +- No canvas work; delivers the independent-dials benefit immediately. + +Headless guard: =#oklchtest= asserts that switching =pkModel= to OKLCH preserves +the selected color, that toggling the AA/AAA mask does *not* reset =pkModel=, and +that switching =pkModel= does *not* reset =pkMode=. + +** Phase 4b — Chroma×Lightness plane + +- When =pkModel= is OKLCH, render the SV box (=#sv=, line 448) as a Chroma (x) by + Lightness (y) plane at the current fixed hue; the hue strip is unchanged. The + crosshair maps to (C, L) instead of (S, V). +- *Gamut masking*: high chroma is unreachable at some L/H, so grey out the + out-of-gamut region of the plane — reuse the =drawMask()= pattern (line 613), + swapping the per-pixel test from "contrast < threshold" to "OKLCH(C,L,H) not + in sRGB gamut." The existing AA/AAA contrast mask can overlay on top. +- *Render cost*: =drawMask()= already samples at =step=4= and runs =contrast()= + per cell; the gamut test adds an OKLCH→sRGB conversion per cell, and a naive + per-cell binary search on top would be expensive while dragging. Bound it: use + a coarse sampling step, cache the rendered plane on a key of + (hue + dimensions + mask mode + background hex) so it only recomputes when one + changes, and defer the redraw until pointer movement settles. The background + hex is in the key because when the AA/AAA contrast overlay is active the mask + depends on =MAP['bg']=, so a background edit must invalidate the cached plane. + The in-gamut test per cell + needs only a forward conversion + channel-range check, not the full binary + search (that is reserved for committing a chosen color). +- This per-pixel gamut render is the only genuinely new rendering logic in the + spec, which is why it is sequenced last. + +Headless guard: open the picker in OKLCH mode on a known hex via a hash; assert +the C×L crosshair lands at the expected plane coordinates and that a known +out-of-gamut coordinate is masked. + +* Verification strategy (whole feature) + +The test surface is *layered* — a proper pyramid, broad and fast at the bottom, +thin and DOM-bound at the top: + +1. *Unit (Node, the core)* — =test-colormath.mjs= imports =colormath.js= and + asserts the math directly under =node --test=. No browser. This is the bulk of + the coverage and the place exhaustive testing lives (every conversion, both + APCA polarities + chromatic, gamut clamp, ΔE ordering, round-trip property + over random hexes). *Coverage is measured here* with Node's built-in reporter + (no extra dependency): =node --test --experimental-test-coverage scripts/theme-studio/=. + Target for =colormath.js= is ≥90% line/branch (testing.md's utility-code bar); + in practice a pure, fully-fixtured module should land at or near 100%, and a + gap points at an untested branch worth a case. Coverage of the *core* is a + gate; coverage of the browser-executed UI code is out of scope for v1 (it + needs CDP/c8 instrumentation and the UI is verified by assertion, not line + count). +2. *UI wiring (browser hash tests)* — only the things that genuinely need a DOM + or layout, now that the math is tested below them: =#cursortest= (crosshair + pixel position — needs real layout), =#readouttest= (Phase 2, spans populated), + =#deltatest= (Phase 3, warning list rendered), =#oklchtest= (Phase 4a, + =pkModel= / =pkMode= independence + color preserved across mode switch), the 4b + plane test (canvas render + gamut mask). Each appends a =PASS/FAIL= node; + command shape: + =google-chrome-stable --headless=new --dump-dom 'file://…/theme-studio.html#readouttest'=. +3. *Integration smoke* — =#selftest= (data roundtrip), re-run every phase to + confirm no regression. +4. *Inline-integrity* — a check (Node or grep) that the generated + =theme-studio.html= contains the =colormath.js= source verbatim, so the + tested module and the shipped inline copy cannot drift. + +Per-phase loop: edit the source (=colormath.js= for math, =generate.py= for the +page — never hand-edit =theme-studio.html=); =python3 generate.py= to regenerate; +=node --check= the emitted =<script>=; run the phase's tests (Node unit tests for +Phase 1, the matching hash test for UI phases); re-run =#selftest= and the +inline-integrity check; Chrome eyeball for the visible phases (2, 3, 4). + +On coverage and why this shape: =generate.py= (~1120 lines) and =samples.py= +(~269) are the templating/assembly + data layer — string-emission and a sample +corpus — so Python unit tests there are low value and stay out of scope. The +logic worth hammering is the color *math*, which is JavaScript; extracting it to +=colormath.js= makes it directly unit-testable in Node instead of only reachable +through the rendered app. That is the correction this revision makes: the earlier +draft tested the math through browser hash tests, which coupled math correctness +to the DOM and capped coverage at what the UI exercises. With the core extracted, +the math gets exhaustive direct unit tests and the browser tests shrink to UI +wiring — the thin-UI-over-tested-core shape an API-first build would have +produced. The separate =build-theme.el= converter keeps its 22 ERT tests. + +* Documentation + +Folded into the phases, landing with the code each describes: + +- README (=scripts/theme-studio/README.md=): document OKLCH, APCA, and ΔE; the + meaning of the signed APCA value; that WCAG remains the compatibility baseline + and APCA is an additional perceptual diagnostic, not a replacement. +- Add the exact commands beside the existing run instructions: the Node unit run + with coverage (=node --test --experimental-test-coverage scripts/theme-studio/=) + and the headless hash tests (=#readouttest=, =#deltatest=, =#oklchtest=, the 4b + plane test). + +* Acceptance criteria + +- *Phase 1*: =colormath.js= extracted and inlined by =generate.py=; + =node --test= green — achromatic, chromatic, and round-trip conversions within + epsilon; APCA matches the pinned fixtures (magnitude and sign, both polarities, + plus a chromatic fixture); gamut clamp preserves L/H within epsilon, reduces C, + returns =clamped= true on out-of-gamut and false unchanged on in-gamut; + inline-integrity check confirms the page contains =colormath.js= verbatim; + =node --test --experimental-test-coverage= reports =colormath.js= at ≥90% + line/branch. +- *Phase 2*: picker shows OKLCH L/C/H and APCA Lc (with polarity label) next to + the WCAG ratio; values match the Node-test references for hand-checked colors; + no behavior change to existing flows; tables unchanged; =#selftest= still PASS; + =#readouttest= PASS. +- *Phase 3*: a palette with two near-identical colors raises a visible warning + naming the pair and ΔE, sorted closest-first, capped at 5 with "and N more"; a + well-spread palette raises none; chip titles carry nearest-neighbor ΔE; + =#deltatest= PASS. +- *Phase 4a*: dragging L changes only lightness (C and H readouts hold); same for + C and H independently; =pkModel= and =pkMode= are independent (=#oklchtest= + PASS); clamp shows status text. +- *Phase 4b*: the C×L plane crosshair opens on the current color's (C, L); + out-of-gamut regions are masked; the plane render stays responsive while + dragging (cached on hue/dims/mask key). + +* Implementation phases + +One shippable phase per increment, in dependency order, each gated on its own +headless test plus a clean =#selftest=. These map to the drop-in =todo.org= +tasks (filed in workflow Phase 6, after Craig confirms Ready): + +1. *Math foundation* — extract the color core into =colormath.js= (OKLab/OKLCH, + APCA-W3 0.1.9, ΔE-OK, gamut clamp, plus the migrated lin/rl/contrast/hsv + helpers); =generate.py= inlines it; =test-colormath.mjs= unit tests + the + inline-integrity check; gate =node --test= green. +2. *Picker readouts* — OKLCH L/C/H + APCA Lc spans beside WCAG; gate + =#readouttest= + =#selftest=. +3. *Palette ΔE warnings* — pairwise ΔE, sorted/capped warning, chip-title + nearest-neighbor; gate =#deltatest=. +4a. *OKLCH sliders + color-model control* — =pkModel= separate from =pkMode=, + L/C/H inputs, clamp status; gate =#oklchtest=. +4b. *Chroma×Lightness plane* — gamut-masked C×L render with caching; gate the 4b + plane test. + +A test-surface task keeps the Node unit tests, the UI hash tests, the +inline-integrity check, =#selftest=, the script syntax check, and manual Chrome +validation green across the feature. + +* Review dispositions + +Modified or rejected recommendations only; everything else in the Codex review +(2026-06-08) was accepted as written and woven into the body above. + +- *Modified — APCA "transcribe vs vendor" question (high-priority finding 4).* + The review asked the spec to "state whether the project is comfortable + transcribing the algorithm rather than vendoring a package." Reframed as + settled rather than open: the tool is a single self-contained generated HTML + file with no build step or package manager, so transcription is the only path + consistent with the architecture. The source URL + version pin and the fixture + values were accepted in full. +- *Modified — ΔE-OK default threshold (high-priority finding 1 / open question).* + The review accepted ΔE-OK on its native scale "with a named default threshold" + but left the number to the author. Concretized to 0.02 (the OKLab + just-noticeable-difference floor) as a named, calibratable constant, so the + warning fires only on genuinely indistinguishable pairs rather than deferring + the choice to implementation. + +* Review and iteration history + +** 2026-06-08 Mon @ 13:00:34 -0500 — Codex — reviewer +- *What changed or was recommended:* Created the implementation-readiness review + and marked the spec =Not ready=. The review recommends resolving the listed + open questions, pinning the APCA source/fixtures, specifying OKLCH gamut clamp + semantics, separating color-model mode from contrast-mask mode, and converting + the increments into todo-ready implementation phases. +- *Why:* The feature direction fits the current theme-studio architecture, but + coding from this draft would force the implementer to decide product behavior + around DeltaE thresholds, APCA placement, OKLCH UI state, and out-of-gamut + colors. +- *Artifacts:* the review file (consumed and deleted on response). + +** 2026-06-08 Mon @ 13:08:00 -0500 — Claude Code — responder +- *What changed:* Processed every Codex recommendation. Accepted all five + high-priority findings — resolved the four open questions into "Agreed + decisions (v1)", fixed the gamut clamp as binary-search chroma reduction + returning a =clamped= flag with picker status text, separated =pkModel= + (HSV/OKLCH) from =pkMode= (mask), pinned APCA-W3 0.1.9 with black/white + fixtures, and added an "Implementation phases" decomposition (1, 2, 3, 4a, 4b) + with per-phase test gates. Accepted the medium findings: chromatic =#mathtest= + fixtures, ΔE warning sort/cap copy, C×L render-cost bounds, refreshed + background to include the package-face table surface, the named hash tests + (=#readouttest=, =#oklchtest=), and the README/doc additions. Two modifications + recorded in "Review dispositions": the APCA transcribe-vs-vendor question + (settled by the no-build architecture) and the ΔE-OK default threshold (set to + 0.02). +- *Why:* Converge to implementation-ready — remove every product decision from + the coding path. Rubric raised Draft → Ready pending Craig's confirm on the + four baked-in v1 decisions. +- *Artifacts:* consumed and deleted =theme-studio-perceptual-color-metrics-spec-review.org=; + see "Review dispositions" and "Agreed decisions (v1)". Implementation tasks to + be filed in =todo.org= on confirm (workflow Phase 6). + +** 2026-06-08 Mon @ 13:15:26 -0500 — Codex — reviewer +- *What changed or was recommended:* Re-reviewed the updated spec and marked it + =Not ready= on one remaining blocker: the APCA section pins =apca-w3= 0.1.9 and + says to transcribe constants verbatim, but still shows rounded luminance + coefficients instead of the exact published =colorSpace= constants. Also noted + two non-blocking edits: include background hex in the 4b mask cache key when + contrast masking is active, and either provide the =#67809c= OKLCH fixture + values or make that second chromatic fixture optional. +- *Why:* The implementation is otherwise ready-shaped, but APCA math and numeric + fixtures need a single authoritative source before coding starts. +- *Artifacts:* [[file:theme-studio-perceptual-color-metrics-spec-review.org][theme-studio-perceptual-color-metrics-spec-review.org]] + +** 2026-06-08 Mon @ 13:19:15 -0500 — Claude Code — responder +- *What changed:* Processed Codex's second pass. Accepted all three findings, no + modify/reject. Blocking: replaced the rounded APCA luminance coefficients + (=0.2126=/=0.7152=/=0.0722=) with the exact APCA-W3 0.1.9 =colorSpace= + constants (=0.2126729=/=0.7151522=/=0.0721750=), and stated the remaining APCA + constants are transcribed verbatim from the pinned source rather than restated + in the spec, so there is one authority. Non-blocking: added =MAP['bg']= to the + Phase 4b mask cache key (the AA/AAA overlay depends on background); added + concrete OKLCH fixture values for =#67809c= (L 0.591, C 0.052, H 252°); added a + chromatic APCA fixture to =#mathtest= so black/white can't hide coefficient + drift. +- *Why:* Remove the last numeric ambiguity so =#mathtest= fixtures and the + implementation agree on one exact APCA algorithm. Reviewer confirmed no design + gaps remain — only the author-confirm caveat. +- *Artifacts:* consumed and deleted the second + =theme-studio-perceptual-color-metrics-spec-review.org=. Rubric stays =Ready, + pending author confirm= on the four v1 decisions; Phase 6 task-filing still + holds for that confirm. + +** 2026-06-08 Mon @ 13:21:11 -0500 — Codex — reviewer +- *What changed or was recommended:* Re-read the spec-review workflow and + critically reviewed the updated spec against the current theme-studio code, + README, prior review dispositions, and task tracking. Assigned =Ready with + caveats=: no blocking implementation ambiguity remains, and the prior APCA + coefficient issue is resolved by the exact =apca-w3= =colorSpace= constants. + The only caveat is the spec's explicit author-confirm step before Phase 1. +- *Why:* The spec now defines v1 scope, APCA source/fixtures, OKLCH gamut policy, + picker state ownership, DeltaE threshold/copy, performance guardrails, docs, + acceptance criteria, and shippable phases well enough for implementation. +- *Artifacts:* No review file written; no blocking findings. + +** 2026-06-08 Mon @ 15:21:55 -0500 — Craig Jennings (review) + Claude Code (responder) — final review +- *What changed:* Craig's final-review questions (six cj comments) answered and + the load-bearing ones folded into the body. Factual correction: WCAG 3 is still + a Working Draft (final Recommendation ~2028–2030) and APCA was moved *out* of + the WCAG 3 draft in 2023 — so the Background no longer frames APCA as a coming + standard, only as an independent diagnostic, with WCAG 2.x kept as baseline + because nothing has replaced it. Clarifications added to the Agreed decisions: + what CIEDE2000 is and why ΔE-OK suffices (#1); that no low-contrast preset + exists today and what one would add (#2); that table APCA is low-complexity and + the deferral is about density not difficulty (#3); the reason HSV stays default + until the 4b plane lands (#4). The Verification section now states Python + coverage is ~0% by design (the generator is output-tested via hash tests) and + the target is per-behavior hash-test gating, not a line-coverage number. +- *Why:* Craig interrogated the decisions before confirming; the answers either + reinforce them or give him what he needs to override. No decision changed. +- *Artifacts:* WCAG 3 status via W3C/industry sources (June 2026); repo check + confirmed no Python test files / pytest config for =scripts/theme-studio/=. + +** 2026-06-08 Mon @ 15:34:36 -0500 — Craig Jennings (direction) + Claude Code (responder) — testing-strategy override +- *What changed:* Craig overrode the v1 testing approach. The prior draft tested + the color math through browser hash tests (=#mathtest=), which coupled math + correctness to the DOM and capped coverage at what the UI exercises. Corrected + to a layered pyramid: the pure math is extracted into =scripts/theme-studio/colormath.js= + (an importable ES module that =generate.py= inlines into the page), unit-tested + directly in Node (=test-colormath.mjs=, =node --test=) with exhaustive fixtures + + a round-trip property test; the browser hash tests shrink to UI wiring only + (=#cursortest=/=#readouttest=/=#deltatest=/=#oklchtest=/4b-plane); =#selftest= + stays as integration smoke; an inline-integrity check guards the module and the + inlined copy against drift. =#mathtest= is removed — the math is no longer a UI + concern. Updated Phase 1, Verification, Acceptance, Implementation phases, and + Documentation to match. Language correction: the math is JavaScript (emitted by + the Python), so the "Python unit tests" instinct lands as Node unit tests on the + extracted JS core; the Python stays templating/data and is out of test scope. +- *Why:* Test the core directly, keep the UI thin — the API-first shape this + app grew past. Direct unit tests on the math are faster, more exhaustive, and + not limited by the UI surface. +- *Decisions 1-4 confirmed* as written (4: OKLCH readouts always shown; only the + editing model is opt-in until 4b). Phase 6 task-filing + commit still pending + Craig's go. diff --git a/docs/design/theme-studio-seeding-engine-spec.org b/docs/design/theme-studio-seeding-engine-spec.org new file mode 100644 index 00000000..bcbf43db --- /dev/null +++ b/docs/design/theme-studio-seeding-engine-spec.org @@ -0,0 +1,350 @@ +#+TITLE: theme-studio — seeding engine (role table to guide-correct defaults) +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-08 + +* Status + +Spec / review incorporated (Codex, 2026-06-08). Turns the color-assignment guide's seed table +and shade budget into an executable seeding engine: the tool opens with every +tier (syntax, UI faces, org-mode package faces) already colored to the guide's +defaults, so the user retunes hues with the picker rather than building a theme +from blank. Also reseeds the bundled =dupre= theme to the canonical compact +mapping (it currently diverges on two roles). + +Derives directly from =scripts/theme-studio/theme-coloring-guide.org= — the seed +table (role to palette-family / weight / channel) and the Shade budget (how many +shades each hue family carries). This spec encodes that table as data, classifies +each tier's faces into roles, and applies the table to produce the defaults. + +Rubric: *Ready.* Craig answered the four open questions (folded into Agreed +decisions) and Codex's review is incorporated. One decision reshapes the plan: v1 +generates shades with OKLCH (Craig's call), reusing the perceptual-metrics +=colormath.js= core, so this feature sequences after that spec's Phase 1. Two v1 +phases, each headless-testable. + +* Background — how the tool seeds today + +=scripts/theme-studio/generate.py= holds three face inventories, each with its +own ad-hoc default source: + +- *Syntax* — =CATS=, 21 categories keyed =bg p kw bi pp fnd fnc dec ty prop con + num str esc re doc cm cmd var op punc=. Defaults come from =COLS= (in + =samples.py=) into =MAP= and =BOLD=. There is no role layer; each category + carries a hand-set color. +- *UI faces* — =UI_FACES= (20 faces) with defaults in =UIMAP=, hand-authored. + This map already follows the guide closely (state faces are background-only, + active louder than idle, error/warning/success on the conventional hues), which + is the validation that the guide's principles describe a good UI tier rather + than invent one. +- *Package faces* — =APPS[app].faces=, each row =[face, label, default-dict]=. + =seedPkgmap()= reads the per-face default-dict. About twenty bespoke packages + (org, magit, elfeed, mu4e, ghostel, dashboard, lsp-mode, flycheck, dired, + dirvish, calibredb, erc, signel, pearl, slack, telega, shr, and more) carry + curated seed colors; generic inventory packages (from =package-inventory.json=) + seed to the default foreground. + +Three problems this spec addresses: + +1. *No role layer.* Each tier's defaults are set face-by-face by hand. There is + no single place that says "definitions are the warm anchor, bold" and projects + it onto syntax, UI, and org at once. The guide now states that table; the tool + does not consume it. +2. *dupre diverges from its own guide.* The compact mapping says builtins are + blue-grey and function definitions are gold; =dupre= assigns builtins to blue + (=bi= shares =kw='s hue) and definitions to silver (=fnd=). The guide records + this as a known divergence to be reseeded. +3. *Tiers do not open guide-correct.* UI is close by luck of hand-tuning; syntax + carries dupre's divergence; org's long tail is unseeded. Opening seeded across + all three is the goal. + +* Goal + +A seeding engine with three parts and one surfacing rule: + +1. *The seed model as data* — a named palette with the shade budget, a + role-to-treatment table, and a face-to-role map per tier. The guide's table, + made executable. +2. *A =seed()= operation* — applies the role table through each tier's + face-to-role map to produce the default assignments (=MAP=/=BOLD= for syntax, + =UIMAP= for UI, =PKGMAP= defaults for packages). +3. *Reseed dupre* — regenerate =dupre-revised.json= from the engine so it matches + the compact mapping (builtins blue-grey, definitions gold). + +Surfacing rule (Craig): the tool *opens seeded*. The syntax tier is already +guide-correct on load, so the user adjusts hues with the picker, then scrolls to +the UI faces. A "reseed from guide" button restores the defaults on demand. + +Non-goals: role-mapping the non-org bespoke packages (org is the one document +package worth a role map; the other ~20 keep their existing curated =APPS= seeds, +and reseed resets them to those defaults rather than flattening them — see +Package scope); per-tier reseed controls (v1 reseeds all three owned tiers at +once). + +* The seed model + +** Palette and shade budget + +A named swatch set, one to three shades per hue family, per the guide's Shade +budget. The names are the contract. v1 *generates* the shades with OKLCH (Craig's +call): each family is anchored by a base hue (the dupre anchors — blue, gold, +regal, sage, terracotta), and its quieter or brighter shades are derived by +stepping OKLCH lightness/chroma from that anchor, using the perceptual-metrics +=colormath.js= core. Generation is a first guess; any hue that reads wrong gets a +hand-authored override swatch. Rough shape: + +- *Neutrals:* =ground= (bg), =bg-dim=, =fg=, =muted-fg=, =comment=. +- *Blue:* =blue= (keyword), =blue-grey= (builtin — blue at lower chroma/lightness). +- *Gold:* =gold= (definition), =gold-quiet= (call). +- *Violet:* =regal= (types/decorators). +- *Green:* =sage= (string), =sage-muted= (docstring), =sage-bright= (escape). +- *Teal:* =teal= (regexp). +- *Terracotta:* =terracotta= (numbers/constants). +- *Signal:* =red=, =amber=, =green=, =blue= (reused) for error/warning/success/link. + +Roughly fifteen swatches across seven or eight hues. The builtin =blue-grey= and +the call =gold-quiet= are the swatches dupre is missing today and gains on +reseed. + +** Role-to-treatment table + +The guide's seed table as data: each role maps to a swatch, a weight, an optional +slant/underline, and a channel (foreground or background). One literal object, +e.g. + +#+begin_src js +ROLES = { + base: {swatch:'fg', weight:'normal', channel:'fg'}, + structure: {swatch:'muted-fg', weight:'normal', channel:'fg'}, + control: {swatch:'blue', weight:'bold', channel:'fg'}, + builtin: {swatch:'blue-grey', weight:'normal', channel:'fg'}, + def: {swatch:'gold', weight:'bold', channel:'fg'}, + call: {swatch:'gold-quiet', weight:'normal', channel:'fg'}, + type: {swatch:'regal', weight:'normal', channel:'fg'}, + string: {swatch:'sage', weight:'normal', channel:'fg'}, + docstring: {swatch:'sage-muted', slant:'italic', channel:'fg'}, + escape: {swatch:'sage-bright',weight:'normal', channel:'fg'}, + literal: {swatch:'terracotta', weight:'normal', channel:'fg'}, + comment: {swatch:'comment', slant:'italic', channel:'fg'}, + state: {swatch:'tint', channel:'bg'}, + sig_error: {swatch:'red', channel:'fg'}, + sig_warn: {swatch:'amber', channel:'fg'}, + sig_ok: {swatch:'green', channel:'fg'}, + sig_link: {swatch:'blue', underline:true, channel:'fg'}, + heading: {swatch:'ramp', channel:'fg'}, // see heading ramp +} +#+end_src + +** Face-to-role maps + +*** Syntax (CATS key to role) + +=p=, =var= to base; =op=, =punc=, =cmd= to structure; =kw= to control; =pp= to +control (shared, optionally muted); =bi= to builtin; =fnd= to def; =fnc= to call; +=dec=, =ty=, =prop= to type; =con=, =num= to literal; =str= to string; =doc= to +docstring; =esc=, =re= to escape (=re= to a teal variant if present); =cm= to +comment; =cmd= to structure (delimiter, dimmer). =bg= is the ground, set +directly. + +*** UI faces (UI_FACES to role) + +=region=, =hl-line=, =highlight=, =show-paren-match= to state (background tint, +no fg); =isearch= to an active match chip (may invert); =lazy-highlight= to a +quieter match; =isearch-fail=, =show-paren-mismatch= to sig_error; =error= to +sig_error, =warning= to sig_warn, =success= to sig_ok; =link= to sig_link; +=mode-line= to active chrome, =mode-line-inactive=, =line-number=, =fringe=, +=vertical-border= to idle/receding chrome; =line-number-current-line= to active +chrome; =cursor= to its own; =minibuffer-prompt= to control. + +*** Org-mode (face to one of six roles) + +=org-level-1..8= to heading ramp; =org-meta-line=, =org-drawer=, +=org-special-keyword=, =org-property-value=, =org-block-begin-line= / +=org-block-end-line=, =org-ellipsis=, =org-tag=, =org-date=, +=org-document-info-keyword= to markup-recede; =org-block=, =org-code=, +=org-verbatim=, =org-inline-src-block= to code-like (reuse the syntax literal +lane); =org-todo= / imminent deadlines to sig (warm), =org-upcoming-deadline= to +sig_warn, =org-scheduled= / =org-done= to receded/cool (with =org-done= taking +strikethrough); =org-link= to sig_link; =org-quote=, =org-verse= to emphasis +(italic). The org long tail that does not classify seeds to base, as today. + +** Package scope + +The role engine owns three default sources: syntax, UI, and the *org-mode* +package faces. It does not touch the other ~20 bespoke packages in =APPS= (magit, +elfeed, mu4e, and the rest): their curated seed colors stay exactly as today, and +the reseed button *resets them to their existing =APPS= defaults* rather than +role-generating or flattening them to foreground. Generic inventory packages keep +seeding empty/default. So =seed(model)= returns =packages.org-mode= only; the +non-org defaults continue to flow from =seedPkgmap()= over the curated =APPS= +dicts, and reseed re-runs =seedPkgmap()= for them. A =#seedtest= asserts a non-org +bespoke package (e.g. magit) keeps its curated seed after open and after reseed. + +Reseeding preserves the package-face import guarantees already established by +=mergePackagesInto= / =packagesForExport= (unknown app/face preservation, old-JSON +compatibility, recoverable references to deleted palette colors); this spec does +not re-decide them. + +** Heading ramp + +=org-level-1..8= share one hue across three or four lightness steps (the guide +does not spend eight distinct shades). v1 generates the steps with OKLCH: from a +base hue, step lightness down per level (level 1 strongest and bold, deeper levels +quieter), cycling the steps past level 4. This uses the same =colormath.js= shade +generation as the palette above. + +* The seed() operation + +A pure function, =seed(model)= returns ={syntax, ui, packages}= default +assignments: + +- *syntax*: for each =CATS= key, look up its role, resolve the role's swatch to a + hex and its weight, produce =MAP[key]= and =BOLD[key]=. +- *ui*: for each =UI_FACES= face, resolve its role to =UIMAP[face]= ({fg, bg, + bold, italic, underline}), honoring the channel (state roles set bg only). +- *packages.org-mode*: for each org face, resolve its role to a default-dict + ({fg, bg, bold, italic, strike, inherit, height}). + +The output is exactly the shape =exportObj()= already emits (=assignments=, +=ui=, =packages=), so =seed()= produces a =theme.json= the existing import path +loads unchanged. =packages= carries only =org-mode= (Package scope); the non-org +curated defaults flow through =seedPkgmap()= as today. Reseeding dupre is +=seed(model)= combined with the curated package seeds, written to +=dupre-revised.json= (the canonical package-aware artifact — see Surfacing). + +* Surfacing in the tool + +- *Open seeded.* The page's initial =MAP=/=UIMAP= come from =seed(model)= (inlined + defaults), not from hand-set =COLS=/=UIMAP=; =PKGMAP= comes from =seed(model)='s + org defaults plus =seedPkgmap()= over the curated =APPS= dicts for the rest. On + load the syntax tier is guide-correct; the user retunes hues and scrolls to UI. +- *Reseed button.* A "reseed from guide" control reapplies the seeds to all three + owned tiers and resets the non-org packages to their curated =APPS= defaults. It + warns first, naming the scope: "Reseed syntax, UI, and package defaults from the + guide? This discards current color assignments." +- *Canonical artifact.* The reseeded bundle is written to =dupre-revised.json=, + the full package-aware file the README and =build-theme.el= example use. + =dupre.json= stays a legacy minimal import fixture (no =packages= key) unless + deliberately migrated. Importing the reseeded =dupre-revised.json= and opening + fresh land on the same state. + +* Implementation phases + +1. *Seed model + seed() + tests.* Add the palette anchors + OKLCH shade + generation (reusing =colormath.js=), the =ROLES= table, and the three + face-to-role maps as data in =generate.py= (or a sibling inlined like + =samples.py=); write the pure =seed()=. Gate: =#seedtest= asserts representative + faces land on the right swatch/weight/channel in each tier (=bi= blue-grey, + =fnd= gold + bold, =var= base, =op= / =punc= muted, =doc= italic; =region= / + =hl-line= bg-only, =link= underlined, =error= / =warning= / =success= on signal + hues, active vs inactive chrome differentiated; =org-level-1= strongest, + =org-code= the fixed-pitch literal lane, =org-done= receded/struck) AND that a + non-org bespoke package (e.g. magit) keeps its curated seed. +2. *Open seeded + reseed + dupre-revised regen.* Wire the initial state to + =seed(model)= (plus =seedPkgmap()= for the non-org packages); add the all-tier + reseed button with the scope-named overwrite warning, resetting non-org + packages to their =APPS= defaults; regenerate =dupre-revised.json= from the + engine. Gate: =#selftest= still PASS; a headless check that default-on-open + equals =seed(model)=; an *artifact round-trip* check that the regenerated + =dupre-revised.json= imports back to the same seeded state (package defaults and + source markers included); a Chrome eyeball that the seeded syntax tier reads as + a coherent dupre. + +Dependency: v1 reuses the perceptual-metrics =colormath.js= core for OKLCH shade +generation, so it sequences after that spec's Phase 1 (the math foundation). No +second color-math implementation. + +* vNext candidates + +- Per-tier reseed controls (reseed just syntax, just UI, just org) after the + all-at-once v1 button. +- Role-mapping selected non-org bespoke packages beyond org, if their curated + defaults prove worth regenerating from the table. +- The guide-support views and advisories already tracked in =todo.org=. + +* Acceptance criteria + +- *Phase 1*: =seed()= is pure and table-driven; representative faces in all three + tiers resolve to the guide's seed-table treatment; a non-org bespoke package + keeps its curated seed; OKLCH generation produces the family shades and the + heading ramp; =#seedtest= PASS. +- *Phase 2*: the tool opens with syntax/UI/org seeded from =seed(model)= and the + non-org packages on their curated =APPS= defaults; the reseed button restores + all three owned tiers (and resets non-org to curated defaults) behind a + scope-named warning; =dupre-revised.json= is regenerated, matches the compact + mapping (=bi= blue-grey, =fnd= gold), and round-trips back to =seed(model)= on + import; =#selftest= PASS; a Chrome eyeball confirms a coherent dupre. + +* Agreed decisions (v1) + +Answered by Craig (2026-06-08), folded in. + +1. *Palette swatch source.* Generate the shades with OKLCH and fix hues that read + wrong by hand override (Craig overrode the hand-authored recommendation). This + moves OKLCH generation into v1 and makes the feature reuse the + perceptual-metrics =colormath.js= core, sequencing after that spec's Phase 1. +2. *Heading ramp depth.* Three or four distinct lightness steps, cycled across + levels 1-8. +3. *Converter sharing.* Tool-only for v1; =build-theme.el= consumes the exported + =theme.json= regardless. +4. *Reseed scope.* All three owned tiers at once; per-tier reseed is vNext. + +* Review dispositions + +Codex's review (2026-06-08) was accepted in full. The items below note the two +findings that corrected factual errors in the draft and the one open choice this +response resolved; everything else was woven into the body as written. + +- *Corrected — package scope (high-priority finding 2).* The draft said non-org + packages "seed to the default foreground." Wrong: =APPS= carries curated seeds + for ~20 bespoke packages. Rewritten so the role engine owns only org among + packages and the rest keep their curated =APPS= defaults, with reseed resetting + to those (see Package scope). +- *Corrected — canonical artifact (high-priority finding 3).* The draft named + =dupre.json=; the package-aware bundle is =dupre-revised.json=. Replaced + throughout, with =dupre.json= noted as the legacy minimal fixture. +- *Resolved — OKLCH dependency (high-priority finding 1).* The review offered two + routes to OKLCH-in-v1 (depend on the perceptual-metrics core, or build a local + minimal helper). Chose the dependency, to avoid a second color-math + implementation. + +* Sources + +- =scripts/theme-studio/theme-coloring-guide.org= — the seed table and Shade + budget this engine executes. +- =scripts/theme-studio/generate.py= — =CATS=, =UI_FACES=/=UIMAP=, =APPS= / + =seedPkgmap=, =exportObj= (the target shape). +- =docs/design/theme-studio-perceptual-color-metrics-spec.org= — the + =colormath.js= core that v1 OKLCH shade generation uses. + +* Review and iteration history + +** 2026-06-08 Mon @ 19:00:17 -0500 — Codex — reviewer +- *What changed or was recommended:* Created the implementation-readiness review + and marked the spec =Not ready=. The review treats the inline =cj: comment= + blocks as answered product input, then flags three remaining implementation + blockers: the palette-source answer moves OKLCH generation into v1 while the + phase plan still says vNext; the package seeding scope conflicts with the + current many-bespoke-app =APPS= surface; and the spec names =dupre.json= even + though the current README/build path uses =dupre-revised.json= as the full + package-aware artifact. +- *Why:* The role-table direction is sound, but those conflicts would force the + implementer to decide dependency order, package-default preservation, and + which bundled JSON file to write. +- *Artifacts:* the review file (consumed and deleted on response; see Review dispositions). + +** 2026-06-08 Mon @ 19:06:06 -0500 — Claude Code — responder +- *What changed:* Folded Craig's four answers into Agreed decisions and accepted every Codex finding. OKLCH shade generation moved into v1 (reusing =colormath.js=, sequenced after perceptual-metrics Phase 1); package scope rewritten so the engine owns only org and the ~20 non-org bespoke packages keep their curated =APPS= seeds (reseed resets to those); =dupre.json= references replaced with =dupre-revised.json= (canonical, package-aware) and =dupre.json= noted legacy; phases collapsed to two v1 phases with a vNext section; added the non-org-preserved and artifact-round-trip tests, the scope-named reseed warning, and the import-guarantee citation. +- *Why:* The draft described an older, narrower package surface and the wrong dupre artifact; left uncorrected, an implementer could wipe curated package defaults or write the wrong file. Two of the three blockers fixed errors in my draft. Rubric Draft to Ready. +- *Artifacts:* consumed and deleted the review file; see Review dispositions and Agreed decisions. + +** 2026-06-08 Mon @ 19:11:06 -0500 — Codex — reviewer +- *What changed or was recommended:* Re-reviewed the incorporated spec against + the current generator, README, task tracking, and previous findings. Assigned + =Ready=: the OKLCH dependency, non-org package seed preservation, and + =dupre-revised.json= artifact story are now explicit. Fixed one stale + non-blocking source note that still referred to Phase 3. +- *Why:* The spec now gives an implementer a coherent v1: two phases, explicit + dependency on perceptual-metrics Phase 1, table-driven =seed()=, open-seeded + and reseed behavior, package preservation rules, artifact round-trip tests, + and vNext boundaries. +- *Artifacts:* No review file written; no blocking findings. |
