aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/theme-studio-seeding-engine-spec.org
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-04 15:38:00 -0500
committerCraig Jennings <c@cjennings.net>2026-07-04 15:38:00 -0500
commit38200c6683e55860b044568cd70004dcbc7c4031 (patch)
tree55af7d1c91b6a5c58eda66fe30e2a4b7ae0b776d /docs/specs/theme-studio-seeding-engine-spec.org
parent0be572e43f8118f6c678a536b3d97d7e976e840f (diff)
downloaddotemacs-38200c6683e55860b044568cd70004dcbc7c4031.tar.gz
dotemacs-38200c6683e55860b044568cd70004dcbc7c4031.zip
docs(specs): adopt status-heading lifecycle convention across specs
Migrate 29 legacy specs off the old shape (a status suffix in the filename plus a :STATUS: property drawer) onto the docs-lifecycle status heading: a top-level heading carrying the org lifecycle keyword and a dated history line, with the two #+TODO sequences in the header. Dropping the -doing/-implemented/-superseded suffixes means a status change no longer forces a rename and link surgery. Each keyword comes from the spec's own recorded status. The four specs already on the heading form are untouched, and every inbound reference now points at the new names. The status board is one grep: rg '^\* (DRAFT|READY|DOING|IMPLEMENTED|SUPERSEDED|CANCELLED) ' docs/specs/
Diffstat (limited to 'docs/specs/theme-studio-seeding-engine-spec.org')
-rw-r--r--docs/specs/theme-studio-seeding-engine-spec.org358
1 files changed, 358 insertions, 0 deletions
diff --git a/docs/specs/theme-studio-seeding-engine-spec.org b/docs/specs/theme-studio-seeding-engine-spec.org
new file mode 100644
index 00000000..9e943b3b
--- /dev/null
+++ b/docs/specs/theme-studio-seeding-engine-spec.org
@@ -0,0 +1,358 @@
+#+TITLE: theme-studio — seeding engine (role table to guide-correct defaults)
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-06-08
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING theme-studio — seeding engine (role table to guide-correct defaults)
+:PROPERTIES:
+:ID: b70b37f2-37df-4c8e-ac2f-1f20d12e33dd
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
+
+* 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/specs/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.