#+TITLE: Emacs Config #+AUTHOR: Craig Jennings #+ARCHIVE: %s::* Emacs Resolved * Emacs Priority Scheme Use priority to express impact and urgency, not task type. Bugs, refactors, tests, chores, and features can all be high or low priority. - =[#A]= Urgent risk or current workflow blocker. Use for credential exposure, security/privacy leaks, data loss, destructive behavior, startup breakage, failing tests that block work, or a feature/refactor that unblocks a core daily workflow. - =[#B]= Important planned work. Use for concrete bugs, high-leverage architecture cleanup, brittle load-order/test gaps, dependency failures, or feature work with a clear design and expected near-term use. - =[#C]= Useful but optional. Use for low-risk cleanup, ergonomics, smoke tests, investigations with limited current impact, or feature work that would improve the setup but is not yet a committed workflow. - =[#D]= Someday/maybe or watchlist. Use for speculative features, tiny polish, upstream/package tracking, optimizations without current pain, or deferred ideas that should not compete with active maintenance. For =PROJECT= headings, use the highest priority of the meaningful child work inside the project. If a project only contains exploration or review, assign the priority by the expected decision value rather than the number of files touched. Use tags to describe the work shape: - =:bug:= means the current behavior is wrong or likely broken. - =:feature:= means the task adds a new user-visible capability or workflow. - =:refactor:= means the task changes structure/ownership without primarily changing behavior. - =:quick:= means the task appears low effort and localized. It is a planning hint, not a promise; remove it if the task grows during implementation. - =:solo:= means Claude can do the task end to end with no input from Craig: bounded scope, no design or preference call, and verifiable in the local setup (tests, byte-compile, launch). Tasks needing a policy/preference decision, visual judgment, or a live remote do not get =:solo:=. Tags are additive. For example, a small wrong-behavior fix can be =:bug:quick:=, and a feature that requires internal restructuring can be =:feature:refactor:=. * Emacs Open Work ** TODO [#C] Evaluate jamescherti essential-emacs-packages list :packages:research: Review [[https://www.jamescherti.com/essential-emacs-packages/][James Cherti's essential Emacs packages]] for anything worth installing. Cross-check each candidate against what is already in the config (=modules/= + =init.el=), skip the ones already present, and shortlist the genuinely new ones with a one-line rationale. Future-installation research, not a commitment to install. ** DONE [#B] theme-studio comprehensive previews (org/magit/elfeed/ghostel/mu4e/dashboard) :feature:theme:theme-studio: CLOSED: [2026-06-08 Mon] Expanded the bespoke previews to near-complete face coverage and added three new ones. org now exercises 83/88 faces (document + agenda; the 5 skipped are non-visual: org-hide, org-indent, org-clock-overlay, org-default, org-date-selected). magit 97/98 (status buffer + blame/reflog/sequence/bisect/signature sampler rows). elfeed 13/13. New bespoke previews: ghostel 19/19 (mock terminal, 16 ANSI colors + default + fake cursor), mu4e 37/37 (curated face list, not in the generated inventory; headers list + message view + compose), dashboard 8/8. So clicking a face row flashes a real preview element for nearly every face. Originally filed as just the org preview. ** DONE [#A] theme-studio theme.json -> dupre-*.el converter :feature:theme:theme-studio: CLOSED: [2026-06-08 Mon] Built as scripts/theme-studio/build-theme.el (sibling to build-inventory.el), emitting a single self-contained themes/-theme.el deftheme (not the palette/faces/theme trio — a theme.json carries resolved per-face hex, not dupre's semantic layer). All four tiers convert: default from assignments.bg/.p, syntax categories -> font-lock/tree-sitter faces with bold/italic sets, UI passthrough, packages with :inherit/:height/weight/slant. 20 ERT tests in tests/test-build-theme.el (Normal/Boundary/Error + an end-to-end load + a WCAG-AA assertion on the round-tripped result). One mapping limitation documented: the dec (decorator) key has no independent Emacs face (Emacs renders decorators with font-lock-type-face, which ty owns), so dec is omitted and decorators follow the type color. The last link in the pipeline: turn a theme.json exported by the theme-studio into a real loadable Emacs theme. Elisp (per Craig), TDD — this is the correctness-sensitive piece. Inputs (all on disk; no chat history needed): - theme.json contract: =scripts/theme-studio/README.md= (theme.json section) and =docs/design/theme-studio-package-faces-spec.org= (State and export policy, Relative height, Inheritance). - Reference face layout: existing =themes/dupre-palette.el= + =themes/dupre-faces.el= + =themes/dupre-theme.el=, and =tests/test-dupre-theme.el= (WCAG-contrast helper to reuse). - Conventions: =.claude/rules/elisp.md=, =.claude/rules/elisp-testing.md=. Scope: 1. Read theme.json. Set =default= from =assignments.bg= / =assignments.p=. 2. Author the syntax category -> font-lock face map (~21 keys: kw->font-lock-keyword-face, str->font-lock-string-face, fnd->font-lock-function-name-face, fnc->font-lock-function-call-face, op->font-lock-operator-face, punc->font-lock-punctuation-face, etc. incl. the Emacs-29 tree-sitter additions). Apply =bold= / =italic= sets. 3. UI faces: the =ui= keys are already real face names (region, cursor, mode-line, ...) -> near 1:1 passthrough of fg/bg. 4. Package faces: =packages= -> each face spec, writing =:inherit PARENT= for inherited faces + only the overridden attrs, =:height= when != 1.0, weight/slant. 5. Emit a deftheme file (or palette+faces+theme trio mirroring dupre's layout). TDD targets: old-JSON (no packages) loads; every category maps; round-trip of fg/bg/bold/italic/inherit/height into valid face specs; WCAG-contrast assertion on the result. Decide whether the converter lives under =scripts/theme-studio/= (emits to =themes/=) or =themes/=. ** TODO [#B] Dupre diff-changed / diff-refine-changed legibility :bug:dupre: Surfaced 2026-06-07 from a pearl session designing its modified-ticket indicator (pearl marks a changed field by inheriting =diff-changed=). dupre's =diff-refine-changed= is bright gold (#ffd700) under near-white text (#f0fef0) -- WCAG contrast ~1.35, unreadable as a plain background. It only looks fine inside diff-mode because diff-mode overlays its own dark foreground. =diff-changed= (#875f00 amber) is ~5.49, readable but off the modus model. Every modus variant keeps both faces legible (contrast 9-16) by pairing a dark low-saturation background with a hue-matched foreground. Ask: 1. Rework dupre's =diff-changed= and =diff-refine-changed= on modus lines: dark low-saturation background, legible foreground (plain default fg for simplicity, or hue-tinted per modus -- decide), and keep refine slightly stronger than changed (refine is the word-level emphasis inside a changed region; modus keeps them distinct). 2. While there, audit dupre's broader diff/palette faces against modus conventions (background/foreground tinting, contrast targets) and flag where it diverges. Reference values -- modus-vivendi: refine-changed bg #4a4a00 fg #efef80, changed bg #363300 fg #efef80. modus-operandi: refine-changed bg #fac090 fg #553d00, changed bg #ffdfa9 fg #553d00. Side-by-side legibility render: [[file:assets/2026-06-07-dupre-diff-face-legibility-compare.png][assets/2026-06-07-dupre-diff-face-legibility-compare.png]]. ** TODO [#B] dupre-theme test failures :bug:dupre:tests: A full =make test= run (2026-06-07) is green across 516 of 517 files; the only failures are 4 tests in =tests/test-dupre-theme.el=, long pre-existing. Two root causes. For each, decide whether the palette or the test assertion is canonical, then fix the loser so =make test= goes fully green. *** TODO Background drift: 3 tests expect #151311, palette bg is #0d0b0a =dupre-get-color-base= (test:46), =dupre-theme-default-face= (test:84), and =dupre-with-colors-binds-values= (test:62) all assert the default background is "#151311", but =themes/dupre-palette.el= defines =bg= as "#0d0b0a". The committed palette looks intentional, so the three assertions are likely just stale -- confirm #0d0b0a is the wanted background, then update the tests. *** TODO org-todo color mismatch: test expects #ff2a00, theme renders #a7502d =dupre-theme-org-todo= (test:130) asserts the org-todo foreground is "#ff2a00" (intense-red), but the theme renders "#a7502d" (red-1). Design call: should org-todo be the bright intense-red or the muted red-1? Fix whichever side loses the decision. ** TODO [#C] dupre-clear theme — contrast-first AAA sibling :feature:theme:dupre: Build a new theme (working name "dupre-clear", final name TBD) that takes dupre's color identity and rebuilds it Prot's way: contrast-first, targeting WCAG AAA (~7:1 on the ground), where the in-progress dupre revision is mood/depth-first and lands at AA. Same hues (dupre blue, emerald, gold, terracotta, regal violet, mint) brightened to clear the AAA floor; same modus-style role mapping (blue keywords bold, gold functions, violet types, emerald strings, terracotta constants, silver default, warm-grey comments, metallic greys, navy + regal fills). Build the dupre revision first; this reuses its hue choices as the starting point. Full design + methodology + starting palette + open questions in the spec: [[file:docs/design/dupre-clear-theme.org][docs/design/dupre-clear-theme.org]]. Key prerequisite/context: the dupre-redesign entry in =.ai/session-context.org= (the AA palette this brightens). Hardest slot: blue keywords (a deep dupre blue can't be AAA on near-black — decide brighten vs keep-AA-exception vs lift-the-ground). ** DONE [#B] theme-studio tier-3 package faces :feature:theme:theme-studio: CLOSED: [2026-06-08 Mon] Package-specific face editing in the theme-studio: org/magit/elfeed bespoke (complete face tables + live previews) plus a generated all-package inventory so every installed package is themeable. Spec is Ready, all opens resolved: [[file:docs/design/theme-studio-package-faces-spec.org][docs/design/theme-studio-package-faces-spec.org]]. Phases below run in dependency order; phases 1-5 deliver the three high-value apps, phase 6 opens the long tail, phase 7 documents. The =theme.json= -> =dupre-*.el= converter (Elisp) is a separate downstream task. *** 2026-06-08 Mon @ 00:17:41 -0500 Phase 1 — package state + schema landed Added =APPS= (org starter) and =PKGMAP= ({app:{face:{fg,bg,bold,italic,inherit,height,source}}}), pure helpers (=seedPkgmap= / =packagesForExport= / =mergePackagesInto=), and wired export/import for the =packages= key with old-JSON compat. The =height= float (relative size, read off the face not cascaded through inherit) and the fixed-pitch inherits are seeded in the org starter. No UI yet (Phase 3). Verified: node-check, plus a guarded =#selftest= harness (headless Chrome) confirming seed->export->import round-trip, old-JSON merge, and inherit/height/source survival — all PASS. *** 2026-06-08 Mon @ 02:16:24 -0500 Phase 2 — curated app data (org/magit/elfeed) landed Filled =APPS= with the complete own-defface sets built from embedded face-name lists + a curated seed-color map: org 88 (85 seeded, incl. org-agenda, heading heights, fixed-pitch inherits), magit 98 (64 seeded), elfeed 13 (all seeded). Long-tail faces seed to default fg. Verified: 199 faces total, no seed typos / no dupes, schema self-test PASS seeding all of them. Seeded-default aesthetics still go to Manual testing once the Phase 3 UI lands. *** 2026-06-08 Mon @ 02:23:56 -0500 Phase 3 — package face table UI landed Added the "package faces" section: app selector (org/magit/elfeed), per-app face table with fg/bg dropdowns, bold/italic toggles, inherit dropdown (base faces + the app's own faces), relative-height stepper, live contrast readout on the effective (inherit-resolved) color, per-face and per-app reset, and a text filter. Refactored the fg/bg dropdown into a shared =colorDropdown= helper the ui-faces table now also uses (no =uiSelect= fork). Palette edits propagate to package faces; import/export carry them. Right pane is the generic preview (face names in their own resolved colors) until the bespoke org/magit/elfeed previews land (phases 4-5). Verified: node, headless screenshot, schema self-test PASS. *** 2026-06-08 Mon @ 02:27:51 -0500 Phase 4 — org preview landed Added =renderOrgPreview()=: a mock org document painted live from the org package faces (title, headings with heights, TODO/DONE, tag, scheduled date, property drawer, inline code/verbatim, link, checkbox, quote, src block, header-row table). The preview pane dispatches on the app's preview key; org-mode gets this, others keep the generic list. Verified: node, headless screenshot, self-test PASS. *** 2026-06-08 Mon @ 02:30:42 -0500 Phase 5 — magit + elfeed previews landed Bespoke =renderMagitPreview()= (status buffer: head/branches, untracked, a diff hunk with context/added/removed, recent commits with hashes/authors/keyword/tag) and =renderElfeedPreview()= (search list: filter, dated entries with feed/unread-title/read-title/tags, log lines by level). The preview label now names the app and notes generic vs bespoke. Verified: node, headless screenshots, self-test PASS. *** 2026-06-08 Mon @ 02:32:44 -0500 Phase 6 — generated all-package inventory landed =build-inventory.el= (loaded into a running Emacs) groups every installed package's faces by the defining package and writes =package-inventory.json=. =generate.py= embeds it and merges each package into the dropdown as an editable generic app, leaving org/magit/elfeed bespoke. 40 apps now (3 bespoke + 37 inventory, 643 faces). Committed data artifact, refreshed by reloading the .el; never browser-side discovery. Verified: node, self-test PASS, app count + bespoke-preserved checks. *** 2026-06-08 Mon @ 02:34:01 -0500 Phase 7 — docs landed Rewrote =README.md= for the full tool: three face tiers + palette, the in-page picker (with the AA/AAA mask), package faces (bespoke vs generic previews), modeled inheritance + relative height (family stays in font-config.el), the packages schema with inherit/height/source, export-vs-save, and the inventory-refresh command (=build-inventory.el=) + its loaded-config dependency. Notes =theme-studio.html= is generated. Test-surface fixtures tracked separately below. *** 2026-06-08 Mon @ 02:40:00 -0500 theme-studio tier 3 — test surface landed Extended the guarded =#selftest= harness (headless Chrome) to assert the acceptance criteria against the real emitted code: old-JSON import (no =packages=), full round-trip (fg/bg/bold/italic/inherit/height/source), cleared-state export, unknown-package/face preservation, and inheritance-cycle termination — all PASS. The two DOM-coupled regressions are handled structurally: =updateColor= remaps =PKGMAP= on a palette-color edit, and =PKGMAP= stores hexes so a deleted palette color leaves package refs in the "(gone)" recoverable state. =generate.py= rebuilds =theme-studio.html= each run. ** DONE [#B] theme-studio perceptual color metrics :feature:theme:theme-studio: CLOSED: [2026-06-08 Mon] Spec (Ready, opens confirmed 2026-06-08): [[file:docs/design/theme-studio-perceptual-color-metrics-spec.org][docs/design/theme-studio-perceptual-color-metrics-spec.org]]. OKLCH model + perceptual-L/APCA readouts + pairwise ΔE, for building low-contrast themes by metric rather than by eye. All five phases shipped 2026-06-08 (commits 49342bf5, 78260018, 77c7f126, 163d3730, 22605426, 582d8a6a): colormath.js core inlined + WCAG/HSV helpers migrated; picker OKLCH/APCA readouts; palette ΔE warnings; OKLCH edit-model dials; C×L gamut plane. 17 Node tests (colormath 100/93.75/100), six browser hash gates green, inline-integrity guard. vNext deferrals (low-contrast preset, CIEDE2000) remain the two [#D] tasks below. Manual eyeballs tracked under Manual testing. *** 2026-06-08 Mon @ 19:43:50 -0500 Color-math foundation + Node tests landed Pure color core in =scripts/theme-studio/colormath.js= (OKLab/OKLCH, APCA-W3 0.1.9 exact constants, ΔE-OK, binary-search gamut clamp returning ={hex,clamped}=) shipped in 49342bf5; this phase finished the integration in 78260018. =generate.py= now inlines the colormath.js body into the page script (export-stripped, =COLORMATH_J= placeholder), and the page's lin/rl/contrast/rating/hsv2rgb/rgb2hsv/hex2rgb/rgb2hex copies moved into the module — =rl= reuses the canonical =lin= (0.04045 cutoff), byte-identical to the old 0.03928 form on every #rrggbb (no 8-bit channel falls between the cutoffs; verified over 200k pairs, zero contrast change). =test-colormath.mjs= gained Normal/Boundary/Error cases for the migrated helpers, a seeded hsv-rgb round-trip property test, and an inline-integrity check that the generated page carries the module body verbatim. Gate met: =node --test scripts/theme-studio/*.mjs= 15 pass, colormath.js 100% line / 93.75% branch / 100% func; =node --check= on the spliced script clean; =#selftest= + =#cursortest= PASS in headless Chrome. NOTE: =node --test = directory-globbing is broken on Node v26 (tries to load the dir as a module) — use the =*.mjs= glob form. *** 2026-06-08 Mon @ 19:55:53 -0500 Picker OKLCH/APCA readouts landed Phase 2 shipped in 77c7f126. Second readout row (=.pinfo2=) under the WCAG ratio: OKLCH L/C/H + signed APCA Lc against the ground color, always shown; sign convention in the APCA tooltip + README. Tables unchanged (APCA picker-only per Agreed-decision #3). =pkReadout= drives the spans from the inlined colormath functions. Gate met: =#readouttest= asserts the spans match the live computation AND the known dupre-blue OKLCH reference (L 0.591 / C 0.052 / H 252°, APCA Lc -34 on ground) with WCAG unchanged; =#selftest= + =#cursortest= still PASS; 15 Node tests green. Headless-rendered values verified against a node cross-check. Visual eyeball is the open "Perceptual readouts read well in the picker" item under Manual testing. *** 2026-06-08 Mon @ 20:44:39 -0500 Palette ΔE warnings landed Phase 3 shipped in 163d3730. =renderPalette= runs a pairwise OKLab ΔE over PALETTE via the pure =paletteDeltas()= (one pass → sub-threshold pairs + per-color nearest distance); warns on pairs below the named =DELTAE_MIN= (0.02), sorted closest-first, capped at 5 with "and N more"; each chip's tooltip gains its nearest-neighbor ΔE. Names go through =esc= before the warning markup. Gate met: =#deltatest= PASS (near pair fires + names itself; spread palette quiet; 7-color cluster caps at 5 ascending + overflow suffix). #readouttest/#selftest/#cursortest + 15 Node tests still green. Screenshot-verified the warning render (terracotta "too-similar colors" header + "blue / blue2 — ΔE 0.007, hard to distinguish", placed between palette and add-color controls). Pushed below. *** 2026-06-08 Mon @ 21:05:28 -0500 OKLCH sliders + color-model control landed Phase 4a shipped in 22605426. Picker gains an edit-model toggle (HSV/OKLCH) in its own =pkModel= state, orthogonal to =pkMode= (AA/AAA mask) — separate handlers, distinct toggle colors (blue vs gold). OKLCH mode shows L/C/H as paired range+number inputs driving =oklch2hex= → hex/swatch/readouts/HSV-cursor; out-of-gamut chroma snaps the dials to the reachable color + shows "chroma clamped to sRGB". HSV stays default; SV square still edits HSV (C×L plane is 4b); SV drag in OKLCH mode refreshes the dials. =openPicker= re-asserts the model via =setPkModel= so the toggle highlight can't drift (caught on screenshot). Gate met: =#oklchtest= PASS (color preserved on model switch; mask toggle leaves pkModel; model switch leaves pkMode; dials drive color to a known OKLCH target; out-of-gamut C raises clamp status). All 5 browser gates + 15 Node tests green; screenshot-verified the dials + toggle highlight. *** 2026-06-08 Mon @ 21:41:49 -0500 Chroma×Lightness plane landed Phase 4b shipped in 582d8a6a. OKLCH mode renders the SV square as a C(x)×L(y) plane at the current hue; crosshair maps to (C,L), hue strip selects H. Out-of-gamut region greyed (#15120f), AA/AAA contrast mask overlays the reachable colors. Per-cell gamut test is forward-only (=oklch2oklab=→=oklab2lrgb=→=inGamut=), never the binary search (that stays in =oklch2hex= for committing). colormath.js exports =oklab2lrgb=/=inGamut=/=lrgb2hex= with direct Node tests (one pins inGamut to oklch2hex's clamped flag). Bitmap cached on (hue+dims+mask+bg) so C/L drags reuse it; hue drags ride browser pointermove-to-frame coalescing (synchronous render measured ~7ms math/5600 cells — no explicit rAF defer; flagged if jank appears). HSV path untouched. Gate met: =#planetest= (crosshair at C/L; OOG cell grey; in-gamut cell colored). Screenshot-verified the plane (gamut-boundary shape, crosshair at C=0 for grey). NOTE for Craig: OKLCH_CMAX=0.4 matches the C dial domain, so much of the plane is gamut-grey at low-chroma hues — a tighter max fills more area but desyncs the crosshair scale from the dial; your eyeball call. *** 2026-06-08 Mon @ 21:41:49 -0500 Test surface green across the feature Final state: 17 Node unit tests (colormath.js 100% line / 93.75% branch / 100% func), six browser hash gates (=#cursortest=/=#readouttest=/=#deltatest=/=#oklchtest=/=#planetest=/=#selftest=), inline-integrity check, =node --check= on the spliced page, README updated. All green. NOTE: =node --test = directory-globbing is broken on Node v26 — use =node --test scripts/theme-studio/*.mjs=. ** TODO Manual testing and validation :verify:theme-studio: Exercised once the phases above land. *** TODO Seeded package-face defaults look right What we're verifying: the seeded org/magit/elfeed default colors read well before any tuning. - Open =scripts/theme-studio/theme-studio.html= in Chrome - Switch the app selector to org-mode, then magit, then elfeed - Read each preview pane against the dupre ground Expected: each package's seeded defaults look coherent and legible; nothing is unreadable or jarring. Override anything off in the tool. *** TODO Large face tables stay usable What we're verifying: org's ~88-face and magit's ~111-face tables stay navigable. - Select org-mode and scroll the grouped face table - Type "agenda" in the filter - Reassign one face and watch the preview Expected: rows are grouped, the filter narrows them, and a reassignment updates the preview live. *** TODO Perceptual readouts read well in the picker What we're verifying: the OKLCH L/C/H and APCA Lc readouts are legible and correctly placed beside the WCAG number. - Open =scripts/theme-studio/theme-studio.html= in Chrome and open the picker on a few colors - Read the OKLCH and APCA values against the WCAG ratio Expected: the new readouts are clear, the APCA sign/polarity is understandable, and nothing crowds the readout bar. *** TODO ΔE warnings read clearly What we're verifying: the too-similar-pair warning is legible and the cap behaves. - Build a palette with two near-identical colors, then a well-spread one - Read the warning line each time Expected: the close pair is named with its ΔE, sorted closest-first, capped at 5 with "and N more"; the well-spread palette shows no warning. *** TODO OKLCH editor feels right What we're verifying: the OKLCH sliders / C×L plane edit cleanly and clamping is visible. - Switch the picker to OKLCH mode and drag L, then C, then H - Push chroma past the sRGB gamut, then toggle the AA/AAA mask Expected: each axis moves independently; the C×L plane (once 4b lands) opens on the current color; "chroma clamped to sRGB" shows on clamp; toggling the mask does not reset OKLCH mode. ** TODO [#B] theme-studio guide-support features :feature:theme-studio: From the color-assignment guide work (2026-06-08): make the tool support the guide without mandating it — everything a seed, an advisory, or a view, never a gate. Two specs to write, both deriving from the rewritten guide and its seed table ([[file:scripts/theme-studio/theme-coloring-guide.org][theme-coloring-guide.org]]). *** 2026-06-08 Mon @ 19:08:00 -0500 Seeding-engine spec written and Ready [[file:docs/design/theme-studio-seeding-engine-spec.org][theme-studio-seeding-engine-spec.org]] — role table + face→role maps for syntax/UI/org, OKLCH shade generation, reseed dupre-revised to the compact mapping. Codex-reviewed, Ready. Implementation tracked under the seeding-engine parent below. *** TODO Guide-support views and advisories spec :solo: Five optional surfaces, all dismissible and non-blocking, in one collapsible panel where they advise: (1) CVD-simulation toggle on previews (deuteranopia/protanopia/tritanopia); (2) squint/blur preview toggle; (3) lightness-ramp view + palette advisories (accent count over 6-8, roles separated only by red/green) — depends on the OKLCH/ΔE core; (4) definition-vs-call / weight advisories; (5) state-over-syntax preview (region/search/diff tint over real syntax-colored text). Sequence: rewritten guide reviewed → seeding-engine spec → this. Advisories (3, 4) layer on the perceptual-metrics feature. ** TODO [#B] theme-studio seeding engine :feature:theme:theme-studio: Spec (Ready): [[file:docs/design/theme-studio-seeding-engine-spec.org][spec]]. Role table → guide-correct defaults for syntax/UI/org; reseed dupre-revised.json to the compact mapping; opens seeded with an all-tier reseed button. Depends on the perceptual-metrics colormath.js core for OKLCH shade generation, so it runs after that feature's Phase 1. *** TODO Seed model + seed() + #seedtest :solo: Phase 1. Palette anchors + OKLCH shade generation (reusing colormath.js), the ROLES table, and the three face→role maps as data; pure seed(). Gate: #seedtest asserts representative syntax/UI/org faces resolve correctly (bi→blue-grey, fnd→gold+bold, region bg-only, link underlined, org-level-1 strongest, org-code literal lane) and a non-org bespoke package (magit) keeps its curated seed. *** TODO Open-seeded + reseed + dupre-revised regen :solo: Phase 2. Initial state from seed() plus seedPkgmap for the non-org packages; all-tier reseed button with a scope-named overwrite warning, resetting non-org to their APPS defaults; regenerate dupre-revised.json. Gate: #selftest PASS; default-on-open equals seed(); artifact round-trip (regenerated dupre-revised.json imports back to the same seeded state); Chrome eyeball. *** TODO Seeding-engine test surface :solo:tests: Keep #seedtest, #selftest, the default-on-open check, the dupre-revised round-trip, node --check, and Chrome validation green. ** DONE [#B] theme-studio refactor — extract app from generate.py :feature:theme-studio:refactor: CLOSED: [2026-06-09 Tue] Examined 2026-06-09. generate.py is 1378 lines, ~1300 of them a single triple-quoted string holding the whole app (CSS + HTML + ~1000+ lines of JS). That string is the root of every refactor here: the app logic can't be unit-tested (only =colormath.js= is, because it is the one extracted module); backslash-doubling in the string caused real bugs this session (the multi-line export strip, the =#deltatest= regex); and there is no lint, highlight, or brace-check until Chrome runs it. The rest of the directory is healthy: =colormath.js= (pure, 100/96 tested) and =build-theme.el= (13 small functions) are the model. Run the whole set in NO-APPROVALS mode: TDD per stage (characterization hash tests before each behavior-preserving move; node unit tests as extraction makes logic importable), commit + push at each green stage. Tooling committed at c7518d6f before starting. Order: DONE (2026-06-09): Stages 1-5 + 7 landed and pushed (origin/main tip dd90eca9); Stage 6 deliberately skipped (optional, works today). generate.py went 1378→~500 lines; the app now lives in real files (styles.css, app.js, app-core.js) inlined at generate time. The escaping-bug class is gone (str.replace is literal), the dedup is done (unified dropdowns/sort/clear-unlocked, shared crHtml/mkStyleButtons/effFg helpers), and the pure app logic is unit-tested (app-core.js, 18 node tests). Three new permanent gates added along the way: =#locktest=, =#sorttest=, and the app-core integrity + node suite. =make theme-studio-test= = 13 python + 43 node + spliced-check + 8 hash gates, all green. *** 2026-06-09 Tue @ 05:01:11 -0500 Stage 1 — #locktest net + extracted styles.css/app.js Added the =#locktest= browser gate first (commit d04f44dd): it pins, across all three tiers, that mkLockCell disables a row's control (syntax swatch div via data-locked, UI select via .disabled) and that clear-unlocked wipes unlocked rows while skipping locked ones. Proved it goes red when a lock guard is removed. Then extracted the =