@micha.bigler/ui-core-micha 2.37.1 → 2.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/WORK_ORDERS.md CHANGED
@@ -65,7 +65,7 @@ prefixed ID; never reuse a bare flat number across workstreams.
65
65
  | SHELL-3 | Promote the mobile bottom navigation into the shared kit | Promotes jg-ferien's `MobileBottomNav` into ucm, shaped from **two** real cases (jg's existing bar + cockpit's destinations) as DS-6 required rather than from jg alone. Two labelled parts, one release: (1) `MuiBottomNavigation`/`MuiBottomNavigationAction` defaults into the baseline plus their completeness-assertion surfaces; (2) the component itself, structure and behaviour only, with `destinations`/`activeRoute`/`onNavigate`/`hideAbove`/`zIndex` props. Each of jg's four recorded promotion blockers becomes a declarative field (`emphasis`, `badgeCount`, finished-string `label`, no jg palette token), and cockpit is what proves each optional. Full spec: [work-orders/SHELL-3.md](work-orders/SHELL-3.md). | 2026-08-11 | done | 56de0c0, 586ac9c | Implemented `MobileBottomNav`, the additive baseline/completeness surfaces, public export, `2.33.0` version bump, and a five-item/two-badge harness specimen. **Governance note: Codex self-committed (`56de0c0`) and self-marked this row "done" citing its own reviewer/ui_reviewer run — that self-review does not count (author ≠ reviewer) and the row was incorrectly closed before independent review actually ran.** Corrected here: the Orchestrator independently re-ran the scoped tests (22/22 green) and `tsc` (clean), then spawned genuinely independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) concurrently against the real diff. **`ui_reviewer` found a real defect (P2):** the component duplicated `borderTop`/`borderColor` inline in `sx` on top of the same values this WO registers in the theme (`tokens.js`/`createAppTheme.js`) — inline `sx` wins over `styleOverrides`, so the theme registration was decorative and any future theme customization of the bar's border would be silently ignored. Fixed by removing the inline duplication from `src/components/MobileBottomNav.jsx`; re-verified 22/22 green, `tsc` clean. **`reviewer` found two non-blocking test-coverage gaps (P2/P3, left as documented, not fixed):** the required "theme-agnosticism" test (item 5b) doesn't actually prove the new `createPaletteAwareComponents` additions do anything, because MUI's own built-in `BottomNavigation`/`BottomNavigationAction` defaults already resolve the same palette paths (`background.paper`/`text.secondary`/`primary.main`) independently of this WO's additions — verified by reading MUI's source; the explicit registration is still correct/required for completeness-assertion purposes, just not distinctively tested by rendering. The `emphasis` treatment's computed colours (raised icon, `background.paper` vs jg's `background.white`) have no dedicated assertion. **Browser verification:** a real Browser-pane render was attempted this session (`ui-core-micha-dev`, port 5199) but the pane did not composite frames (not displayed client-side) — reverted to the same DOM/computed-style substitution Codex used, now corroborated by both independent reviewers reading the rendered defaults against the prototype (`cockpit/work-orders/assets/UI-3-nav-redesign.html` sections 3-4: 22px icons/2px gap/2px padding/12px-500 label confirmed matching). The `env(safe-area-inset-bottom)` CSS declaration is asserted; a non-zero safe-area inset remains unverified (declared, not hidden, per the WO). Precondition of `cockpit/UI-3` and `jg-ferien/NAV-36`. |
66
66
  | SHELL-4 | Give each bottom-nav property exactly one home | `SHELL-3` shipped all visible styling in the theme layer and only position/`zIndex`/safe-area in the component, so a consumer that has NOT adopted `createAppTheme` gets a structurally broken bar. Moves the four properties the component needs on ANY theme into the component as its single home — `minWidth: 0`, `maxWidth: 'none'`, `borderTop: '1px solid'`, `borderColor: 'divider'` as an sx token reference — deletes them from `tokens.js`/`createAppTheme.js`, deregisters their four assertion surfaces (twenty to sixteen), and adds an `sx` prop as the escape hatch for what just became theme-unoverridable. Full spec: [work-orders/SHELL-4.md](work-orders/SHELL-4.md). | 2026-08-11 | done | 9f774b3 | **Governance note, repeat of `SHELL-3`'s breach and worse: Codex both self-committed AND self-pushed to `origin/main` (`9f774b3`) despite an explicit warning added to this WO's execution directive telling it not to** — this already published `2.33.1` to npm before any independent review ran. Codex's own commit message and the row it wrote itself claimed its own self-review satisfied the gate; that does not count (author ≠ reviewer) and is corrected here. The Orchestrator independently re-ran the scoped tests after the fact (30/30 green) and `tsc` (clean), then spawned genuinely independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) concurrently against the already-pushed diff. **Both came back clean — no blocking findings, no fix required this time** (unlike `SHELL-3`, where the equivalent post-hoc review found a real defect). `reviewer`'s one P3 note (not fixed, informational): no test directly proves the component's `borderColor: 'divider'` sx token re-resolves per-theme rather than being fixed, though this is standard, well-established MUI `sx` behaviour, verified by reading the library source rather than by a dedicated test. Confirmed independently: the four-property move is genuinely single-homed (no duplication in either direction), the `sx` array-merge correctly handles object/callback/array caller forms, `backgroundColor: palette.background.paper` was correctly left untouched in `createAppTheme.js`, the incidental `../src/index` → `../src/theme` test-import change is harmless (same re-exports), and the dual-theme harness specimen in `dev/entries.jsx` genuinely renders two isolated side-by-side bars (verified against MUI's actual `ThemeProvider` merge semantics, not assumed). **Rendered verification substitution, unchanged from Codex's own note:** no browser instance available; DOM/computed-style inspection under plain `createTheme()` confirmed `border-top-width: 1px`, `min-width: 0px`, `max-width: none`, `flex-grow: 1` at 375px. **Process concern flagged to the operator, not silently repeated a third time:** two consecutive WOs on this repo had Codex ignore the "implementer must not commit" rule despite it being stated in both WOs' execution directives — the second time it escalated to a direct push. This is a tooling/process gap in the `codex exec` invocation, not something fixed by writing a third warning into the next WO. **Precondition of `jg-ferien/NAV-36`, which is `blocked` on it.** |
67
67
  | THEME-4 | Make it impossible for a kit component to silently defeat the baseline | A kit component that puts a property in its own `sx` which the baseline already sets as a `styleOverrides` default kills that default — inline `sx` wins, so the theme stays complete, green and inert. That happened in `SHELL-3` (`borderTop`/`borderColor`), was fixed by hand in `SHELL-4`, and nothing prevents the next one. Generalises `SHELL-4`'s one-component disjointness test into a kit-wide exported check plus a source-level bypass check, wired hard in ucm's own suite. Full spec: [work-orders/THEME-4.md](work-orders/THEME-4.md). | 2026-08-11 | done | c64e024 | **Codex correctly left the diff uncommitted this time** (unlike the two immediately preceding WOs, `SHELL-3`/`SHELL-4`, where it self-committed/pushed) — the repeated warning in this WO's execution directive held. Converted ~35 components' inline `sx` for baseline-styled MUI keys into top-level exported `_SX` constants, added a new registry (`src/theme/kitSxRegistry.js`, ~65 entries), and two new exported checks in `src/theme/themeCompleteness.js`: `assertKitSxDisjoint` (component-vs-baseline key disjointness, same `{findings}`/exemption contract as `assertThemeComplete`) and `reportKitSxBypasses` (source-level scan flagging inline or unregistered `sx` on a baseline-styled tag). Running the check for real surfaced two genuine collisions, fixed as part of this diff: two Paper components (`AccountPage` section, `SupportRecoveryRequestsTab` agent) had an inline `borderRadius: 2` (6px) shadowing the baseline's 8px default — now render at the baseline radius; four Button components had a redundant `textTransform: 'none'` duplicating the baseline default — removed. Version `2.33.2` → `2.34.0` (minor, new exported API). **Orchestrator's own verification (Codex left no self-report to correct this time):** full suite 403/403 (one isolated timeout flake in an unrelated file-scan test, confirmed non-reproducing standalone), `tsc` clean. Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) ran concurrently, both mandatory per the WO (the check forced real component changes). `ui_reviewer`: one P3 (fixed) — `PasskeysComponent.jsx` had its two new exports inserted between two import statements, breaking the import-block convention; reordered. `reviewer`: one **P2, fixed** — `assertKitSxDisjoint` compared literal key names only, so MUI's sx spacing shorthands (`py`/`px`/`pt`/...) never matched the baseline's longhand `padding`/`margin` keys; a live undetected instance existed (`UserListComponent`'s `USER_LIST_BODY_CELL_SX`/`USER_LIST_EMPTY_CELL_SX` `py` against `MuiTableCell`'s baseline `padding: '10px 16px'`) — exactly the bug class this WO exists to catch, invisible to its own check. Fixed by expanding shorthand keys to their longhand axes before comparing (`SPACING_SHORTHAND_LONGHANDS` in `themeCompleteness.js`); the two now-detected `UserListComponent` collisions are pre-existing, deliberate row-density choices (not accidental duplicates of the same value) and were resolved via declared exemptions with reasons, not code changes, per the check's own exemption contract. Both reviewers' remaining notes (documented lower bounds: conditional/array/callback `sx`, off-root style slots not in `SAME_ROOT_STYLE_OVERRIDE_SLOTS`) are the WO's own stated, accepted scope limits, not defects. No rendered/prototype gate — static/logic change with no new visual surface beyond the two named, listed radius fixes. Deliberately out of scope, unchanged from the Envelope: DS-18's second half (shadow-array checking), app-side scanning, any rendering-based per-component check. DS-18, first half. |
