aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-08 19:16:41 -0500
committerCraig Jennings <c@cjennings.net>2026-06-08 19:16:41 -0500
commit4f4416fc50de824a4aa004605bcf90bc28b5cf27 (patch)
treea7724a35109a0437834508fc4d4b73467f1f9379 /docs
parent5c42cbcd91a81276508b094a191b9226e6aeb878 (diff)
downloaddotemacs-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')
-rw-r--r--docs/design/theme-studio-seeding-engine-spec.org350
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.