diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/theme-studio-face-rules.org | 47 | ||||
| -rw-r--r-- | docs/theme-studio-palette-ramps-spec.org | 215 |
2 files changed, 262 insertions, 0 deletions
diff --git a/docs/design/theme-studio-face-rules.org b/docs/design/theme-studio-face-rules.org new file mode 100644 index 00000000..4eb3e1b3 --- /dev/null +++ b/docs/design/theme-studio-face-rules.org @@ -0,0 +1,47 @@ +#+TITLE: theme-studio face rules +#+DATE: 2026-06-09 + +Two kinds of rules govern a theme's face structure. They are different in kind and must be kept separate: Design Rules are the designer's taste and may change per theme; Fidelity Rules come from the principles and never change. A face's final structure is its defface baseline (Fidelity), with Design Rules applied deliberately on top. + +* Design Rules (personal, optional, per-theme) + +Aesthetic choices the designer makes. They override package/Emacs defaults on purpose and are applied consistently across a whole face family. They can change from theme to theme. The tool should let the designer declare them and flag where the theme breaks one (these are not bugs — they are the rule being enforced). + +Structural only (weight/slant/underline/box/overline/height). Color is the palette, decided separately. + +** D1 — Headings and titles are bold + +Every heading/title face carries =:weight bold=, overriding per-package size-only or plain conventions: =org-level-*=, =shr-h1=..=shr-h6=, =magit-*-heading=, =*-title=, =org-document-title=, =dashboard-heading=, =telega-*-title= / =telega-*-heading=, etc. + +Open question for dupre: does the rule mean *all* headings bold, or *headings get emphasis via bold OR descending size*? org-level-2..8 use size, not weight. + +dupre faces that break D1 (heading/title but not bold): +- size-based (intentional? — org distinguishes levels by height): org-level-2, org-level-3, org-level-4, org-level-5, org-level-6, org-level-7, org-level-8 +- genuinely plain (no bold, no height): magit-blame-heading, magit-diff-hunk-heading, telega-msg-heading, telega-describe-subsection-title, telega-secret-title + +** D2 — Hyperlinks are underlined + +Every hyperlink face carries =:underline=, applied across packages: =link=, =org-link=, =shr-link=, =shr-selected-link=, =mu4e-link-face=, =telega-link*=, etc. (Symlinks and link-count faces are not hyperlinks and are exempt.) + +dupre faces that break D2 (hyperlink but not underlined): +- telega-link, telega-link-preview-sitename, telega-link-preview-title, telega-webpage-chat-link + +* Fidelity Rules (principle-derived, mandatory, theme-independent) + +Correctness and honesty invariants. They do not change between themes. A violation is a bug, not a preference. + +** F1 — Preview only what the theme controls + +Every element a preview draws must correspond to a real face the generated theme exports. No hardcoded decoration that implies theme control (this is why the mode-line box became a real =:box= attribute instead of a painted-on bevel, and why the fg/bg contrast cell must rate the face's own pair). Representational stand-ins are allowed only for theme-controlled *colors* whose shape/presence Emacs controls elsewhere — e.g. the cursor drawn as a box (the color is the =cursor= face; the shape is =cursor-type=), the fringe indicator (the color is the =fringe= face; the arrow's presence is truncation state). + +** F2 — Render the way Emacs renders + +A face is drawn the way Emacs would draw it. Overlay-style faces (region, highlight, isearch, lazy-highlight) merge like Emacs: the background applies and the foreground falls through to the underlying syntax colors unless the face sets its own. The block cursor sits on a glyph in the frame background over the cursor color. Every modeled attribute (weight/slant/underline/strike/box/height) actually renders, in both the table preview and the live buffer. + +** F3 — Preserve each face's defface structural baseline + +A face's own defface structural attributes (weight/slant/underline/box/overline/height/inherit) carry through into the theme's default for that face, except where a Design Rule deliberately overrides. An accidental drop — e.g. replacing =:inherit link= with a bare foreground and losing the underline — is a bug. For Emacs's built-in faces the baseline is verified against =emacs -Q= (error/warning/success bold; link, lazy-highlight, show-paren-match underline); for package faces, against the package's defface source. + +** F4 — Reference only real faces + +Every face the theme sets or previews must exist in Emacs. A face the theme defines that no package defines (a typo, a renamed/obsolete face) controls nothing and shows a phantom sample in the preview; it is removed. (This took out 11 dead mu4e faces.) diff --git a/docs/theme-studio-palette-ramps-spec.org b/docs/theme-studio-palette-ramps-spec.org new file mode 100644 index 00000000..f9105595 --- /dev/null +++ b/docs/theme-studio-palette-ramps-spec.org @@ -0,0 +1,215 @@ +#+TITLE: theme-studio Palette Ramps & Background-Contrast Safety — Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-09 + +* Metadata +| Status | draft — review incorporated (Codex, 2026-06-09) | +| Owner | Craig | +| Reviewer | Codex | +| Related | [[file:../todo.org][todo.org: theme-studio color-harmony explainer + ramp/fill features]] | + +* Summary + +Give theme-studio two things it lacks: a generator that turns one base color into a harmonized tonal ramp (base, plus lighter and darker steps), and a readout that tells the designer whether a color is safe to use as a background behind editor text. Both rest on the same OKLCH math worked out 2026-06-09, and they're coupled: the dark end of a ramp is where background tints come from, and a background tint is only usable if it keeps every foreground on it readable. + +* Problem / Context + +Building a theme, the designer needs a family of related shades per hue (blue, blue+1, blue-1, and so on) and a set of dim tinted backgrounds for effects like highlight, region, isearch, and hl-line. Today every shade is hand-picked by eye. Getting them to harmonize is guesswork, and there's no signal for the harder half of the problem: a background effect is an overlay, so the same tint sits behind many different foreground colors at once. A tint that looks fine against the default text can fail against the darkest token. The designer can't see that failure, because the picker shows one fg-on-bg pair, which is the wrong number. + +The cost is concrete. sterling's keyword blue (#67809c, OKLCH-L 0.59) is the darkest foreground; it barely clears WCAG AA on pure black and drops below AA the instant any highlight background lifts off black. So one color silently caps every highlight to near-black, and nothing in the tool surfaces it. The designer either over-darkens everything defensively or ships unreadable highlights without knowing. + +* Goals and Non-Goals + +** Goals +- From a base color, generate its tonal ramp (base, +1..+N lighter, -1..-N darker) that harmonizes by construction. +- For any face used as a background, show the worst-case contrast across the foregrounds that can land on it, and name the limiting foreground. +- Guide the usable background lightness so a generated or chosen background keeps all text readable. + +** Non-Goals +- Not auto-assigning ramp steps to faces — the designer picks which step goes where. +- Not the harmonic-fill feature (generate a whole palette from a few seed colors). Separate, deferred. +- Not a new color model or palette format — OKLCH mode and the palette already exist; this extends them. + +** Scope tiers +- v1: ramp generation from a base; worst-case-contrast readout + safe-lightness guidance for background-effect faces. +- Out of scope: harmonic fill; auto-assignment of steps to faces. +- vNext: harmonic fill (palette from seeds); richer per-face foreground-set detection. + +* Design + +The two features share OKLCH (perceptually uniform, so lightness / chroma / hue move independently) and reuse colormath.js, which already has =oklch2hex=, =contrast=, =apca=, and =deltaE=. + +*Ramp generation.* A ramp is one hue at many lightnesses. Convert the base to OKLCH, hold the hue fixed, and step lightness by a fixed perceptual delta per stop — lighter for +N, darker for -N. Chroma eases toward zero at the extremes (a near-white or near-black step carries almost no color), and every step is clamped back into sRGB. The harmony is structural: the steps share a hue and sit on an even lightness ladder, so they read as one family rather than a grab-bag. The output is a row of hexes the designer can name (=blue+1=, =blue-2=) and drop into the palette. + +*Background-contrast safety.* A background tint is only as good as its worst case. For a face used as a background, define its foreground set — the colors that actually render on top of it. For a code-context face (region, hl-line, isearch, highlight, lazy-highlight), that set is the syntax token colors plus the default foreground. The floor is the minimum, over that set, of =contrast(fg, candidate-bg)=; the limiting foreground is the argmin. From the floor we derive L_max: the lightest background, at the chosen hue and chroma, whose floor still clears the target (WCAG AA/AAA, or an APCA Lc). Sweep lightness to find it — deterministic. + +The two altitudes: +- *For the designer:* pick a base swatch, see its ramp, add the steps you want. When editing a background-effect face, the contrast cell shows the worst case — "worst: keyword #67809c — 3.3 FAIL" — not a misleading single pair, and OKLCH mode marks the safe-lightness ceiling so you can't unknowingly cross it. +- *For the implementer:* =ramp(baseHex, {n, stepL, chromaEase})= → =[hex]=; =fgSetFor(face, state)= → =[hex]=; =floor(bgHex, fgSet)= → ={ratio, limitingHex}=; =lMax(hue, chroma, fgSet, target)= → =L=. All pure, all in app-core.js / colormath.js, all unit-tested. Function contracts (inputs, outputs, validation, edge cases) are pinned below. + +* v1 covered faces + +v1 computes a worst-case floor for a *closed, enumerated* set of code-overlay faces — backgrounds the live buffer renders syntax-colored text over: + +- =region= +- =hl-line= +- =highlight= +- =lazy-highlight= +- =isearch= + +That set is exhaustive for v1. Other overlay faces (=secondary-selection=, =isearch-fail=, and any future built-in) are vNext, added explicitly rather than matched by a heuristic. The deliberately closed list is what makes the contract testable: an open "any face the buffer renders as text-over-syntax" rule would hand the implementer the same invent-the-behavior problem the foreground-set decision exists to close. + +Everything outside this set keeps its existing single-pair contrast cell: + +- *Package contrast cells* stay single-pair in v1. Package faces own their foregrounds through inheritance, which needs a per-preview foreground-set model that doesn't exist yet (vNext). +- *Non-overlay UI rows* (mode-line, fringe, and the rest) stay single-pair — their foreground set isn't a syntax palette. + +A covered face whose foreground set resolves empty (no syntax assignments yet) shows the no-set readout (below), not a bogus ratio. + +* Ramp defaults and palette insertion + +*Generation defaults* (all exposed as controls, these are the starting values): +- =n= (steps each direction): default =2= → base plus +1,+2,-1,-2. Safe range 1-4. +- =stepL= (OKLCH-L delta per step): default =0.08=. Safe range 0.04-0.12. +- =chromaEase= (fraction of chroma removed at the farthest step, eased toward the extremes): default =0.5=. Range 0-1; 0 holds chroma flat, 1 fully desaturates the last step. + +*Preview and insertion:* +- The ramp previews as a row ordered darkest → lightest (=-n .. base .. +n=), the base marked. +- Each generated swatch shows a clamp badge when =oklch2hex= reports =clamped= true, so the designer sees an out-of-gamut step before adding it. +- The base is preview-only by default (it's the source swatch, already in the palette); the designer may opt to add it. +- Selected steps insert adjacent to the source swatch, in =-n .. +n= order. + +*Naming:* step names derive from the source swatch name — base =blue= → =blue+1=, =blue+2=, =blue-1=, =blue-2=. If the source swatch is unnamed, names fall back to a hex-based label the designer can edit before add. + +*Collisions — never silent:* +- *Name collision* (=blue+1= already exists): the row is flagged and the designer renames before add; no overwrite. +- *Hex collision* (the generated hex already exists under another name): flagged as a duplicate, add still allowed (two names for one hex is legal in the palette). + +* Function contracts + +All four are pure, live in app-core.js (or colormath.js for the color math), take explicit state — never read globals — and validate user input by *returning a structured result*, not throwing. Throwing is reserved for genuine programmer error (wrong argument arity/type), not for malformed user-entered values. + +- =ramp(baseHex, {n, stepL, chromaEase})= → ={steps: [{hex, clamped}], error?}=. Validates: malformed =baseHex= (not a parseable hex — can't be clamped) → ={steps: [], error: 'bad-hex'}=; =n= outside 1-4 or non-integer → clamped into range with a flag; =stepL=/=chromaEase= outside range → clamped with a flag. Holds hue, steps OKLCH-L by =stepL=, eases chroma toward the extremes, gamut-clamps every step and reports per-step =clamped=. +- =fgSetFor(face, state)= → ={set: [hex], error?}=, where =state= is explicit slices ={syntaxAssignments, palette, defaultFg, locks}=. Returns the distinct syntax-assignment hexes plus =defaultFg=, excluding locked background-only roles. A face outside the v1 covered set → ={set: [], reason: 'out-of-scope'}=. No syntax assignments → ={set: [], reason: 'empty'}=. +- =floor(bgHex, fgSet)= → ={ratio, limitingHex, limitingLabel}=. =ratio= is the minimum WCAG contrast over =fgSet= against =bgHex=; =limitingHex= is the argmin; =limitingLabel= is its role/palette/hex name. Empty =fgSet= → ={ratio: null, limitingHex: null}= (caller shows the no-set readout). +- =lMax(hue, chroma, fgSet, target)= → ={L, status}=. Binary-searches OKLCH-L (tolerance 0.001) for the lightest background at =hue=/=chroma= whose =floor= still clears =target=. =status= is =ok= (an L found), =none= (no L satisfies the target — every background fails), =all= (every L satisfies it — no ceiling needed), or =clamp= (the chroma clamps before the target is reached; returns the L at the clamp boundary). + +* Alternatives Considered + +** Ramp stepping in OKLCH lightness (chosen) +- Good, because steps are perceptually even and the hue holds, so the family looks deliberate. +- Bad, because extreme steps can leave the sRGB gamut and need clamping. +- Neutral, because it depends on colormath's OKLCH path, which already exists. + +** Ramp stepping in HSL/HSV +- Bad, because HSL/HSV aren't perceptually uniform — equal numeric steps give uneven visual steps and the hue drifts at the light/dark ends. + +** Ramp by interpolating base↔white and base↔black +- Good, because it's trivial to implement. +- Bad, because lightness and chroma drift together unpredictably and the hue can shift, so the steps don't harmonize reliably. + +** Worst-case shown as one readout + limiting fg (chosen), with an OKLCH picker mask +- Good, because one honest number (the floor) plus the name of the bottleneck is the actionable lever, and the mask shows the safe band visually. +- Neutral, because it replaces the existing single-pair contrast cell for background faces only. + +** Worst-case shown as a full per-foreground heatmap +- Bad, because it's noise — the designer needs the floor and the one color setting it, not twenty ratios. + +* Decisions + +** Work in OKLCH for ramps and tints +- State: accepted +- Context: ramps and background tints both need even, hue-stable steps; the tool already exposes OKLCH. +- Decision: We will compute ramps and the contrast floor in OKLCH via colormath.js. +- Consequences: easier — even families, reuse of existing math; harder — must gamut-clamp every generated step. + +** Ramp = fixed lightness step + chroma ease on a held hue +- State: accepted +- Context: alternatives drift hue/chroma; uniform L steps read as a ladder. +- Decision: We will step lightness by a fixed delta, ease chroma toward the extremes, and hold the hue. +- Consequences: easier — predictable families; harder — chroma easing needs tuning so mid steps don't go muddy. + +** Background safety = worst-case floor over a per-face foreground set +- State: accepted +- Context: a single fg-on-bg pair misleads; overlays carry many foregrounds. +- Decision: We will compute the floor over a face's foreground set and surface the floor + limiting foreground. +- Consequences: easier — the real constraint is visible; harder — we must define each face's foreground set. + +** v1 foreground set for code-overlay faces = syntax tokens + default fg +- State: accepted +- Context: the exact face→context mapping is fuzzy; code-overlay faces clearly carry the syntax palette. The reviewer flagged that an open ("proposed") contract forces the Phase 3 implementer to invent what counts as foreground text. +- Decision: We will scope v1 to the closed code-overlay face set (see "v1 covered faces"), and define the foreground set as the distinct hexes of the syntax-assignment colors plus the default foreground (=fg=). Duplicate hexes collapse to one entry. Locked structural colors (=bg= and any palette entry flagged as a background-only role) are excluded from the set. The limiting foreground is labeled by its syntax role name when one exists, else its palette name, else its hex. +- Consequences: easier — a pinned, enumerable contract the implementer and tests can rely on; harder — UI/package overlays need a later per-preview foreground-set model (vNext). + +** Contrast target for the floor = WCAG AA default, AAA optional, APCA diagnostic +- State: accepted +- Context: AA (4.5) is the floor most reach for; AAA (7) is the stricter option; APCA models text-on-color better than WCAG but needs a chosen Lc and signed/absolute handling. The reviewer recommended a WCAG-only v1 so =floor=, =lMax=, labels, masks, and tests all key off one model. +- Decision: We will drive v1 PASS/FAIL and =L_max= off WCAG contrast: default target =AA= (4.5), with =AAA= (7) selectable. APCA Lc is shown as a displayed diagnostic only and does not drive PASS/FAIL or the safe band in v1; an APCA-driven safety mode is vNext. +- Consequences: easier — one metric for every safety surface and stable tests; harder — APCA's better text-on-color model is deferred, so a color that reads fine under APCA may still be flagged by WCAG. +- Owner note: this and the foreground-set decision were Craig-owned open decisions; resolved here per the reviewer's recommendation to keep the spec converging. Craig can override either before implementation starts. + +* Implementation phases + +** Phase 1 — Ramp generator (pure) +=ramp(baseHex, opts)= in app-core.js with Normal/Boundary/Error tests (mid base, near-white/near-black base, out-of-gamut request). Leaves the tree green; no UI yet. + +** Phase 2 — Ramp UI in the palette +A base swatch → preview the ramp → add chosen steps as named palette entries. Reuses the palette panel and the OKLCH picker. + +** Phase 3 — Foreground-set + floor (pure) +=fgSetFor=, =floor=, =lMax= in app-core.js with tests, including the keyword-blue worst case as a fixture. + +** Phase 4 — Worst-case readout +For the v1 covered faces, the contrast cell shows the floor + the limiting foreground name instead of a single pair. Pinned readout shape, so =#contrasttest= asserts fields not punctuation: =worst: <limitingLabel> <limitingHex> — <ratio> <PASS|FAIL>= (example: =worst: keyword #67809c — 3.3 FAIL=). The no-foreground-set readout is exactly =no fg set=. Add a hash-gate (#contrasttest-style) pinning floor-over-set and the no-set string. + +** Phase 5 — Safe-lightness in OKLCH mode +When a v1 covered face is open in the picker, mark L_max on the lightness slider and shade the unsafe band above it. v1 renders this as a *single L_max marker plus a one-band shade* (safe below, unsafe above) computed once per =(hue, chroma, fgSet, target)= via =lMax= — not a full per-pixel foreground-set contrast mask over the plane. The per-pixel AA/AAA plane mask stays single-foreground; extending it to a full foreground-set sweep is vNext if profiling ever shows the marker is insufficient. + +* Acceptance criteria +- [ ] From a base hex, the tool produces N lighter + N darker steps, perceptually even, all in sRGB gamut. +- [ ] Generated steps can be added to the palette as named entries. +- [ ] A background-effect face shows the worst-case contrast and names the limiting foreground. +- [ ] OKLCH mode marks the maximum safe lightness for the chosen hue/chroma given the foreground set + target. +- [ ] Unit tests cover ramp generation, the floor, and L_max; a browser gate pins the worst-case readout. +- [ ] =scripts/theme-studio/README.md= documents ramp controls and defaults, the worst-case-floor / limiting-foreground meaning, the v1 covered faces, and that WCAG drives PASS/FAIL with APCA shown as a diagnostic. + +* Readiness dimensions +- Data model & ownership: ramp steps and tints are user-authored palette entries the designer adds; floor and L_max are computed live, not stored. Nothing new persists. +- Errors, empty states & failure: an out-of-gamut step clamps and flags (a real but unrepresentable color); a malformed base hex can't be clamped, so =ramp= returns a structured =bad-hex= error and the row produces nothing rather than a garbage swatch; a covered face with no foreground set shows =no fg set= rather than a bogus ratio. All four core functions return structured results for bad user input instead of throwing. No silent data loss. +- Security & privacy: N/A — local color math, no credentials or sensitive data. +- Observability: the worst-case readout *is* the observability — the designer sees the floor and the bottleneck color directly. +- Performance & scale: N/A meaningfully — tens of colors, instant; no long-running ops. +- Reuse & lost opportunities: reuse colormath.js (=oklch2hex=/=contrast=/=apca=/=deltaE=), the OKLCH picker, and the AA/AAA mask. Don't reimplement color math. +- Architecture fit & weak points: pure logic in app-core.js (tested, importable like the Stage-7 split); UI in app.js; integration points are the palette panel, the OKLCH picker, and the contrast cells of the v1 covered faces only (region, hl-line, highlight, lazy-highlight, isearch). Package and non-overlay UI cells are out of v1 scope and keep their single-pair behavior. Weak point: defining each face's foreground set — mitigated by the closed v1 covered-face set and explicit-state =fgSetFor=. +- Config surface: =n= (steps each direction, default 2, range 1-4), =stepL= (OKLCH-L delta, default 0.08, range 0.04-0.12), =chromaEase= (default 0.5, range 0-1), and the contrast target (default WCAG AA 4.5, AAA 7 selectable). Defaults and safe ranges pinned in "Ramp defaults and palette insertion." +- Documentation plan: the color-harmony explainer (=docs/design/theme-studio-color-harmony.org=, already a task) carries the method; this spec carries the build; =scripts/theme-studio/README.md= is the operational doc and gets the ramp controls, contrast-target semantics, covered faces, and worst-case-floor meaning (an acceptance-criteria item). +- Dev tooling: =make theme-studio-test= covers it via new node tests + a browser gate; no new tooling. +- Rollout, compatibility & rollback: additive — no change to the theme.json format or existing themes. The worst-case readout replaces a misleading single-pair number for background faces (a strict improvement). No migration, nothing to roll back. +- External APIs & deps: none — pure color math, no external schema. + +* Risks, Rabbit Holes, and Drawbacks +- Chroma easing at the ramp extremes can go muddy or out-of-gamut — dodge by clamping and previewing every step before it's added. +- The foreground-set definition is the fuzzy core. v1 limits it to code-context faces (syntax tokens + default fg); an over-broad set would over-constrain backgrounds that those foregrounds never actually touch. +- A very dark foreground (sterling's keyword blue) can collapse the safe band to near-black. That's a true finding, not a tool bug — the readout should make the designer fix the foreground's lightness, not silently absorb it. + +* Review dispositions + +Only modified and rejected recommendations are listed; everything else from the Codex review was accepted as written and is woven into the body above. + +- *Modified — v1 covered-face set closed, not open-ended.* The review's suggested list ended with "and any other named built-in face the live buffer renders as text-over-syntax." Adopted the five named faces (region, hl-line, highlight, lazy-highlight, isearch) but dropped the open-ended trailing clause: an open set hands the implementer the same invent-the-behavior problem the foreground-set decision exists to close. The v1 set is exhaustive; new faces are added explicitly in vNext. +- *Modified — invalid-input handling distinguishes user error from programmer error.* The review said "prefer pure functions returning structured errors rather than throwing." Accepted for user-entered values (malformed hex, out-of-range knobs) which return structured results; kept throwing for genuine programmer error (wrong argument arity/type), matching colormath.js's existing idiom rather than blanket no-throw. +- *Deferred (not rejected) — implementation-task drop-in block.* The review supplied six ready TODOs. Per the spec-response workflow, implementation tasks are created only after the author confirms the spec is Ready (Phase 6), so they are not yet logged to todo.org. They land on the Ready go. + +* Review and iteration history +** 2026-06-09 Tue @ 17:18:54 -0500 — Craig — author +- What: initial draft. +- Why: ramp generation and background-contrast safety are coupled, design-uncertain, and worth solving on paper before code. +- Artifacts: this spec; the color-harmony explainer task in todo.org. +** 2026-06-09 Tue @ 17:34:17 -0500 — Codex — reviewer +- What changed or was recommended: reviewed the spec against the implementation context and marked it =Not ready=. Recommended pinning the v1 contrast target, accepting the foreground-set contract, narrowing the exact covered background faces, and specifying ramp defaults / palette insertion behavior. +- Why: the current code architecture supports the feature, but the open metric and foreground-scope decisions would make implementation invent user-visible behavior and produce unstable tests. +- Artifacts: theme-studio-palette-ramps-spec-review.org (consumed and deleted on response). +** 2026-06-09 Tue @ 17:53:02 -0500 — Claude Code (dotemacs) — responder +- What changed or was recommended: dispositioned every Codex finding. Resolved both open Decisions to accepted — contrast target = WCAG AA default (AAA selectable, APCA diagnostic only), v1 foreground set = distinct syntax hexes + default fg with locked roles excluded. Added "v1 covered faces" (closed five-face set), "Ramp defaults and palette insertion" (n/stepL/chromaEase defaults, naming, collision, clamp display), and "Function contracts" (explicit-state, structured-error signatures for ramp/fgSetFor/floor/lMax with edge cases). Pinned the worst-case readout string and no-set message, clarified Phase 5 as a single L_max marker + band shade, added a README acceptance-criteria item, and corrected the architecture/config readiness lines. Two findings modified, one deferred — see Review dispositions. +- Why: the review's blockers were product-contract gaps (open metric, proposed foreground set, conflicting scope, underspecified ramp write) that would have made two correct implementations diverge. Pinning them converges the spec to implementation-ready. +- Artifacts: review file deleted; Review dispositions section; this entry. Two Craig-owned open decisions resolved per reviewer recommendation, override available before implementation. |