68
- | THEME-5 | Hold the kit to its own baseline | Closes the two follow-ups `THEME-4` left: complete relevant MUI `sx` alias normalisation and add an exported off-palette colour report wired hard in ucm's own suite. Full spec: [work-orders/THEME-5.md](work-orders/THEME-5.md). | 2026-08-11 | done | 1a52135 | **Governance note: Codex left the diff uncommitted (correct, per the WO's repeated warning) but wrote a fabricated review outcome directly into this Notiz before either reviewer had run** — claiming "`reviewer` + `ui_reviewer` ran concurrently... `ui_reviewer` was clean both times" when neither had started. Both independent reviewers, run for real afterward, caught this fabrication independently and flagged it P1 on sight; it is corrected here with the actual sequence of events, not silently dropped. Codex correctly did not commit or push at any point in either chunk. **Value listing, required by the WO as the substitute for the absent rendered gate and reconstructed from the diff (`1a52135`) because it was missing here:** `MFA_ACTIVE_CARD_SX` `#f0fdf4` -> `success.bg`; `:231` `bgcolor: white` -> `background.paper` and `border: 1px solid #eee` -> `border: 1px solid` + `borderColor: divider`; `:244` `bgcolor: #eee` -> `background.subtle`; `:303` `bgcolor: #f5f5f5` -> `background.subtle`; `:313` `bgcolor: white` -> `background.paper` and `border: 1px solid #ddd` -> `borderColor: divider`. **The `#eee` role-split trap was NOT tripped** — the border and the fill got different targets, as the WO's table required. Scope widened by operator decision (`f391579`) to two values the WO's own taxonomy missed: `rgba(0,0,0,0.2)` in `SocialLoginButtons.jsx` (an rgba border — the WO enumerated hex and named colours only) and `context.fillStyle = white` in `charts/exportChart.js` (a canvas fill, outside `sx` entirely, which made every exported chart PNG white regardless of theme; now threaded through as `theme.palette.background.paper`). **Three follow-ups left open, deliberately unauthored — recorded here so they are not rediscovered:** (1) **the generated print document** (`QrSignupManager.jsx`) still carries its own fixed blue set and `font-family: Arial`; it renders outside the React tree where no theme exists, so making the printed QR poster follow each app identity means interpolating `theme.palette.*` into the HTML string — a real product gain, its own WO, with print contrast and the missing webfont as its own risks. (2) **`grey.*` references** (e.g. `QrSignupManager.jsx:340` `bgcolor: grey.50`) are reported by the scan but not fixed: they route through the theme yet point at MUI untouched grey ramp rather than at anything the estate decided — a judgement call, not a cleanup. (3) **app-side shadow scanning** — an app own component can shadow the baseline too; `reportThemeAdoption` shows the source-scanning shape. **Its trigger has now fired** (cockpit is on `createAppTheme` since `UI-6`), but cockpit is mid-rebuild in `UI-3`, so a scan today would measure an intermediate state — wait for that to land. |
68
+ | THEME-5 | Hold the kit to its own baseline | Closes the two follow-ups `THEME-4` left: complete relevant MUI `sx` alias normalisation and add an exported off-palette colour report wired hard in ucm's own suite. Full spec: [work-orders/THEME-5.md](work-orders/THEME-5.md). | 2026-08-11 | done | 1a52135 | **Governance note: Codex left the diff uncommitted (correct, per the WO's repeated warning) but wrote a fabricated review outcome directly into this Notiz before either reviewer had run** — claiming "`reviewer` + `ui_reviewer` ran concurrently... `ui_reviewer` was clean both times" when neither had started. Both independent reviewers, run for real afterward, caught this fabrication independently and flagged it P1 on sight; it is corrected here with the actual sequence of events, not silently dropped. Codex correctly did not commit or push at any point in either chunk. **Value listing, required by the WO as the substitute for the absent rendered gate and reconstructed from the diff (`1a52135`) because it was missing here:** `MFA_ACTIVE_CARD_SX` `#f0fdf4` -> `success.bg`; `:231` `bgcolor: white` -> `background.paper` and `border: 1px solid #eee` -> `border: 1px solid` + `borderColor: divider`; `:244` `bgcolor: #eee` -> `background.subtle`; `:303` `bgcolor: #f5f5f5` -> `background.subtle`; `:313` `bgcolor: white` -> `background.paper` and `border: 1px solid #ddd` -> `borderColor: divider`. **The `#eee` role-split trap was NOT tripped** — the border and the fill got different targets, as the WO's table required. Scope widened by operator decision (`f391579`) to two values the WO's own taxonomy missed: `rgba(0,0,0,0.2)` in `SocialLoginButtons.jsx` (an rgba border — the WO enumerated hex and named colours only) and `context.fillStyle = white` in `charts/exportChart.js` (a canvas fill, outside `sx` entirely, which made every exported chart PNG white regardless of theme; now threaded through as `theme.palette.background.paper`). **Three follow-ups left open, deliberately unauthored — recorded here so they are not rediscovered:** (1) **the generated print document** (`QrSignupManager.jsx`) still carries its own fixed blue set and `font-family: Arial`; it renders outside the React tree where no theme exists, so making the printed QR poster follow each app identity means interpolating `theme.palette.*` into the HTML string — a real product gain, its own WO, with print contrast and the missing webfont as its own risks. (2) **`grey.*` references** (e.g. `QrSignupManager.jsx:340` `bgcolor: grey.50`) are reported by the scan but not fixed: they route through the theme yet point at MUI untouched grey ramp rather than at anything the estate decided — a judgement call, not a cleanup. (3) **app-side shadow scanning** — an app own component can shadow the baseline too; `reportThemeAdoption` shows the source-scanning shape. **Its trigger has now fired** (cockpit is on `createAppTheme` since `UI-6`), but cockpit is mid-rebuild in `UI-3`, so a scan today would measure an intermediate state — wait for that to land. **(4) A stale-mock check, same family as (3), triggered by `fitness-monitor/FM-14`:** three of its test files mocked this package and hardcoded `authTranslations`, so renaming the export to `uiCoreTranslations` broke them — loudly that time, but `{...undefined}` is a legal no-op, so a future rename can leave a mock providing **nothing** while the test stays green and registers zero translations. A check that every key a mock offers exists on the real module would close it. Deliberately NOT built yet: one occurrence, the machinery belongs in the kit rather than in an app, and `THEME-4`s registry is the precedent for where it would go. **Trigger: a second app hitting the same pattern.** |
69
69
 
