aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/design/signal-client-review.org2
-rw-r--r--docs/design/subr-mock-migration.org (renamed from docs/subr-mock-migration-spec.org)2
-rw-r--r--docs/design/utility-inventory.org6
-rw-r--r--docs/specs/ai-kb-spec.org12
-rw-r--r--docs/specs/ai-vterm-spec.org (renamed from docs/specs/ai-vterm-spec-superseded.org)12
-rw-r--r--docs/specs/cache-helper-design-spec.org (renamed from docs/specs/cache-helper-design-spec-implemented.org)14
-rw-r--r--docs/specs/company-to-corfu-migration-spec.org12
-rw-r--r--docs/specs/coverage-spec.org (renamed from docs/specs/coverage-spec-implemented.org)12
-rw-r--r--docs/specs/debug-profiling-spec.org12
-rw-r--r--docs/specs/dev-setup-project-spec.org12
-rw-r--r--docs/specs/dupre-clear-theme-spec.org12
-rw-r--r--docs/specs/face-font-diagnostic-popup-spec.org (renamed from docs/specs/face-font-diagnostic-popup-spec-implemented.org)13
-rw-r--r--docs/specs/flycheck-modeline-customization-spec.org (renamed from docs/specs/flycheck-modeline-customization-spec-implemented.org)12
-rw-r--r--docs/specs/gloss-spec.org (renamed from docs/specs/gloss-spec-doing.org)11
-rw-r--r--docs/specs/google-keep-emacs-integration-spec.org9
-rw-r--r--docs/specs/init-load-graph-spec.org (renamed from docs/specs/init-load-graph-spec-doing.org)20
-rw-r--r--docs/specs/keybinding-console-safety-spec.org (renamed from docs/specs/keybinding-console-safety-spec-doing.org)14
-rw-r--r--docs/specs/messenger-unification-spec.org13
-rw-r--r--docs/specs/music-config-without-emms-spec.org12
-rw-r--r--docs/specs/org-faces-spec.org (renamed from docs/specs/org-faces-spec-implemented.org)13
-rw-r--r--docs/specs/signal-client-spec.org (renamed from docs/specs/signal-client-spec-doing.org)11
-rw-r--r--docs/specs/theme-studio-completion-preview-spec.org11
-rw-r--r--docs/specs/theme-studio-nerd-icons-colors-spec.org9
-rw-r--r--docs/specs/theme-studio-package-faces-spec.org (renamed from docs/specs/theme-studio-package-faces-spec-doing.org)12
-rw-r--r--docs/specs/theme-studio-palette-generator-spec.org (renamed from docs/specs/theme-studio-palette-generator-spec-doing.org)13
-rw-r--r--docs/specs/theme-studio-perceptual-color-metrics-spec.org (renamed from docs/specs/theme-studio-perceptual-color-metrics-spec-implemented.org)12
-rw-r--r--docs/specs/theme-studio-preview-locate-spec.org13
-rw-r--r--docs/specs/theme-studio-seeding-engine-spec.org (renamed from docs/specs/theme-studio-seeding-engine-spec-doing.org)21
-rw-r--r--docs/specs/theme-studio-semantic-theme-architecture-spec.org15
-rw-r--r--docs/specs/theme-studio-structured-output-spec.org13
-rw-r--r--docs/specs/utility-consolidation-spec.org (renamed from docs/specs/utility-consolidation-spec-doing.org)20
-rw-r--r--docs/specs/vterm-to-ghostel-migration-spec.org (renamed from docs/specs/vterm-to-ghostel-migration-spec-implemented.org)14
-rw-r--r--modules/org-config.el26
-rw-r--r--scripts/theme-studio/app.js3
-rw-r--r--scripts/theme-studio/browser-gates.js31
-rw-r--r--scripts/theme-studio/generate.py6
-rw-r--r--scripts/theme-studio/seed-core.js249
-rw-r--r--scripts/theme-studio/test-seed-core.mjs173
-rw-r--r--scripts/theme-studio/theme-studio.html279
-rw-r--r--tests/test-org-config-noop-state-log.el59
40 files changed, 1081 insertions, 134 deletions
diff --git a/docs/design/signal-client-review.org b/docs/design/signal-client-review.org
index 7e8a73e9..e1ac462e 100644
--- a/docs/design/signal-client-review.org
+++ b/docs/design/signal-client-review.org
@@ -5,7 +5,7 @@
* Scope reviewed
- =.ai/workflows/spec-review.org=.
-- =docs/specs/signal-client-spec-doing.org=, including the base design, open-question dispositions, initiate-message workflow, architecture additions, accepted caveats, test plan, scope summary, and readiness rubric.
+- =docs/specs/signal-client-spec.org=, including the base design, open-question dispositions, initiate-message workflow, architecture additions, accepted caveats, test plan, scope summary, and readiness rubric.
- =modules/signal-config.el=, including =cj/signal--parse-contacts=, notify-suppression helpers, private config loading, and current =use-package signel= wiring.
- =~/code/signel/signel.el=, including =signel-start=, =signel--send-rpc=, =signel--dispatch=, =signel--handle-error=, =signel--handle-receive=, =signel--insert-msg=, =signel--insert-system-msg=, =signel--send-input=, =signel-chat=, and dashboard commands.
- =tests/test-signal-config.el=, covering contact parsing and notify-suppression helpers.
diff --git a/docs/subr-mock-migration-spec.org b/docs/design/subr-mock-migration.org
index 26f1dd57..874d03ef 100644
--- a/docs/subr-mock-migration-spec.org
+++ b/docs/design/subr-mock-migration.org
@@ -10,7 +10,7 @@ primitives (native-comp robustness) :test:refactor:solo:=) so the scope and
approach can be settled before any code moves. Execution is deferred; this
document is the discussion vehicle.
-Companion reference: [[file:native-comp-subr-mocking.org][native-comp-subr-mocking.org]] holds the full mechanism, the
+Companion reference: [[file:../native-comp-subr-mocking.org][native-comp-subr-mocking.org]] holds the full mechanism, the
upstream research, and the 2026-06-21 decision. This spec does not restate the
mechanism; it plans the remaining work that decision deferred.
diff --git a/docs/design/utility-inventory.org b/docs/design/utility-inventory.org
index 8438a592..9b811429 100644
--- a/docs/design/utility-inventory.org
+++ b/docs/design/utility-inventory.org
@@ -4,7 +4,7 @@
* Status
-Living inventory. Phase 1 of [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]]. Records the current state of helpers identified in the spec's Candidate Extraction Table plus any new candidates discovered during module walkthroughs. Decisions become concrete tasks in =todo.org= for Phase 2+.
+Living inventory. Phase 1 of [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]]. Records the current state of helpers identified in the spec's Candidate Extraction Table plus any new candidates discovered during module walkthroughs. Decisions become concrete tasks in =todo.org= for Phase 2+.
* Scope
@@ -82,7 +82,7 @@ Caller counts in the inventory below reflect grep results from 2026-05-10. The c
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
-| =cj/modeline-vc-cache-*= helpers (key/get/put/clear/valid-p) | =modeline-config.el:108-140= | private | buffer-local vars | mutates buffer-local state | =cj-cache.el= / =cj/cache-valid-p=, =cj/cache-get=, =cj/cache-put=, =cj/cache-clear= | 1 (within file) | =test-modeline-config-vc-cache.el= | Medium | Defer | Good pattern, but variable-local cache shape differs from the agenda/refile caches. Needs design before extraction. Spec calls out a Phase 5 design addendum at =docs/specs/cache-helper-design-spec-implemented.org=. |
+| =cj/modeline-vc-cache-*= helpers (key/get/put/clear/valid-p) | =modeline-config.el:108-140= | private | buffer-local vars | mutates buffer-local state | =cj-cache.el= / =cj/cache-valid-p=, =cj/cache-get=, =cj/cache-put=, =cj/cache-clear= | 1 (within file) | =test-modeline-config-vc-cache.el= | Medium | Defer | Good pattern, but variable-local cache shape differs from the agenda/refile caches. Needs design before extraction. Spec calls out a Phase 5 design addendum at =docs/specs/cache-helper-design-spec.org=. |
| agenda/refile cache vars and build flags | =org-agenda-config.el=, =org-refile-config.el= | n/a | timers, file scans | scans filesystem, sets vars | =cj-cache.el= / =cj/cache-value-or-rebuild= | 2 | none | Medium | Defer | TTL/build/invalidate lifecycle; higher risk than the modeline cache. Same Phase 5 work. |
** Logging / Warnings
@@ -144,7 +144,7 @@ These become =todo.org= entries (or update existing ones) as Phase 2 starts.
** Deferred (track in =todo.org= but no commit yet)
-- Cache abstraction (modeline + agenda/refile) -- needs Phase 5 design addendum at =docs/specs/cache-helper-design-spec-implemented.org=.
+- Cache abstraction (modeline + agenda/refile) -- needs Phase 5 design addendum at =docs/specs/cache-helper-design-spec.org=.
- =cj/--open-with-is-launcher-p= -- move when external-open ownership is finalized.
- =cj/log-silently= rename -- low value; do during incidental =system-lib= work.
- HTML/text helpers (=strip-html=, =clean-text=) -- defer until a second consumer.
diff --git a/docs/specs/ai-kb-spec.org b/docs/specs/ai-kb-spec.org
index fbd35ca5..b35b3ac6 100644
--- a/docs/specs/ai-kb-spec.org
+++ b/docs/specs/ai-kb-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 03742426-35ce-41c5-aed7-d4e248e91833
-:STATUS: not-started
-:END:
#+TITLE: Design: AI Knowledge Base (ai-kb)
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-24
#+OPTIONS: toc:nil num:nil
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: AI Knowledge Base (ai-kb)
+:PROPERTIES:
+:ID: 03742426-35ce-41c5-aed7-d4e248e91833
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/ai-vterm-spec-superseded.org b/docs/specs/ai-vterm-spec.org
index 0b6bfb86..7015a862 100644
--- a/docs/specs/ai-vterm-spec-superseded.org
+++ b/docs/specs/ai-vterm-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 3abd0270-e87c-42b7-9b3a-ef60300db99d
-:STATUS: superseded
-:END:
#+TITLE: Design: ai-vterm — in-Emacs Claude launcher
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-07
#+OPTIONS: toc:nil num:nil
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* SUPERSEDED Design: ai-vterm — in-Emacs Claude launcher
+:PROPERTIES:
+:ID: 3abd0270-e87c-42b7-9b3a-ef60300db99d
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword SUPERSEDED from existing :STATUS: superseded + -superseded filename (Craig's prior determination)
* Status
diff --git a/docs/specs/cache-helper-design-spec-implemented.org b/docs/specs/cache-helper-design-spec.org
index 27c818dc..5bfb661b 100644
--- a/docs/specs/cache-helper-design-spec-implemented.org
+++ b/docs/specs/cache-helper-design-spec.org
@@ -1,14 +1,18 @@
-:PROPERTIES:
-:ID: 647c5101-21c2-47bb-aaa7-72c757f45fb7
-:STATUS: implemented
-:END:
#+TITLE: Cache Helper Design Addendum
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-10
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Cache Helper Design Addendum
+:PROPERTIES:
+:ID: 647c5101-21c2-47bb-aaa7-72c757f45fb7
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
-Phase 5 design addendum to [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]]. Specifies the cache API to extract before any code moves.
+Phase 5 design addendum to [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]]. Specifies the cache API to extract before any code moves.
* Problem
diff --git a/docs/specs/company-to-corfu-migration-spec.org b/docs/specs/company-to-corfu-migration-spec.org
index a7b059a3..ef094937 100644
--- a/docs/specs/company-to-corfu-migration-spec.org
+++ b/docs/specs/company-to-corfu-migration-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 68733ba2-37a7-4a7b-bfaa-b845d82ff1e7
-:STATUS: not-started
-:END:
#+TITLE: Design: Migrate from Company to Corfu (with prescient integration)
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-15
#+OPTIONS: toc:nil num:nil
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: Migrate from Company to Corfu (with prescient integration)
+:PROPERTIES:
+:ID: 68733ba2-37a7-4a7b-bfaa-b845d82ff1e7
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/coverage-spec-implemented.org b/docs/specs/coverage-spec.org
index 65734fb3..e2ac4b3c 100644
--- a/docs/specs/coverage-spec-implemented.org
+++ b/docs/specs/coverage-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 7d7f4486-fad7-4f0a-bd9a-775bd4cd8f7e
-:STATUS: implemented
-:END:
#+TITLE: Design: Coverage Reporting
#+AUTHOR: Craig Jennings
#+DATE: 2026-04-22
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Design: Coverage Reporting
+:PROPERTIES:
+:ID: 7d7f4486-fad7-4f0a-bd9a-775bd4cd8f7e
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
diff --git a/docs/specs/debug-profiling-spec.org b/docs/specs/debug-profiling-spec.org
index 5961071b..3492d3a2 100644
--- a/docs/specs/debug-profiling-spec.org
+++ b/docs/specs/debug-profiling-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: c713b431-ae14-498d-aba9-b84d52f981b6
-:STATUS: not-started
-:END:
#+TITLE: Design: debug-profiling.el module
#+AUTHOR: Craig Jennings
#+DATE: 2026-04-26
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: debug-profiling.el module
+:PROPERTIES:
+:ID: c713b431-ae14-498d-aba9-b84d52f981b6
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/dev-setup-project-spec.org b/docs/specs/dev-setup-project-spec.org
index 5d64f368..058784a5 100644
--- a/docs/specs/dev-setup-project-spec.org
+++ b/docs/specs/dev-setup-project-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 596fce5d-1bab-46e7-8567-d4a2e0923091
-:STATUS: not-started
-:END:
#+TITLE: Design: cj/dev-setup-project
#+AUTHOR: Craig Jennings
#+DATE: 2026-04-22
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: cj/dev-setup-project
+:PROPERTIES:
+:ID: 596fce5d-1bab-46e7-8567-d4a2e0923091
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/dupre-clear-theme-spec.org b/docs/specs/dupre-clear-theme-spec.org
index 578eb240..8027ee2a 100644
--- a/docs/specs/dupre-clear-theme-spec.org
+++ b/docs/specs/dupre-clear-theme-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 20df7f50-4759-47ba-9782-8dd25a2e173e
-:STATUS: not-started
-:END:
#+TITLE: dupre-clear — a contrast-first AAA sibling theme
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-07
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT dupre-clear — a contrast-first AAA sibling theme
+:PROPERTIES:
+:ID: 20df7f50-4759-47ba-9782-8dd25a2e173e
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/face-font-diagnostic-popup-spec-implemented.org b/docs/specs/face-font-diagnostic-popup-spec.org
index 3e8fadcd..aae355f9 100644
--- a/docs/specs/face-font-diagnostic-popup-spec-implemented.org
+++ b/docs/specs/face-font-diagnostic-popup-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: 98f065cf-8bd5-46a0-ac24-da94d66855ad
-:STATUS: implemented
-:END:
#+TITLE: Face and Font Diagnostic Popup — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-14
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Face and Font Diagnostic Popup — Spec
+:PROPERTIES:
+:ID: 98f065cf-8bd5-46a0-ac24-da94d66855ad
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Metadata
diff --git a/docs/specs/flycheck-modeline-customization-spec-implemented.org b/docs/specs/flycheck-modeline-customization-spec.org
index 59567be6..2a58b447 100644
--- a/docs/specs/flycheck-modeline-customization-spec-implemented.org
+++ b/docs/specs/flycheck-modeline-customization-spec.org
@@ -1,11 +1,15 @@
-:PROPERTIES:
-:ID: 76979608-956e-474f-90a8-8d0c958101a0
-:STATUS: implemented
-:END:
#+TITLE: Design: Flycheck modeline customization
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-15
#+OPTIONS: toc:nil num:nil
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Design: Flycheck modeline customization
+:PROPERTIES:
+:ID: 76979608-956e-474f-90a8-8d0c958101a0
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
diff --git a/docs/specs/gloss-spec-doing.org b/docs/specs/gloss-spec.org
index 320b83eb..06a7bf50 100644
--- a/docs/specs/gloss-spec-doing.org
+++ b/docs/specs/gloss-spec.org
@@ -1,10 +1,13 @@
+#+TITLE: Design — gloss (Glossary Lookup with Online-Sourced Selection)
+#+DATE: 2026-04-28
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Design — gloss (Glossary Lookup with Online-Sourced Selection)
:PROPERTIES:
:ID: 295f9969-ccef-4df9-945b-9e08d8069daf
-:STATUS: doing
:END:
-#+TITLE: Design — gloss (Glossary Lookup with Online-Sourced Selection)
-#+DATE: 2026-04-28
-#+STATUS: Draft
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Problem
diff --git a/docs/specs/google-keep-emacs-integration-spec.org b/docs/specs/google-keep-emacs-integration-spec.org
index 376522ab..96fd83e5 100644
--- a/docs/specs/google-keep-emacs-integration-spec.org
+++ b/docs/specs/google-keep-emacs-integration-spec.org
@@ -1,7 +1,14 @@
#+TITLE: Google Keep <-> Emacs integration — Spec
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-24
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Google Keep <-> Emacs integration — Spec
+:PROPERTIES:
+:ID: 4c796fb9-1d3e-42a9-9b76-eb286eee8732
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from Metadata Status: v1 implemented, v2 next -> work ongoing
* Metadata
| Status | v1 implemented (Phases 1-3); live setup pending; v2 next |
diff --git a/docs/specs/init-load-graph-spec-doing.org b/docs/specs/init-load-graph-spec.org
index 05dd9e0a..0feebfc9 100644
--- a/docs/specs/init-load-graph-spec-doing.org
+++ b/docs/specs/init-load-graph-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: e1fd137e-e164-42f4-a658-f4d32fbe3228
-:STATUS: doing
-:END:
#+TITLE: Design: Untangle the init.el Load Graph
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-04
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Design: Untangle the init.el Load Graph
+:PROPERTIES:
+:ID: e1fd137e-e164-42f4-a658-f4d32fbe3228
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Status
@@ -178,7 +182,7 @@ Foundation modules should be able to load in batch mode without package,
network, timer, or UI-package side effects.
Adding a new Layer 1 module requires a coordinated update to the
-=system-lib.el= dependency budget in [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]].
+=system-lib.el= dependency budget in [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]].
Topic libraries introduced by the utility project join Layer 1 only when their
first consumer is foundation-eager. Otherwise they are Layer 2 and loaded by an
@@ -395,7 +399,7 @@ Worked example:
;; Runtime requires: user-constants, seq, subr-x.
;; Direct test load: yes (batch-safe; private config is optional).
;;
-;; See also: docs/specs/init-load-graph-spec-doing.org, tests/test-calendar-sync.el.
+;; See also: docs/specs/init-load-graph-spec.org, tests/test-calendar-sync.el.
;;
;;; Code:
#+end_src
@@ -452,7 +456,7 @@ Inventory rules:
- Every module required by =init.el= must be represented before Phase 2 starts.
- Discoveries during later phases update the inventory.
- This inventory is independent from the helper inventory owned by
- [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]].
+ [[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]].
Exit criteria:
@@ -647,7 +651,7 @@ rollback shapes.
This sibling project can run beside Phase 2. When explicit-dependency work finds
a generic duplicated helper, the sibling project owns the extraction commit when
the helper is in scope for that project. See
-[[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec-doing.org]] for candidate
+[[id:fc2e3926-b4a1-4b45-92eb-20841e13f655][utility-consolidation-spec.org]] for candidate
helpers, naming rules, dependency budgets, migration phases, and test policy.
* Testing Strategy
diff --git a/docs/specs/keybinding-console-safety-spec-doing.org b/docs/specs/keybinding-console-safety-spec.org
index 4a1dec81..c7d1baf7 100644
--- a/docs/specs/keybinding-console-safety-spec-doing.org
+++ b/docs/specs/keybinding-console-safety-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 540bf06b-16b8-46c6-b459-c40d1b9c795d
-:STATUS: doing
-:END:
#+TITLE: Keymap Consolidation — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-12
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Keymap Consolidation — Spec
+:PROPERTIES:
+:ID: 540bf06b-16b8-46c6-b459-c40d1b9c795d
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Metadata
| Status | doing |
@@ -894,7 +898,7 @@ translation block being retired). =C-l= appears only minibuffer-local in
- Why: a touched key family broke in GUI and is dead in console; the fix path is
cross-cutting (18 keys, a translation layer to retire, a console-safety
architecture) with real trade-offs, so it clears the spec bar.
-- Artifacts: docs/specs/keybinding-console-safety-spec-doing.org; supersedes the
+- Artifacts: docs/specs/keybinding-console-safety-spec.org; supersedes the
pre-template draft docs/design/keybinding-console-safety.org.
** 2026-06-12 Fri @ 18:30:30 -0500 — Craig Jennings — review response
- What: processed Craig's four review comments. Recorded his first-choice
diff --git a/docs/specs/messenger-unification-spec.org b/docs/specs/messenger-unification-spec.org
index 92985f59..c92ba1a7 100644
--- a/docs/specs/messenger-unification-spec.org
+++ b/docs/specs/messenger-unification-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: 4bfc2011-8ffc-4765-8886-91df12141171
-:STATUS: not-started
-:END:
#+TITLE: Messenger Unification — Shared Window Placement and Key Conventions
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-11
-#+STATUS: Draft — decisions 1-9 settled (Craig, 2026-06-11/12); held open for further ideas before Ready
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Messenger Unification — Shared Window Placement and Key Conventions
+:PROPERTIES:
+:ID: 4bfc2011-8ffc-4765-8886-91df12141171
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started (held open for more ideas)
* Problem
diff --git a/docs/specs/music-config-without-emms-spec.org b/docs/specs/music-config-without-emms-spec.org
index 32fd6736..c63706e5 100644
--- a/docs/specs/music-config-without-emms-spec.org
+++ b/docs/specs/music-config-without-emms-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 423bc355-18d3-4e39-9e7a-f768b865d95b
-:STATUS: not-started
-:END:
#+TITLE: Design: music-config Without EMMS
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-15
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Design: music-config Without EMMS
+:PROPERTIES:
+:ID: 423bc355-18d3-4e39-9e7a-f768b865d95b
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Status
diff --git a/docs/specs/org-faces-spec-implemented.org b/docs/specs/org-faces-spec.org
index c8855906..94fe7bb4 100644
--- a/docs/specs/org-faces-spec-implemented.org
+++ b/docs/specs/org-faces-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: 35578114-8c29-43af-97a2-fdfea01a802e
-:STATUS: implemented
-:END:
#+TITLE: Org Header-Row Faces — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-15
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Org Header-Row Faces — Spec
+:PROPERTIES:
+:ID: 35578114-8c29-43af-97a2-fdfea01a802e
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Metadata
| Status | implemented |
diff --git a/docs/specs/signal-client-spec-doing.org b/docs/specs/signal-client-spec.org
index beee0acf..13f67115 100644
--- a/docs/specs/signal-client-spec-doing.org
+++ b/docs/specs/signal-client-spec.org
@@ -1,10 +1,13 @@
+#+TITLE: Design: Signal client in Emacs (forked signel)
+#+DATE: 2026-05-26
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Design: Signal client in Emacs (forked signel)
:PROPERTIES:
:ID: 0cabd6ee-c458-47b5-a8af-3ee054b25821
-:STATUS: doing
:END:
-#+TITLE: Design: Signal client in Emacs (forked signel)
-#+DATE: 2026-05-26
-#+STATUS: Draft
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Problem
I want a Signal chat client inside Emacs: link it as a secondary device to my phone, pick a contact from my contact list, hold a text 1:1 conversation (read and send), and get a desktop notification on incoming messages, with an optional sound. Signal has no official API, so this is built on =signal-cli=, the mature headless CLI, driven over JSON-RPC.
diff --git a/docs/specs/theme-studio-completion-preview-spec.org b/docs/specs/theme-studio-completion-preview-spec.org
index 588f35a9..7d0c2608 100644
--- a/docs/specs/theme-studio-completion-preview-spec.org
+++ b/docs/specs/theme-studio-completion-preview-spec.org
@@ -1,7 +1,14 @@
#+TITLE: Theme Studio Minibuffer-Completion Preview — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-23
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Theme Studio Minibuffer-Completion Preview — Spec
+:PROPERTIES:
+:ID: 2462f067-4c8d-4c33-a5be-54c0abc2eb1d
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from Metadata Status: Not ready, review found blockers
* Metadata
| Status | Not ready — first Codex review found implementation-readiness blockers (2026-06-23) |
@@ -195,7 +202,7 @@ Add the caption naming minibuffer-prompt + highlight as living in UI Faces. When
- Manual: open theme-studio in Chrome on the owner's inventory and confirm the Vertico section + baseline render, orderless/marginalia toggle, and vertico-current shows no background.
* References / Appendix
-- Reuse: [[file:theme-studio-preview-locate-spec.org][theme-studio-preview-locate-spec.org]] (hover/click locate), [[file:theme-studio-package-faces-spec-doing.org][theme-studio-package-faces-spec-doing.org]].
+- Reuse: [[file:theme-studio-preview-locate-spec.org][theme-studio-preview-locate-spec.org]] (hover/click locate), [[file:theme-studio-package-faces-spec.org][theme-studio-package-faces-spec.org]].
- Spike: /tmp completion-face-preview.el (verified render; not committed — informs this spec, not grown into it).
- Live face values captured 2026-06-23 (WIP theme): minibuffer-prompt #899bb1/#100f0f bold; orderless-match-face-0..3 #cbd0d6 / #c99990 / #c5d4ae / #bea9dc bold italic; vertico-current inherits highlight (#eddba7 bold, no background).
diff --git a/docs/specs/theme-studio-nerd-icons-colors-spec.org b/docs/specs/theme-studio-nerd-icons-colors-spec.org
index c0f07b6d..94a5d178 100644
--- a/docs/specs/theme-studio-nerd-icons-colors-spec.org
+++ b/docs/specs/theme-studio-nerd-icons-colors-spec.org
@@ -1,7 +1,14 @@
#+TITLE: Theme-driven nerd-icons colors + theme-studio filetype legend — Spec
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-23
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* READY Theme-driven nerd-icons colors + theme-studio filetype legend — Spec
+:PROPERTIES:
+:ID: 6df4e8a3-1fca-452a-9416-3fa0647b8dff
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword READY from Metadata Status: Ready pending Craig's go
* Metadata
| Status | Ready pending Craig's go — Codex review rounds 1-3 incorporated |
diff --git a/docs/specs/theme-studio-package-faces-spec-doing.org b/docs/specs/theme-studio-package-faces-spec.org
index 566f34db..4390f0fc 100644
--- a/docs/specs/theme-studio-package-faces-spec-doing.org
+++ b/docs/specs/theme-studio-package-faces-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 8f37a1fd-cfd3-4b25-92e5-772468092bdc
-:STATUS: doing
-:END:
#+TITLE: theme-studio — package faces (tier 3), starting with org-mode
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-07
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING theme-studio — package faces (tier 3), starting with org-mode
+:PROPERTIES:
+:ID: 8f37a1fd-cfd3-4b25-92e5-772468092bdc
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Status
diff --git a/docs/specs/theme-studio-palette-generator-spec-doing.org b/docs/specs/theme-studio-palette-generator-spec.org
index b98e1078..2d8fc5c6 100644
--- a/docs/specs/theme-studio-palette-generator-spec-doing.org
+++ b/docs/specs/theme-studio-palette-generator-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: 2df157b8-c7c1-47a9-b080-d9586c6f424c
-:STATUS: doing
-:END:
#+TITLE: Theme Studio Palette Generator -- Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-14
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Theme Studio Palette Generator -- Spec
+:PROPERTIES:
+:ID: 2df157b8-c7c1-47a9-b080-d9586c6f424c
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Metadata
| Status | doing |
diff --git a/docs/specs/theme-studio-perceptual-color-metrics-spec-implemented.org b/docs/specs/theme-studio-perceptual-color-metrics-spec.org
index 57a4c70b..233632a5 100644
--- a/docs/specs/theme-studio-perceptual-color-metrics-spec-implemented.org
+++ b/docs/specs/theme-studio-perceptual-color-metrics-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: 15db8ae3-fc14-49f3-9ed5-d5ff59790904
-:STATUS: implemented
-:END:
#+TITLE: theme-studio — perceptual color metrics (OKLCH, APCA, ΔE)
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-08
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED theme-studio — perceptual color metrics (OKLCH, APCA, ΔE)
+:PROPERTIES:
+:ID: 15db8ae3-fc14-49f3-9ed5-d5ff59790904
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
diff --git a/docs/specs/theme-studio-preview-locate-spec.org b/docs/specs/theme-studio-preview-locate-spec.org
index dee27e8c..2f07d9dd 100644
--- a/docs/specs/theme-studio-preview-locate-spec.org
+++ b/docs/specs/theme-studio-preview-locate-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: fbcf0e20-1328-42b4-aa36-3401509e7816
-:STATUS: ready-pending-go
-:END:
#+TITLE: Theme Studio Preview Element Locate — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-15
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* READY Theme Studio Preview Element Locate — Spec
+:PROPERTIES:
+:ID: fbcf0e20-1328-42b4-aa36-3401509e7816
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword READY from existing :STATUS: ready-pending-go
* Metadata
| Status | Ready pending Craig's go — four reviews incorporated (Codex, 2026-06-23) |
diff --git a/docs/specs/theme-studio-seeding-engine-spec-doing.org b/docs/specs/theme-studio-seeding-engine-spec.org
index baf9f5b0..25fab26c 100644
--- a/docs/specs/theme-studio-seeding-engine-spec-doing.org
+++ b/docs/specs/theme-studio-seeding-engine-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: b70b37f2-37df-4c8e-ac2f-1f20d12e33dd
-:STATUS: doing
-:END:
#+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
@@ -317,7 +321,7 @@ response resolved; everything else was woven into the body as written.
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-implemented.org= — the
+- =docs/specs/theme-studio-perceptual-color-metrics-spec.org= — the
=colormath.js= core that v1 OKLCH shade generation uses.
* Review and iteration history
@@ -352,3 +356,10 @@ response resolved; everything else was woven into the body as written.
and reseed behavior, package preservation rules, artifact round-trip tests,
and vNext boundaries.
- *Artifacts:* No review file written; no blocking findings.
+
+** 2026-07-04 Sat @ 18:33:29 -0500 — Claude Code — implementation finding (Phase 2 blocked)
+- *What changed or was recommended:* Phase 1 landed clean (commit f407959b). Starting Phase 2, tracing the actual generator surfaced that a load-bearing premise of this spec is false against the code. The spec's Package scope says the ~20 non-org bespoke packages "keep their curated =APPS= seed colors" as the state the engine preserves and reseed resets to. They don't: =apply_default_face_seeds= (=app_inventory.py:139=) unconditionally overwrites *every* bespoke package face with =defaults.seed(face, False)= — the Emacs-default snapshot color — whenever =emacs-default-faces.json= is present (it is). So in the built page the curated dupre =SEED= dicts in =face_data.py= (=ORG_SEED=, =MAGIT_SEED=, …) are shadowed and never live: org faces open empty, and magit/elfeed/mu4e/the rest open on light-theme snapshot hexes (e.g. =magit-section-heading= #8b6508 on #f2f2f2), not their dupre curated colors.
+- *Why it blocks Phase 2:* Two acceptance criteria then contradict under the real data. "Non-org packages keep their curated =APPS= defaults" and "a Chrome eyeball confirms a coherent dupre" cannot both hold — seeding syntax/UI/org to dark dupre while the package tiers stay on light snapshot hexes reads incoherent. A second wrinkle: =seed()= (JS) emits only org among packages by design, but the non-org dupre colors live in =face_data.py= (Python, name-resolved), so the =dupre-revised.json= emitter cannot be pure-Node — it needs a Python contribution or a different source for the non-org package colors.
+- *The fork (Craig to decide before the spec is revised):* (1) reseed the non-org packages from the curated dupre =SEED= dicts in =face_data.py=, resolved against a dupre palette — coherent, honors "curated," but the seeded build must stop =apply_default_face_seeds= from shadowing them and the emitter needs a Python side; or (2) keep the Emacs-default snapshot hexes — literally "keep current defaults," but the theme reads incoherent and the coherence gate fails.
+- *Disposition:* Craig chose to pause Phase 2 and revise the spec first (its "curated seeds are live" premise needs correcting). Spec kept =DOING=; Phase-2 and test-surface build tasks marked =:blocked:= in =todo.org= with a =VERIFY= capturing the direction decision. Phase 1 (=seed()= + =#seedtest=) stands.
+- *Artifacts:* the =todo.org= seeding-engine subtree (Phase-2 subtask + VERIFY).
diff --git a/docs/specs/theme-studio-semantic-theme-architecture-spec.org b/docs/specs/theme-studio-semantic-theme-architecture-spec.org
index 01ef1902..cab75712 100644
--- a/docs/specs/theme-studio-semantic-theme-architecture-spec.org
+++ b/docs/specs/theme-studio-semantic-theme-architecture-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: fe980b12-451a-4d8b-a550-d99f9ec49f45
-:STATUS: not-started
-:END:
#+TITLE: Theme Studio Semantic Theme Architecture -- Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-14
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Theme Studio Semantic Theme Architecture -- Spec
+:PROPERTIES:
+:ID: fe980b12-451a-4d8b-a550-d99f9ec49f45
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Metadata
| Status | not-started |
@@ -257,7 +260,7 @@ Rollout should keep the current flat output path as the default and add a separa
- Modus Themes source: [[https://github.com/protesilaos/modus-themes][github.com/protesilaos/modus-themes]]
- Current converter: [[file:../scripts/theme-studio/build-theme.el][scripts/theme-studio/build-theme.el]]
- Current Theme Studio README: [[file:../scripts/theme-studio/README.md][scripts/theme-studio/README.md]]
-- Package-face model spec: [[id:8f37a1fd-cfd3-4b25-92e5-772468092bdc][theme-studio-package-faces-spec-doing.org]]
+- Package-face model spec: [[id:8f37a1fd-cfd3-4b25-92e5-772468092bdc][theme-studio-package-faces-spec.org]]
* Review and iteration history
** 2026-06-14 Sunday @ 14:37:00 -0500 -- Craig -- author
diff --git a/docs/specs/theme-studio-structured-output-spec.org b/docs/specs/theme-studio-structured-output-spec.org
index ad189b7e..10aea1a8 100644
--- a/docs/specs/theme-studio-structured-output-spec.org
+++ b/docs/specs/theme-studio-structured-output-spec.org
@@ -1,11 +1,14 @@
-:PROPERTIES:
-:ID: eaac7707-ed05-43df-9e51-b17c1d672531
-:STATUS: not-started
-:END:
#+TITLE: Theme-Studio Structured Theme Output — Spec
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-15
-#+TODO: TODO | DONE SUPERSEDED CANCELLED
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DRAFT Theme-Studio Structured Theme Output — Spec
+:PROPERTIES:
+:ID: eaac7707-ed05-43df-9e51-b17c1d672531
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DRAFT from existing :STATUS: not-started
* Metadata
| Status | not-started |
diff --git a/docs/specs/utility-consolidation-spec-doing.org b/docs/specs/utility-consolidation-spec.org
index b0a5fe2b..871295d7 100644
--- a/docs/specs/utility-consolidation-spec-doing.org
+++ b/docs/specs/utility-consolidation-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: fc2e3926-b4a1-4b45-92eb-20841e13f655
-:STATUS: doing
-:END:
#+TITLE: Design: Consolidate Shared Utility Helpers
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-04
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* DOING Design: Consolidate Shared Utility Helpers
+:PROPERTIES:
+:ID: fc2e3926-b4a1-4b45-92eb-20841e13f655
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword DOING from existing :STATUS: doing
* Status
@@ -295,7 +299,7 @@ Worked =system-lib.el= header:
;; Private helpers rename without alias when all call sites change in the
;; same commit.
;;
-;; See also: docs/specs/utility-consolidation-spec-doing.org for design rationale.
+;; See also: docs/specs/utility-consolidation-spec.org for design rationale.
;;
;;; Code:
#+end_src
@@ -332,7 +336,7 @@ Load shape:
- =cj-cache.el= follows the first real cache consumer's layer, likely Layer 2 if
modeline/agenda/refile remain eager or near-eager.
- Coordinate every new topic library with
- [[id:e1fd137e-e164-42f4-a658-f4d32fbe3228][init-load-graph-spec-doing.org]] before migrating its first consumer.
+ [[id:e1fd137e-e164-42f4-a658-f4d32fbe3228][init-load-graph-spec.org]] before migrating its first consumer.
* Naming Rules
@@ -785,7 +789,7 @@ Recommendation:
design addendum proves the API can drive the alignment.
- Then decide whether modeline's buffer-local cache can use the same library or
should remain specialized.
-- Phase 5 step 1 produces =docs/specs/cache-helper-design-spec-implemented.org=. Until that
+- Phase 5 step 1 produces =docs/specs/cache-helper-design-spec.org=. Until that
file exists, =cj-cache.el= must not be created. The addendum is the
prerequisite for any cache extraction commit.
@@ -906,7 +910,7 @@ Inventory artifact:
- Treat the inventory as living documentation. Cleared high-priority candidates
may move to Phase 2 before the whole inventory is complete.
- This inventory is independent from the module-shape inventory maintained by
- [[id:e1fd137e-e164-42f4-a658-f4d32fbe3228][init-load-graph-spec-doing.org]]. The two projects may walk the same files, but they
+ [[id:e1fd137e-e164-42f4-a658-f4d32fbe3228][init-load-graph-spec.org]]. The two projects may walk the same files, but they
record different facts in separate artifacts.
For each helper record:
diff --git a/docs/specs/vterm-to-ghostel-migration-spec-implemented.org b/docs/specs/vterm-to-ghostel-migration-spec.org
index 1be4fe22..f3d39d22 100644
--- a/docs/specs/vterm-to-ghostel-migration-spec-implemented.org
+++ b/docs/specs/vterm-to-ghostel-migration-spec.org
@@ -1,10 +1,14 @@
-:PROPERTIES:
-:ID: b54c94a0-d762-4b41-afd7-cf5593ce6675
-:STATUS: implemented
-:END:
#+TITLE: Migration: vterm → ghostel (single terminal engine)
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-04
+#+TODO: TODO | DONE
+#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+* IMPLEMENTED Migration: vterm → ghostel (single terminal engine)
+:PROPERTIES:
+:ID: b54c94a0-d762-4b41-afd7-cf5593ce6675
+:END:
+- 2026-07-04 Sat @ 15:30:41 -0500 — retrofitted to status-heading convention; keyword IMPLEMENTED from existing :STATUS: implemented + -implemented filename (Craig's prior determination)
* Status
@@ -175,7 +179,7 @@ Audited file set.
** Docs (active references only — historical notes stay)
- =todo.org= current task link (already updated to this -spec path).
-- =docs/design/module-inventory.org=, =docs/specs/init-load-graph-spec-doing.org= —
+- =docs/design/module-inventory.org=, =docs/specs/init-load-graph-spec.org= —
update active =vterm-config= / =ai-vterm= references to the new names.
** Tests (~35 files)
diff --git a/modules/org-config.el b/modules/org-config.el
index 6f25752f..a9fc4811 100644
--- a/modules/org-config.el
+++ b/modules/org-config.el
@@ -263,6 +263,23 @@ whole row line."
;; ----------------------------- Org TODO Settings ---------------------------
+(defun cj/org--noop-state-log-p (purpose state prev-state)
+ "Return non-nil when a state-change log carries no information.
+PURPOSE is the `org-add-log-setup' purpose symbol; STATE and PREV-STATE
+are the new and previous TODO states. A \\='state transition whose new
+and previous states are identical (and non-nil) is a no-op worth
+suppressing."
+ (and (eq purpose 'state)
+ state prev-state
+ (equal state prev-state)))
+
+(defun cj/org--suppress-noop-state-log (orig-fn &optional purpose state prev-state how extra)
+ "Around-advice for `org-add-log-setup' that drops no-op state logs.
+Call ORIG-FN with PURPOSE STATE PREV-STATE HOW EXTRA unless the entry is
+a no-op identical-state transition (see `cj/org--noop-state-log-p')."
+ (unless (cj/org--noop-state-log-p purpose state prev-state)
+ (funcall orig-fn purpose state prev-state how extra)))
+
(defun cj/org-todo-settings ()
"All org-todo related settings are grouped and set in this function."
@@ -283,9 +300,16 @@ whole row line."
(setq org-enforce-todo-checkbox-dependencies t)
(setq org-deadline-warning-days 7) ;; warn me w/in a week of deadlines
(setq org-treat-insert-todo-heading-as-state-change nil) ;; log task creation
- (setq org-log-into-drawer nil) ;; don't log into drawer
+ ;; state changes log into :LOGBOOK: drawers, never inline: an inline log
+ ;; line can wedge between a heading and its planning line and break org's
+ ;; planning-line parser (dropping the entry from agenda views)
+ (setq org-log-into-drawer t)
(setq org-log-done 'time) ;; record a CLOSED timestamp on TODO->DONE
+ ;; drop no-op "State X from X" transitions (identical from/to) that carry
+ ;; no information; the advice dedups, so re-running this is safe
+ (advice-add 'org-add-log-setup :around #'cj/org--suppress-noop-state-log)
+
;; inherit parents properties (sadly not schedules or deadlines)
(setq org-use-property-inheritance t))
diff --git a/scripts/theme-studio/app.js b/scripts/theme-studio/app.js
index 44a01514..6a2daad2 100644
--- a/scripts/theme-studio/app.js
+++ b/scripts/theme-studio/app.js
@@ -40,6 +40,9 @@ APP_CORE_J
// Pure color/UI-boundary helpers (normHex, ratingColor, textOn), inlined from
// app-util.js. textOn uses rl from the colormath core above.
APP_UTIL_J
+// The seeding engine (seed model + pure seed()), inlined from seed-core.js. Uses
+// oklchOf/oklch2hex from the colormath core above; the #seedtest gate runs seed().
+SEED_CORE_J
// Pure palette-generator planner and browser-side generator panel.
PALETTE_GENERATOR_CORE_J
PALETTE_GENERATOR_UI_J
diff --git a/scripts/theme-studio/browser-gates.js b/scripts/theme-studio/browser-gates.js
index 3ccec8ea..e0132bcd 100644
--- a/scripts/theme-studio/browser-gates.js
+++ b/scripts/theme-studio/browser-gates.js
@@ -77,6 +77,37 @@ function pkgSelftest(){
const d=document.createElement('div');d.id='selftest';d.textContent='SELFTEST '+verdict+' roundtrip='+roundtrip+' oldjson='+oldjson+' inherit='+inherited+' height='+height+' cleared='+cleared+' unknown='+unknown+' cycle='+cyc;document.body.appendChild(d);
}
if(location.hash==='#selftest')pkgSelftest();
+// Seeding-engine gate (open with #seedtest): the pure seed() projects the guide's
+// role table onto the three owned tiers. Assert representative faces land on the
+// right swatch/weight/channel, and that a non-org bespoke package (magit) keeps
+// its curated APPS seed (seed() owns org among packages, nothing else).
+if(location.hash==='#seedtest')gate('seedtest',A=>{
+ const m=buildModel();
+ const s=seed(m,{cats:CATS.map(c=>c[0])});
+ // syntax tier
+ A(s.syntax.bi.fg===m.swatch['blue-grey'],'bi=blue-grey');
+ A(s.syntax.fnd.fg===m.swatch.gold&&s.syntax.fnd.weight==='bold','fnd=gold+bold');
+ A(s.syntax.fnc.fg===m.swatch['gold-quiet']&&s.syntax.fnc.weight!=='bold','fnc=gold-quiet');
+ A(s.syntax.var.fg===m.swatch.fg,'var=base');
+ A(s.syntax.op.fg===m.swatch['muted-fg']&&s.syntax.punc.fg===m.swatch['muted-fg'],'op/punc=structure');
+ A(s.syntax.kw.fg===m.swatch.blue&&s.syntax.kw.weight==='bold','kw=control');
+ A(s.syntax.doc.slant==='italic','doc=italic');
+ A(s.syntax.bg.fg===m.swatch.ground,'bg=ground');
+ // UI tier
+ A(!!s.ui.region.bg&&!s.ui.region.fg,'region bg-only');
+ A(!!s.ui.link.underline&&s.ui.link.fg===m.swatch.blue,'link underlined');
+ A(s.ui.error.fg===m.swatch.red&&s.ui.warning.fg===m.swatch.amber&&s.ui.success.fg===m.swatch.green,'signals on convention hues');
+ A(s.ui['mode-line'].fg!==s.ui['mode-line-inactive'].fg,'active!=idle chrome');
+ // org tier
+ const org=s.packages['org-mode'];
+ A(Object.keys(s.packages).length===1,'packages=org-mode only');
+ A(oklchOf(org['org-level-1'].fg).L>oklchOf(org['org-level-2'].fg).L&&org['org-level-1'].weight==='bold','org-level-1 strongest+bold');
+ A(org['org-code'].fg===m.swatch.terracotta&&org['org-code'].inherit==='fixed-pitch','org-code literal lane');
+ A(!!org['org-done'].strike,'org-done struck');
+ // non-org bespoke package keeps its curated seed (untouched by seed())
+ const mag=seedPkgmap()['magit'];
+ A(!!mag&&!!mag['magit-section-heading']&&!!mag['magit-section-heading'].fg,'magit keeps curated seed');
+});
// Lock-mechanism gate (open with #locktest): two behaviors the refactor must
// preserve, across all three tiers. (1) Locking a row disables its controls via
// the shared mkLockCell. (2) reset/erase batch actions update editable rows but
diff --git a/scripts/theme-studio/generate.py b/scripts/theme-studio/generate.py
index b0fafefd..477407ee 100644
--- a/scripts/theme-studio/generate.py
+++ b/scripts/theme-studio/generate.py
@@ -146,6 +146,11 @@ APP_CORE_BODY=strip_exports(read_text('app-core.js'))
# test-app-util.mjs. Its `import rl` line is stripped on inline (rl is already in
# the page from the colormath core).
APP_UTIL_BODY=strip_exports(read_text('app-util.js'))
+# The seeding engine (seed-core.js): the seed model as data and the pure seed().
+# Inlined below the colormath core (its only dependency) so the browser #seedtest
+# runs the same code the Node tests import. Its `import` from colormath is stripped
+# on inline, where oklchOf/oklch2hex are already present.
+SEED_CORE_BODY=strip_exports(read_text('seed-core.js'))
# Pure palette-generator planner and its browser UI panel, split from the shared
# app core so generation behavior and panel wiring can evolve locally.
PALETTE_GENERATOR_CORE_BODY=strip_exports(read_text('palette-generator-core.js'))
@@ -422,6 +427,7 @@ def _build():
.replace("CONTROLS_J",CONTROLS_BODY)
.replace("PREVIEWS_J",PREVIEWS_BODY)
.replace("APP_UTIL_J",APP_UTIL_BODY)
+ .replace("SEED_CORE_J",SEED_CORE_BODY)
.replace("PALETTE_GENERATOR_CORE_J",PALETTE_GENERATOR_CORE_BODY)
.replace("PALETTE_GENERATOR_UI_J",PALETTE_GENERATOR_UI_BODY)
.replace("PALETTE_ACTIONS_J",PALETTE_ACTIONS_BODY)
diff --git a/scripts/theme-studio/seed-core.js b/scripts/theme-studio/seed-core.js
new file mode 100644
index 00000000..fb7362d4
--- /dev/null
+++ b/scripts/theme-studio/seed-core.js
@@ -0,0 +1,249 @@
+// seed-core.js — the theme-studio seeding engine (Phase 1).
+//
+// The seed model as data and the pure seed() operation. This is
+// theme-coloring-guide.org made executable: a named palette (OKLCH-generated
+// shades over a handful of dupre anchor hues), a role-to-treatment table (the
+// guide's seed table), and a face-to-role map for each of the three owned tiers
+// (syntax, UI, org). seed(model) classifies every face and applies the table,
+// producing default assignments in the shape the import path already consumes.
+//
+// Pure: no DOM, no side effects. node imports this module for its unit tests;
+// generate.py strips the import line and inlines the body into the page (below
+// the colormath core, so oklchOf/oklch2hex are already present) so the browser
+// #seedtest runs the same code. One source of truth, like colormath.js.
+//
+// Scope (Package scope in the spec): seed() owns syntax, UI, and org among
+// packages. The other ~20 bespoke packages keep their curated APPS seeds, so
+// seed().packages carries only org-mode; the rest flow through seedPkgmap().
+import { oklchOf, oklch2hex } from './colormath.js';
+
+// --- anchors -------------------------------------------------------------
+// The base hues, taken from the bundled dupre palette. Each accent family is
+// anchored here; its quieter/brighter shades are OKLCH-derived (below). Neutrals
+// are taken directly — no ground is pure-white text, and pure black stays the
+// ground only (guide principle 5).
+const ANCHORS = {
+ ground: '#000000', 'bg-dim': '#1a1714',
+ fg: '#a9b2bb', // base identity (dupre silver): comfortable, not pure white
+ 'muted-fg': '#838d97', // structure lane (dupre steel)
+ comment: '#5e6770', // low-contrast comment lane (dupre pewter)
+ blue: '#67809c', gold: '#e8bd30', regal: '#9b5fd0',
+ sage: '#5d9b86', terracotta: '#cb6b4d',
+};
+
+// --- OKLCH shade helpers -------------------------------------------------
+// Step an anchor by a lightness delta and a chroma multiplier, hue held. A
+// quieter shade is darker + lower chroma; a brighter shade is the reverse.
+function shade(hex, dL, cMul) {
+ const { L, C, H } = oklchOf(hex);
+ return oklch2hex(clampL(L + dL), Math.max(0, C * cMul), H).hex;
+}
+// A color placed by absolute OKLCH — used for the signal hues, which are
+// conventional angles rather than shifts of a syntax accent.
+function atHue(L, C, H) { return oklch2hex(clampL(L), C, ((H % 360) + 360) % 360).hex; }
+function clampL(L) { return L < 0 ? 0 : L > 1 ? 1 : L; }
+
+// The heading ramp: one hue across four descending lightness steps (level 1
+// strongest). Deeper org levels cycle through these past level 4.
+function headingRamp(anchorHex) {
+ const { C, H } = oklchOf(anchorHex);
+ return [0.78, 0.68, 0.58, 0.48].map((L) => oklch2hex(L, C, H).hex);
+}
+
+// Build the named swatch set + heading ramp from the anchors. The blue-grey
+// builtin and gold-quiet call are the two shades dupre lacks and gains here.
+function buildModel(anchors = ANCHORS) {
+ const a = anchors;
+ const swatch = {
+ ground: a.ground, 'bg-dim': a['bg-dim'], fg: a.fg,
+ 'muted-fg': a['muted-fg'], comment: a.comment,
+ blue: a.blue,
+ 'blue-grey': shade(a.blue, -0.05, 0.5), // builtin: blue hue, lower chroma/lightness
+ gold: a.gold,
+ 'gold-quiet': shade(a.gold, -0.08, 0.6), // call: quieter same-hue gold
+ regal: a.regal,
+ sage: a.sage,
+ 'sage-muted': shade(a.sage, -0.03, 0.6), // docstring
+ 'sage-bright': shade(a.sage, 0.08, 1.15), // escape
+ teal: (() => { const { L, C, H } = oklchOf(a.sage); return oklch2hex(clampL(L + 0.05), C * 1.1, H - 25).hex; })(), // regexp
+ terracotta: a.terracotta,
+ red: atHue(0.62, 0.15, 29), // signal: error / deletion
+ amber: atHue(0.80, 0.14, 75), // signal: warning / modified
+ green: atHue(0.72, 0.14, 145),// signal: success / addition
+ tint: atHue(0.32, 0.03, oklchOf(a.blue).H), // transient state bg (quiet)
+ 'tint-strong': atHue(0.42, 0.06, oklchOf(a.blue).H), // active match chip
+ };
+ return { swatch, ramp: headingRamp(a.blue), roles: ROLES };
+}
+
+// --- the role-to-treatment table (the guide's seed table as data) --------
+// Each role maps to a swatch, an optional weight/slant/underline, and a channel
+// (fg is identity, bg is state). channel defaults to fg.
+const ROLES = {
+ base: { swatch: 'fg' },
+ structure: { swatch: 'muted-fg' },
+ control: { swatch: 'blue', weight: 'bold' },
+ builtin: { swatch: 'blue-grey' },
+ def: { swatch: 'gold', weight: 'bold' },
+ call: { swatch: 'gold-quiet' },
+ type: { swatch: 'regal' },
+ string: { swatch: 'sage' },
+ docstring: { swatch: 'sage-muted', slant: 'italic' },
+ escape: { swatch: 'sage-bright' },
+ regexp: { swatch: 'teal' },
+ literal: { swatch: 'terracotta' },
+ comment: { swatch: 'comment', slant: 'italic' },
+ sig_error: { swatch: 'red' },
+ sig_warn: { swatch: 'amber' },
+ sig_ok: { swatch: 'green' },
+ sig_link: { swatch: 'blue', underline: true },
+ state: { swatch: 'tint', channel: 'bg' },
+};
+
+// A blank full face spec; seed fills only the fields a role sets.
+function blankSpec() {
+ return { fg: null, bg: null, weight: null, slant: null, underline: null, strike: null, inherit: null, height: null };
+}
+
+// Resolve a ROLES role against the model into a face spec.
+function resolveRole(model, role) {
+ const r = model.roles[role];
+ const hex = model.swatch[r.swatch];
+ const s = blankSpec();
+ if (r.channel === 'bg') s.bg = hex; else s.fg = hex;
+ if (r.weight) s.weight = r.weight;
+ if (r.slant) s.slant = r.slant;
+ if (r.underline) s.underline = { style: 'line', color: null };
+ return s;
+}
+
+// --- face-to-role maps ---------------------------------------------------
+
+// Syntax: CATS key -> role. bg is handled specially (the ground).
+const SYNTAX_ROLES = {
+ p: 'base', var: 'base',
+ op: 'structure', punc: 'structure', neg: 'structure', cmd: 'structure',
+ kw: 'control', pp: 'control',
+ bi: 'builtin',
+ fnd: 'def', fnc: 'call',
+ dec: 'type', ty: 'type', prop: 'type',
+ con: 'literal', num: 'literal',
+ str: 'string', doc: 'docstring',
+ esc: 'escape', dmark: 'escape',
+ re: 'regexp', rxgb: 'regexp', rxgc: 'regexp',
+ cm: 'comment',
+ warn: 'sig_warn',
+};
+
+// UI: face -> either a ROLES role (state/signal/link/control) or an inline
+// chrome spec. Chrome is inherently multi-attribute (fg + bg, active vs idle),
+// so it does not force through the single-swatch role resolver.
+function uiSeed(model) {
+ const sw = model.swatch, out = {};
+ const role = (r) => resolveRole(model, r);
+ const spec = (o) => Object.assign(blankSpec(), o);
+ // Transient state: background tint, no foreground. lazy-highlight (other
+ // matches) shares the quiet tint; isearch (current match) gets a louder chip.
+ for (const f of ['region', 'hl-line', 'highlight', 'show-paren-match', 'lazy-highlight']) out[f] = role('state');
+ out.isearch = spec({ bg: sw['tint-strong'] }); // active match, louder chip
+ // Signals (convention hues) with a weight for redundancy.
+ out.error = spec({ fg: sw.red, weight: 'bold' });
+ out.warning = spec({ fg: sw.amber, weight: 'bold' });
+ out.success = spec({ fg: sw.green, weight: 'bold' });
+ out['isearch-fail'] = spec({ fg: sw.red, weight: 'bold' });
+ out['show-paren-mismatch'] = spec({ bg: sw.red }); // shape + color, not color alone
+ out.link = role('sig_link');
+ // Chrome: active brighter than idle (guide principle 3).
+ out['mode-line'] = spec({ fg: sw.fg, bg: sw['bg-dim'] });
+ out['mode-line-inactive'] = spec({ fg: sw['muted-fg'], bg: sw['bg-dim'] });
+ out['mode-line-highlight'] = spec({ fg: sw.fg });
+ for (const f of ['header-line', 'tab-bar', 'tab-line']) out[f] = spec({ fg: sw['muted-fg'], bg: sw['bg-dim'] });
+ out['line-number'] = spec({ fg: sw.comment });
+ out['line-number-current-line'] = spec({ fg: sw.fg });
+ out.fringe = spec({ fg: sw.comment });
+ out['vertical-border'] = spec({ fg: sw['bg-dim'] });
+ out['minibuffer-prompt'] = role('control');
+ out.cursor = spec({ bg: sw.fg });
+ return out;
+}
+
+// Org: face -> role/heading/inline. Faces not named here seed to base.
+const ORG_MARKUP = ['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', 'org-macro', 'org-target', 'org-footnote-def'];
+const ORG_CODE = ['org-code', 'org-verbatim', 'org-inline-src-block'];
+const ORG_LINK = ['org-link', 'org-cite', 'org-cite-key', 'org-footnote'];
+const ORG_EMPHASIS = ['org-quote', 'org-verse'];
+
+function orgSeed(model, orgFaces) {
+ const sw = model.swatch, out = {};
+ const role = (r) => resolveRole(model, r);
+ const spec = (o) => Object.assign(blankSpec(), o);
+ for (const face of orgFaces) {
+ const lvl = /^org-level-([1-8])$/.exec(face);
+ if (lvl) {
+ const i = Number(lvl[1]);
+ out[face] = spec({ fg: model.ramp[(i - 1) % model.ramp.length], weight: i === 1 ? 'bold' : null });
+ } else if (face === 'org-document-title') {
+ out[face] = spec({ fg: sw.gold, weight: 'bold' });
+ } else if (ORG_CODE.includes(face)) {
+ out[face] = spec({ fg: sw.terracotta, inherit: 'fixed-pitch' });
+ } else if (face === 'org-block') {
+ out[face] = spec({ bg: sw['bg-dim'], inherit: 'fixed-pitch' });
+ } else if (ORG_LINK.includes(face)) {
+ out[face] = spec({ fg: sw.blue, underline: { style: 'line', color: null } });
+ } else if (ORG_MARKUP.includes(face)) {
+ out[face] = spec({ fg: sw['muted-fg'] });
+ } else if (face === 'org-todo' || face === 'org-imminent-deadline') {
+ out[face] = spec({ fg: sw.red, weight: 'bold' });
+ } else if (face === 'org-upcoming-deadline') {
+ out[face] = spec({ fg: sw.amber });
+ } else if (face === 'org-scheduled' || face === 'org-scheduled-today') {
+ out[face] = spec({ fg: sw.comment });
+ } else if (face === 'org-done' || face === 'org-headline-done' || face === 'org-agenda-done') {
+ out[face] = spec({ fg: sw.comment, strike: { color: null } });
+ } else if (ORG_EMPHASIS.includes(face)) {
+ out[face] = spec({ slant: 'italic' });
+ } else {
+ out[face] = role('base');
+ }
+ }
+ return out;
+}
+
+// The org faces the engine seeds. Kept in step with ORG_FACES in face_data.py;
+// a face present here but absent there (or the reverse) simply seeds/omits it.
+// The representative set the guide names is what matters for the tier.
+const ORG_FACES = ('org-document-title org-document-info org-document-info-keyword '
+ + 'org-level-1 org-level-2 org-level-3 org-level-4 org-level-5 org-level-6 org-level-7 org-level-8 '
+ + 'org-headline-done org-todo org-done org-priority org-tag org-special-keyword org-drawer '
+ + 'org-property-value org-warning org-link org-cite org-cite-key org-footnote org-date '
+ + 'org-macro org-target org-block org-block-begin-line org-block-end-line org-code org-verbatim '
+ + 'org-inline-src-block org-quote org-verse org-meta-line org-ellipsis '
+ + 'org-scheduled org-scheduled-today org-upcoming-deadline org-imminent-deadline '
+ + 'org-agenda-done org-table org-formula').split(' ');
+
+// --- seed(): apply the table through each tier's face-to-role map --------
+// Returns {syntax, ui, packages} default assignments. packages carries only
+// org-mode (Package scope); the non-org curated defaults flow through
+// seedPkgmap() over the APPS dicts, untouched by the engine.
+function seed(model, opts = {}) {
+ const cats = opts.cats || CATS_KEYS;
+ const orgFaces = opts.orgFaces || ORG_FACES;
+ const syntax = {};
+ for (const k of cats) {
+ if (k === 'bg') { syntax.bg = Object.assign(blankSpec(), { fg: model.swatch.ground }); continue; }
+ const rname = SYNTAX_ROLES[k] || 'base';
+ syntax[k] = resolveRole(model, rname);
+ }
+ return { syntax, ui: uiSeed(model), packages: { 'org-mode': orgSeed(model, orgFaces) } };
+}
+
+// The syntax categories the engine seeds, kept in step with CATS in generate.py.
+// Passed explicitly by the page (opts.cats) from the live CATS so the two never
+// drift; this literal is the standalone/test default.
+const CATS_KEYS = ['bg', 'p', 'kw', 'bi', 'pp', 'fnd', 'fnc', 'dec', 'ty', 'prop',
+ 'con', 'num', 'str', 'esc', 're', 'rxgb', 'rxgc', 'doc', 'dmark', 'cm', 'cmd',
+ 'var', 'op', 'neg', 'punc', 'warn'];
+
+export { ANCHORS, buildModel, seed, ROLES, SYNTAX_ROLES, ORG_FACES, CATS_KEYS };
diff --git a/scripts/theme-studio/test-seed-core.mjs b/scripts/theme-studio/test-seed-core.mjs
new file mode 100644
index 00000000..8ad6fc60
--- /dev/null
+++ b/scripts/theme-studio/test-seed-core.mjs
@@ -0,0 +1,173 @@
+// Unit tests for the seeding engine (seed-core.js): the seed model as data and
+// the pure seed() operation. seed() projects theme-coloring-guide.org's role/seed
+// table onto the three owned tiers (syntax, UI, org), reusing colormath.js OKLCH
+// generation for the palette shades and heading ramp.
+// Run: node --test scripts/theme-studio/
+
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { buildModel, seed, ROLES } from './seed-core.js';
+import { oklchOf } from './colormath.js';
+
+const here = fileURLToPath(new URL('.', import.meta.url));
+const HEX = /^#[0-9a-f]{6}$/;
+
+// --- the model: OKLCH-generated palette shades ---------------------------
+
+test('buildModel: every swatch is a valid in-gamut hex', () => {
+ const m = buildModel();
+ for (const [name, hex] of Object.entries(m.swatch)) {
+ assert.match(hex, HEX, `swatch ${name} is not a hex: ${hex}`);
+ }
+});
+
+test('buildModel: builtin blue-grey is the blue hue at lower chroma and lightness', () => {
+ const m = buildModel();
+ const blue = oklchOf(m.swatch.blue), grey = oklchOf(m.swatch['blue-grey']);
+ assert.ok(grey.C < blue.C, 'blue-grey should have lower chroma than blue');
+ assert.ok(grey.L < blue.L, 'blue-grey should be darker than blue');
+ assert.ok(Math.abs(grey.H - blue.H) < 15, 'blue-grey should keep the blue hue');
+});
+
+test('buildModel: the heading ramp descends in lightness, level 1 strongest', () => {
+ const m = buildModel();
+ assert.equal(m.ramp.length, 4);
+ for (let i = 1; i < m.ramp.length; i++) {
+ assert.ok(oklchOf(m.ramp[i - 1]).L > oklchOf(m.ramp[i]).L,
+ `ramp step ${i} should be darker than step ${i - 1}`);
+ }
+});
+
+// --- seed(): syntax tier -------------------------------------------------
+
+test('seed: syntax builtin (bi) resolves to blue-grey', () => {
+ const m = buildModel(), s = seed(m).syntax;
+ assert.equal(s.bi.fg, m.swatch['blue-grey']);
+ assert.notEqual(s.bi.weight, 'bold');
+});
+
+test('seed: syntax definition (fnd) is gold and bold; call (fnc) is quieter gold, not bold', () => {
+ const m = buildModel(), s = seed(m).syntax;
+ assert.equal(s.fnd.fg, m.swatch.gold);
+ assert.equal(s.fnd.weight, 'bold');
+ assert.equal(s.fnc.fg, m.swatch['gold-quiet']);
+ assert.notEqual(s.fnc.weight, 'bold');
+});
+
+test('seed: syntax base + structure + keyword + literal + string land on their swatches', () => {
+ const m = buildModel(), s = seed(m).syntax;
+ assert.equal(s.var.fg, m.swatch.fg); // base identity
+ assert.equal(s.p.fg, m.swatch.fg);
+ assert.equal(s.op.fg, m.swatch['muted-fg']); // structure
+ assert.equal(s.punc.fg, m.swatch['muted-fg']);
+ assert.equal(s.kw.fg, m.swatch.blue); // control
+ assert.equal(s.kw.weight, 'bold');
+ assert.equal(s.num.fg, m.swatch.terracotta); // literal
+ assert.equal(s.con.fg, m.swatch.terracotta);
+ assert.equal(s.str.fg, m.swatch.sage); // string
+ assert.equal(s.ty.fg, m.swatch.regal); // type
+});
+
+test('seed: docstring and comment take italic; comment is the low-contrast lane', () => {
+ const m = buildModel(), s = seed(m).syntax;
+ assert.equal(s.doc.slant, 'italic');
+ assert.equal(s.cm.slant, 'italic');
+ assert.equal(s.cm.fg, m.swatch.comment);
+});
+
+test('seed: regexp and escape use the teal/bright-green lanes; bg is the ground', () => {
+ const m = buildModel(), s = seed(m).syntax;
+ assert.equal(s.re.fg, m.swatch.teal);
+ assert.equal(s.rxgb.fg, m.swatch.teal);
+ assert.equal(s.esc.fg, m.swatch['sage-bright']);
+ assert.equal(s.bg.fg, m.swatch.ground);
+});
+
+// --- seed(): UI tier -----------------------------------------------------
+
+test('seed: transient state faces are background-only (no foreground)', () => {
+ const m = buildModel(), u = seed(m).ui;
+ for (const f of ['region', 'hl-line', 'highlight', 'show-paren-match']) {
+ assert.ok(u[f].bg, `${f} should carry a background tint`);
+ assert.ok(!u[f].fg, `${f} should not set a foreground`);
+ }
+});
+
+test('seed: link is blue and underlined (redundant encoding)', () => {
+ const m = buildModel(), u = seed(m).ui;
+ assert.equal(u.link.fg, m.swatch.blue);
+ assert.ok(u.link.underline, 'link should be underlined');
+});
+
+test('seed: signal faces sit on the convention hues, with weight for redundancy', () => {
+ const m = buildModel(), u = seed(m).ui;
+ assert.equal(u.error.fg, m.swatch.red);
+ assert.equal(u.error.weight, 'bold');
+ assert.equal(u.warning.fg, m.swatch.amber);
+ assert.equal(u.success.fg, m.swatch.green);
+ assert.equal(u['isearch-fail'].fg, m.swatch.red);
+});
+
+test('seed: active chrome differs from idle chrome', () => {
+ const m = buildModel(), u = seed(m).ui;
+ assert.notEqual(u['mode-line'].fg, u['mode-line-inactive'].fg);
+ assert.notEqual(u['line-number-current-line'].fg, u['line-number'].fg);
+});
+
+// --- seed(): org package tier -------------------------------------------
+
+test('seed: packages carries only org-mode (non-org bespoke packages untouched)', () => {
+ const m = buildModel(), p = seed(m).packages;
+ assert.deepEqual(Object.keys(p), ['org-mode']);
+});
+
+test('seed: org headings ramp — level 1 strongest and bold, deeper levels quieter', () => {
+ const m = buildModel(), org = seed(m).packages['org-mode'];
+ assert.equal(org['org-level-1'].weight, 'bold');
+ assert.ok(oklchOf(org['org-level-1'].fg).L > oklchOf(org['org-level-2'].fg).L,
+ 'org-level-1 should be lighter (stronger) than org-level-2');
+});
+
+test('seed: org code-like faces reuse the syntax literal lane', () => {
+ const m = buildModel(), org = seed(m).packages['org-mode'];
+ assert.equal(org['org-code'].fg, m.swatch.terracotta);
+ assert.equal(org['org-code'].inherit, 'fixed-pitch');
+});
+
+test('seed: org link underlined; org-done receded with strikethrough; org-todo warm', () => {
+ const m = buildModel(), org = seed(m).packages['org-mode'];
+ assert.ok(org['org-link'].underline, 'org-link should be underlined');
+ assert.ok(org['org-done'].strike, 'org-done should be struck through');
+ assert.equal(org['org-todo'].fg, m.swatch.red);
+});
+
+// --- purity --------------------------------------------------------------
+
+test('seed: pure — two calls deep-equal and the model is not mutated', () => {
+ const m = buildModel();
+ const before = JSON.stringify(m);
+ const a = seed(m), b = seed(m);
+ assert.deepEqual(a, b);
+ assert.equal(JSON.stringify(m), before, 'seed() must not mutate the model');
+});
+
+test('ROLES: the table exposes the guide roles as data', () => {
+ assert.equal(ROLES.builtin.swatch, 'blue-grey');
+ assert.equal(ROLES.def.swatch, 'gold');
+ assert.equal(ROLES.def.weight, 'bold');
+ assert.equal(ROLES.state.channel, 'bg');
+ assert.equal(ROLES.sig_link.underline, true);
+});
+
+// --- inline-integrity ----------------------------------------------------
+// The page must carry seed-core.js's body (sans import/export) verbatim — the
+// same strip generate.py applies. Requires `python3 generate.py`.
+import { stripInlinedBody } from './inline-strip.mjs';
+
+test('inline-integrity: theme-studio.html contains the seed-core.js body verbatim', () => {
+ const body = stripInlinedBody(readFileSync(here + 'seed-core.js', 'utf8'));
+ const html = readFileSync(here + 'theme-studio.html', 'utf8');
+ assert.ok(html.includes(body), 'generated page is missing the seed-core.js body verbatim');
+});
diff --git a/scripts/theme-studio/theme-studio.html b/scripts/theme-studio/theme-studio.html
index 18f3a540..43c1af12 100644
--- a/scripts/theme-studio/theme-studio.html
+++ b/scripts/theme-studio/theme-studio.html
@@ -1355,6 +1355,254 @@ function contrastTitle(r){
if(r>=4.5) return n+' (grey): passes WCAG AA, not AAA';
return n+' (red): fails WCAG AA';
}
+// The seeding engine (seed model + pure seed()), inlined from seed-core.js. Uses
+// oklchOf/oklch2hex from the colormath core above; the #seedtest gate runs seed().
+// seed-core.js — the theme-studio seeding engine (Phase 1).
+//
+// The seed model as data and the pure seed() operation. This is
+// theme-coloring-guide.org made executable: a named palette (OKLCH-generated
+// shades over a handful of dupre anchor hues), a role-to-treatment table (the
+// guide's seed table), and a face-to-role map for each of the three owned tiers
+// (syntax, UI, org). seed(model) classifies every face and applies the table,
+// producing default assignments in the shape the import path already consumes.
+//
+// Pure: no DOM, no side effects. node imports this module for its unit tests;
+// generate.py strips the import line and inlines the body into the page (below
+// the colormath core, so oklchOf/oklch2hex are already present) so the browser
+// #seedtest runs the same code. One source of truth, like colormath.js.
+//
+// Scope (Package scope in the spec): seed() owns syntax, UI, and org among
+// packages. The other ~20 bespoke packages keep their curated APPS seeds, so
+// seed().packages carries only org-mode; the rest flow through seedPkgmap().
+
+// --- anchors -------------------------------------------------------------
+// The base hues, taken from the bundled dupre palette. Each accent family is
+// anchored here; its quieter/brighter shades are OKLCH-derived (below). Neutrals
+// are taken directly — no ground is pure-white text, and pure black stays the
+// ground only (guide principle 5).
+const ANCHORS = {
+ ground: '#000000', 'bg-dim': '#1a1714',
+ fg: '#a9b2bb', // base identity (dupre silver): comfortable, not pure white
+ 'muted-fg': '#838d97', // structure lane (dupre steel)
+ comment: '#5e6770', // low-contrast comment lane (dupre pewter)
+ blue: '#67809c', gold: '#e8bd30', regal: '#9b5fd0',
+ sage: '#5d9b86', terracotta: '#cb6b4d',
+};
+
+// --- OKLCH shade helpers -------------------------------------------------
+// Step an anchor by a lightness delta and a chroma multiplier, hue held. A
+// quieter shade is darker + lower chroma; a brighter shade is the reverse.
+function shade(hex, dL, cMul) {
+ const { L, C, H } = oklchOf(hex);
+ return oklch2hex(clampL(L + dL), Math.max(0, C * cMul), H).hex;
+}
+// A color placed by absolute OKLCH — used for the signal hues, which are
+// conventional angles rather than shifts of a syntax accent.
+function atHue(L, C, H) { return oklch2hex(clampL(L), C, ((H % 360) + 360) % 360).hex; }
+function clampL(L) { return L < 0 ? 0 : L > 1 ? 1 : L; }
+
+// The heading ramp: one hue across four descending lightness steps (level 1
+// strongest). Deeper org levels cycle through these past level 4.
+function headingRamp(anchorHex) {
+ const { C, H } = oklchOf(anchorHex);
+ return [0.78, 0.68, 0.58, 0.48].map((L) => oklch2hex(L, C, H).hex);
+}
+
+// Build the named swatch set + heading ramp from the anchors. The blue-grey
+// builtin and gold-quiet call are the two shades dupre lacks and gains here.
+function buildModel(anchors = ANCHORS) {
+ const a = anchors;
+ const swatch = {
+ ground: a.ground, 'bg-dim': a['bg-dim'], fg: a.fg,
+ 'muted-fg': a['muted-fg'], comment: a.comment,
+ blue: a.blue,
+ 'blue-grey': shade(a.blue, -0.05, 0.5), // builtin: blue hue, lower chroma/lightness
+ gold: a.gold,
+ 'gold-quiet': shade(a.gold, -0.08, 0.6), // call: quieter same-hue gold
+ regal: a.regal,
+ sage: a.sage,
+ 'sage-muted': shade(a.sage, -0.03, 0.6), // docstring
+ 'sage-bright': shade(a.sage, 0.08, 1.15), // escape
+ teal: (() => { const { L, C, H } = oklchOf(a.sage); return oklch2hex(clampL(L + 0.05), C * 1.1, H - 25).hex; })(), // regexp
+ terracotta: a.terracotta,
+ red: atHue(0.62, 0.15, 29), // signal: error / deletion
+ amber: atHue(0.80, 0.14, 75), // signal: warning / modified
+ green: atHue(0.72, 0.14, 145),// signal: success / addition
+ tint: atHue(0.32, 0.03, oklchOf(a.blue).H), // transient state bg (quiet)
+ 'tint-strong': atHue(0.42, 0.06, oklchOf(a.blue).H), // active match chip
+ };
+ return { swatch, ramp: headingRamp(a.blue), roles: ROLES };
+}
+
+// --- the role-to-treatment table (the guide's seed table as data) --------
+// Each role maps to a swatch, an optional weight/slant/underline, and a channel
+// (fg is identity, bg is state). channel defaults to fg.
+const ROLES = {
+ base: { swatch: 'fg' },
+ structure: { swatch: 'muted-fg' },
+ control: { swatch: 'blue', weight: 'bold' },
+ builtin: { swatch: 'blue-grey' },
+ def: { swatch: 'gold', weight: 'bold' },
+ call: { swatch: 'gold-quiet' },
+ type: { swatch: 'regal' },
+ string: { swatch: 'sage' },
+ docstring: { swatch: 'sage-muted', slant: 'italic' },
+ escape: { swatch: 'sage-bright' },
+ regexp: { swatch: 'teal' },
+ literal: { swatch: 'terracotta' },
+ comment: { swatch: 'comment', slant: 'italic' },
+ sig_error: { swatch: 'red' },
+ sig_warn: { swatch: 'amber' },
+ sig_ok: { swatch: 'green' },
+ sig_link: { swatch: 'blue', underline: true },
+ state: { swatch: 'tint', channel: 'bg' },
+};
+
+// A blank full face spec; seed fills only the fields a role sets.
+function blankSpec() {
+ return { fg: null, bg: null, weight: null, slant: null, underline: null, strike: null, inherit: null, height: null };
+}
+
+// Resolve a ROLES role against the model into a face spec.
+function resolveRole(model, role) {
+ const r = model.roles[role];
+ const hex = model.swatch[r.swatch];
+ const s = blankSpec();
+ if (r.channel === 'bg') s.bg = hex; else s.fg = hex;
+ if (r.weight) s.weight = r.weight;
+ if (r.slant) s.slant = r.slant;
+ if (r.underline) s.underline = { style: 'line', color: null };
+ return s;
+}
+
+// --- face-to-role maps ---------------------------------------------------
+
+// Syntax: CATS key -> role. bg is handled specially (the ground).
+const SYNTAX_ROLES = {
+ p: 'base', var: 'base',
+ op: 'structure', punc: 'structure', neg: 'structure', cmd: 'structure',
+ kw: 'control', pp: 'control',
+ bi: 'builtin',
+ fnd: 'def', fnc: 'call',
+ dec: 'type', ty: 'type', prop: 'type',
+ con: 'literal', num: 'literal',
+ str: 'string', doc: 'docstring',
+ esc: 'escape', dmark: 'escape',
+ re: 'regexp', rxgb: 'regexp', rxgc: 'regexp',
+ cm: 'comment',
+ warn: 'sig_warn',
+};
+
+// UI: face -> either a ROLES role (state/signal/link/control) or an inline
+// chrome spec. Chrome is inherently multi-attribute (fg + bg, active vs idle),
+// so it does not force through the single-swatch role resolver.
+function uiSeed(model) {
+ const sw = model.swatch, out = {};
+ const role = (r) => resolveRole(model, r);
+ const spec = (o) => Object.assign(blankSpec(), o);
+ // Transient state: background tint, no foreground. lazy-highlight (other
+ // matches) shares the quiet tint; isearch (current match) gets a louder chip.
+ for (const f of ['region', 'hl-line', 'highlight', 'show-paren-match', 'lazy-highlight']) out[f] = role('state');
+ out.isearch = spec({ bg: sw['tint-strong'] }); // active match, louder chip
+ // Signals (convention hues) with a weight for redundancy.
+ out.error = spec({ fg: sw.red, weight: 'bold' });
+ out.warning = spec({ fg: sw.amber, weight: 'bold' });
+ out.success = spec({ fg: sw.green, weight: 'bold' });
+ out['isearch-fail'] = spec({ fg: sw.red, weight: 'bold' });
+ out['show-paren-mismatch'] = spec({ bg: sw.red }); // shape + color, not color alone
+ out.link = role('sig_link');
+ // Chrome: active brighter than idle (guide principle 3).
+ out['mode-line'] = spec({ fg: sw.fg, bg: sw['bg-dim'] });
+ out['mode-line-inactive'] = spec({ fg: sw['muted-fg'], bg: sw['bg-dim'] });
+ out['mode-line-highlight'] = spec({ fg: sw.fg });
+ for (const f of ['header-line', 'tab-bar', 'tab-line']) out[f] = spec({ fg: sw['muted-fg'], bg: sw['bg-dim'] });
+ out['line-number'] = spec({ fg: sw.comment });
+ out['line-number-current-line'] = spec({ fg: sw.fg });
+ out.fringe = spec({ fg: sw.comment });
+ out['vertical-border'] = spec({ fg: sw['bg-dim'] });
+ out['minibuffer-prompt'] = role('control');
+ out.cursor = spec({ bg: sw.fg });
+ return out;
+}
+
+// Org: face -> role/heading/inline. Faces not named here seed to base.
+const ORG_MARKUP = ['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', 'org-macro', 'org-target', 'org-footnote-def'];
+const ORG_CODE = ['org-code', 'org-verbatim', 'org-inline-src-block'];
+const ORG_LINK = ['org-link', 'org-cite', 'org-cite-key', 'org-footnote'];
+const ORG_EMPHASIS = ['org-quote', 'org-verse'];
+
+function orgSeed(model, orgFaces) {
+ const sw = model.swatch, out = {};
+ const role = (r) => resolveRole(model, r);
+ const spec = (o) => Object.assign(blankSpec(), o);
+ for (const face of orgFaces) {
+ const lvl = /^org-level-([1-8])$/.exec(face);
+ if (lvl) {
+ const i = Number(lvl[1]);
+ out[face] = spec({ fg: model.ramp[(i - 1) % model.ramp.length], weight: i === 1 ? 'bold' : null });
+ } else if (face === 'org-document-title') {
+ out[face] = spec({ fg: sw.gold, weight: 'bold' });
+ } else if (ORG_CODE.includes(face)) {
+ out[face] = spec({ fg: sw.terracotta, inherit: 'fixed-pitch' });
+ } else if (face === 'org-block') {
+ out[face] = spec({ bg: sw['bg-dim'], inherit: 'fixed-pitch' });
+ } else if (ORG_LINK.includes(face)) {
+ out[face] = spec({ fg: sw.blue, underline: { style: 'line', color: null } });
+ } else if (ORG_MARKUP.includes(face)) {
+ out[face] = spec({ fg: sw['muted-fg'] });
+ } else if (face === 'org-todo' || face === 'org-imminent-deadline') {
+ out[face] = spec({ fg: sw.red, weight: 'bold' });
+ } else if (face === 'org-upcoming-deadline') {
+ out[face] = spec({ fg: sw.amber });
+ } else if (face === 'org-scheduled' || face === 'org-scheduled-today') {
+ out[face] = spec({ fg: sw.comment });
+ } else if (face === 'org-done' || face === 'org-headline-done' || face === 'org-agenda-done') {
+ out[face] = spec({ fg: sw.comment, strike: { color: null } });
+ } else if (ORG_EMPHASIS.includes(face)) {
+ out[face] = spec({ slant: 'italic' });
+ } else {
+ out[face] = role('base');
+ }
+ }
+ return out;
+}
+
+// The org faces the engine seeds. Kept in step with ORG_FACES in face_data.py;
+// a face present here but absent there (or the reverse) simply seeds/omits it.
+// The representative set the guide names is what matters for the tier.
+const ORG_FACES = ('org-document-title org-document-info org-document-info-keyword '
+ + 'org-level-1 org-level-2 org-level-3 org-level-4 org-level-5 org-level-6 org-level-7 org-level-8 '
+ + 'org-headline-done org-todo org-done org-priority org-tag org-special-keyword org-drawer '
+ + 'org-property-value org-warning org-link org-cite org-cite-key org-footnote org-date '
+ + 'org-macro org-target org-block org-block-begin-line org-block-end-line org-code org-verbatim '
+ + 'org-inline-src-block org-quote org-verse org-meta-line org-ellipsis '
+ + 'org-scheduled org-scheduled-today org-upcoming-deadline org-imminent-deadline '
+ + 'org-agenda-done org-table org-formula').split(' ');
+
+// --- seed(): apply the table through each tier's face-to-role map --------
+// Returns {syntax, ui, packages} default assignments. packages carries only
+// org-mode (Package scope); the non-org curated defaults flow through
+// seedPkgmap() over the APPS dicts, untouched by the engine.
+function seed(model, opts = {}) {
+ const cats = opts.cats || CATS_KEYS;
+ const orgFaces = opts.orgFaces || ORG_FACES;
+ const syntax = {};
+ for (const k of cats) {
+ if (k === 'bg') { syntax.bg = Object.assign(blankSpec(), { fg: model.swatch.ground }); continue; }
+ const rname = SYNTAX_ROLES[k] || 'base';
+ syntax[k] = resolveRole(model, rname);
+ }
+ return { syntax, ui: uiSeed(model), packages: { 'org-mode': orgSeed(model, orgFaces) } };
+}
+
+// The syntax categories the engine seeds, kept in step with CATS in generate.py.
+// Passed explicitly by the page (opts.cats) from the live CATS so the two never
+// drift; this literal is the standalone/test default.
+const CATS_KEYS = ['bg', 'p', 'kw', 'bi', 'pp', 'fnd', 'fnc', 'dec', 'ty', 'prop',
+ 'con', 'num', 'str', 'esc', 're', 'rxgb', 'rxgc', 'doc', 'dmark', 'cm', 'cmd',
+ 'var', 'op', 'neg', 'punc', 'warn'];
// Pure palette-generator planner and browser-side generator panel.
// Pure palette-generator planner. It depends on the shared palette-column model
// from app-core.js, but owns candidate hue selection, naming, contrast filtering,
@@ -4241,6 +4489,37 @@ function pkgSelftest(){
const d=document.createElement('div');d.id='selftest';d.textContent='SELFTEST '+verdict+' roundtrip='+roundtrip+' oldjson='+oldjson+' inherit='+inherited+' height='+height+' cleared='+cleared+' unknown='+unknown+' cycle='+cyc;document.body.appendChild(d);
}
if(location.hash==='#selftest')pkgSelftest();
+// Seeding-engine gate (open with #seedtest): the pure seed() projects the guide's
+// role table onto the three owned tiers. Assert representative faces land on the
+// right swatch/weight/channel, and that a non-org bespoke package (magit) keeps
+// its curated APPS seed (seed() owns org among packages, nothing else).
+if(location.hash==='#seedtest')gate('seedtest',A=>{
+ const m=buildModel();
+ const s=seed(m,{cats:CATS.map(c=>c[0])});
+ // syntax tier
+ A(s.syntax.bi.fg===m.swatch['blue-grey'],'bi=blue-grey');
+ A(s.syntax.fnd.fg===m.swatch.gold&&s.syntax.fnd.weight==='bold','fnd=gold+bold');
+ A(s.syntax.fnc.fg===m.swatch['gold-quiet']&&s.syntax.fnc.weight!=='bold','fnc=gold-quiet');
+ A(s.syntax.var.fg===m.swatch.fg,'var=base');
+ A(s.syntax.op.fg===m.swatch['muted-fg']&&s.syntax.punc.fg===m.swatch['muted-fg'],'op/punc=structure');
+ A(s.syntax.kw.fg===m.swatch.blue&&s.syntax.kw.weight==='bold','kw=control');
+ A(s.syntax.doc.slant==='italic','doc=italic');
+ A(s.syntax.bg.fg===m.swatch.ground,'bg=ground');
+ // UI tier
+ A(!!s.ui.region.bg&&!s.ui.region.fg,'region bg-only');
+ A(!!s.ui.link.underline&&s.ui.link.fg===m.swatch.blue,'link underlined');
+ A(s.ui.error.fg===m.swatch.red&&s.ui.warning.fg===m.swatch.amber&&s.ui.success.fg===m.swatch.green,'signals on convention hues');
+ A(s.ui['mode-line'].fg!==s.ui['mode-line-inactive'].fg,'active!=idle chrome');
+ // org tier
+ const org=s.packages['org-mode'];
+ A(Object.keys(s.packages).length===1,'packages=org-mode only');
+ A(oklchOf(org['org-level-1'].fg).L>oklchOf(org['org-level-2'].fg).L&&org['org-level-1'].weight==='bold','org-level-1 strongest+bold');
+ A(org['org-code'].fg===m.swatch.terracotta&&org['org-code'].inherit==='fixed-pitch','org-code literal lane');
+ A(!!org['org-done'].strike,'org-done struck');
+ // non-org bespoke package keeps its curated seed (untouched by seed())
+ const mag=seedPkgmap()['magit'];
+ A(!!mag&&!!mag['magit-section-heading']&&!!mag['magit-section-heading'].fg,'magit keeps curated seed');
+});
// Lock-mechanism gate (open with #locktest): two behaviors the refactor must
// preserve, across all three tiers. (1) Locking a row disables its controls via
// the shared mkLockCell. (2) reset/erase batch actions update editable rows but
diff --git a/tests/test-org-config-noop-state-log.el b/tests/test-org-config-noop-state-log.el
new file mode 100644
index 00000000..125eb300
--- /dev/null
+++ b/tests/test-org-config-noop-state-log.el
@@ -0,0 +1,59 @@
+;;; test-org-config-noop-state-log.el --- Suppress no-op state-change logs -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; org's state-change logging writes "- State X from X" lines for no-op
+;; transitions (identical from/to state), which carry no information. With
+;; `org-log-into-drawer' nil those lines also land inline, where they wedge
+;; between a heading and its planning line and break org's parser. Two fixes
+;; in `cj/org-todo-settings': log state changes into :LOGBOOK: drawers
+;; (`org-log-into-drawer' t), and suppress no-op state logs at the
+;; `org-add-log-setup' choke point via `cj/org--suppress-noop-state-log'.
+
+;;; Code:
+
+(require 'ert)
+(require 'org) ;; declares the org-log-* vars special
+(require 'org-config)
+
+(ert-deftest test-org-config-log-into-drawer-enabled ()
+ "Normal: cj/org-todo-settings enables org-log-into-drawer (LOGBOOK)."
+ (let ((org-log-into-drawer nil))
+ (cj/org-todo-settings)
+ (should (eq org-log-into-drawer t))))
+
+(ert-deftest test-org-config-noop-state-log-p-identical-is-noop ()
+ "Normal: identical from/to state on a 'state purpose is a no-op."
+ (should (cj/org--noop-state-log-p 'state "TODO" "TODO")))
+
+(ert-deftest test-org-config-noop-state-log-p-real-change-not-noop ()
+ "Normal: a genuine state change is not a no-op."
+ (should-not (cj/org--noop-state-log-p 'state "DONE" "TODO")))
+
+(ert-deftest test-org-config-noop-state-log-p-nil-prev-not-noop ()
+ "Boundary: a nil previous state (initial log) is not a no-op."
+ (should-not (cj/org--noop-state-log-p 'state "TODO" nil)))
+
+(ert-deftest test-org-config-noop-state-log-p-non-state-purpose-not-noop ()
+ "Boundary: identical strings under a non-state purpose are not suppressed."
+ (should-not (cj/org--noop-state-log-p 'note "x" "x")))
+
+(ert-deftest test-org-config-noop-state-log-p-nil-both-not-noop ()
+ "Error: a nil/nil state pair is not a suppressible no-op."
+ (should-not (cj/org--noop-state-log-p 'state nil nil)))
+
+(ert-deftest test-org-config-suppress-advice-skips-noop ()
+ "Normal: the advice does NOT call through for a no-op state transition."
+ (let ((called nil))
+ (cj/org--suppress-noop-state-log
+ (lambda (&rest _) (setq called t)) 'state "TODO" "TODO")
+ (should-not called)))
+
+(ert-deftest test-org-config-suppress-advice-passes-real-change ()
+ "Normal: the advice calls through for a genuine state change."
+ (let ((called nil))
+ (cj/org--suppress-noop-state-log
+ (lambda (&rest _) (setq called t)) 'state "DONE" "TODO")
+ (should called)))
+
+(provide 'test-org-config-noop-state-log)
+;;; test-org-config-noop-state-log.el ends here