TECH — id-119: Design-system config-readiness
TECH — id-119: Design-system config-readiness
Section titled “TECH — id-119: Design-system config-readiness”- Task:
ID-119. Predecessor:RESEARCH.md(the Warm Meridian audit, verbatim). - Process: light specs (no PRODUCT) — this TECH is self-contained: goals, invariants, per-finding design, deterministic evidence, sequencing, and the decisions reserved for Liam.
- Author basis: deterministic extraction of
app/globals.css(script-verified counts, not estimates).
1. Goal & non-goals
Section titled “1. Goal & non-goals”Goal: make a reskin / rebrand a configuration operation, not a literal-hunt. Concretely: collapse the hand-maintained colour-literal wall onto a small primitive layer, lift application-domain tokens out of the core design system, adopt one token-naming convention, and route the hardcoded product-name string through the existing branding config.
Non-goals (deferred to /design-sync): choosing brand direction A/B, the typography identity
axis (audit finding 4), the logo/wordmark, and any deliberate recolour. ID-119 changes the
structure of the token system and must reproduce current colours within tolerance — it is a
refactor, not a redesign. The brand-colour injection path
(default.json → Zod → buildBrandStyleProps, auto-derived foreground/dark/ring) is exemplary and
is left untouched.
2. Behaviour invariants (verifiable)
Section titled “2. Behaviour invariants (verifiable)”- B-1 Visual parity. After F1+F2+F3, every rendered colour is identical to
mainexcept for an explicitly-approved snap list (see §3.5). Verified by computed-style diff + visual snapshot. - B-2 Zero utility churn (default path). Generated Tailwind utility class names
(
bg-bid-won,text-governance-approved, …) are unchanged; component files compile without edit unless an opt-in rename is approved (F3 Option B). - B-3 Single source per colour. No raw status/domain OKLCH literal is typed more than once; tuning a colour is a one-primitive edit that ripples to light + dark.
- B-4 Core ⊅ domain. The core token file contains no product-taxonomy token
(
bid/governance/entity/domain/relevance/template/phase/tag/quality/…). - B-5 Product name from config. No user-facing surface, AI prompt, or export hardcodes the
product-name string; all read it from
BRANDING/CLIENT_CONFIG. The value stays “Knowledge Hub” (no rename in this Task).
3. F1 — functional colour ramps
Section titled “3. F1 — functional colour ramps”3.1 Evidence (script-verified, app/globals.css)
Section titled “3.1 Evidence (script-verified, app/globals.css)”| Bucket | Literal defs | Target | Fidelity |
|---|---|---|---|
Total --color-* / --domain-* literal defs (light+dark) | 378 | — | — |
| Categorical badge defs (entity/domain/relevance), 16 distinct hues / 21 cats | 92 (24%) | 1 formula × hue-var list | exact |
| Neutral defs (C < 0.03) | 61 | reference existing --stone-* ramp | exact |
| Chromatic status defs | 225 | ~6 status ramps × role-steps | exact for duplicates; bounded snaps |
Bespoke shadcn defaults (--color-star, --color-priority-*, non-integer hue) | 4 | keep as literals | n/a |
Top duplications eliminated: success-green oklch(0.55 0.15 160) ×11, green-tint
oklch(0.93 0.04 160) ×12, amber oklch(0.6 0.14 70) ×10, red-tint oklch(0.95 0.03 20) ×9 —
each currently re-typed across freshness / confidence / governance / bid / quality / verified /
tag / template, then again in .dark.
3.2 Primitive layer (new)
Section titled “3.2 Primitive layer (new)”Add, above the semantic block in :root:
/* Functional status ramps — role steps reproduce existing literals */--green-solid: oklch(0.55 0.15 160);--green-solid-deep: oklch(0.5 0.15 160);--green-border: oklch(0.8 0.06 160);--green-tint: oklch(0.93 0.04 160);--amber-solid: oklch(0.6 0.14 70);--amber-border: oklch(0.82 0.06 70);--amber-tint: oklch(0.97 0.02 70);--amber-tint-strong: oklch(0.93 0.04 70);--red-solid: oklch(0.55 0.2 20);--red-solid-deep: oklch(0.5 0.2 20);--red-border: oklch(0.82 0.06 20);--red-tint: oklch(0.95 0.03 20);--blue-solid: oklch(0.55 0.12 240);--blue-border: oklch(0.8 0.06 240);--blue-tint: oklch(0.95 0.02 240);--violet-solid: oklch(0.55 0.15 290);--violet-border: oklch(0.8 0.06 290);--violet-tint: oklch(0.95 0.02 290);--teal-solid: oklch(0.55 0.15 195);--teal-border: oklch(0.8 0.06 195);--teal-tint: oklch(0.93 0.04 195);.dark redefines the same ramp names with the dark values (e.g. --green-solid: oklch(0.7 0.13 160),
--green-tint: oklch(0.25 0.06 160)). Because status tokens reference the ramp, the per-token .dark
block (currently ~189 lines) collapses to the ramp redefinitions only — dark mode becomes “swap the
ramps” (B-3).
3.3 Categorical badge formula (entity / domain / relevance)
Section titled “3.3 Categorical badge formula (entity / domain / relevance)”These badges already follow a strict formula with only hue varying. Replace the per-category
literals with a hue variable consumed by oklch():
--hue-entity-person: 350;--hue-entity-organisation: 240; /* … 21 per-category vars, 16 distinct values … */--color-entity-person-bg: oklch(0.93 0.04 var(--hue-entity-person));--color-entity-person-text: oklch(0.35 0.12 var(--hue-entity-person));/* .dark: oklch(0.25 0.06 var(--hue-…)) / oklch(0.8 0.1 var(--hue-…)) */oklch() accepts custom-property components in all current evergreen targets; the executor must
add a smoke check (computed style resolves) + confirm Tailwind v4 @theme inline passes the value
through. Reskinning a category is then a one-number edit. (Token names preserved → B-2.)
3.4 @theme inline implication
Section titled “3.4 @theme inline implication”Ramp primitives are raw values, not registered as utilities. Status/domain tokens keep their existing
@theme inline registrations unchanged, so bg-*/text-* utilities are byte-identical (B-2).
3.5 Fidelity & the snap decisions (for Liam)
Section titled “3.5 Fidelity & the snap decisions (for Liam)”The 92 categorical + 61 neutral + all exact-duplicate chromatic defs reproduce exactly. The only fidelity decisions are near-neighbour snaps inside the amber family (it spans hues 45/55/65/70/85 and several close L/C pairs). Rule: the executor produces the complete snap list (old → new with ΔL/ΔC/ΔH); no snap exceeding ΔL 0.03 / ΔC 0.03 / ΔH 5 is applied without an explicit entry on the decision list. Liam approves the snap list at the per-subtask gate. Everything else is exact.
4. F2 — split core vs application-domain tokens
Section titled “4. F2 — split core vs application-domain tokens”- Domain (move out):
--color-bid-*,--color-governance-*,--domain-*,--color-entity-*,--color-relevance-*,--color-template-*,--color-phase-*,--color-tag-*,--color-quality-*,--color-freshness-*,--color-confidence-*,--color-priority-tier-*,--color-source-si-*,--color-draft-badge-*,--color-verified*,--hue-entity-*. - Core (keep):
--stone-*, shadcn semantic (--background…--ring),--success/--destructive, the F1 ramps,--chart-*,--radius*,--font-*,--spacing-*, the a11y vars,--shadow-*. - Ambiguous → reasoned call:
--color-status-*,--color-priority-*,--color-star,--color-review-*,--color-progress*,--color-highlight*,--color-action-publish→ these are generic-enough UI status; keep in core. (Rationale recorded in the move subtask.) - Target: new
app/styles/domain-tokens.cssholding the domain tokens’:root+.darkdefinitions and their@theme inlineregistrations,@imported fromapp/globals.css. - Critical wiring risk: Tailwind v4 must still see the
@themeblock. The executor verifies@importplacement (relative to@import 'tailwindcss'and the@themesource order) actually generates the domain utilities — a wrong order silently drops every domain utility (gate: grep the built CSS for a sample utility). Depends on F1 landing first (domain file references ramps).
5. F3 — normalise token naming
Section titled “5. F3 — normalise token naming”Convention (the deliverable): shadcn semantic tokens stay bare (--background, --primary);
the new ramps are scoped --<family>-<role>; application status/domain tokens retain the established
--color-* / --domain-* public contract.
Why not mass-rename the --color-* contract: it is consumed two ways —
| Consumer | Count | Renaming the CSS-var definition… |
|---|---|---|
Tailwind utility classes (bg-bid-won) in components | 87 files | …does not change them if the @theme RHS is updated (utility name is decoupled). |
Direct var(--color-*) / var(--domain-*) in inline styles | 18 files / 54 refs | …does break these regardless — they reference the literal var name. |
Options for Liam:
- A — convention-only (recommended, ~0 churn): publish the convention, apply it to the new
ramps + F2 domain file; leave the
--color-*status contract intact. Delivers the audit’s intent (one coherent convention going forward) at no component churn. - B-lite — drop redundant
--color-on definitions, keep utility names: touches the 18 inline-var()files (54 refs). Moderate, cosmetic. - B — full rename incl. utilities: 87 files. High churn; better folded into the actual rebrand.
Decision (Liam, this session): B-lite — adopt the convention and drop the redundant
--color- prefix on status/domain definitions (keeping utility class names), touching the 18
inline-var() files (54 refs). The full utility-class rename (Option B, ~87 files) is tracked as
bl-349, to bundle with the rebrand visual pass. F3 runs after F1+F2.
6. Strings — product-name consolidation
Section titled “6. Strings — product-name consolidation”Evidence: “Knowledge Hub” in 33 source files — app/ 3, components/ 7, lib/ 40 (incl. AI
skills 6, procurement exports 3, plus the config source-of-truth), scripts/ 10.
- Keep (source of truth):
lib/branding/clients/default.json,lib/client-config.ts. - UI (server): import
BRANDING.productName/productShortNamedirectly. - UI (client components): verify
BRANDINGis client-safe (it importsclientEnv+ awindow-guardedfsrequire) — if not directly importable client-side, thread via a branding context/provider (checkapp/layout.tsxfor an existing one before adding). - AI skill prompts (
lib/ai/skills/*.md, 6): introduce a{PRODUCT_NAME}placeholder resolved at prompt-assembly, mirroring the existing{CLIENT_PRODUCT_NAME}/buildDisambiguationBlock+.replaceAllmechanism inlib/ai/classify.ts. Risk: alters prompt text → re-run classification eval to confirm no regression. - Exports (
lib/procurement/procurement-export-{xlsx,docx}.ts, 3): read fromBRANDING. Risk: these may have golden-file / parity tests (__tests__/fixtures/qa-docx-parity/) — update fixtures intentionally, do not let output drift silently. scripts/(10) + comments: leave (tooling / prose), unless trivially a user-facing CLI string.
Independent of F1–F3 → can run in parallel (subject to §7 isolation note).
7. Sequencing, risks, execution
Section titled “7. Sequencing, risks, execution”- Order: F1a (additive primitives) → F1b (rewire + collapse
.dark+ snap list) → F2 (extract domain file) → F3 (convention, Option A) ; Strings parallel ; wrap (full regression + snapshot + token guidance doc) last. - Worktree isolation is currently unreliable (native Agent
isolation:"worktree"falls back to the shared checkout — observed S383). The F1→F3 chain is inherently serial anyway; serialise the CSS-touching executors; Strings may interleave only if isolation is confirmed, else serialise too. - GitNexus / ast-dataflow: CSS custom properties are not graph-indexed — use literal greps for the var sweeps; use GitNexus impact only for the TS-side string consolidation.
- Per-subtask gate: every CSS subtask ends with
bun build+ a computed-style/visual diff vsmain; the Checker verifies B-1..B-5 and the snap list.
8. Open decisions for Liam (gate)
Section titled “8. Open decisions for Liam (gate)”- F1 snap list — approve the bounded amber near-neighbour snaps (produced in F1b). (Recommend: approve within ΔL/ΔC 0.03, ΔH 5 — decided at F1b time.)
- F3 scope — ✅ DECIDED: B-lite (18 inline-
var()files). Full utility rename (Option B, 87 files) →bl-349. - Strings depth — ✅ DECIDED: all three (UI + AI-prompt + exports), with the eval + golden-fixture gates.