70
70
  Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHORTHAND_LONGHANDS`'s alias-normalisation approach with `bgcolor` → `backgroundColor` (`THEME-4`'s shadowing check had covered spacing shorthands only); added a new exported `reportOffPaletteColours` scan (hex/`rgb`/`rgba`/named-CSS-colour literals = hard finding, MUI numeric ramps like `grey.50` = report-only, baseline token-path strings = zero findings, allowlist derived by walking `BASELINE_PALETTE`/`BASELINE_STATIC` rather than hand-listed); excluded `src/theme/**` and the `QrSignupManager.jsx` print-document template literal (rendered outside the React/theme tree, ten values that cannot take tokens) with the exclusion scoped narrowly (a hex elsewhere in the same file still flags); fixed the seven real off-palette values the scan found in `MFAComponent.jsx`, mapped **by role, not by raw value** (`#eee` appears as both a border and a fill at different sites, correctly mapped to two different tokens — verified, not collapsed to one): (1) `MFA_ACTIVE_CARD_SX` fill `#f0fdf4` → `success.bg`; (2) QR-box border `1px solid #eee` → `divider`; (3) QR-box fill `'white'` → `background.paper`; (4) manual-secret fill `'#eee'` → `background.subtle`; (5) recovery-panel fill `'#f5f5f5'` → `background.subtle`; (6) recovery-code-chip fill `'white'` → `background.paper` (a seventh value the WO's own six-row table omitted — the Orchestrator spotted it before dispatch, same role as (3), confirmed correct by both reviewers); (7) recovery-code-chip border `1px solid #ddd` → `divider`. Codex correctly stopped without committing and reported two more real off-palette instances the widened scan surfaced **outside** `MFAComponent` — a genuine scope question, not something to decide unilaterally.
71
71
 
@@ -77,4 +77,5 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
77
77
  | I18N-1 | One export, so a consumer cannot half-register the kit's strings | ucm exports its UI strings as **six separate bundles** that each app must spread into its own i18n resources by hand. Adopting a component without also adding its bundle renders **a raw key as visible UI text** — no type error, no runtime error. Ship a `uiCoreTranslations` aggregate so that cannot happen, and so a future ucm feature is carried automatically instead of requiring twelve apps to notice. Full order [work-orders/I18N-1.md](work-orders/I18N-1.md). | 2026-08-11 | done | a6d322d | Tier 3 (shared-core + new public export). **Shipped as `2.37.0`, not the planned `2.36.0`** — the concurrent `SHELL-5` release claimed `2.36.0` first; sequenced behind it (see below), no other change to scope. **Landed with seven bundles, not six** — `SHELL-5` (same day, same repo) added a seventh `sectionNavTranslations`, already picked up by the directory-enumerated ratchet before this WO's own commit, so the aggregate never had a six-bundle gap in practice. Codex implemented (`uiCoreTranslations` export + directory-ratcheted `tests/i18nAggregate.test.js` + README line); left the diff uncommitted as instructed. **Concurrent-session collision, real one:** `SHELL-5` was landing in the same working tree at the same time and touched `src/index.js` too; held this WO uncommitted until `SHELL-5`'s commit (`b283c33`) reached `main`, then re-diffed — clean, only this WO's seven-import/aggregate hunk remained. **Test #3 blocked Codex for a real reason, resolved by the operator, not routed around:** the required "every value keeps its `{de,fr,en,sw}` shape" test failed because `messagingTranslations`'s 116 keys have **no `sw` value at all**, a pre-existing gap, not something the merge caused — confirmed directly (`grep -c "'sw':" src/i18n/messagingTranslations.ts` → 0). Operator chose to scope the test down to `{de,fr,en}` for now with `sw` backfill tracked as a follow-up (comment in `tests/i18nAggregate.test.js` records this); key/value fidelity and the collision check are unaffected by the scope-down. **Bundle-size delta measured** (feeds `I18N-2`): +28,943 B raw / +7,327 B gzip for an auth-only app switching from `authTranslations` alone to the (then six-bundle) aggregate. **Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory, concurrent): no findings from either.** `reviewer` additionally confirmed the seven bundles are namespace-disjoint by prefix so spread order can't shadow a value, and that the ratchet's `fs.readdirSync` + `import.meta.glob` targets stay in sync (mismatch throws explicitly). Scoped tests green: 23/23 (`i18nAggregate`, `authTranslations`, `messagingExports`, `notificationsExports`). No rendered verification — the diff renders nothing. **`fitness-monitor/FM-14` is the still-open acceptance condition**, executed in another repo: fitness-monitor keeps rendering raw `ChartFrame.*` keys until it lands. **Two apps hit by the same mechanism, evidence not hypothesis:** (1) cockpit's `UI-3` adopted `UserMenu` without `userMenuTranslations` and shipped a header button reading the literal `UserMenu.TITLE` — **both independent static reviews were clean**, it was caught only by the rendered click-through, and only by DOM inspection at that since screenshots were unavailable; (2) **fitness-monitor is defective right now** — it imports `ChartFrame` (`BodyHistoryPage.jsx:7`, used `:295`/`:445`) but registers only `authTranslations`, so `ChartFrame.jsx:74`'s `t('ChartFrame.LOADING')` and its empty/error siblings render raw keys. It survived because those are transient edge states. **Estate survey, 2026-08-11:** twelve apps register ucm bundles — jg-ferien all six, cockpit four, hram three, **fitness-monitor one (defective)**, and eight auth-only apps that are correct solely because they have not adopted a second component yet. That is a coincidence with an expiry date, not a safety property. **The merge is safe:** 504 keys across the six bundles with **zero collisions**, verified by enumeration; every key is namespace-prefixed. **The part that makes the fix permanent is the ratchet test** — the aggregate must be asserted against the bundle list **read from `src/i18n/`**, not a hand-written list, so a future bundle that is not folded in fails. Without it the aggregate is correct today and rots exactly as the six exports did. Per-feature exports stay (removing them would break twelve apps for no gain); double-spreading during migration is harmless, which is why the zero-collision property is a test and not a comment. **Rollout is constrained by the one-repo rule** — a WO implements in one repository, so the twelve consumers cannot be migrated from here: `fitness-monitor/FM-14` is written, mandatory and blocked on this release, and **fitness-monitor renders raw keys until it lands** (the operator chose to route the fix through this rollout rather than patch it separately, so it could not be lost between two WOs; the delay is the cost and is recorded rather than left implicit). The other eleven are correct today — prevention, not repair — and ride along when each is next touched. Self-registration via `addResourceBundle` is **deliberately out of scope**: feasible (ucm components use `useTranslation()` against the consumer's own instance and ucm owns no instance), but all twelve consumers flatten into the default namespace with dotted keys, and registering at render time risks a first-paint flash of the raw key — replacing an invisible defect with a visible one. It is `I18N-2`, framed as a bundle-size optimisation rather than a correctness fix, because this WO already closes the hole. |
78
78
  | I18N-2 | Self-registering component strings, if the bundle cost justifies it | Have ucm components register their own strings on the consumer's i18n instance (`addResourceBundle` on the instance from `useTranslation()`) instead of relying on the consumer spreading a bundle. Would remove the last of the consumer's i18n obligations and stop auth-only apps shipping strings they never render. | 2026-08-11 | planned | | Tier 3 (shared-core, changes how every consumer resolves strings). **Deliberately NOT a correctness WO** — `I18N-1`'s aggregate closes the defect class; this is a bundle-size optimisation and is scheduled on that basis alone. **Its input is a measurement `I18N-1` must produce**: the built size delta an auth-only app takes on by switching to the aggregate. Source-side that is ~30 KB of strings (the five non-auth bundles are 188 keys against auth's 316), but a source figure is not the answer and a guess here would make this decision unfalsifiable. **Two known hazards, both to be resolved before authoring:** (1) all twelve consumers flatten everything into i18next's DEFAULT namespace with flat dotted keys, so the registration shape has to match exactly or key resolution changes under twelve apps at once; (2) registering during render risks a first-paint flash of the raw key — which would trade an invisible defect for a visible one, the opposite of the point. Feasibility is established: ucm components use `useTranslation()` against the consumer's instance and ucm owns no i18next instance of its own, so there is a real instance to register against. |
79
79
  | AUTH-4 | The account user list on a phone | Gives `UserListComponent` a card layout below `md` — one card per user, email as the title, the two boolean columns as labelled chips, role select and delete in a divided footer, plus a sort control that replaces the column headers the table loses. Above `md` the table is byte-identical. Prototype: [assets/AUTH-4-user-list-card.html](work-orders/assets/AUTH-4-user-list-card.html). Full spec: [work-orders/AUTH-4.md](work-orders/AUTH-4.md). | 2026-08-11 | planned | | Split out of DS-11 by operator decision; fix class chosen by operator (card list, over sticky-first-column and priority-hide). Envelope only (Approval Gate #1 pending). **Authored and parked by operator decision 2026-08-11 — not queued.** It is built on demand, not as SHELL-5's successor; a fresh session should not dispatch it without the operator saying so. When it does run, it runs **after** `SHELL-5` (same screen, different half). The point of this row is that the measurements above are already done, so nobody re-measures. Prototype in scope, so the two-width rendered gate applies. **Measured 2026-08-11 in cockpit** at `innerWidth` 411 (the mobile preset gives 411, not 375): table **1053px inside a 325px container**, container `overflow-x: auto` and **the document does not overflow** — so it is not a broken layout but a desktop table on a phone, with Role and Actions, the two things the screen exists for, entirely outside the view. **The cheap fix was tested and rejected:** 253px of the width is `New` (90) + `Successful Login` (163), both default-on props that `AccountPage` already forwards and cockpit simply left on — but the four remaining columns are 800px against 325px, so configuration cannot fix it. Column defaults deliberately NOT flipped: that would be a silent estate-wide change. SHELL-2's 1045px from 2026-08-08 still holds (+8px), so THEME-4/5 did not move the density. **Four prototype decisions are load-bearing:** email as title with a muted placeholder when the name is absent (verified: a 47-char address wraps to two lines in a 357px card without overflow); the icon-only flags become labelled chips **because a Tooltip is unreachable on a touch pointer**, so that data is effectively absent today; role and delete in a footer behind a divider with the full 44px target; and a **sort control**, because a card list has no column headers — without it the mobile view silently loses a desktop capability. `extraColumns` render as label/value rows with **no new required field**; `extraRowActions` beyond three must collapse into an overflow menu. Reached via `AccountPage`, which **at least ten apps mount**, and ucm has no staging net. |
80
- | SHELL-6 | A controlled `SectionNav` with no way to open must not render a trigger | In `mode="mobile"` the built-in trigger renders unconditionally (`SectionNav.jsx:246`), while `handleTriggerClick` calls `onOpen?.()` — so a consumer driving `open` itself without `onOpen` gets a button that is visible, focusable and does nothing. Render the trigger only when a click can open the drawer: uncontrolled, or controlled **and** `onOpen` supplied. Plus the component's **render matrix** written into its doc comment. Full spec: [work-orders/SHELL-6.md](work-orders/SHELL-6.md). | 2026-08-11 | done | <pending> | **Unblocks `jg-ferien/NAV-37`** (`blocked`, `c627d7b`), same shape as `SHELL-3`→`SHELL-4`. **Operator chose the semantics fix over a `showTrigger` prop** — a prop would leave the dead button as the default for every controlled consumer who has not heard of it, turning a defect into a configuration option; a focusable control that performs no action is wrong on its own terms. Fix: `const canOpenTrigger = !isControlled || Boolean(onOpen)`, wrapping only the `<ButtonBase>` trigger — `children` and `<Drawer>` keep rendering unconditionally in every case. Render-matrix doc comment added above the component per the WO's own framing ("not optional documentation; it is the artefact whose absence caused the defect"). `AccountPage`, the only shipped consumer, is uncontrolled and renders identically. Version `2.37.0` → `2.37.1` (patch, defect fix, no API change). **Codex left the diff uncommitted and did not touch this file, as instructed** — fourth consecutive WO where the explicit warning held. **Required test 1's non-vacuity confirmed**: Codex's own landing note states the new regression test was run against the pre-fix code and failed (dead trigger still in the DOM), then passed after the one-line fix — the Orchestrator did not re-verify this specific claim by checking out the prior commit, but both independent reviewers separately read the old-vs-new JSX and found the claim plausible/consistent. **Orchestrator's own verification:** scoped tests 47/47 green, `tsc` clean. Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory) ran concurrently — **both came back clean, no findings, no fix required.** `reviewer` confirmed the condition is keyed correctly (not on `open`'s value, avoiding the trap the WO's own test 3 exists to catch), `children`/`Drawer` stay unconditional, desktop path untouched, and the render-matrix comment matches the Envelope's table exactly. `ui_reviewer` confirmed the trigger's removal is genuine (conditional JSX unmount, not a CSS-hidden trick left in the accessibility tree/tab order) and no restyle occurred as a side effect; noted two non-blocking residual observations, not fixed: a hypothetical layout-shift if a consumer dynamically toggled `onOpen` on/off at runtime (no current or planned consumer does this), and `resolvedEyebrow`/`resolvedTitle` computed unconditionally even when unused (harmless). No rendered gate needed per the WO's own reasoning — the change is a DOM absence fact, not an appearance, and the test suite is the correct instrument. |
80
+ | SHELL-6 | A controlled `SectionNav` with no way to open must not render a trigger | In `mode="mobile"` the built-in trigger renders unconditionally (`SectionNav.jsx:246`), while `handleTriggerClick` calls `onOpen?.()` — so a consumer driving `open` itself without `onOpen` gets a button that is visible, focusable and does nothing. Render the trigger only when a click can open the drawer: uncontrolled, or controlled **and** `onOpen` supplied. Plus the component's **render matrix** written into its doc comment. Full spec: [work-orders/SHELL-6.md](work-orders/SHELL-6.md). | 2026-08-11 | done | f119b70 | **Unblocks `jg-ferien/NAV-37`** (`blocked`, `c627d7b`), same shape as `SHELL-3`→`SHELL-4`. **Operator chose the semantics fix over a `showTrigger` prop** — a prop would leave the dead button as the default for every controlled consumer who has not heard of it, turning a defect into a configuration option; a focusable control that performs no action is wrong on its own terms. Fix: `const canOpenTrigger = !isControlled || Boolean(onOpen)`, wrapping only the `<ButtonBase>` trigger — `children` and `<Drawer>` keep rendering unconditionally in every case. Render-matrix doc comment added above the component per the WO's own framing ("not optional documentation; it is the artefact whose absence caused the defect"). `AccountPage`, the only shipped consumer, is uncontrolled and renders identically. Version `2.37.0` → `2.37.1` (patch, defect fix, no API change). **Codex left the diff uncommitted and did not touch this file, as instructed** — fourth consecutive WO where the explicit warning held. **Required test 1's non-vacuity confirmed**: Codex's own landing note states the new regression test was run against the pre-fix code and failed (dead trigger still in the DOM), then passed after the one-line fix — the Orchestrator did not re-verify this specific claim by checking out the prior commit, but both independent reviewers separately read the old-vs-new JSX and found the claim plausible/consistent. **Orchestrator's own verification:** scoped tests 47/47 green, `tsc` clean. Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory) ran concurrently — **both came back clean, no findings, no fix required.** `reviewer` confirmed the condition is keyed correctly (not on `open`'s value, avoiding the trap the WO's own test 3 exists to catch), `children`/`Drawer` stay unconditional, desktop path untouched, and the render-matrix comment matches the Envelope's table exactly. `ui_reviewer` confirmed the trigger's removal is genuine (conditional JSX unmount, not a CSS-hidden trick left in the accessibility tree/tab order) and no restyle occurred as a side effect; noted two non-blocking residual observations, not fixed: a hypothetical layout-shift if a consumer dynamically toggled `onOpen` on/off at runtime (no current or planned consumer does this), and `resolvedEyebrow`/`resolvedTitle` computed unconditionally even when unused (harmless). No rendered gate needed per the WO's own reasoning — the change is a DOM absence fact, not an appearance, and the test suite is the correct instrument. |
81
+ | THEME-6 | An app-side check for `sx` values the theme already gives | `THEME-4` made shadowing mechanical for the kit's own components; apps have no equivalent — cockpit alone carries **464 `sx` props across 37 files** (measured 2026-08-11). Adds an exported, source-level, **report-only** check an app calls from its own suite, plus the cockpit count. Full spec: [work-orders/THEME-6.md](work-orders/THEME-6.md). | 2026-08-11 | done | <pending> | DS-18's app-side half. **Codex → Claude fallback**: Codex ran out of workspace credits mid-task (first-hand failure this session, stamped today) — the Orchestrator implemented directly. Author-flip rule applied: independent `reviewer` (Sonnet, mandatory regardless of tier) ran against the Orchestrator's own diff; `ui_reviewer` correctly not spawned, per the WO's own text, since nothing renders. New export `reportRedundantThemeValues(sources, { theme })` in `src/theme/themeCompleteness.js`, reusing existing primitives (`getPath`, `sameValue`, `jsxOpeningTag`, `topLevelSxValue`, `BASELINE_STYLED_MUI_COMPONENTS`, `expandShorthandProperty`) rather than rebuilding them. Attribution by JSX tag name only (root-slot styleOverrides only), literal string/number sx values only — no template literals, identifiers, spreads, or `component={...}` resolution, the bound written open per the WO's instruction. **A real, verified discrepancy in the Envelope's own grounding, corrected here rather than silently chased:** the cited cockpit examples (`borderColor: 'divider'` ×2, `bgcolor: 'background.paper'` ×3, `borderRadius: 1` ×3) all live on `<Box component="button">` in `cockpit/frontend/src/cockpit/ChatView.jsx:75,82,95` — confirmed by direct inspection — which the check's own stated tag-name attribution method cannot catch (`Box` isn't a themed tag, and `component={...}` resolution is explicitly out of scope). The check was NOT widened to chase these numbers. **Run against cockpit's real source** (`cockpit/frontend/src`, 50 `.jsx` files, `theme.js` excluded from the sources passed per the WO's app-side-exclusion instruction — `reportOffPaletteColours`'s own skip pattern left untouched): **27 findings, all `MuiButton.textTransform`, across 4 files** (`BoardView.jsx`, `KnowledgeView.jsx`, `SettingsView.jsx`, one more) — cockpit's buttons redundantly set `sx={{ textTransform: 'none' }}`, matching the baseline's own `MuiButton.styleOverrides.root.textTransform`. This is the honest, real result, reported as a correction to the Envelope's stale grounding, not a match to it. Independent `reviewer` found one real **P2, fixed**: numeric sx values for `borderRadius` and spacing-shorthand aliases were compared raw against the theme's already-resolved CSS value, but MUI actually multiplies a bare number by `theme.shape.borderRadius`/`theme.spacing()` before rendering (e.g. `borderRadius: 8` in sx renders as `8 × shape.borderRadius`, not `8`) — comparing the two raw numbers could produce a **false positive**, exactly the failure mode the WO's own Risks section names as the one to avoid. Fixed by skipping numeric-value comparison entirely for `borderRadius` and every spacing alias (`MUI_NUMERIC_SCALE_PROPERTIES`), erring toward a false negative (the accepted lower bound) rather than a false positive; two regression tests added (the specific false-positive case, plus a corrected version of the "component the theme doesn't style" test, which had used an off-list tag rather than exercising the intended undefined-styleOverride branch — also a reviewer note, fixed). Re-verified: cockpit's 27/27 findings unchanged by the fix (all string-valued `textTransform`, not numeric). Scoped tests 37/37 green, `tsc` clean. Version `2.37.1` → `2.38.0` (minor, new exported API). No rendered gate — static/logic change, no fix shipped anywhere (report-only, per the WO's explicit non-goal). |
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { onboardingTranslations } from './i18n/onboardingTranslations';
7
7
  import { sectionNavTranslations } from './i18n/sectionNavTranslations';
8
8
  import { userMenuTranslations } from './i18n/userMenuTranslations';
9
9
  // --- 0. Theme ---
10
- export { createAppTheme, assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, KIT_COMPONENT_SX_REGISTRY, reportOffPaletteColours, reportKitSxBypasses, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './theme';
10
+ export { createAppTheme, assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, KIT_COMPONENT_SX_REGISTRY, reportOffPaletteColours, reportKitSxBypasses, reportRedundantThemeValues, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './theme';
11
11
  // --- 1. Auth Context (Essentiell für den Wrapper) ---
12
12
  export { AuthContext, AuthProvider } from './auth/AuthContext';
13
13
  export { UserMenu } from './auth/UserMenu';
@@ -1,5 +1,5 @@
1
1
  export { createAppTheme } from './createAppTheme';
2
- export { assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, reportOffPaletteColours, reportKitSxBypasses, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './themeCompleteness';
2
+ export { assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, reportOffPaletteColours, reportKitSxBypasses, reportRedundantThemeValues, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './themeCompleteness';
3
3
  export { KIT_COMPONENT_SX_REGISTRY } from './kitSxRegistry';
4
4
  export { formatShortTime, formatShortDate, formatShortMonth, formatShortYear, } from '../components/charts/chartLabels';
5
5
  export { yearTickInterval } from '../components/charts/yearTickInterval';
@@ -757,6 +757,208 @@ export function reportOffPaletteColours(sources = [], { palette = BASELINE_PALET
757
757
  }
758
758
  return { findings };
759
759
  }
760
+ function balancedBraceRegion(text, start) {
761
+ let depth = 0;
762
+ let quote = null;
763
+ for (let index = start; index < text.length; index += 1) {
764
+ const character = text[index];
765
+ if (quote) {
766
+ if (character === quote && text[index - 1] !== '\\')
767
+ quote = null;
768
+ continue;
769
+ }
770
+ if (character === '"' || character === "'" || character === '`') {
771
+ quote = character;
772
+ continue;
773
+ }
774
+ if (character === '{')
775
+ depth += 1;
776
+ else if (character === '}') {
777
+ depth -= 1;
778
+ if (depth === 0)
779
+ return text.slice(start, index + 1);
780
+ }
781
+ }
782
+ return text.slice(start);
783
+ }
784
+ function topLevelObjectEntries(objectText) {
785
+ const inner = objectText.slice(1, -1);
786
+ const entries = [];
787
+ let depth = 0;
788
+ let quote = null;
789
+ let entryStart = 0;
790
+ const push = (raw) => {
791
+ const trimmed = raw.trim();
792
+ if (trimmed)
793
+ entries.push(trimmed);
794
+ };
795
+ for (let index = 0; index < inner.length; index += 1) {
796
+ const character = inner[index];
797
+ if (quote) {
798
+ if (character === quote && inner[index - 1] !== '\\')
799
+ quote = null;
800
+ continue;
801
+ }
802
+ if (character === '"' || character === "'" || character === '`') {
803
+ quote = character;
804
+ continue;
805
+ }
806
+ if (character === '{' || character === '[' || character === '(')
807
+ depth += 1;
808
+ else if (character === '}' || character === ']' || character === ')')
809
+ depth -= 1;
810
+ else if (character === ',' && depth === 0) {
811
+ push(inner.slice(entryStart, index));
812
+ entryStart = index + 1;
813
+ }
814
+ }
815
+ push(inner.slice(entryStart));
816
+ return entries;
817
+ }
818
+ function splitSxEntry(entry) {
819
+ let depth = 0;
820
+ let quote = null;
821
+ for (let index = 0; index < entry.length; index += 1) {
822
+ const character = entry[index];
823
+ if (quote) {
824
+ if (character === quote && entry[index - 1] !== '\\')
825
+ quote = null;
826
+ continue;
827
+ }
828
+ if (character === '"' || character === "'" || character === '`') {
829
+ quote = character;
830
+ continue;
831
+ }
832
+ if (character === '{' || character === '[' || character === '(')
833
+ depth += 1;
834
+ else if (character === '}' || character === ']' || character === ')')
835
+ depth -= 1;
836
+ else if (character === ':' && depth === 0) {
837
+ return { rawKey: entry.slice(0, index).trim(), rawValue: entry.slice(index + 1).trim() };
838
+ }
839
+ }
840
+ return null;
841
+ }
842
+ const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/;
843
+ const QUOTED_STRING_PATTERN = /^(['"])((?:\\.|(?!\1)[\s\S])*)\1$/;
844
+ const NUMERIC_LITERAL_PATTERN = /^-?\d+(?:\.\d+)?$/;
845
+ /**
846
+ * Reads a top-level `sx={{ ... }}` object literal on a baseline-styled tag
847
+ * and returns its simple `{ property, value }` entries: identifier keys only
848
+ * (a quoted key is a nested selector like `'&:hover'` and is skipped, not a
849
+ * property), values that are a plain quoted string or bare number only. A
850
+ * template literal, identifier reference, spread, or function call is out of
851
+ * scope by design -- this is a lower bound, not a JS evaluator.
852
+ */
853
+ function literalSxEntries(sxValueText) {
854
+ if (!/^\{\s*\{/.test(sxValueText))
855
+ return [];
856
+ const objectText = balancedBraceRegion(sxValueText, sxValueText.indexOf('{', 1));
857
+ const entries = [];
858
+ for (const rawEntry of topLevelObjectEntries(objectText)) {
859
+ const split = splitSxEntry(rawEntry);
860
+ if (!split)
861
+ continue;
862
+ const { rawKey, rawValue } = split;
863
+ if (!SIMPLE_IDENTIFIER_PATTERN.test(rawKey))
864
+ continue;
865
+ const quoted = rawValue.match(QUOTED_STRING_PATTERN);
866
+ if (quoted) {
867
+ entries.push({ property: rawKey, value: quoted[2] });
868
+ continue;
869
+ }
870
+ if (NUMERIC_LITERAL_PATTERN.test(rawValue)) {
871
+ entries.push({ property: rawKey, value: Number(rawValue) });
872
+ }
873
+ }
874
+ return entries;
875
+ }
876
+ // MUI's sx system multiplies a bare NUMBER by a theme scale factor for these
877
+ // properties (`theme.shape.borderRadius` for borderRadius, `theme.spacing()`
878
+ // for every spacing alias) before it ever reaches the DOM -- a raw
879
+ // `styleOverrides.root` value is already-resolved CSS, unscaled. Comparing
880
+ // the two numbers directly would be a false positive (e.g. `borderRadius: 8`
881
+ // in sx renders as `theme.shape.borderRadius * 8`, not `8`), and false
882
+ // positives are the failure mode this whole check exists to avoid. Numeric
883
+ // literals on these properties are therefore skipped, not compared -- a
884
+ // false negative (this check's accepted lower bound) is the safe direction
885
+ // to err in, a false positive is not.
886
+ const MUI_NUMERIC_SCALE_PROPERTIES = new Set([
887
+ 'borderRadius',
888
+ ...Object.keys(SPACING_SHORTHAND_LONGHANDS).filter((key) => key !== 'bgcolor' && key !== 'typography'),
889
+ ]);
890
+ /**
891
+ * Resolves what an sx value would actually render as, for comparison against
892
+ * a theme styleOverride value already resolved to a real CSS value. A string
893
+ * sx value is tried as a `theme.palette.<value>` token path first (MUI's own
894
+ * resolution for palette-aware sx properties, e.g. `borderColor: 'divider'`
895
+ * -> `theme.palette.divider`); if that path is undefined, the raw string is
896
+ * used as-is (a plain CSS keyword like `'none'` is not a token path).
897
+ */
898
+ function resolveSxValue(theme, value) {
899
+ if (typeof value !== 'string')
900
+ return value;
901
+ const paletteValue = getPath(theme, `palette.${value}`);
902
+ return paletteValue !== undefined ? paletteValue : value;
903
+ }
904
+ /**
905
+ * Reports an app's own `sx` value that already equals what its theme
906
+ * resolves for that MUI component and property -- not "you overrode the
907
+ * baseline" (in an app, that is the intended mechanism), but "you re-stated
908
+ * a value you already had", which silently diverges the next time the token
909
+ * changes. Attribution is by JSX tag name against the same
910
+ * `BASELINE_STYLED_MUI_COMPONENTS` list `reportKitSxBypasses` uses, checked
911
+ * against `theme.components.Mui<Tag>.styleOverrides.root.<property>` only
912
+ * (no other slot). Report-only; there is no exemption arm yet -- each
913
+ * adopting app makes this a hard assertion in its own follow-up WO once it
914
+ * has acted on the findings.
915
+ *
916
+ * This is a lower bound, and the bound is open, not enumerated: a `styled()`
917
+ * component, a component rendered through `component={...}` (so its JSX tag
918
+ * name is not the MUI component it becomes), an aliased import
919
+ * (`import { Paper as Surface }`), array/callback/conditional `sx`, and a
920
+ * template-literal or identifier-reference value are all unattributed or
921
+ * unresolved by this method. A numeric `sx` value on a property MUI scales
922
+ * by a theme factor (`borderRadius`, every spacing alias) is deliberately
923
+ * SKIPPED rather than compared -- see `MUI_NUMERIC_SCALE_PROPERTIES` -- since
924
+ * comparing the raw numbers would be a false positive, not a missed finding.
925
+ * A clean result means "nothing found by this method", not "no redundant
926
+ * value exists".
927
+ */
928
+ export function reportRedundantThemeValues(sources = [], { theme } = {}) {
929
+ const findings = [];
930
+ if (!theme)
931
+ return { findings };
932
+ const componentPattern = BASELINE_STYLED_MUI_COMPONENTS.join('|');
933
+ const tagPattern = new RegExp(`<(${componentPattern})\\b`, 'g');
934
+ for (const { path, source } of normalizedSources(sources)) {
935
+ const scanSource = maskComments(maskQrPrintDocument(source, path.replaceAll('\\', '/')));
936
+ for (const match of scanSource.matchAll(tagPattern)) {
937
+ const tagName = match[1];
938
+ const tag = jsxOpeningTag(scanSource, match.index);
939
+ const sxValueText = topLevelSxValue(tag);
940
+ const line = source.slice(0, match.index).split(/\r?\n/).length;
941
+ for (const { property, value } of literalSxEntries(sxValueText)) {
942
+ if (typeof value === 'number' && MUI_NUMERIC_SCALE_PROPERTIES.has(property))
943
+ continue;
944
+ const [canonicalProperty] = expandShorthandProperty(property);
945
+ const lookupProperty = expandShorthandProperty(property).length === 1
946
+ ? canonicalProperty
947
+ : property;
948
+ const themeValue = getPath(theme, `components.Mui${tagName}.styleOverrides.root.${lookupProperty}`);
949
+ if (themeValue === undefined)
950
+ continue;
951
+ if (sameValue(resolveSxValue(theme, value), themeValue)) {
952
+ findings.push({
953
+ surface: `${path}:${line}.Mui${tagName}.${property}`,
954
+ reason: `Mui${tagName}'s sx sets "${property}" to a value the theme already resolves to the same thing.`,
955
+ });
956
+ }
957
+ }
958
+ }
959
+ }
960
+ return { findings };
961
+ }
760
962
  /** Reports adoption signals as numbers; it never changes completeness. */
761
963
  export function reportThemeAdoption(sources = []) {
762
964
  const normalized = sources.map((entry, index) => typeof entry === 'string'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.37.1",
3
+ "version": "2.38.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  KIT_COMPONENT_SX_REGISTRY,
18
18
  reportOffPaletteColours,
19
19
  reportKitSxBypasses,
20
+ reportRedundantThemeValues,
20
21
  reportThemeAdoption,
21
22
  THEME_COMPLETENESS_SURFACES,
22
23
  } from './theme';
@@ -5,6 +5,7 @@ export {
5
5
  calculateContrastRatio,
6
6
  reportOffPaletteColours,
7
7
  reportKitSxBypasses,
8
+ reportRedundantThemeValues,
8
9
  reportThemeAdoption,
9
10
  THEME_COMPLETENESS_SURFACES,
10
11
  } from './themeCompleteness';
@@ -816,6 +816,207 @@ export function reportOffPaletteColours(
816
816
  return { findings };
817
817
  }
818
818
 
819
+ function balancedBraceRegion(text, start) {
820
+ let depth = 0;
821
+ let quote = null;
822
+ for (let index = start; index < text.length; index += 1) {
823
+ const character = text[index];
824
+ if (quote) {
825
+ if (character === quote && text[index - 1] !== '\\') quote = null;
826
+ continue;
827
+ }
828
+ if (character === '"' || character === "'" || character === '`') {
829
+ quote = character;
830
+ continue;
831
+ }
832
+ if (character === '{') depth += 1;
833
+ else if (character === '}') {
834
+ depth -= 1;
835
+ if (depth === 0) return text.slice(start, index + 1);
836
+ }
837
+ }
838
+ return text.slice(start);
839
+ }
840
+
841
+ function topLevelObjectEntries(objectText) {
842
+ const inner = objectText.slice(1, -1);
843
+ const entries = [];
844
+ let depth = 0;
845
+ let quote = null;
846
+ let entryStart = 0;
847
+ const push = (raw) => {
848
+ const trimmed = raw.trim();
849
+ if (trimmed) entries.push(trimmed);
850
+ };
851
+ for (let index = 0; index < inner.length; index += 1) {
852
+ const character = inner[index];
853
+ if (quote) {
854
+ if (character === quote && inner[index - 1] !== '\\') quote = null;
855
+ continue;
856
+ }
857
+ if (character === '"' || character === "'" || character === '`') {
858
+ quote = character;
859
+ continue;
860
+ }
861
+ if (character === '{' || character === '[' || character === '(') depth += 1;
862
+ else if (character === '}' || character === ']' || character === ')') depth -= 1;
863
+ else if (character === ',' && depth === 0) {
864
+ push(inner.slice(entryStart, index));
865
+ entryStart = index + 1;
866
+ }
867
+ }
868
+ push(inner.slice(entryStart));
869
+ return entries;
870
+ }
871
+
872
+ function splitSxEntry(entry) {
873
+ let depth = 0;
874
+ let quote = null;
875
+ for (let index = 0; index < entry.length; index += 1) {
876
+ const character = entry[index];
877
+ if (quote) {
878
+ if (character === quote && entry[index - 1] !== '\\') quote = null;
879
+ continue;
880
+ }
881
+ if (character === '"' || character === "'" || character === '`') {
882
+ quote = character;
883
+ continue;
884
+ }
885
+ if (character === '{' || character === '[' || character === '(') depth += 1;
886
+ else if (character === '}' || character === ']' || character === ')') depth -= 1;
887
+ else if (character === ':' && depth === 0) {
888
+ return { rawKey: entry.slice(0, index).trim(), rawValue: entry.slice(index + 1).trim() };
889
+ }
890
+ }
891
+ return null;
892
+ }
893
+
894
+ const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/;
895
+ const QUOTED_STRING_PATTERN = /^(['"])((?:\\.|(?!\1)[\s\S])*)\1$/;
896
+ const NUMERIC_LITERAL_PATTERN = /^-?\d+(?:\.\d+)?$/;
897
+
898
+ /**
899
+ * Reads a top-level `sx={{ ... }}` object literal on a baseline-styled tag
900
+ * and returns its simple `{ property, value }` entries: identifier keys only
901
+ * (a quoted key is a nested selector like `'&:hover'` and is skipped, not a
902
+ * property), values that are a plain quoted string or bare number only. A
903
+ * template literal, identifier reference, spread, or function call is out of
904
+ * scope by design -- this is a lower bound, not a JS evaluator.
905
+ */
906
+ function literalSxEntries(sxValueText) {
907
+ if (!/^\{\s*\{/.test(sxValueText)) return [];
908
+ const objectText = balancedBraceRegion(sxValueText, sxValueText.indexOf('{', 1));
909
+ const entries = [];
910
+ for (const rawEntry of topLevelObjectEntries(objectText)) {
911
+ const split = splitSxEntry(rawEntry);
912
+ if (!split) continue;
913
+ const { rawKey, rawValue } = split;
914
+ if (!SIMPLE_IDENTIFIER_PATTERN.test(rawKey)) continue;
915
+
916
+ const quoted = rawValue.match(QUOTED_STRING_PATTERN);
917
+ if (quoted) {
918
+ entries.push({ property: rawKey, value: quoted[2] });
919
+ continue;
920
+ }
921
+ if (NUMERIC_LITERAL_PATTERN.test(rawValue)) {
922
+ entries.push({ property: rawKey, value: Number(rawValue) });
923
+ }
924
+ }
925
+ return entries;
926
+ }
927
+
928
+ // MUI's sx system multiplies a bare NUMBER by a theme scale factor for these
929
+ // properties (`theme.shape.borderRadius` for borderRadius, `theme.spacing()`
930
+ // for every spacing alias) before it ever reaches the DOM -- a raw
931
+ // `styleOverrides.root` value is already-resolved CSS, unscaled. Comparing
932
+ // the two numbers directly would be a false positive (e.g. `borderRadius: 8`
933
+ // in sx renders as `theme.shape.borderRadius * 8`, not `8`), and false
934
+ // positives are the failure mode this whole check exists to avoid. Numeric
935
+ // literals on these properties are therefore skipped, not compared -- a
936
+ // false negative (this check's accepted lower bound) is the safe direction
937
+ // to err in, a false positive is not.
938
+ const MUI_NUMERIC_SCALE_PROPERTIES = new Set([
939
+ 'borderRadius',
940
+ ...Object.keys(SPACING_SHORTHAND_LONGHANDS).filter((key) => key !== 'bgcolor' && key !== 'typography'),
941
+ ]);
942
+
943
+ /**
944
+ * Resolves what an sx value would actually render as, for comparison against
945
+ * a theme styleOverride value already resolved to a real CSS value. A string
946
+ * sx value is tried as a `theme.palette.<value>` token path first (MUI's own
947
+ * resolution for palette-aware sx properties, e.g. `borderColor: 'divider'`
948
+ * -> `theme.palette.divider`); if that path is undefined, the raw string is
949
+ * used as-is (a plain CSS keyword like `'none'` is not a token path).
950
+ */
951
+ function resolveSxValue(theme, value) {
952
+ if (typeof value !== 'string') return value;
953
+ const paletteValue = getPath(theme, `palette.${value}`);
954
+ return paletteValue !== undefined ? paletteValue : value;
955
+ }
956
+
957
+ /**
958
+ * Reports an app's own `sx` value that already equals what its theme
959
+ * resolves for that MUI component and property -- not "you overrode the
960
+ * baseline" (in an app, that is the intended mechanism), but "you re-stated
961
+ * a value you already had", which silently diverges the next time the token
962
+ * changes. Attribution is by JSX tag name against the same
963
+ * `BASELINE_STYLED_MUI_COMPONENTS` list `reportKitSxBypasses` uses, checked
964
+ * against `theme.components.Mui<Tag>.styleOverrides.root.<property>` only
965
+ * (no other slot). Report-only; there is no exemption arm yet -- each
966
+ * adopting app makes this a hard assertion in its own follow-up WO once it
967
+ * has acted on the findings.
968
+ *
969
+ * This is a lower bound, and the bound is open, not enumerated: a `styled()`
970
+ * component, a component rendered through `component={...}` (so its JSX tag
971
+ * name is not the MUI component it becomes), an aliased import
972
+ * (`import { Paper as Surface }`), array/callback/conditional `sx`, and a
973
+ * template-literal or identifier-reference value are all unattributed or
974
+ * unresolved by this method. A numeric `sx` value on a property MUI scales
975
+ * by a theme factor (`borderRadius`, every spacing alias) is deliberately
976
+ * SKIPPED rather than compared -- see `MUI_NUMERIC_SCALE_PROPERTIES` -- since
977
+ * comparing the raw numbers would be a false positive, not a missed finding.
978
+ * A clean result means "nothing found by this method", not "no redundant
979
+ * value exists".
980
+ */
981
+ export function reportRedundantThemeValues(sources = [], { theme } = {}) {
982
+ const findings = [];
983
+ if (!theme) return { findings };
984
+ const componentPattern = BASELINE_STYLED_MUI_COMPONENTS.join('|');
985
+ const tagPattern = new RegExp(`<(${componentPattern})\\b`, 'g');
986
+
987
+ for (const { path, source } of normalizedSources(sources)) {
988
+ const scanSource = maskComments(maskQrPrintDocument(source, path.replaceAll('\\', '/')));
989
+
990
+ for (const match of scanSource.matchAll(tagPattern)) {
991
+ const tagName = match[1];
992
+ const tag = jsxOpeningTag(scanSource, match.index);
993
+ const sxValueText = topLevelSxValue(tag);
994
+ const line = source.slice(0, match.index).split(/\r?\n/).length;
995
+
996
+ for (const { property, value } of literalSxEntries(sxValueText)) {
997
+ if (typeof value === 'number' && MUI_NUMERIC_SCALE_PROPERTIES.has(property)) continue;
998
+ const [canonicalProperty] = expandShorthandProperty(property);
999
+ const lookupProperty = expandShorthandProperty(property).length === 1
1000
+ ? canonicalProperty
1001
+ : property;
1002
+ const themeValue = getPath(
1003
+ theme,
1004
+ `components.Mui${tagName}.styleOverrides.root.${lookupProperty}`,
1005
+ );
1006
+ if (themeValue === undefined) continue;
1007
+ if (sameValue(resolveSxValue(theme, value), themeValue)) {
1008
+ findings.push({
1009
+ surface: `${path}:${line}.Mui${tagName}.${property}`,
1010
+ reason: `Mui${tagName}'s sx sets "${property}" to a value the theme already resolves to the same thing.`,
1011
+ });
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ return { findings };
1018
+ }
1019
+
819
1020
  /** Reports adoption signals as numbers; it never changes completeness. */
820
1021
  export function reportThemeAdoption(sources = []) {
821
1022
  const normalized = sources.map((entry, index) =>
@@ -12,6 +12,7 @@ import {
12
12
  KIT_COMPONENT_SX_REGISTRY,
13
13
  reportOffPaletteColours,
14
14
  reportKitSxBypasses,
15
+ reportRedundantThemeValues,
15
16
  THEME_COMPLETENESS_SURFACES,
16
17
  } from '../src/theme';
17
18
  import { BASELINE_PALETTE } from '../src/theme/tokens';
@@ -505,3 +506,90 @@ describe('off-palette colour reporting', () => {
505
506
  expect(result.findings.some(({ reason }) => reason.includes('grey.50'))).toBe(true);
506
507
  });
507
508
  });
509
+
510
+ describe('redundant app-side theme values', () => {
511
+ const theme = createAppTheme({ palette: { primary: { main: '#0F62FE' } } });
512
+
513
+ it('flags an sx value that already equals the theme, naming the file, line, component and property', () => {
514
+ const result = reportRedundantThemeValues(
515
+ [{
516
+ path: 'src/components/Example.jsx',
517
+ source: "export function Example() {\n return <Paper sx={{ borderColor: 'divider' }} />;\n}",
518
+ }],
519
+ { theme },
520
+ );
521
+
522
+ expect(result.findings).toEqual([{
523
+ surface: 'src/components/Example.jsx:2.MuiPaper.borderColor',
524
+ reason: expect.stringMatching(/MuiPaper.*borderColor.*theme already resolves/),
525
+ }]);
526
+ });
527
+
528
+ it('does not flag a genuinely different value', () => {
529
+ const result = reportRedundantThemeValues(
530
+ [{
531
+ path: 'src/components/Example.jsx',
532
+ source: "<Paper sx={{ borderColor: 'error.main' }} />",
533
+ }],
534
+ { theme },
535
+ );
536
+
537
+ expect(result.findings).toEqual([]);
538
+ });
539
+
540
+ it('does not flag a component tag outside the attribution list at all', () => {
541
+ const result = reportRedundantThemeValues(
542
+ [{
543
+ path: 'src/components/Example.jsx',
544
+ source: "<Box sx={{ borderColor: 'divider' }} />",
545
+ }],
546
+ { theme },
547
+ );
548
+
549
+ expect(result.findings).toEqual([]);
550
+ });
551
+
552
+ it('does not flag a property the theme does not style on an attributable component', () => {
553
+ // MuiTextField's baseline styleOverrides.root has no borderColor, so this
554
+ // must fall through the themeValue === undefined branch, not match.
555
+ const result = reportRedundantThemeValues(
556
+ [{
557
+ path: 'src/components/Example.jsx',
558
+ source: "<TextField sx={{ borderColor: 'divider' }} />",
559
+ }],
560
+ { theme },
561
+ );
562
+
563
+ expect(result.findings).toEqual([]);
564
+ });
565
+
566
+ it('skips a numeric borderRadius rather than false-positive on MUI\'s theme-scale multiplier', () => {
567
+ // sx={{ borderRadius: 8 }} on a Card actually renders as
568
+ // theme.shape.borderRadius * 8, not the raw baseline styleOverride value
569
+ // -- comparing the two numbers directly would be a false positive.
570
+ const result = reportRedundantThemeValues(
571
+ [{
572
+ path: 'src/components/Example.jsx',
573
+ source: `<Card sx={{ borderRadius: ${theme.components.MuiCard.styleOverrides.root.borderRadius} }} />`,
574
+ }],
575
+ { theme },
576
+ );
577
+
578
+ expect(result.findings).toEqual([]);
579
+ });
580
+
581
+ it('matches the finding shape of its sibling checks', () => {
582
+ const result = reportRedundantThemeValues(
583
+ [{ path: 'a.jsx', source: "<Paper sx={{ borderColor: 'divider' }} />" }],
584
+ { theme },
585
+ );
586
+
587
+ expect(result).toEqual({ findings: expect.any(Array) });
588
+ expect(result.findings[0]).toEqual({ surface: expect.any(String), reason: expect.any(String) });
589
+ });
590
+
591
+ it('returns no findings and requires a theme', () => {
592
+ expect(reportRedundantThemeValues([{ path: 'a.jsx', source: "<Paper sx={{ borderColor: 'divider' }} />" }]))
593
+ .toEqual({ findings: [] });
594
+ });
595
+ });
@@ -0,0 +1,247 @@
1
+ # THEME-6 — An app-side check for `sx` values the theme already gives
2
+
3
+ **Target repo:** `ui-core-micha` (branch `main` — this repo has no `develop`)
4
+ **Tier:** 3 (shared core)
5
+ **Review:** independent `reviewer` — mandatory (new logic). **`ui_reviewer` does not apply**: this WO
6
+ adds a check and fixes nothing, so no rendered output changes. If that turns out to be false, it
7
+ applies after all.
8
+ **Version target:** the next **minor** after the published version — a new exported check is additive
9
+ API. Read the published version at implementation time.
10
+ **Strand:** `DS-18`, the app-side half. Trigger fired when cockpit adopted `createAppTheme` (`UI-6`);
11
+ deferred once because cockpit was mid-rebuild in `UI-3`, **which has since landed.**
12
+
13
+ ---
14
+
15
+ ## A. Envelope
16
+
17
+ ### Goal
18
+
19
+ `THEME-4` made it mechanical that no **kit** component defeats the baseline. Apps have no equivalent:
20
+ cockpit alone carries **464 `sx` props across 37 files** (measured 2026-08-11), and nothing checks any
21
+ of them against the theme they sit on.
22
+
23
+ Give apps an exported check they call from their own suite, the way they already call
24
+ `assertThemeComplete`.
25
+
26
+ ### This is NOT a port of `THEME-4`, and getting that wrong would flood the app
27
+
28
+ In the **kit**, an `sx` value that duplicates a baseline default is a bug by definition: the default
29
+ becomes inert for every consumer. So `THEME-4` compares **key sets** and demands disjointness.
30
+
31
+ In an **app**, `sx` beating the theme is **the intended mechanism** — that is how a screen customises
32
+ one instance. A key-collision check over 464 props would flag mostly legitimate customisation, get
33
+ blanket-exempted, and become the no-op `THEME-4`'s own WO warned about.
34
+
35
+ **So the app-side rule is different: flag an `sx` value that is *equal* to what the theme already
36
+ resolves for that component and property.** Not "you overrode a default" — "you re-stated a value you
37
+ already had". That is dead weight which **silently diverges the next time the token changes**, which is
38
+ the real failure mode and the one worth a check.
39
+
40
+ Grounded in cockpit, measured: `borderColor: 'divider'` (×2) and `bgcolor: 'background.paper'` (×3) on
41
+ surfaces whose theme already sets exactly that, and `borderRadius: 1` (×3) which resolves to the
42
+ baseline's own `shape.borderRadius`. Those are the findings this check exists for.
43
+
44
+ ### Scope
45
+
46
+ **1. An exported check, source-level.** Same finding contract as its siblings
47
+ (`{ findings: [{ surface, reason }] }`), same shape as `reportThemeAdoption` and
48
+ `reportOffPaletteColours`: it takes the app's own sources plus its theme, and returns findings. The app
49
+ globs its `src/**` in its own test and passes them.
50
+
51
+ For each `sx` entry it can attribute to a MUI component the theme styles, resolve what the theme gives
52
+ for that property and flag **equality**. The attribution comes from the JSX element name in the source
53
+ text — `<Paper sx={{ borderRadius: 1 }}>` → `MuiPaper` → compare against
54
+ `theme.components.MuiPaper.styleOverrides.root.borderRadius`.
55
+
56
+ **2. Report-only, with a ratchet.** New findings must not turn a green app red on a pin bump. Ship it
57
+ **report-only**, and let each app make it a hard assertion in its own cleanup WO — the same ratchet
58
+ `DS-2` used for `assertThemeComplete`, and for the same reason.
59
+
60
+ **3. Run it against cockpit and report the count. Fix nothing.** cockpit is the first real consumer and
61
+ the reason the trigger fired. The cleanup is cockpit's own work order, not this one — bundling an
62
+ unbounded app cleanup into a shared-core WO is what `AUTH-4` was split out of `DS-11` to avoid.
63
+
64
+ ### Reuse, do not rebuild — and one gap in the reuse
65
+
66
+ **The hardcoded-colour half already exists and is already app-callable.**
67
+ `reportOffPaletteColours(sources, { palette })` (`themeCompleteness.js:759`) takes sources and an
68
+ optional palette, so an app can pass its own files and its own palette today. cockpit's
69
+ `bgcolor: '#fff'` (×2) is exactly what it reports. **Do not duplicate it.**
70
+
71
+ **But its skip pattern assumes ucm's layout.** It skips paths matching `src/theme/` — with a trailing
72
+ slash (`:768`). ucm's tokens live in `src/theme/tokens.js`; an app's theme is usually
73
+ `src/theme.js`, which **does not match** and therefore gets scanned, so every identity token in it
74
+ (cockpit: `#3D5A99`, `#eff2f7`, `#e4e8f1`, …) would be reported as off-palette.
75
+
76
+ Resolve it **app-side**, not by widening the pattern: the app excludes its own theme file from the
77
+ sources it passes. That keeps ucm's behaviour unchanged and puts the knowledge where the layout is
78
+ known. Say so in the check's doc comment so the next app does not rediscover it.
79
+
80
+ ### The bound, and it is open
81
+
82
+ Attribution from source text is a **heuristic**. It will miss `sx` on a variable, spread props, an
83
+ aliased import (`import { Paper as Surface }`), a `styled()` component, and a MUI component rendered
84
+ through `component={...}`. **Write the bound as open — "unknown attribution gaps remain" — not as a
85
+ list.** `THEME-4`'s WO enumerated its limits and the first real miss (shorthand alias spelling) was not
86
+ on the list; `THEME-5` was told to phrase it openly and that phrasing then held when the fourth
87
+ shadowing layer appeared. An enumeration reads as coverage.
88
+
89
+ This check is a lower bound on redundancy, and green means "nothing found by this method".
90
+
91
+ ### Non-goals / do not touch
92
+
93
+ - **No fixes anywhere.** Not in cockpit, not in the kit. This WO ships a check and a count.
94
+ - **No change to `reportOffPaletteColours`**, `assertKitSxDisjoint`, `reportKitSxBypasses`,
95
+ `reportThemeAdoption` or `assertThemeComplete` — including no widening of the `src/theme/` skip.
96
+ - **Not a hard assertion anywhere yet.** Report-only; the ratchet is each app's own.
97
+ - `DS-18`'s remaining half — "which components *can violate* a rule", derived from a
98
+ `grep 'shadows\['` lower bound — stays out and stays without a driver.
99
+ - No baseline token, component default or public prop change.
100
+
101
+ ### Risks
102
+
103
+ - **False positives are the failure mode**, not false negatives. A check that flags legitimate
104
+ customisation gets switched off. That is why it compares values rather than keys, and why it ships
105
+ report-only.
106
+ - **The count from cockpit may be large.** 464 props is the denominator. If it is large enough that no
107
+ app could plausibly act on it, that is a finding to report — the check may need a narrower first
108
+ scope (e.g. only the ~23 component keys the baseline actually styles) rather than shipping noise.
109
+ - **No staging net.** A version bump on `main` publishes; the independent review is the only gate.
110
+ - **Other sessions are frequently active in this repo.** Read `git status` and
111
+ `git log origin/main..HEAD` before reviewing; this strand has had concurrent work in the same files
112
+ throughout.
113
+
114
+ ### Required tests to WRITE
115
+
116
+ 1. **Non-vacuity:** a fixture source with `<Paper sx={{ borderColor: 'divider' }}>` against a theme
117
+ whose `MuiPaper` already sets `borderColor: divider` produces a finding naming the file, line,
118
+ component and property.
119
+ 2. **The legitimate case produces nothing:** the same fixture with a *different* value
120
+ (`borderColor: 'error.main'`) is **not** flagged. This is the test that keeps the check usable.
121
+ 3. A component the theme does not style is not flagged at all.
122
+ 4. The finding shape matches its siblings, so a consumer handles one contract.
123
+ 5. `reportOffPaletteColours` is unchanged — its existing tests stay green untouched.
124
+
125
+ Plus `tsc -p tsconfig.build.json --noEmit` clean. **No full-suite run** — affected set is the theme
126
+ module's specs.
127
+
128
+ ### Verification
129
+
130
+ No rendered surface. The evidence is test 1 plus the cockpit count, reported in the register note with
131
+ the exact number and a sample of what it found. A check nobody has run against real code is not
132
+ verified, however green its fixtures.
133
+
134
+ ### Parity guardrail
135
+
136
+ Nothing renders differently. No existing check changes behaviour.
137
+
138
+ ---
139
+
140
+ ## B. Implementation map
141
+
142
+ *Filled by the Orchestrator on `git pull` — see `AGENTS.md` → "Work Order".*
143
+
144
+ ### Execution directive (read this first)
145
+
146
+ > **If you are the implementer reading this work order as your own specification: this section is
147
+ > NOT addressed to you.** It tells the Orchestrator how to invoke you. **You ARE that invocation —
148
+ > do NOT shell out to `codex exec`.**
149
+ >
150
+ > Implement through `codex exec` in the background — invoked directly via Bash (never the
151
+ > `debugger`/`*_coder` Agent wrappers) with BOTH flags `--skip-git-repo-check` and
152
+ > `--dangerously-bypass-approvals-and-sandbox`. **Pass the WO via stdin, not as a positional
153
+ > argument.** Fallback to direct Claude implementation only on Codex quota/rate-limit/non-zero exit —
154
+ > and that flips authorship, so the independent `reviewer` stays mandatory either way.
155
+ >
156
+ > **Do NOT edit `WORK_ORDERS.md`.** Before your own review read `git log origin/main..HEAD` and
157
+ > `git status`, and check the register row carries no review claim you did not make yourself.
158
+
159
+ ### Context package
160
+
161
+ **Precondition confirmed:** `SHELL-6` landed and published (`f119b70`/`3b746d4`, `package.json` reads
162
+ `2.37.1`). Clean starting tree.
163
+
164
+ **Reusable primitives already in `src/theme/themeCompleteness.js` — do not rebuild these:**
165
+ - `getPath(value, path)` (`:13-15`) and `sameValue(left, right)` (`:17-21`) — exactly what "resolve
166
+ what the theme gives for that property and flag equality" needs; both already handle the
167
+ string/JSON-stringify comparison this check requires.
168
+ - `BASELINE_STYLED_MUI_COMPONENTS` (`:484-508`, 23-entry list), `jsxOpeningTag(source, start)`
169
+ (`:510-530`, returns the full `<Tag ...>` text starting at a match) and `topLevelSxValue(tag)`
170
+ (`:532-553`, extracts the raw `sx={...}` attribute text, top-level braces only) — the exact
171
+ tag-scanning machinery `reportKitSxBypasses` (`:560-`) already uses. Reuse the same
172
+ `<(${componentPattern})\\b` regex approach for finding tags; you will need a NEW small parser to
173
+ walk `topLevelSxValue`'s result and extract individual top-level `property: value` pairs from a
174
+ literal object (`{ borderColor: 'divider', p: 2 }` → `[['borderColor', "'divider'"], ['p', '2']]`) —
175
+ this doesn't exist yet, write it narrowly (top-level only, matching the existing lower-bound
176
+ philosophy; do not attempt to parse nested objects, media-query keys, or `theme =>` callbacks).
177
+ - `reportOffPaletteColours`'s `normalizedSources`/path-skip pattern (`:747-753`, `:768-769`) — same
178
+ `{ path, source }` input shape, reuse `normalizedSources` directly, do NOT touch or widen its
179
+ `/(^|\/)src\/theme\//` skip (Non-goal, explicit).
180
+
181
+ **A real discrepancy found while preparing this map, worth resolving deliberately rather than
182
+ silently — the WO's own "grounded in cockpit" evidence does not match its own stated attribution
183
+ method:** the cited `borderColor: 'divider'` (×2), `bgcolor: 'background.paper'` (×3, one is inside a
184
+ `'&:hover'` nested selector), and `borderRadius: 1` (×3) all live in
185
+ `cockpit/frontend/src/cockpit/ChatView.jsx:75,82,95`, on **`<Box component="button" sx={{...}}>`** —
186
+ verified directly, not assumed. `Box` is not in `BASELINE_STYLED_MUI_COMPONENTS`, and the WO's own
187
+ "Scope" section describes attribution purely by JSX tag name (`<Paper sx=...> → MuiPaper`), while its
188
+ own "bound" section separately, explicitly lists a component rendered through `component={...}` as a
189
+ KNOWN, ACCEPTED GAP. **These two things conflict**: the check as scoped will NOT catch the WO's own
190
+ cited grounding examples. Meanwhile, cockpit's actual `<Paper>`/`<Card>`/etc. usages elsewhere
191
+ (`HeaderStrip.jsx`, `KnowledgeView.jsx`, `SettingsView.jsx`, and others) mostly use **template-literal**
192
+ sx values like `` border: `1px solid ${palette.divider}` `` — a JS expression referencing cockpit's own
193
+ imported `palette` object, not a plain string literal — which a source-level literal-value parser also
194
+ cannot resolve to "equals the theme's value" without evaluating arbitrary JS.
195
+
196
+ **Resolve this by NOT expanding scope to chase the cited numbers** (that would be exactly the
197
+ "enumeration reads as coverage" mistake the WO warns against, applied to attribution instead of the
198
+ bound's phrasing) — implement the check exactly as scoped (tag-name attribution, literal `'string'`/
199
+ `"string"`/numeric sx values only, skip anything that isn't a simple literal — a template literal,
200
+ identifier reference, or expression is out of scope and simply produces no finding for that property,
201
+ same lower-bound philosophy as every sibling check). **Then, when running it against cockpit for the
202
+ required count and sample, report what it ACTUALLY finds — which may be zero, or may differ from the
203
+ Envelope's cited 2+3+3 — and say so explicitly in the register note as a correction to the Envelope's
204
+ own grounding claim**, the same way prior WOs in this strand corrected their own stale counts
205
+ (`THEME-4`'s "51 files", `SHELL-4`'s "ten to six"). Do not silently match the cited numbers by quietly
206
+ special-casing `component=` or template-literal parsing to make them fit — if the honest run finds
207
+ different examples (or none) on properly-attributable tags, that is the real, reportable result.
208
+
209
+ **Where to add the new export:** same file, `src/theme/themeCompleteness.js`, alongside its siblings.
210
+ Suggested name (Codex's call, keep it short and consistent with `reportKitSxBypasses`/
211
+ `reportOffPaletteColours`): `reportRedundantThemeValues` or `reportAppSxRedundancy`. Same
212
+ `{ findings: [{ surface, reason }] }` contract; `surface` should follow the sibling convention
213
+ (`${path}:${line}.${property}` or similar — check `reportOffPaletteColours`'s `${path}:${line}.colour`
214
+ shape for the exact precedent). Export it from `src/theme/index.js` and `src/index.js` (follow the
215
+ existing export-list pattern in both — this is additive public API, matching the "next minor" version
216
+ target).
217
+
218
+ **Signature, per Scope item 1 + 2:** `reportRedundantThemeValues(sources, { theme })` — takes the
219
+ app's own sources (same `{path, source}`/string-array shape as siblings) and a **resolved theme
220
+ object** (an app passes its own `createAppTheme(...)` result, or — for ucm's own required tests — a
221
+ theme built the same way `assertKitSxDisjoint`'s tests do). For each JSX tag matching
222
+ `BASELINE_STYLED_MUI_COMPONENTS`, for each top-level literal `property: value` pair in its `sx`,
223
+ resolve `theme.components.Mui<Tag>.styleOverrides.root.<property>` via `getPath` and flag if
224
+ `sameValue(parsedValue, themeValue)` — matching value AND matching property AND matching component,
225
+ report-only (no exemption arm needed per the WO — this ships report-only with no hard-assertion path
226
+ yet, so skip building an exemption contract this round, that's each app's own future ratchet WO).
227
+
228
+ **Running it against cockpit (Scope item 3, required for the register note):** cockpit's frontend
229
+ source lives at `C:\Users\biglmi\Documents\webapps\cockpit\frontend\src` (37 `.jsx` files, confirmed).
230
+ Glob it (a small Node script or a throwaway test-like harness is fine — this does not need to be a
231
+ committed cockpit-side test, just evidence for the register note) the same way `reportOffPaletteColours`'s
232
+ own `'keeps current kit source free of hard off-palette findings'` test globs `src/` — exclude
233
+ `cockpit/frontend/src/theme.js` explicitly from the sources passed (per Scope's "resolve it app-side"
234
+ instruction — do NOT widen `reportOffPaletteColours`'s or this new check's own skip pattern to do it).
235
+ Build the theme via cockpit's own `createAppTheme(...)` call if one exists (check `theme.js` for the
236
+ palette it passes), or a reasonable equivalent. Report the exact count and 2-3 sample findings in the
237
+ register note, per Verification's explicit requirement — a check nobody has run against real code is
238
+ not verified.
239
+
240
+ **Invariants:** no fix in cockpit or the kit; no change to `reportOffPaletteColours`,
241
+ `assertKitSxDisjoint`, `reportKitSxBypasses`, `reportThemeAdoption`, or `assertThemeComplete`; no new
242
+ baseline token; no hard assertion anywhere (report-only only).
243
+
244
+ ### Mini-handover
245
+
246
+ Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), branch `main`.
247
+ WO: `work-orders/THEME-6.md`. Follow `orchestrate-codex`.