diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-08 19:16:41 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-08 19:16:41 -0500 |
| commit | 4f4416fc50de824a4aa004605bcf90bc28b5cf27 (patch) | |
| tree | a7724a35109a0437834508fc4d4b73467f1f9379 /docs/design/theme-studio-seeding-engine-spec.org | |
| parent | 5c42cbcd91a81276508b094a191b9226e6aeb878 (diff) | |
| download | dotemacs-4f4416fc50de824a4aa004605bcf90bc28b5cf27.tar.gz dotemacs-4f4416fc50de824a4aa004605bcf90bc28b5cf27.zip | |
docs(theme-studio): add seeding-engine spec
The seeding engine turns the color guide's seed table into executable defaults: a role-to-treatment table, a face-to-role map per tier (syntax, UI, org), and a pure seed() that opens the tool guide-correct and reseeds dupre-revised to the compact mapping. v1 generates the shades with OKLCH, reusing the perceptual-metrics colormath.js core, so it sequences after that feature's Phase 1. todo.org carries the two implementation phases.
Diffstat (limited to 'docs/design/theme-studio-seeding-engine-spec.org')
| -rw-r--r-- | docs/design/theme-studio-seeding-engine-spec.org | 350 |
1 files changed, 350 insertions, 0 deletions
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. |
