aboutsummaryrefslogtreecommitdiff
path: root/docs/design/theme-selector-package-faces-spec.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/theme-selector-package-faces-spec.org')
-rw-r--r--docs/design/theme-selector-package-faces-spec.org511
1 files changed, 511 insertions, 0 deletions
diff --git a/docs/design/theme-selector-package-faces-spec.org b/docs/design/theme-selector-package-faces-spec.org
new file mode 100644
index 00000000..60c4e9c5
--- /dev/null
+++ b/docs/design/theme-selector-package-faces-spec.org
@@ -0,0 +1,511 @@
+#+TITLE: theme-selector — package faces (tier 3), starting with org-mode
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-06-07
+
+* Status
+
+Spec / Craig's first-round answers folded in (2026-06-07). Proposes a third tier
+for the theme-selector (scripts/theme-selector/) that lets a theme colorize
+package-specific faces, built one application at a time. v1 apps: org-mode
+(incl. org-agenda), magit, elfeed. Codex review incorporated (2026-06-07): added
+implementation phases, acceptance criteria, the package-face inventory source
+(hybrid, split), and state/export semantics. Rubric now =Ready with caveats=.
+Three opens remain for Craig: confirm the inheritance representation
+(absolute-default + opt-in inherit), confirm hybrid-and-split inventory (vs
+curated-only v1), and the custom color picker timing.
+
+* Background — the three tiers
+
+The theme-selector already models two tiers of faces:
+
+1. *Syntax* — the font-lock / tree-sitter categories (keyword, string, type,
+ comment, etc.), in the "code/color assignments" table.
+2. *UI* — Emacs's built-in interface faces (cursor, region, mode-line, fringe,
+ line numbers, isearch, and the rest), in the "ui faces" table with the live
+ mock-frame preview.
+
+Tier 3 is *package faces*: faces a package declares with =defface= so a theme
+can color the package as it wishes. The running config has 1,146 such faces
+across 186 packages (magit 111, lsp-mode 97, telega 91, web-mode 82, org ~30
+core, and a long tail). No theme colors all of them; quality themes hand-pick
+the packages the user actually lives in and theme those.
+
+This spec adds a tier-3 section to the tool, structured so applications are
+added one at a time. org-mode ships first.
+
+* Goal
+
+A new "package faces" section with:
+
+1. An *application dropdown* — pick which package's faces to edit. v1 ships
+ org-mode (including org-agenda), magit, and elfeed; the rest of Craig's
+ packages (calibredb, ghostel, mu4e, IRC, org-drill, dirvish + dired, slack)
+ follow one at a time.
+2. A *face table* for the selected app — one row per face in the app's complete
+ set, each with a foreground dropdown, a background dropdown, bold / italic
+ toggles, and an optional inherit, all drawing from the same palette as the
+ other tables. Grouped, with a text filter for the large apps.
+3. A *preview pane* for the selected app — a realistic mock of that package
+ rendered with the live theme, the way the ui-faces mock-frame shows the UI
+ faces in a buffer. org-mode gets a mock org document.
+
+The export (=theme.json=) gains a =packages= object so the build step can set
+these faces too.
+
+* UI placement
+
+A new top-level section under the ui-faces row:
+
+#+begin_example
+<h1>package faces</h1>
+[ application: (org-mode v) ]
+<div class="cols stretch">
+ left = the selected app's face table (fg / bg / B / I per face)
+ right = the selected app's preview pane (e.g. the org document mock)
+</div>
+#+end_example
+
+Same two-column stretch layout as the ui-faces row, so the preview matches the
+table's height.
+
+* Data model
+
+A single data structure drives everything, keyed by application:
+
+#+begin_src js
+APPS = {
+ "org-mode": {
+ label: "org-mode",
+ faces: [
+ // face, human label, default {fg, bg, bold, italic}
+ ["org-document-title", "document title", {fg:"gold", bold:true}],
+ ["org-level-1", "heading 1", {fg:"blue", bold:true}],
+ ["org-level-2", "heading 2", {fg:"gold"}],
+ ["org-level-3", "heading 3", {fg:"regal"}],
+ ["org-todo", "TODO keyword", {fg:"terracotta", bold:true}],
+ ["org-done", "DONE keyword", {fg:"sage", bold:true}],
+ ["org-link", "link", {fg:"blue"}], // base `link`
+ ["org-code", "inline code", {fg:"terracotta"}],
+ ["org-verbatim", "verbatim", {fg:"steel"}],
+ ["org-block", "src block body", {fg:"white", bg:"bg-dim"}],
+ ["org-block-begin-line","block delim", {fg:"pewter", bg:"bg-dim"}],
+ ["org-table", "table", {fg:"steel"}],
+ ["org-date", "timestamp", {fg:"steel"}],
+ ["org-tag", "tag", {fg:"tan"}],
+ ["org-special-keyword","keyword/drawer", {fg:"pewter"}],
+ ["org-meta-line", "#+meta line", {fg:"pewter"}],
+ ["org-checkbox", "checkbox", {fg:"gold"}],
+ ["org-headline-done", "done headline", {fg:"pewter"}],
+ ],
+ preview: "org" // names the preview renderer
+ },
+ // magit, elfeed, ... added later with the same shape
+}
+#+end_src
+
+Defaults reference palette *names* (blue, gold, ...) resolved to hexes at load,
+so a curated app seeds sensibly from the current palette. The user reassigns
+any face from the palette dropdowns exactly like the other tables.
+
+State mirrors the other tiers: a =PKGMAP= of
+={app: {face: {fg, bg, bold, italic, inherit}}}=, edited live, rendered into
+the table and the preview. The =APPS= block above shows ~18 org faces only as a
+shape illustration; the real org entry is the complete set below.
+
+** Data model — org face set (complete)
+
+Per the completeness decision, org's table lists org's entire own =defface= set
+(org-faces.el + org-agenda.el), ~88 faces, grouped. Seed defaults for the
+prominent groups; the long tail seeds to fg or an =inherit= of its group base,
+which the user overrides. The groups (face names verbatim from the running
+Emacs):
+
+- *Document:* org-document-title, org-document-info, org-document-info-keyword
+- *Headings:* org-level-1 .. org-level-8, org-headline-todo, org-headline-done
+- *Status / keywords:* org-todo, org-done, org-priority, org-tag, org-tag-group,
+ org-special-keyword, org-drawer, org-property-value, org-checkbox,
+ org-checkbox-statistics-todo, org-checkbox-statistics-done, org-warning
+- *Links / dates / refs:* org-link, org-footnote, org-date, org-sexp-date,
+ org-date-selected, org-target, org-macro, org-cite, org-cite-key
+- *Blocks / code / quote:* org-block, org-block-begin-line, org-block-end-line,
+ org-code, org-verbatim, org-inline-src-block, org-quote, org-verse,
+ org-latex-and-related
+- *Tables / columns:* org-table, org-table-header, org-table-row, org-formula,
+ org-column, org-column-title
+- *Lists / meta / structure:* org-list-dt, org-meta-line, org-ellipsis,
+ org-hide, org-indent, org-archived, org-default, org-dispatcher-highlight
+- *Agenda — structure & dates:* org-agenda-structure,
+ org-agenda-structure-secondary, org-agenda-structure-filter, org-agenda-date,
+ org-agenda-date-today, org-agenda-date-weekend, org-agenda-date-weekend-today,
+ org-agenda-current-time, org-agenda-done, org-agenda-dimmed-todo-face
+- *Agenda — calendar & filters:* org-agenda-calendar-event,
+ org-agenda-calendar-sexp, org-agenda-calendar-daterange, org-agenda-diary,
+ org-agenda-clocking, org-agenda-column-dateline, org-agenda-restriction-lock,
+ org-agenda-filter-category, org-agenda-filter-effort, org-agenda-filter-regexp,
+ org-agenda-filter-tags
+- *Scheduling / deadlines / clock:* org-scheduled, org-scheduled-today,
+ org-scheduled-previously, org-upcoming-deadline, org-upcoming-distant-deadline,
+ org-imminent-deadline, org-time-grid, org-clock-overlay, org-mode-line-clock,
+ org-mode-line-clock-overrun
+
+The org *preview* below stays a curated document exercising the prominent
+faces; the *table* carries the complete set so every face is assignable, even
+the ones the preview doesn't draw. magit and elfeed get the same treatment
+(complete own-defface set in the table, a bespoke preview for the common faces).
+
+* The org preview
+
+A mock org document painted from PKGMAP["org-mode"] plus the palette ground/fg.
+One bespoke renderer (=renderOrgPreview()=) drawing a representative document:
+
+#+begin_example
+#+TITLE: Project Notes <- org-document-title
+#+AUTHOR: ... <- org-meta-line / document-info
+
+* Inbox :work: <- org-level-1 + org-tag
+** TODO Draft the spec <- org-level-2 + org-todo
+ SCHEDULED: <2026-06-08 Sun> <- org-special-keyword + org-date
+** DONE Ship the tool <- org-level-2 + org-done (headline-done)
+*** Heading three <- org-level-3
+ A line with =inline code=, <- org-code
+ ~verbatim~, and a [[link]]. <- org-verbatim + org-link
+ - [X] a checkbox item <- org-checkbox
+
+ #+begin_src elisp <- org-block-begin-line
+ (message "hi") <- org-block
+ #+end_src <- org-block-end-line
+
+ | name | hex | <- org-table (header row org-table-header)
+ |------+---------|
+ | blue | #67809c |
+#+end_example
+
+Each marked element is a span colored from the corresponding PKGMAP face. The
+preview rebuilds whenever a package face or the palette changes, same as the
+mock frame.
+
+org, magit, and elfeed get bespoke preview renderers (magit -> a status buffer
+mock, elfeed -> a search-list mock). Every *other* package is still fully
+themeable: its face *table* is always present and editable, only the rich
+*preview* is replaced by a generic fallback — each face's name rendered in its
+own colors on the ground. So a user can theme every package they have the
+moment its face list is added; the bespoke preview is a polish layer on top, not
+a gate. This is the v1 answer to "some will want to touch every package."
+
+* Export schema
+
+=theme.json= gains a =packages= key:
+
+#+begin_src json
+{
+ "name": "dupre",
+ "palette": [...],
+ "assignments": {...},
+ "bold": [...], "italic": [...],
+ "ui": {...},
+ "packages": {
+ "org-mode": {
+ "org-level-1": {"fg":"#67809c","bg":null,"bold":true,"italic":false,"inherit":null},
+ "org-level-2": {"fg":"#e8bd30","bg":null,"bold":false,"italic":false,"inherit":"org-level-1"},
+ "org-todo": {"fg":"#cb6b4d","bg":null,"bold":true,"italic":false,"inherit":null}
+ }
+ }
+}
+#+end_src
+
+=inherit= is optional and =null= when absent. When set, the converter writes
+=:inherit PARENT= plus only the overridden attributes.
+
+Only faces the user actually touched (or the curated defaults) are written. The
+build step's converter sets each as a normal face. Backward compatible: a file
+without =packages= loads fine.
+
+* Build-step consumption
+
+The eventual =theme.json= -> =dupre-*.el= converter already owns tiers 1 and 2.
+Tier 3 adds, per package face:
+
+#+begin_src elisp
+(org-level-1 ((t (:foreground "#67809c" :weight bold))))
+(org-todo ((t (:foreground "#cb6b4d" :weight bold))))
+#+end_src
+
+No new converter machinery — package faces are just more faces. This is the
+TDD-worthy part (JSON in, valid faces out), same as the rest of the converter.
+
+* Scope for v1
+
+- Build the section, the app dropdown, and the face tables + previews for the
+ three v1 apps: org-mode (incl. org-agenda), magit, elfeed.
+- org's table carries its complete own-defface set (~88 faces, grouped above),
+ seeded with defaults; the org preview draws the prominent ones.
+- Every other installed package is reachable in the dropdown with an editable
+ face table and the generic fallback preview, so any package can be themed.
+- Wire export/import of the =packages= key (with the optional =inherit= field).
+- Leave the converter for the separate build-step task (Elisp, per Craig); the
+ spec only needs the schema to be right.
+
+* Implementation phases
+
+Phased so each step ships without a broken intermediate, and the three bespoke
+apps don't wait on the all-package inventory.
+
+1. *State + schema.* Add =PKGMAP= ({app:{face:{fg,bg,bold,italic,inherit,source}}})
+ and the =APPS= registry. Extend export/import with the =packages= key; old
+ JSON (no =packages=) still imports cleanly. No UI yet.
+2. *Curated app data.* Complete own-defface face lists + seeded defaults for org
+ (incl. org-agenda), magit, elfeed, in =APPS=. Pure data.
+3. *Package face table UI.* App selector; grouped rows; fg/bg dropdowns + bold /
+ italic toggles + optional inherit; per-face and per-app reset; a text filter
+ (org/magit are large); a contrast readout per fg/bg. Built on a generalized
+ face-control helper shared with the ui-faces table, not a fork of =uiSelect=.
+4. *Org preview.* =renderOrgPreview()=, live, refreshing on palette/face change.
+5. *Magit + elfeed previews.* Bespoke mocks (magit status buffer, elfeed search
+ list).
+6. *Generated all-package inventory* (the "theme every package" path). A build
+ step queries Emacs for installed packages' faces grouped by package, writes a
+ data file =generate.py= embeds; the dropdown then lists every package with an
+ editable table + the generic fallback preview. Lands after phases 1-5 without
+ blocking the three bespoke apps.
+7. *Docs + validation.* README =packages= schema + inventory-refresh command;
+ regenerate HTML; fixtures + manual checklist.
+
+Phases 1-5 deliver the three high-value apps fully; phase 6 opens the long tail;
+phase 7 documents.
+
+* Package face inventory source
+
+*Hybrid, split across phases.* Curated app metadata (org/magit/elfeed: complete
+face lists, seeded defaults, bespoke previews) is hand-maintained in =APPS= and
+ships in phases 2-5. A *generated* =PACKAGE_FACE_INVENTORY= — produced by a build
+step that asks the running Emacs for each installed package's faces grouped by
+package, written to a JSON/Python data file =generate.py= embeds — supplies the
+generic fallback packages and ships in phase 6.
+
+Why hybrid and split: the static generator can't discover packages at runtime in
+the browser, so "theme every package" needs a generated inventory; but making the
+full inventory a prerequisite for the three bespoke apps invites the scope
+explosion the review flagged. Splitting it lets v1's core ship first; the
+inventory is additive.
+
+The generated inventory is an *input artifact* to =generate.py= (a committed data
+file refreshed by an explicit command), never browser-side discovery. The refresh
+command's dependency on a loaded Emacs config is documented.
+
+Open for Craig: confirm hybrid-and-split, or keep v1 to curated org/magit/elfeed
+only and defer the generated inventory entirely.
+
+* State and export policy
+
+Each package face object carries a =source= marker so export can tell a seeded
+default from a user edit from a deliberate clear:
+
+#+begin_src js
+{ fg:"#67809c", bg:null, bold:true, italic:false, inherit:null, source:"default" }
+// source: "default" (seeded) | "user" (edited) | "cleared" (user removed a default)
+#+end_src
+
+Export policy:
+
+- Write =default= and =user= entries.
+- Write =cleared= entries — they must suppress a curated default on reload.
+- Omit untouched faces that have no default.
+- When =inherit= is set, write =inherit= plus only the explicit overrides.
+- Preserve package faces present in an imported file but absent from the current
+ inventory (or warn) — don't silently drop them.
+
+Import tolerates a missing =packages= key, unknown app keys, unknown face keys,
+and a missing =inherit=. A deleted palette color leaves package face references
+in the same "(gone)" recoverable state syntax colors use. Inheritance cycles are
+rejected (treated as no inheritance) during preview resolution.
+
+* Acceptance criteria
+
+- Existing =dupre.json= (no =packages= key) imports cleanly.
+- Export includes =packages= once defaults or edits exist;
+ =fg/bg/bold/italic/inherit/source= round-trip through import/export.
+- org, magit, elfeed appear in the app selector with complete grouped face tables.
+- (phase 6) generic inventory packages appear with editable tables + fallback
+ previews, the fallback visibly labeled as generic.
+- A palette color update propagates to package faces the same way it does to
+ syntax / ui faces.
+- =python3 scripts/theme-selector/generate.py= rebuilds =theme-selector.html=.
+- README documents the =packages= schema, inheritance, and the inventory source.
+
+* Extensibility (adding the next app)
+
+1. Add an entry to =APPS= (label, curated face list with palette-name defaults,
+ preview key).
+2. Optionally write a bespoke preview renderer; until then the generic fallback
+ renders.
+3. Nothing else changes — the dropdown, table, export, and import are all
+ data-driven off =APPS= / =PKGMAP=.
+
+* Agreed decisions
+
+Craig's answers to the first review round, baked in (the body sections above
+reflect these; this records the decisions):
+
+1. *Curated set is complete, not iterative.* For org, list its *entire* own
+ defface set (org-faces.el + org-agenda.el), ~88 faces, not a hand-picked
+ ~18. The user wants every choice present, not a set that grows on demand.
+ See "Data model — org face set" for the full grouped list.
+2. *Seed curated defaults.* Seed sensible fg/bg and weight per face (headings,
+ title, TODO/DONE bold; agenda dates and deadlines colored by role). The user
+ reassigns from there.
+3. *App order: org, magit, elfeed for v1.* Then the rest one at a time, drawn
+ from the packages Craig actually runs: calibredb, ghostel, mu4e, the IRC
+ client, org-drill, dirvish + dired, slack. A finite "most-used" list gets
+ picked later; we do not try to do everything at once.
+4. *Generic fallback is real, not display-only.* Any package not given a
+ bespoke preview still gets a fully editable face table (so a user can theme
+ *every* package they have); only the rich preview is missing, replaced by a
+ swatch-in-context fallback. Bespoke previews ship for org, magit, elfeed.
+
+* Inheritance representation (answer to Craig's question)
+
+Craig asked how inheritance would be represented. Proposal:
+
+Each face carries an optional =inherit= field naming another face (or =null=).
+The face's own =fg/bg/bold/italic= are *overrides* layered on top of what it
+inherits.
+
+#+begin_src js
+["org-level-2", "heading 2", {inherit:"org-level-1", fg:"gold"}]
+// exports as: (org-level-2 ((t (:inherit org-level-1 :foreground "#e8bd30"))))
+["org-agenda-date-today", "agenda today", {inherit:"org-agenda-date", bold:true}]
+// exports as: (org-agenda-date-today ((t (:inherit org-agenda-date :weight bold))))
+#+end_src
+
+In the tool, an inheriting row shows an "inherits <face>" chip; attributes left
+unset render greyed (they come from the parent) until the user overrides one.
+The converter writes =:inherit PARENT= followed by only the overridden
+attributes.
+
+*Recommendation: default to absolute values, offer inherit as opt-in.* Emacs
+face inheritance surprises people — an inherited background or weight rides
+along silently — so seeding every face with absolute attributes is the
+predictable default. Inheritance is available for the cases where a cascade is
+genuinely wanted (all heading levels off one base; agenda-date variants off
+=org-agenda-date=), expressed with the =inherit= field above. This keeps the
+common path obvious and the export deterministic, while still letting a user
+model the relationships org itself uses.
+
+Decision pending Craig's confirm: absolute-default with opt-in inherit (above),
+or model inheritance for the face families that have it out of the box.
+
+* Custom color picker (proposal)
+
+Craig wants a custom in-page color picker to replace the native browser swatch.
+The native =<input type=color>= opens the OS color chooser, which the page
+cannot size or restyle; a custom picker is the only way to get a larger,
+on-theme picker and to show the palette/contrast in the picker itself.
+
+Proposed widget — a popup anchored to the swatch, drawn in-page:
+
+- A *saturation/value square* (click or drag to set S and V) plus a *hue
+ slider* down the side. Standard HSV picker geometry.
+- A *hex field* synced both ways with the square/slider (already exists in the
+ add-color row; the picker writes to it).
+- The current *palette* shown as clickable chips along the bottom, so picking
+ an existing color is one click and the overlap problem (many roles, one
+ color) is visible while choosing.
+- A live *contrast readout* against the current background (ratio + AAA / AA /
+ FAIL) updating as the color moves, so a color is judged for legibility at
+ pick time, not after assignment.
+- Sized generously (the native popup's size was the original complaint); opens
+ on click of the swatch, closes on pick or click-away.
+
+Implementation: ~120 lines of vanilla JS/canvas (or CSS gradients) for the
+square + slider, reusing the existing =rl()= / =contrast()= / =rating()=
+helpers for the readout and =normHex()= for the field sync. No dependency. It
+replaces the =<input type=color>= in the add-color row and, later, becomes the
+picker the package-face dropdowns can also invoke.
+
+It stays *off* the tier-3 critical path: a separate task before or after the
+package-face build, not folded into it, since folding it in widens the blast
+radius for no dependency benefit. Build it only sooner if package-face editing
+proves painful with the native swatch.
+
+Open question for Craig: custom picker before tier 3, after it, or vNext.
+
+* Files touched
+
+- =scripts/theme-selector/generate.py= — the section, =APPS= data, the package
+ face table, =renderOrgPreview()=, export/import of =packages=.
+- =scripts/theme-selector/theme-selector.html= — regenerated.
+- (later) the =theme.json= -> =dupre-*.el= converter (Elisp) — consumes
+ =packages=.
+
+* Review dispositions
+
+Codex review (2026-06-07), =Not ready=. Findings processed:
+
+- *Modified — generated inventory (high, blocking).* Codex recommended a hybrid
+ inventory so every installed package is reachable. Accepted the hybrid, but
+ *split* it: the generated all-package inventory is its own phase (6), after the
+ three bespoke apps (phases 1-5), rather than a v1 prerequisite. Reason: Codex
+ named scope explosion as the main risk, and gating org/magit/elfeed on a
+ full-inventory mechanism is exactly that. The split keeps v1's core shippable
+ and makes "theme every package" additive. Confirm-with-Craig flagged as an
+ open.
+- *Modified — preview depth (UX obs).* Codex suggested level 4-8 examples in the
+ org preview. The preview stays a curated document drawing the prominent faces
+ (incl. a couple of deeper levels as representative); the complete level set
+ lives in the *table*, which is where every face is assignable. A full 8-level
+ preview block would bloat the mock without adding assignability.
+
+Everything else in the review accepted as written: implementation phases,
+acceptance criteria, the =source= state field + export policy, curated-vs-complete
+wording, keeping the custom picker off the critical path, unknown-import
+preservation, the test-strategy fixtures, and the UX/architecture/robustness
+observations (grouping + filter, reset controls, package-fg/bg contrast readout,
+generalized face-control helper, package style kept inside the package object,
+"(gone)" recoverable state, inheritance-cycle rejection).
+
+* Review and iteration history
+
+** 2026-06-07 Sun @ 18:17:14 -0500 — Claude Code (emacs-d) — author + responder
+- *What:* Folded Craig's first-round cj-comment answers into the body. Curated
+ org set changed from ~18 to org's complete own-defface set (~88, grouped, incl.
+ org-agenda). v1 apps fixed to org/magit/elfeed with the rest deferred to a
+ one-at-a-time list. Generic fallback clarified as a fully editable table for
+ every package (only the rich preview is bespoke). Answered the inheritance
+ question with an optional =inherit= field (absolute-default, opt-in cascade)
+ and added it to the schema. Added a custom-color-picker proposal at Craig's
+ request. Converted Open questions to Agreed decisions; two genuine opens
+ remain (inheritance confirm, picker timing).
+- *Why:* Craig answered the five open questions inline and asked for the
+ spec-response fold plus a custom-picker proposal. Completeness over iterative
+ growth was his explicit call on the curated set.
+- *Artifacts:* This spec. The converter language (Elisp) confirmed by Craig.
+ Note: Craig referenced a Codex spec review, but no
+ =theme-selector-package-faces-review.org= (or any =*-review.org=) exists in the
+ repo — the review file could not be found, so only the author-side cj-comment
+ fold was done; the Codex review remains to be processed when its file is
+ located.
+
+** 2026-06-07 Sun @ 18:20:16 -0500 — Codex — reviewer
+- *What changed or was recommended:* Created the missing formal review and
+ marked the spec =Not ready=. The review recommends adding implementation
+ phases, acceptance criteria, an explicit package-face inventory source, and
+ default/user/cleared/inherited export semantics.
+- *Why:* The direction is coherent and the first-round decisions are folded in,
+ but v1 now depends on behavior that is not yet implementable from the current
+ static generator without a defined inventory and state/export contract.
+- *Artifacts:* [[file:theme-selector-package-faces-spec-review.org][theme-selector-package-faces-spec-review.org]]
+
+** 2026-06-07 Sun @ 18:28:02 -0500 — Claude Code (emacs-d) — responder
+- *What:* Ran spec-response against the Codex review. Added Implementation
+ phases (7), Package face inventory source (hybrid, split), State and export
+ policy (=source= field + export rules), and Acceptance criteria. Wove in the
+ UX/architecture/robustness observations. Two reasoned modifies (generated
+ inventory split into its own phase; org preview stays curated rather than
+ all-8-levels), everything else accepted — see Review dispositions. Status
+ moved to =Ready with caveats=. Deleted the review file.
+- *Why:* The four blocking findings were real implementation-contract gaps; the
+ inventory split answers Codex's own scope-explosion warning while still
+ reaching "theme every package."
+- *Artifacts:* This spec (Review dispositions section); review file deleted per
+ the spec-response close-out. Three opens remain for Craig (inheritance confirm,
+ hybrid-inventory confirm, picker timing).