@micha.bigler/ui-core-micha 2.41.1 → 2.41.2
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/.github/workflows/publish.yml +4 -4
- package/WORK_ORDERS.md +6 -2
- package/dist/i18n/uiCoreTranslations.js +8 -0
- package/dist/index.js +6 -8
- package/dist/theme/createAppTheme.js +1 -3
- package/dist/theme/fonts.js +3 -0
- package/dist/theme/index.js +1 -0
- package/package.json +13 -1
- package/src/i18n/uiCoreTranslations.js +17 -0
- package/src/index.js +6 -17
- package/src/theme/createAppTheme.js +1 -3
- package/src/theme/fonts.js +3 -0
- package/src/theme/index.js +2 -0
- package/tests/packageTreeShaking.test.js +126 -0
- package/work-orders/DX-2.md +188 -0
- package/work-orders/DX-3.md +195 -0
- package/work-orders/I18N-3.md +139 -0
|
@@ -109,14 +109,14 @@ jobs:
|
|
|
109
109
|
if: steps.version_check.outputs.should_publish == 'true'
|
|
110
110
|
run: pnpm install --frozen-lockfile
|
|
111
111
|
|
|
112
|
-
- name: Run tests
|
|
113
|
-
if: steps.version_check.outputs.should_publish == 'true'
|
|
114
|
-
run: pnpm run test
|
|
115
|
-
|
|
116
112
|
- name: Build
|
|
117
113
|
if: steps.version_check.outputs.should_publish == 'true'
|
|
118
114
|
run: pnpm run build
|
|
119
115
|
|
|
116
|
+
- name: Run tests
|
|
117
|
+
if: steps.version_check.outputs.should_publish == 'true'
|
|
118
|
+
run: pnpm run test
|
|
119
|
+
|
|
120
120
|
- name: Ensure recent npm for trusted publishing
|
|
121
121
|
if: steps.version_check.outputs.should_publish == 'true'
|
|
122
122
|
run: npm install -g npm@latest
|
package/WORK_ORDERS.md
CHANGED
|
@@ -27,7 +27,11 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
27
27
|
|
|
28
28
|
| ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
|
|
29
29
|
|---|---|---|---|---|---|---|
|
|
30
|
-
|
|
|
30
|
+
| DX-3 | `publish.yml` tests before it builds, so `2.41.2` cannot publish | `DX-2` landed on `main` (`31675d8`, `e75192f`) and **its publish failed** — run [32029216699](https://github.com/bigler-webapps/ui-core-micha/actions/runs/32029216699), `completed/failure`. `publish.yml` runs `Run tests` **before** `Build`; `DX-2`'s new `tests/packageTreeShaking.test.js` copies the package's own `dist/` to assemble a fake installed consumer, and **`dist/` is gitignored** (`.gitignore:2`), so in a clean checkout it does not exist yet: `ENOENT ... lstat '.../dist'`, 510/511 passing with the one new test as the sole failure. The test is neither flaky nor wrong — it inspects the built artifact, which is the point; it cannot pass in the order the workflow runs. It was green locally at 511/511 only because a `dist/` from an earlier build was lying around. Full order [`work-orders/DX-3.md`](work-orders/DX-3.md) | 2026-08-17 | blocked | 67fe140 | Tier 3 — CI surface, independent `reviewer` mandatory for two lines; no `ui_reviewer`. Fix: move `Build` above `Run tests`, **keeping `if: steps.version_check.outputs.should_publish == 'true'` on both** — dropping one is the only way a reorder can go wrong, so the reviewer is asked to confirm it explicitly. Codex implemented (available today per `.claude/codex-status.md`, no prior fallback needed) — one-file, 4-line diff, nothing else touched. **Independent `reviewer` (Sonnet) confirmed: only file changed, both `if:` conditions retained on Build and Run-tests, order now install→Build→Run-tests→npm-bump→Publish, `tsc` (Build) emits `dist/` before `vitest run` (tests) needs it — no findings, safe to commit.** **Status `blocked`, not `done`: the code fix is committed but the dispatch that actually re-publishes `2.41.2` needs an explicit operator go** (`publish.yml` is not on the dispatch safe-allowlist, and its `paths` filter excludes `.github/workflows/**` so this commit alone does not re-trigger it). Move to `done` only once `npm view @micha.bigler/ui-core-micha version` returns `2.41.2` — acceptance is the registry, not the green tile. **The dispatch is the part most likely to be missed:** `publish.yml`'s trigger `paths` cover `package.json`/`pnpm-lock.yaml`/`src/**`/`tsconfig.build.json` but **not `.github/workflows/**`**, so landing this fix does NOT re-trigger the publish — `2.41.2` sits unpublished until `publish.yml` is dispatched on `main`, which needs explicit operator confirmation (not on the safe-allowlist). No version bump: the artifact being published is DX-2's. **Acceptance is `npm view` returning `2.41.2`, not a green tile** — the publish status of this package has now been misreported in both directions in one session. Do NOT skip, delete or self-build the tree-shaking test; a skip would silently retire the only check that `sideEffects` actually tree-shakes. **Recorded, out of scope:** `publish.yml` is the ONLY workflow here that runs tests, and only when `should_publish` is true — so ucm's suite runs in CI *only on a version bump*, and a `src/**` change without one is never tested. Real gap for a twelve-consumer shared core, separate decision. |
|
|
31
|
+
| DX-2 | Declare `sideEffects` so consumers can tree-shake the kit | `package.json` has **no `sideEffects` field** and no `exports` map, so a bundler must assume every module can have an import side effect and cannot drop unused exports — twelve consumers ship the whole kit whatever they import. Measured, not theorised: `hram/THEME-2` switched from three named bundles to the full aggregate and the built size changed by **−11 bytes**. Full order [`work-orders/DX-2.md`](work-orders/DX-2.md) | 2026-08-12 | done | 31675d8 | Tier 3. Codex implemented (available today, no prior fallback needed). **Array form used, not blanket `false`** — the per-module audit found exactly one real import-time side effect group: font CSS. `src/theme/createAppTheme.js` and `src/theme/index.js` both used to `import '@fontsource/dm-sans/*.css'` at module scope; isolated into a new `src/theme/fonts.js`, declared in the `sideEffects` array (both `src/`/`dist/` forms) alongside `index.js`, `theme/index.js`, `theme/createAppTheme.js`, and `notifications/serviceWorker/sw.js` (a genuine `addEventListener` side effect, conservatively listed even though it isn't reachable from `src/index.js` — consumers import it directly). `uiCoreTranslations`'s aggregate-object construction relocated verbatim from `src/index.js` into `src/i18n/uiCoreTranslations.js` (no shape/value change) so the entry module carries no non-trivial logic beyond its own font import. New test `tests/packageTreeShaking.test.js` builds two throwaway consumer bundles against the real `dist/` output via Vite and asserts translations actually tree-shake. **Independent `reviewer` (Sonnet) redid the module-by-module audit from scratch** (walked theme/, auth/apiClient, notifications/realtime, messaging/, onboarding/, i18n/ for CSS imports, `document`/`window`/`self` mutation, axios interceptors, WebSocket construction, i18next `.use()/.init()`) and independently ran `pnpm run build` + the new test — **verdict: classification correct and complete, no missed side effect, safe to commit.** One non-blocking finding, addressed: **`src/index.js`'s font import is unconditional, so `@fontsource/dm-sans` CSS ships in every consumer's bundle regardless of import — it does NOT tree-shake**, and the reviewer flagged that the reported byte delta could be misread as covering the whole entry when it is translations-only. Added a one-line comment at `src/index.js:3` documenting the font/`sideEffects`-array coupling so a future edit can't decouple them silently (the exact false-negative hazard this WO exists to prevent). **Re-measured delta** (built `dist/`, minified, real bundler, supersedes both `I18N-1`'s `+28,943 B`/`+7,327 B` source-reachability figure and `THEME-2`'s `−11 bytes` no-tree-shaking baseline): auth-only **62,294 B raw / 17,653 B gzip** vs. aggregate **90,395 B raw / 25,045 B gzip** → **+28,101 B raw / +7,392 B gzip, translations only — font CSS is a separate, always-shipped, non-tree-shakeable cost not included in this number.** Written into `I18N-2`'s note below. Scoped tests: `pnpm run build` green, new test green, **full existing suite 511/511 green** (WO called for the existing suite as the scoped run, not full-estate). No rendered gate (nothing renders). Version bumped to `2.41.2` (patch by scope, per the WO). **Correction (2026-08-17): the publish failed** — `publish.yml` runs tests before build, and this WO's own new tree-shaking test needs `dist/` (gitignored), so it cannot pass on a clean checkout; registry stayed at `2.41.1`. Fix + re-publish tracked in `DX-3`. |
|
|
32
|
+
| I18N-3 | Backfill Swahili in `messagingTranslations` and restore the four-locale shape test | `messagingTranslations.ts` carries **116 keys with `de`/`fr`/`en` and zero `sw`**, while all six other bundles carry all four locales. `I18N-1` hit this writing its aggregate test, confirmed the gap as pre-existing, and the operator scoped the assertion down to `{de,fr,en}` with the backfill recorded as a follow-up — **which was never written**. Full order [`work-orders/I18N-3.md`](work-orders/I18N-3.md) | 2026-08-12 | planned | | Tier 3 — re-tightens a deliberately loosened test, so every future bundle must then supply `sw` or fail CI. **No app is broken today**: `sw` is configured only in hram and hram does not register messaging — this closes an inconsistency and re-arms a test, which is why there is no urgency justifying unreviewed strings. **The real risk is translation quality**: 116 user-facing strings nobody on the team can check are worse than an honest gap, so the note must **state the translation source**, flag uncertain keys instead of guessing, and reuse `notifications`/`onboarding` terminology rather than coining new terms. `reviewer` mandatory but **cannot validate Swahili** — say so, or a clean verdict implies the translations were checked. Close `I18N-1`'s follow-up note in the same commit. Patch by scope. **A push to `main` publishes.** |
|
|
33
|
+
| THEME-12 | `ScatterChart` ignores the kit's own margin opinion | `THEME-11` gave the kit an opinion about chart margins instead of inheriting MUI's flat 20 px. **`src/components/charts/ScatterChart.jsx` does not reference `withMarginDefaults`** — verified in the source — even though `THEME-10` shipped it in the same `2.41.0`. So every `ScatterChart` consumer re-inherits exactly what `THEME-11` removed. Found 2026-08-12 while surveying the strand; `hram/FIX-15` already bridged it app-side with a local `CHART_MARGIN`, which moves the workaround into the app instead of fixing the kit — the pattern worth not repeating. | 2026-08-12 | planned | | Tier 3 (shared-core, changes rendered output for every scatter consumer). **Not yet authored** — needs a decision first: wire `ScatterChart` to `withMarginDefaults` and let hram drop `CHART_MARGIN`, or decide scatter legitimately needs different margins and document why. The second is plausible — a scatter with a rotated axis title and auto-sized y-tick width is not a bar chart — so this is a question, not a defect to patch blind. `hram/FIX-15` named the gap in its own notes; this row exists so the kit side is tracked where it belongs rather than only in a consumer's WO. |
|
|
34
|
+
| AUTH-5 | `addPublicPath` muss dynamische Routen ausdruecken koennen | `apiClient` leitet bei 401/403 hart auf `/login` um, ausser der Pfad steht auf einer Allowlist — der Mechanismus ist richtig gebaut, kann aber keine **dynamischen** Routen beschreiben: `matchesPublicPath` vergleicht mit `startsWith` (ausser `/`, das Gleichheit verlangt). Eine App mit oeffentlichen Seiten unter einem variablen Segment (`/<slug>`, `/<slug>/team`) kann ihre oeffentliche Flaeche damit nicht ausdruecken: ein konkreter Slug laesst sich eintragen, der naechste faellt wieder heraus. Folge: ein anonymer Besucher landet beim ersten 401 auf `/login` — auf einer Seite, die oeffentlich sein soll, von aussen nicht von "Seite existiert nicht" zu unterscheiden. `addPublicPath` nimmt daher zusaetzlich ein `RegExp` entgegen, geprueft ausschliesslich gegen `pathname`; die eingefrorene Builtin-Liste bleibt string-basiert und unberuehrt. | 2026-08-17 | done | ca09d6e, 2c4a5d3 | Tier 3 (shared-core). reviewer: keine P1/P2-Blocker (ein P2 zu g/y-Flag-Statefulness bei RegExp.test() behoben — lastIndex-Reset + Regressionstest ergaenzt). sec_reviewer: keine Blocker (zwei P3-Hinweise, kein Handlungsbedarf). Kein ui_reviewer noetig (keine Oberflaeche). Veroeffentlicht als 2.41.1 (patch, da Erweiterung einer bestehenden Funktion). jg-ferien PUB-7 kann den Pin jetzt von 2.37.1 auf 2.41.1 ziehen. |
|
|
31
35
|
| PRIM-1 | Promote `StatTile` and `SoftChip` into the shared kit | Both already exist **twice**, independently built, and each second build is an **inline component inside a page** rather than a file — `StatTile` in `fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:554-568` beside hram's `ResearchComponents/StatTile.jsx`, `SoftChip` in `cockpit/frontend/src/cockpit/BoardView.jsx:65-73` (the lane pill) beside hram's `SoftChip.jsx`. The promotion trigger is met on evidence, not waived. The two builds diverged in seven places; all seven decided by the operator 2026-08-12 and rendered in the prototype. Full order [`work-orders/PRIM-1.md`](work-orders/PRIM-1.md) | 2026-08-12 | done | b5fff23 | Tier 3 (shared-core). `reviewer` + `ui_reviewer` mandatory and concurrent; no `sec_reviewer`. **Prototype in scope** — [`work-orders/assets/PRIM-1-dashboard-primitives.html`](work-orders/assets/PRIM-1-dashboard-primitives.html), so the rendered 375/1280 side-by-side is a commit gate; screenshots have not been legibly obtainable here, so a substitution must be **declared**, not silent. Decisions: value `h5` 18/600 · `tabular-nums` always · `flex: 1` with a `minWidth` floor · `accent` kept (fitness-monitor cannot adopt without it) · responsive padding · `status` uses **`overline`** (cockpit independently chose uppercase + .4px tracking, so this satisfies `THEME-4` with **no** baseline change and only hram's chips shift to uppercase) · dot-or-icon and optional border via props. **Token delta list has one entry:** `caveat`'s 12/500 has no baseline variant (`caption` is 12/400) — operator resolved it as keep-the-value, so it must be **exempted in `THEME-4` with that reason stated**, not flattened to 400 and not fixed by extending the baseline here. Neither `StatTile` build has a test; the promoted tests are written from scratch. Minor bump `2.38.0` → `2.39.0` (new capability area). **The three app adoptions are separate follow-up WOs** and land no app change here. Programme strand `DS-5`. **Codex skipped, dated evidence**: `.claude/codex-status.md` records `unavailable` for 2026-08-12 (`cockpit/UI-10`, out of workspace credits, the day's one allowed probe already spent) — Orchestrator implemented directly, flipping authorship. **Independent `reviewer` (Haiku, mandatory regardless of tier per the WO): no findings.** **`ui_reviewer` (Haiku, mandatory, concurrent): two P1 findings, both fixed** — (1) `StatTile`'s padding had only `px` responsive (`{xs:1, sm:1.5}`) with `py` fixed at 1, against the decided "responsive `{xs:1, sm:1.5}`" (matching fitness-monitor's original `p` shorthand, not hram's split values) — changed to a single `p: {xs:1, sm:1.5}`; (2) the `status` variant's typography was under-tested (only `textTransform` checked) — added explicit `fontSize`/`fontWeight`/`letterSpacing` assertions against `theme.typography.overline`, and separately against the `caveat` variant's raw 12/500, both now green. Re-verified 51/51 scoped tests green after fixes, `tsc -p tsconfig.build.json --noEmit` clean. **Rendered gate: declared substitution**, per the WO's own text — the 375/1280 side-by-side screenshot capture is not legibly obtainable in this repo; computed-style/DOM inspection substituted instead, via the test suite itself (fill/border/text colour per tone verified against `alpha`/`darken` of `theme.palette[tone]`; `borderRadius` 999px status vs non-pill caveat; `fontVariantNumeric: tabular-nums`; accent border width 1px vs 2px; `overline` typography values now explicit). No visual click-through was performed. Commit `b5fff23`. |
|
|
32
36
|
| THEME-3 | Add the missing "subtle surface" baseline token | Adds `palette.background.subtle` (`#F4F5F6`, fixed baseline constant alongside `default`/`paper`) to `createAppTheme` and registers it in `THEME_COMPLETENESS_SURFACES`. Found by `cockpit/UI-6`: the `UI-4` design-language sheet's canonical token table names this as a baseline-supplied "inset surface" replacing `cockpit.surface2` (16 of 195 token reads in that WO), but the value was never actually wired into the shipped package — confirmed by grep, not assumed. `cockpit/UI-6` is blocked on this landing + publishing first, per this estate's own "gap in the shared kit is a ucm WO, not a local workaround" rule. Full spec: [work-orders/THEME-3.md](work-orders/THEME-3.md). `2.33.2` (patch). | 2026-08-11 | done | 1b25117 | Tier 3 — shared-core, this repo publishes to npm on every `main` push touching `src/**`, no staging net. Codex implemented in one pass (stdin invocation), exit 0, `RESULT: DONE`, 17/17 of its own new tests green. **Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) ran concurrently — both clean, no findings.** Both independently re-derived the safety mechanism rather than trusting the WO's claim: `MUI_DEFAULT_THEME = createTheme()` never defines `background.subtle` (vanilla MUI only has `.default`/`.paper`), so `assertThemeComplete`'s `sameValue` check can never flag the new surface for any real adopter — the same unexempted treatment `background.default` already gets; `background.paper`'s exemption (needed because MUI's own default theme is also white) was correctly not copied. Both read `fitness-monitor`'s actual `theme.js`/`theme.test.js` directly (the one real production adopter, hard-asserting `assertThemeComplete(theme).findings` equals `[]`) and confirmed this diff cannot break it. `ui_reviewer` additionally grepped every sibling app repo under `webapps/` for `createAppTheme` — no other live consumer exists; cockpit's own adoption (`UI-6`) isn't wired yet. `reviewer` independently recomputed the contrast ratio in a throwaway script and got the same `5.3748:1` the diff claims (not the design sheet's `4.81:1`, which was computed on the sheet's own static canvas — a source discrepancy, not a functional risk; both clear AA). **Orchestrator's own verification:** built the package (`npm run build`) and attempted to run `fitness-monitor`'s real assertions against the fresh `dist/` via raw Node ESM import — hit a pre-existing (confirmed via `git show`, not introduced here) `tsc`-emitted extensionless-import quirk that only affects raw `node`, not the Vite/Vitest resolution every real consumer (incl. fitness-monitor's own suite) uses; fell back to the scoped vitest run instead. Tests: 17/17 green (`tests/createAppTheme.test.js` + `tests/themeCompleteness.test.js`), run by the Orchestrator. No visual/prototype gate — token-only change with no rendered specimen in this repo (per the Envelope); the rendered check happens downstream in `cockpit/UI-6`. Semver: patch, applied by the Orchestrator per the WO's own reasoning (additive, resolves automatically, no adopter code change needed) — confirmed rather than assumed by both reviews. Unblocks `cockpit/UI-6`, which was waiting on this landing + publish. |
|
|
33
37
|
| CHART-7 | `ChartFrame` gains a `titleVariant` prop | Adds `titleVariant` (default `'h6'`, unchanged from today's hardcoded value) to `ChartFrame`, forwarded to the title `Typography` — lets a dense panel grid (e.g. fitness-monitor's seven body-composition panels beside one hero chart) render smaller titles that recede instead of competing with the one chart that matters. One prop, not a `slotProps.title` system — deliberately minimal per the WO. Precondition of fitness-monitor's `FM-12` (DS-10). `2.32.0` (minor, additive). Full spec: [work-orders/CHART-7.md](work-orders/CHART-7.md). | 2026-08-10 | done | 14020d0 | Envelope authored by Expertenchat, no design question (short-form WO). No preconditions. Orchestrator filled a minimal Implementation map (the whole change is two lines in `ChartFrame.jsx`), ran Codex (stdin invocation). Codex added `titleVariant = 'h6'` to the prop destructure, forwarded it to the title `Typography`, wrote all 3 required tests, and bumped `2.31.1` → `2.32.0` per the WO's own stated version target. **Independent `reviewer` + `ui_reviewer` (both Haiku, proportionate to a small low-risk diff) ran concurrently — both found NO findings**, including explicit confirmation that only the prop-destructure line and the one `Typography` line changed (padding, foot row, subtitle/toolbar/slots byte-for-byte unchanged) and that the default-regression test is non-vacuous. Re-verified: 13/13 scoped vitest tests green, `tsc -p tsconfig.build.json --noEmit` clean. **Two-width rendered side-by-side (DS-1 gate)**: no existing harness specimen used `titleVariant`, so the Orchestrator added one (two `ChartFrame`s side by side, default vs. `titleVariant="subtitle2"`) to `dev/entries.jsx` — confirmed live at 375px and 1280px that the override renders visibly smaller/lighter than the unchanged h6 default. |
|
|
@@ -77,7 +81,7 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
|
|
|
77
81
|
**Orchestrator's own verification:** scoped tests (`themeCompleteness`, `createAppTheme`, `MobileBottomNav`, `ChartFrame`) 65/65 green, `tsc -p tsconfig.build.json --noEmit` clean. Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) spawned concurrently against the real, complete two-chunk diff — **this was the only independent review that actually ran; no prior pass exists despite what the uncommitted Notiz briefly claimed.** Both reviewers: **no blocking code findings.** `reviewer`: two P3 notes, not fixed — the print-document masking regex only matches the first `printWindow.document.write(...)` call in a file (currently safe, only one exists; latent fragility if a second is ever added); `exportChartPng`'s new `backgroundColour` parameter has no default (low risk, not exported from `src/index.js`, single caller always supplies it — a footgun only if the function gains a second caller or goes public). `ui_reviewer`: confirmed all nine role-based mappings resolve against real `tokens.js` values and are visually reasonable; flagged the Google-icon border change (`rgba(0,0,0,0.2)` → `divider`, roughly halving visible contrast) as the single largest visual delta of the nine and worth a live look given the operator's explicit no-rendered-gate decision for this WO — not blocking, no rendered gate was added, consistent with that decision. Version `2.34.0` → `2.35.0` (minor, new exported API). No rendered/prototype gate, per the operator's explicit decision — this note is the substitute, all nine changed values listed from-and-to above. DS-18 follow-ups. |
|
|
78
82
|
| SHELL-5 | Promote the secondary-navigation shell | Promotes jg-ferien's `SectionSwitcher` (grouped sidebar + bottom drawer) into the kit as the level *below* the bottom bar, adds the trigger bar that supplies the missing affordance, and makes ucm's own `AccountPage` its first consumer by replacing its horizontal `Tabs` strip. Look and feel follow jg by operator decision. Full spec: [work-orders/SHELL-5.md](work-orders/SHELL-5.md). | 2026-08-11 | done | b283c33 | DS-11. Supersedes `SHELL-2` (now `dropped`); prototype `assets/SHELL-2-account-navigation.html` kept as the composition spec. Each jg-specific concept was made optional and `AccountPage` proves it optional: no `overviewItem` (no "Übersicht" concept), no `rememberedKey`, `APP_HEADER_HEIGHT` → `headerOffset` (default `24`), `zIndex.drawer + 3` kept as default (`SHELL-3`'s bar sits at `+2`, ordering is meaningful), and the 280px two-column grid moved from the consumer into the component. The trigger bar is the one net-new piece (jg has none) — new full-width 56px bar, first thing rendered on mobile, opens a bottom drawer. `AccountPage`'s flat permission-computed `tabs` list bucketed into four fixed groups per the prototype ("Mein Konto"/"Verwaltung"/"Hilfe"/trailing "Weitere" catch-all for ungrouped `extraTabs`), new `Account.GROUP_*` i18n keys; `?tab=` contract and all permission-gating logic byte-for-byte unchanged. New component-owned chrome i18n (`src/i18n/sectionNavTranslations.ts`, all 4 locales) resolves the trap jg's `t("SectionSwitcher.TITLE")` had — no fallback, would have rendered a raw key. **Codex left the diff uncommitted and did not touch this file, as instructed** (the third consecutive WO where the explicit warning about the prior invalid-review pattern held). **Concurrent-session note:** an unrelated `I18N-1` session was uncommitted in the same working tree at the same time (`README.md`, `tests/i18nAggregate.test.js`, an aggregate-translations addition to `src/index.js`) — separated by hunk-level staging (`git add -p`) before this commit; only `SectionNav`'s two export lines in `src/index.js` are in this commit, I18N-1's own hunks were left for its own session to land. **Orchestrator's own verification:** scoped tests green (52/52 after fixes, see below), `tsc` clean. Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory — new logic and a prototype in scope) ran concurrently. **`reviewer` found two real P2s, both fixed:** the drawer's `zIndex` was set via BOTH an exported/registered `sx` object and a separate inline `style` prop on the same element — `style` always wins, so the registered `sx` value was dead code, tested but never actually reaching the DOM; removed the redundant `SECTION_NAV_DRAWER_SX` entirely (and its now-pointless `kitSxRegistry.js` entry), kept only the working `style`-based zIndex. The "optional controlled" `open`/`onClose` API (an Orchestrator design resolution for reconciling jg's controlled drawer with the new internally-triggered one) was incomplete — no `onOpen` callback existed, so a controlled consumer's trigger click was silently a no-op; added `onOpen`, fixed, added a regression test (`AccountPage` itself is unaffected, it uses uncontrolled mode). **`ui_reviewer` found one real P2, fixed:** the mobile trigger's current-section text used `fontWeight={700}`, matching neither the theme's `subtitle1` (600) nor the prototype's `.trigger-value` (16.5px/600) — an undeclared deviation that made it visually indistinguishable from the bold group headers; removed the override. One P3 left as documented, not fixed: `headerOffset` defaults to `24` vs. the prototype's `16` — a defensible generalisation (the component no longer assumes a fixed app-header height the way jg's original did; no consumer today is sensitive to the exact value). **Rendered verification:** the Browser pane could not composite frames in this session (same recurring environment limitation as `SHELL-3`/`SHELL-4`) — DOM/computed-style substitution via the test suite's own assertions (grid columns, drawer dimensions, z-index, theme-token resolution under two themes) plus static composition comparison against the prototype by both independent reviewers, one of which (`ui_reviewer`) is exactly the review type this repo relies on for design fidelity when a real render isn't obtainable. **Follow-ups named, unchanged:** jg migrates onto this and deletes its own drawer (operator requirement, separate WO); the users table is `AUTH-4`, deliberately not here. |
|
|
79
83
|
| 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. |
|
|
80
|
-
| 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 | blocked | | **Deferred by operator decision 2026-08-12** (recorded as `blocked` because the register's status vocabulary has no `deferred`; the blocker is named below). **Its premise did not survive measurement.** `hram/THEME-2` switched from three named bundles to the full aggregate and the built size changed by **−11 bytes**, not the expected increase — because **this package declares no `sideEffects` field** (and no `exports` map), so a bundler must assume every module has side effects and cannot drop unused exports. An auth-only app therefore already ships all seven bundles whatever it imports. That makes this WO's stated input **unmeasurable until tree-shaking works at all**, and it relocates the actual bundle-size lever to a `package.json` change rather than a Tier-3 rework of how twelve consumers resolve strings. Do not author this until `sideEffects` is settled and the delta re-measured against a package that can actually tree-shake. Original framing, kept for the record: Tier 3 (shared-core, changes how every consumer resolves strings); **deliberately NOT a correctness WO** — `I18N-1`'s aggregate closes the defect class. Source-side the non-auth strings are ~30 KB (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. |
|
|
84
|
+
| 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 | blocked | | **Deferred by operator decision 2026-08-12** (recorded as `blocked` because the register's status vocabulary has no `deferred`; the blocker is named below). **Its premise did not survive measurement.** `hram/THEME-2` switched from three named bundles to the full aggregate and the built size changed by **−11 bytes**, not the expected increase — because **this package declares no `sideEffects` field** (and no `exports` map), so a bundler must assume every module has side effects and cannot drop unused exports. An auth-only app therefore already ships all seven bundles whatever it imports. That makes this WO's stated input **unmeasurable until tree-shaking works at all**, and it relocates the actual bundle-size lever to a `package.json` change rather than a Tier-3 rework of how twelve consumers resolve strings. Do not author this until `sideEffects` is settled and the delta re-measured against a package that can actually tree-shake. Original framing, kept for the record: Tier 3 (shared-core, changes how every consumer resolves strings); **deliberately NOT a correctness WO** — `I18N-1`'s aggregate closes the defect class. Source-side the non-auth strings are ~30 KB (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. **`DX-2` (2026-08-17) re-measured the input this WO is blocked on**, against a package that can now actually tree-shake: auth-only **62,294 B raw / 17,653 B gzip** vs. aggregate **90,395 B raw / 25,045 B gzip** → **+28,101 B raw / +7,392 B gzip**, translations only. This is now real, tree-shaking-aware evidence rather than the pre-`DX-2` source-reachability figure — **but the number is unchanged in kind (still a ~7.4 KB gzip cost for a fully auth-only app), so it does not by itself flip the blocked decision.** Still unresolved before this WO can be authored: the two named hazards (flat-namespace registration shape matching exactly across twelve consumers; first-paint raw-key flash risk during render-time registration) — the size delta alone was never the only gate. |
|
|
81
85
|
| 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. |
|
|
82
86
|
| 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. |
|
|
83
87
|
| 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 | 637d262 | 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). |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { authTranslations } from './authTranslations';
|
|
2
|
+
import { chartsTranslations } from './chartsTranslations';
|
|
3
|
+
import { messagingTranslations } from './messagingTranslations';
|
|
4
|
+
import { notificationsTranslations } from './notificationsTranslations';
|
|
5
|
+
import { onboardingTranslations } from './onboardingTranslations';
|
|
6
|
+
import { sectionNavTranslations } from './sectionNavTranslations';
|
|
7
|
+
import { userMenuTranslations } from './userMenuTranslations';
|
|
8
|
+
export const uiCoreTranslations = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, authTranslations), chartsTranslations), messagingTranslations), notificationsTranslations), onboardingTranslations), sectionNavTranslations), userMenuTranslations);
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// index.js (Entry Point deiner Library)
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import { sectionNavTranslations } from './i18n/sectionNavTranslations';
|
|
8
|
-
import { userMenuTranslations } from './i18n/userMenuTranslations';
|
|
2
|
+
// Deliberately unconditional: font CSS ships in every consumer bundle regardless of what they
|
|
3
|
+
// import (not tree-shaken). If this import is ever removed, drop this file from the
|
|
4
|
+
// `sideEffects` array in package.json in the same change — decoupling them silently reintroduces
|
|
5
|
+
// the false-sideEffects-false hazard DX-2 exists to avoid.
|
|
6
|
+
import './theme/fonts';
|
|
9
7
|
// --- 0. Theme ---
|
|
10
8
|
export { createAppTheme, assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, KIT_COMPONENT_SX_REGISTRY, reportOffPaletteColours, reportKitSxBypasses, reportRedundantThemeValues, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './theme';
|
|
11
9
|
// --- 1. Auth Context (Essentiell für den Wrapper) ---
|
|
@@ -91,4 +89,4 @@ export { notificationsTranslations } from './i18n/notificationsTranslations';
|
|
|
91
89
|
export { onboardingTranslations } from './i18n/onboardingTranslations';
|
|
92
90
|
export { chartsTranslations } from './i18n/chartsTranslations';
|
|
93
91
|
export { messagingTranslations } from './i18n/messagingTranslations';
|
|
94
|
-
export
|
|
92
|
+
export { uiCoreTranslations } from './i18n/uiCoreTranslations';
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import '
|
|
2
|
-
import '@fontsource/dm-sans/500.css';
|
|
3
|
-
import '@fontsource/dm-sans/600.css';
|
|
1
|
+
import './fonts';
|
|
4
2
|
import { createTheme, darken, getContrastRatio } from '@mui/material/styles';
|
|
5
3
|
import { BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS, BASELINE_PALETTE, BASELINE_STATIC, MOTION, } from './tokens';
|
|
6
4
|
function findFunction(value, path) {
|
package/dist/theme/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './fonts';
|
|
1
2
|
export { createAppTheme } from './createAppTheme';
|
|
2
3
|
export { assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, reportOffPaletteColours, reportKitSxBypasses, reportRedundantThemeValues, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './themeCompleteness';
|
|
3
4
|
export { KIT_COMPONENT_SX_REGISTRY } from './kitSxRegistry';
|
package/package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micha.bigler/ui-core-micha",
|
|
3
|
-
"version": "2.41.
|
|
3
|
+
"version": "2.41.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"./src/index.js",
|
|
8
|
+
"./dist/index.js",
|
|
9
|
+
"./src/theme/index.js",
|
|
10
|
+
"./dist/theme/index.js",
|
|
11
|
+
"./src/theme/createAppTheme.js",
|
|
12
|
+
"./dist/theme/createAppTheme.js",
|
|
13
|
+
"./src/theme/fonts.js",
|
|
14
|
+
"./dist/theme/fonts.js",
|
|
15
|
+
"./src/notifications/serviceWorker/sw.js",
|
|
16
|
+
"./dist/notifications/serviceWorker/sw.js"
|
|
17
|
+
],
|
|
6
18
|
"repository": {
|
|
7
19
|
"type": "git",
|
|
8
20
|
"url": "https://github.com/bigler-webapps/ui-core-micha"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { authTranslations } from './authTranslations';
|
|
2
|
+
import { chartsTranslations } from './chartsTranslations';
|
|
3
|
+
import { messagingTranslations } from './messagingTranslations';
|
|
4
|
+
import { notificationsTranslations } from './notificationsTranslations';
|
|
5
|
+
import { onboardingTranslations } from './onboardingTranslations';
|
|
6
|
+
import { sectionNavTranslations } from './sectionNavTranslations';
|
|
7
|
+
import { userMenuTranslations } from './userMenuTranslations';
|
|
8
|
+
|
|
9
|
+
export const uiCoreTranslations = {
|
|
10
|
+
...authTranslations,
|
|
11
|
+
...chartsTranslations,
|
|
12
|
+
...messagingTranslations,
|
|
13
|
+
...notificationsTranslations,
|
|
14
|
+
...onboardingTranslations,
|
|
15
|
+
...sectionNavTranslations,
|
|
16
|
+
...userMenuTranslations,
|
|
17
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// index.js (Entry Point deiner Library)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import { sectionNavTranslations } from './i18n/sectionNavTranslations';
|
|
9
|
-
import { userMenuTranslations } from './i18n/userMenuTranslations';
|
|
3
|
+
// Deliberately unconditional: font CSS ships in every consumer bundle regardless of what they
|
|
4
|
+
// import (not tree-shaken). If this import is ever removed, drop this file from the
|
|
5
|
+
// `sideEffects` array in package.json in the same change — decoupling them silently reintroduces
|
|
6
|
+
// the false-sideEffects-false hazard DX-2 exists to avoid.
|
|
7
|
+
import './theme/fonts';
|
|
10
8
|
|
|
11
9
|
// --- 0. Theme ---
|
|
12
10
|
export {
|
|
@@ -142,13 +140,4 @@ export { notificationsTranslations } from './i18n/notificationsTranslations';
|
|
|
142
140
|
export { onboardingTranslations } from './i18n/onboardingTranslations';
|
|
143
141
|
export { chartsTranslations } from './i18n/chartsTranslations';
|
|
144
142
|
export { messagingTranslations } from './i18n/messagingTranslations';
|
|
145
|
-
|
|
146
|
-
export const uiCoreTranslations = {
|
|
147
|
-
...authTranslations,
|
|
148
|
-
...chartsTranslations,
|
|
149
|
-
...messagingTranslations,
|
|
150
|
-
...notificationsTranslations,
|
|
151
|
-
...onboardingTranslations,
|
|
152
|
-
...sectionNavTranslations,
|
|
153
|
-
...userMenuTranslations,
|
|
154
|
-
};
|
|
143
|
+
export { uiCoreTranslations } from './i18n/uiCoreTranslations';
|
package/src/theme/index.js
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { gzipSync } from 'node:zlib';
|
|
6
|
+
|
|
7
|
+
import { build } from 'vite';
|
|
8
|
+
import { afterAll, expect, test } from 'vitest';
|
|
9
|
+
|
|
10
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const packageManifest = JSON.parse(
|
|
12
|
+
await readFile(path.join(packageRoot, 'package.json'), 'utf8'),
|
|
13
|
+
);
|
|
14
|
+
const packageName = packageManifest.name;
|
|
15
|
+
let temporaryRoot;
|
|
16
|
+
|
|
17
|
+
async function bundlePackageExport({ caseName, exportName, manifestField }) {
|
|
18
|
+
temporaryRoot ||= await mkdtemp(path.join(tmpdir(), 'ui-core-tree-shaking-'));
|
|
19
|
+
const consumerRoot = path.join(temporaryRoot, caseName);
|
|
20
|
+
const installedPackageRoot = path.join(
|
|
21
|
+
consumerRoot,
|
|
22
|
+
'node_modules',
|
|
23
|
+
...packageName.split('/'),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
await mkdir(installedPackageRoot, { recursive: true });
|
|
27
|
+
await cp(
|
|
28
|
+
path.join(packageRoot, 'dist'),
|
|
29
|
+
path.join(installedPackageRoot, 'dist'),
|
|
30
|
+
{ recursive: true },
|
|
31
|
+
);
|
|
32
|
+
const installedFontRoot = path.join(
|
|
33
|
+
consumerRoot,
|
|
34
|
+
'node_modules',
|
|
35
|
+
'@fontsource',
|
|
36
|
+
'dm-sans',
|
|
37
|
+
);
|
|
38
|
+
await mkdir(path.dirname(installedFontRoot), { recursive: true });
|
|
39
|
+
await cp(
|
|
40
|
+
path.join(packageRoot, 'node_modules', '@fontsource', 'dm-sans'),
|
|
41
|
+
installedFontRoot,
|
|
42
|
+
{ recursive: true },
|
|
43
|
+
);
|
|
44
|
+
await writeFile(
|
|
45
|
+
path.join(installedPackageRoot, 'package.json'),
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
name: packageName,
|
|
48
|
+
version: packageManifest.version,
|
|
49
|
+
[manifestField]: packageManifest[manifestField],
|
|
50
|
+
sideEffects: packageManifest.sideEffects,
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const entryPath = path.join(consumerRoot, 'entry.js');
|
|
55
|
+
await writeFile(entryPath, `export { ${exportName} } from '${packageName}';\n`);
|
|
56
|
+
|
|
57
|
+
const result = await build({
|
|
58
|
+
configFile: false,
|
|
59
|
+
logLevel: 'silent',
|
|
60
|
+
root: consumerRoot,
|
|
61
|
+
build: {
|
|
62
|
+
minify: true,
|
|
63
|
+
rollupOptions: {
|
|
64
|
+
input: entryPath,
|
|
65
|
+
preserveEntrySignatures: 'strict',
|
|
66
|
+
external: (id) => (
|
|
67
|
+
id !== packageName
|
|
68
|
+
&& !id.startsWith('@fontsource/dm-sans')
|
|
69
|
+
&& !id.startsWith('.')
|
|
70
|
+
&& !path.isAbsolute(id)
|
|
71
|
+
),
|
|
72
|
+
output: {
|
|
73
|
+
format: 'es',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
write: false,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const outputs = Array.isArray(result)
|
|
80
|
+
? result.flatMap((buildResult) => buildResult.output)
|
|
81
|
+
: result.output;
|
|
82
|
+
const entryChunk = outputs.find((output) => output.type === 'chunk' && output.isEntry);
|
|
83
|
+
|
|
84
|
+
expect(entryChunk, `${manifestField} entry should produce a bundle`).toBeDefined();
|
|
85
|
+
return {
|
|
86
|
+
code: entryChunk.code,
|
|
87
|
+
css: outputs
|
|
88
|
+
.filter((output) => output.type === 'asset' && output.fileName.endsWith('.css'))
|
|
89
|
+
.map((output) => output.source.toString())
|
|
90
|
+
.join('\n'),
|
|
91
|
+
rawBytes: Buffer.byteLength(entryChunk.code),
|
|
92
|
+
gzipBytes: gzipSync(entryChunk.code).byteLength,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
afterAll(async () => {
|
|
97
|
+
if (temporaryRoot) await rm(temporaryRoot, { recursive: true, force: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('built package entries resolve and unused translation bundles tree-shake', async () => {
|
|
101
|
+
const authOnly = await bundlePackageExport({
|
|
102
|
+
caseName: 'auth-main',
|
|
103
|
+
exportName: 'authTranslations',
|
|
104
|
+
manifestField: 'main',
|
|
105
|
+
});
|
|
106
|
+
const aggregate = await bundlePackageExport({
|
|
107
|
+
caseName: 'aggregate-module',
|
|
108
|
+
exportName: 'uiCoreTranslations',
|
|
109
|
+
manifestField: 'module',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(authOnly.code).toContain('Auth.LOGIN');
|
|
113
|
+
expect(authOnly.code).not.toContain('ChartFrame.LOADING');
|
|
114
|
+
expect(authOnly.css).toContain('DM Sans');
|
|
115
|
+
expect(aggregate.code).toContain('ChartFrame.LOADING');
|
|
116
|
+
expect(aggregate.rawBytes).toBeGreaterThan(authOnly.rawBytes);
|
|
117
|
+
expect(aggregate.gzipBytes).toBeGreaterThan(authOnly.gzipBytes);
|
|
118
|
+
|
|
119
|
+
const rawDelta = aggregate.rawBytes - authOnly.rawBytes;
|
|
120
|
+
const gzipDelta = aggregate.gzipBytes - authOnly.gzipBytes;
|
|
121
|
+
console.info(
|
|
122
|
+
`tree-shaking measurement: auth=${authOnly.rawBytes} B raw/${authOnly.gzipBytes} B gzip; `
|
|
123
|
+
+ `aggregate=${aggregate.rawBytes} B raw/${aggregate.gzipBytes} B gzip; `
|
|
124
|
+
+ `delta=+${rawDelta} B raw/+${gzipDelta} B gzip`,
|
|
125
|
+
);
|
|
126
|
+
}, 15_000);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# DX-2 — Declare `sideEffects` so consumers can tree-shake the kit
|
|
2
|
+
|
|
3
|
+
**Target repo:** `ui-core-micha` (branch `main`)
|
|
4
|
+
**Tier:** 3 — shared-core, and it changes what every consumer's bundler is permitted to discard.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## A. Envelope
|
|
9
|
+
|
|
10
|
+
### Goal
|
|
11
|
+
|
|
12
|
+
`package.json` declares **no `sideEffects` field**. Without it a bundler must assume every module in the
|
|
13
|
+
package can have an import side effect and therefore cannot drop unused exports. **Twelve consumers ship the
|
|
14
|
+
whole kit regardless of what they import.**
|
|
15
|
+
|
|
16
|
+
This is not a theory. `hram/THEME-2` switched hram from three named translation bundles to the full
|
|
17
|
+
`uiCoreTranslations` aggregate and measured the built-size delta at **−11 bytes** — not the increase everyone
|
|
18
|
+
expected, because the three-bundle import was already pulling all seven.
|
|
19
|
+
|
|
20
|
+
### Why this is worth a Tier-3 change
|
|
21
|
+
|
|
22
|
+
Two decisions currently rest on a number that this defect makes meaningless:
|
|
23
|
+
|
|
24
|
+
- **`I18N-1` measured `+28,943 B` raw / `+7,327 B` gzip** for an auth-only app switching to the aggregate.
|
|
25
|
+
That measured a real difference in *source* reachability which the build never realises. As a decision input
|
|
26
|
+
it is void.
|
|
27
|
+
- **`I18N-2` is `blocked` on exactly this.** It was framed as a bundle-size optimisation via
|
|
28
|
+
self-registration, and its stated input is a size delta that cannot be measured until the package can
|
|
29
|
+
tree-shake at all. **This WO is its unblocker**, and it is a far smaller change than reworking how twelve
|
|
30
|
+
consumers resolve strings.
|
|
31
|
+
|
|
32
|
+
### Scope
|
|
33
|
+
|
|
34
|
+
1. **Prove it first, per module.** Walk every module reachable from the package root and establish that
|
|
35
|
+
importing it has **no** side effect: no top-level statement with an observable effect, no polyfill or
|
|
36
|
+
registry mutation, no `import './x.css'` or equivalent, no module-scope singleton creation whose existence
|
|
37
|
+
something depends on. **This is the work**; the field is one line.
|
|
38
|
+
2. **`package.json`** — add `"sideEffects": false` if step 1 holds for everything. If it does **not** hold for
|
|
39
|
+
some module, add the array form listing exactly those files rather than the blanket `false`.
|
|
40
|
+
3. **Re-measure the delta `I18N-1` got wrong**: the built size of an auth-only app importing only
|
|
41
|
+
`authTranslations` versus the full aggregate, against a package that can now tree-shake. Report the number.
|
|
42
|
+
4. Update `I18N-2`'s register note with that number, so it is either authorable or droppable on evidence.
|
|
43
|
+
|
|
44
|
+
### Non-goals / do not touch
|
|
45
|
+
|
|
46
|
+
- **No `exports` map.** It would allow subpath imports and give a further size win, but it changes which
|
|
47
|
+
import paths resolve at all — a breaking-change risk for twelve consumers in the same commit. Separate
|
|
48
|
+
decision, and this WO's measurement is its input.
|
|
49
|
+
- **No component, theme, chart or i18n change.** Nothing in `src/` is edited for its own sake; the only source
|
|
50
|
+
changes permitted are ones step 1 proves are needed to make a module genuinely side-effect-free, and each
|
|
51
|
+
must be reported rather than folded in silently.
|
|
52
|
+
- **No consumer pin bumps.** Apps pick this up when they next bump.
|
|
53
|
+
- **`I18N-2` itself.** This unblocks it; it does not implement it.
|
|
54
|
+
|
|
55
|
+
### Risks
|
|
56
|
+
|
|
57
|
+
- **The real failure mode is a false `sideEffects: false`.** If a module *does* have an import side effect and
|
|
58
|
+
the field says otherwise, a bundler silently drops it and the consuming app breaks at runtime in a way no
|
|
59
|
+
test here will catch. That is why step 1 is the scope and the field is an afterthought. **If any module is
|
|
60
|
+
uncertain, use the array form and list it** — a partial declaration that is true beats a blanket one that is
|
|
61
|
+
nearly true.
|
|
62
|
+
- **`side effect` includes the theme.** `createAppTheme`, the token module and the registry are the places to
|
|
63
|
+
look hardest: a module that builds an object at import time is fine, but one that *registers* itself
|
|
64
|
+
somewhere is not.
|
|
65
|
+
- **Every consumer's bundle changes shape**, even though no source behaviour changes. That is the intent, and
|
|
66
|
+
it is why this is Tier 3 rather than a chore.
|
|
67
|
+
|
|
68
|
+
### Required tests to WRITE
|
|
69
|
+
|
|
70
|
+
- **A test asserting the field matches reality** is not really possible in-repo — a bundler is what enforces
|
|
71
|
+
it. So instead: **one build-level check** that the package builds and its entry points still resolve after
|
|
72
|
+
the declaration, plus the measurement in scope item 3 recorded as a number in the register note.
|
|
73
|
+
- If step 1 finds a module needing a source change to become side-effect-free, that change gets a test.
|
|
74
|
+
- Otherwise **no new unit tests** — and saying so is the correct answer for a `package.json` field.
|
|
75
|
+
|
|
76
|
+
Scoped run: the existing suite, to confirm nothing regressed. Not a full-estate exercise.
|
|
77
|
+
|
|
78
|
+
### Parity guardrail
|
|
79
|
+
|
|
80
|
+
No API, behaviour or visual change. Every currently-working import must keep working; what changes is only
|
|
81
|
+
what a consumer's bundler may discard.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## B. Implementation map
|
|
86
|
+
|
|
87
|
+
*Filled by the Orchestrator on `git pull` — see `AGENTS.md` → "Work Order".*
|
|
88
|
+
|
|
89
|
+
**Repo root:** `ui-core-micha` (this is the package root — `package.json` lives here, `src/index.js` is
|
|
90
|
+
the package entry per `"main"`/`"module"`). Build: `pnpm run build` (tsc, `tsconfig.build.json`,
|
|
91
|
+
`rootDir: src` → `outDir: dist`). Tests: `pnpm test` (vitest run), config likely under `tests/` or
|
|
92
|
+
`vite.config.mjs`.
|
|
93
|
+
|
|
94
|
+
**Entry point to walk:** [`src/index.js`](../src/index.js) — every export re-exported from here is a
|
|
95
|
+
module the audit must cover. Rough module groups, in the order they're exported: `theme/` (7 exports),
|
|
96
|
+
`auth/` (AuthContext, UserMenu, apiClient, authApi, authService), `layout/` (PageLayout, SectionNav),
|
|
97
|
+
`pages/` (7 page components), `components/` (12 components incl. charts subtree), `i18n/` (7 translation
|
|
98
|
+
bundle modules, pure data objects), `notifications/` (7 exports incl. a `realtime` module — check for
|
|
99
|
+
WebSocket/EventSource construction at module scope, not just component-body), `messaging/` (13 exports,
|
|
100
|
+
same realtime-connection caution), `onboarding/` (8 exports).
|
|
101
|
+
|
|
102
|
+
**Known non-trivial finding — do not silently "fix" this, report it:**
|
|
103
|
+
[`src/theme/createAppTheme.js:1-3`](../src/theme/createAppTheme.js) has three top-level
|
|
104
|
+
`import '@fontsource/dm-sans/400.css'` (+ `/500.css`, `/600.css`) — these ARE import-level side effects
|
|
105
|
+
(CSS injected into the document on import, unconditionally, regardless of whether `createAppTheme` is
|
|
106
|
+
even called). This module must be **excluded** from a blanket `sideEffects: false` — either list
|
|
107
|
+
`src/theme/createAppTheme.js` explicitly in the array form, or (better, since it's a source-level fix
|
|
108
|
+
consistent with the WO's "no false negatives" risk framing) move the three font-CSS imports into a
|
|
109
|
+
separate module (e.g. `src/theme/fonts.css.js` or inline at the one leaf that needs them) so the rest of
|
|
110
|
+
`theme/` stays tree-shakeable and only that one file is excluded. Whichever path is taken, it must be
|
|
111
|
+
**reported explicitly** in the WO's audit output, not folded in silently (Envelope: "each must be
|
|
112
|
+
reported rather than folded in silently").
|
|
113
|
+
|
|
114
|
+
**Areas most likely to hide further side effects (audit these hardest, per the Envelope's own risk
|
|
115
|
+
note):** `theme/` (registry singletons — `KIT_COMPONENT_SX_REGISTRY` is exported as a value; confirm it's
|
|
116
|
+
a plain object literal built at import with no external registration call), `notifications/realtime.js`
|
|
117
|
+
and `messaging/` (anything opening a socket/listener at module scope rather than inside a hook/effect),
|
|
118
|
+
`auth/apiClient.js` (axios instance creation is fine — a side-effect-free object build — but check for
|
|
119
|
+
any `axios.interceptors.*.use(...)` called at module scope outside a factory function, which would be a
|
|
120
|
+
real side effect since it mutates a shared axios default instance).
|
|
121
|
+
|
|
122
|
+
**Do-not-touch:** no `exports` map in `package.json` (Envelope non-goal). No component/theme/chart/i18n
|
|
123
|
+
behaviour change — only source edits that make a module genuinely side-effect-free, each reported.
|
|
124
|
+
|
|
125
|
+
**Progress contract:** emit `PLAN: …` first, then `PROGRESS: [n/total] <module-group>` before auditing
|
|
126
|
+
each group above and `… done` after, and a final `RESULT: DONE` or `RESULT: BLOCKED <reason>`. No gap
|
|
127
|
+
> ~2 min. Work from this package — open files only to verify specific claims, not a blind full-repo read.
|
|
128
|
+
|
|
129
|
+
## Preamble
|
|
130
|
+
|
|
131
|
+
The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there is
|
|
132
|
+
no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and
|
|
133
|
+
the app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
|
|
134
|
+
unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md`** — the register row
|
|
135
|
+
and the review verdicts are the orchestrator's alone. Do NOT `git add`/`commit`/`push` — leave every
|
|
136
|
+
change uncommitted in the working tree for the orchestrator's independent review. WRITE the tests the
|
|
137
|
+
"Required tests to WRITE" section calls for AND **RUN the tests you just wrote** to confirm they execute
|
|
138
|
+
and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review). The
|
|
139
|
+
orchestrator re-runs the authoritative set + does the independent review after you finish — those are
|
|
140
|
+
the gate; your own run does not count as the gate.
|
|
141
|
+
|
|
142
|
+
Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
|
|
143
|
+
`PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
|
|
144
|
+
completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
|
|
145
|
+
`RESULT: DONE|BLOCKED <reason>`.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## C. Orchestrator only
|
|
150
|
+
|
|
151
|
+
> **STOP — if you are the implementer reading this work order as your own specification, this section is NOT
|
|
152
|
+
> addressed to you. Skip it entirely.** It tells the Orchestrator how to invoke you. **You ARE that
|
|
153
|
+
> invocation — do NOT shell out to `codex exec`, do NOT spawn reviewers, do NOT edit `WORK_ORDERS.md`, and do
|
|
154
|
+
> NOT `git add`/`commit`/`push`.**
|
|
155
|
+
|
|
156
|
+
### Execution directive
|
|
157
|
+
|
|
158
|
+
Check `.claude/codex-status.md` first. **No line for the current date means use Codex.** A dated `unavailable`
|
|
159
|
+
line for today means skip the attempt, implement directly in Claude and name the record — that flips
|
|
160
|
+
authorship. Otherwise one probe, outcome written back either way.
|
|
161
|
+
|
|
162
|
+
### Review routing
|
|
163
|
+
|
|
164
|
+
Tier 3: **independent `reviewer`** — mandatory, and the substantive one here: ask it to **independently
|
|
165
|
+
re-check the side-effect audit**, module by module, rather than accept the implementer's list. That audit is
|
|
166
|
+
the entire safety argument. **No `ui_reviewer`** — nothing renders. No `sec_reviewer`.
|
|
167
|
+
|
|
168
|
+
### Verification
|
|
169
|
+
|
|
170
|
+
Nothing renders, so no gate. The verification is the audit plus the re-measured delta, both reported as
|
|
171
|
+
findings rather than assertions.
|
|
172
|
+
|
|
173
|
+
**A push to `main` here IS the npm publish** — `publish.yml` triggers on `push: branches: [main]` with `paths`
|
|
174
|
+
covering `package.json` and `src/**`, and npm publishes cannot be undone. So decide the version deliberately
|
|
175
|
+
before pushing: this is a **patch** by scope (no new capability, no API change), and it will go out the moment
|
|
176
|
+
it lands.
|
|
177
|
+
|
|
178
|
+
### Register + commit
|
|
179
|
+
|
|
180
|
+
One row, `DX-2`. `done` only with the independent reviewer and verdict named, plus the measured delta.
|
|
181
|
+
Update `I18N-2`'s note in the same commit — that cross-reference is half the point of this WO.
|
|
182
|
+
|
|
183
|
+
### Mini-handover
|
|
184
|
+
|
|
185
|
+
Repo: `ui-core-micha`, branch `main`. WO: `work-orders/DX-2.md`. The one-line field is trivial; **the
|
|
186
|
+
per-module side-effect audit is the work**, and a wrong `false` breaks consumers silently at runtime. Use the
|
|
187
|
+
array form if anything is uncertain. Re-measure the aggregate delta and write it into `I18N-2`. Tier 3,
|
|
188
|
+
`reviewer` mandatory and asked to redo the audit. **Pushing to main publishes.** Follow `orchestrate-codex`.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# DX-3 — `publish.yml` tests before it builds, so `2.41.2` cannot publish
|
|
2
|
+
|
|
3
|
+
**Target repo:** `ui-core-micha` (branch `main`)
|
|
4
|
+
**Tier:** 3 — CI is a Tier-3 surface regardless of diff size.
|
|
5
|
+
|
|
6
|
+
Short-form WO: two steps swapped, then an operator-confirmed dispatch.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## A. Envelope
|
|
11
|
+
|
|
12
|
+
### Goal
|
|
13
|
+
|
|
14
|
+
`DX-2` landed on `main` (`31675d8`, `e75192f`) and **its publish failed**, so `@micha.bigler/ui-core-micha`
|
|
15
|
+
is still at `2.41.1` on the registry while `package.json` says `2.41.2`. Nothing DX-2 produced can be
|
|
16
|
+
consumed until this is fixed.
|
|
17
|
+
|
|
18
|
+
Cause, from run
|
|
19
|
+
[32029216699](https://github.com/bigler-webapps/ui-core-micha/actions/runs/32029216699) —
|
|
20
|
+
`completed/failure`:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
FAIL tests/packageTreeShaking.test.js > built package entries resolve and unused translation bundles tree-shake
|
|
24
|
+
Error: ENOENT: no such file or directory, lstat '.../ui-core-micha/dist'
|
|
25
|
+
Test Files 1 failed | 64 passed (65)
|
|
26
|
+
Tests 1 failed | 510 passed (511)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`publish.yml` runs **`Run tests` before `Build`**. `DX-2`'s new test copies the package's own `dist/` to
|
|
30
|
+
assemble a fake installed consumer, and **`dist/` is gitignored** (`.gitignore:2`), so in a clean checkout
|
|
31
|
+
it does not exist yet. The test is not flaky and it is not wrong — it inspects the built artifact, which is
|
|
32
|
+
the whole point of it. **It simply cannot pass in the order the workflow runs.**
|
|
33
|
+
|
|
34
|
+
It passed locally at 511/511 because a `dist/` from an earlier `pnpm run build` was lying around. That is
|
|
35
|
+
the failure mode, not the test.
|
|
36
|
+
|
|
37
|
+
### Scope
|
|
38
|
+
|
|
39
|
+
1. **`.github/workflows/publish.yml`** — move the `Build` step **above** `Run tests`.
|
|
40
|
+
**Both steps carry `if: steps.version_check.outputs.should_publish == 'true'`; that condition must stay
|
|
41
|
+
on each.** Nothing else in the file changes.
|
|
42
|
+
2. **Publish `2.41.2`** — see the dispatch note below, which is the part most likely to be missed.
|
|
43
|
+
|
|
44
|
+
### The dispatch is required, and it is not automatic
|
|
45
|
+
|
|
46
|
+
`publish.yml` triggers on `push` to `main` filtered to `package.json`, `pnpm-lock.yaml`, `src/**` and
|
|
47
|
+
`tsconfig.build.json`. **`.github/workflows/**` is not in that list**, so landing this fix does **not**
|
|
48
|
+
re-trigger the publish. `2.41.2` would sit unpublished until some unrelated `src/**` change happened to
|
|
49
|
+
push it out.
|
|
50
|
+
|
|
51
|
+
So: after the fix lands, **dispatch `publish.yml` on `main`**. `publish.yml` is **not** on the dispatch
|
|
52
|
+
safe-allowlist (`weekly-routines`, `terraform-plan`), so per `AGENTS.md` name the exact workflow and inputs
|
|
53
|
+
and **wait for an explicit operator go** before running it.
|
|
54
|
+
|
|
55
|
+
`should_publish` compares `package.json` against the registry, so with `2.41.2` local and `2.41.1`
|
|
56
|
+
published it will evaluate true and the dispatch will publish. No version bump is needed for this WO — and
|
|
57
|
+
none should be added, because the artifact being published is DX-2's, not this WO's.
|
|
58
|
+
|
|
59
|
+
### Non-goals / do not touch
|
|
60
|
+
|
|
61
|
+
- **`tests/packageTreeShaking.test.js`.** It is correct. Do not make it build `dist/` itself, do not skip it
|
|
62
|
+
when `dist/` is absent, and above all do not delete it — a skip would silently retire the only check that
|
|
63
|
+
the `sideEffects` declaration actually tree-shakes, which is DX-2's entire deliverable.
|
|
64
|
+
- **The `sideEffects` array, `src/theme/fonts.js`, or anything else DX-2 landed.** Reviewed and correct.
|
|
65
|
+
- **The trigger `paths` list.** Adding `.github/workflows/**` would make every workflow edit attempt a
|
|
66
|
+
publish. That is a worse default than one dispatch.
|
|
67
|
+
- **The version.** Stays `2.41.2`.
|
|
68
|
+
- Any other workflow, any source file.
|
|
69
|
+
|
|
70
|
+
### Recorded, deliberately out of scope
|
|
71
|
+
|
|
72
|
+
**`publish.yml` is the only workflow in this repo that runs tests, and it runs them only when
|
|
73
|
+
`should_publish == 'true'`.** So ucm's suite executes in CI *only on a version bump* — a `src/**` change
|
|
74
|
+
that does not bump the version is never tested by CI at all. That is a real gap in a shared-core package
|
|
75
|
+
consumed by twelve apps, and it is a separate decision (a `ci.yml` for this repo, or a `pull_request`
|
|
76
|
+
trigger), not something to fold into a two-line reorder.
|
|
77
|
+
|
|
78
|
+
### Risks
|
|
79
|
+
|
|
80
|
+
- **Low, and the shape is known**: after the swap a build failure surfaces before test results instead of
|
|
81
|
+
after. That is an improvement, not a regression — you want to know the artifact is buildable before
|
|
82
|
+
spending 64 s on tests against it.
|
|
83
|
+
- **The real risk is stopping at the commit.** A landed reorder with no dispatch leaves `2.41.2`
|
|
84
|
+
unpublished and looks done. The acceptance criterion below is the registry, not the workflow file.
|
|
85
|
+
|
|
86
|
+
### Required tests to WRITE
|
|
87
|
+
|
|
88
|
+
**None.** Nothing to unit-test in a step reorder, and manufacturing one would be noise. The verification is
|
|
89
|
+
a green run plus the registry.
|
|
90
|
+
|
|
91
|
+
### Acceptance
|
|
92
|
+
|
|
93
|
+
1. A `publish.yml` run on `main` completes **green**, with `tests/packageTreeShaking.test.js` **passing**
|
|
94
|
+
rather than skipped — 511/511.
|
|
95
|
+
2. **`npm view @micha.bigler/ui-core-micha version` returns `2.41.2`.** The registry is the acceptance,
|
|
96
|
+
not the commit and not the green tile.
|
|
97
|
+
|
|
98
|
+
### Parity guardrail
|
|
99
|
+
|
|
100
|
+
No source, API, behaviour or visual change. What changes is the order two CI steps run in.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## B. Implementation map
|
|
105
|
+
|
|
106
|
+
*Filled by the Orchestrator on `git pull` — see `AGENTS.md` → "Work Order".*
|
|
107
|
+
|
|
108
|
+
**Repo root:** `ui-core-micha` (workflow lives at repo root, `.github/workflows/publish.yml`).
|
|
109
|
+
|
|
110
|
+
**Exact change:** in [`.github/workflows/publish.yml`](../.github/workflows/publish.yml), the step
|
|
111
|
+
order today (verified on `git pull`, lines 112-118) is:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
- name: Run tests
|
|
115
|
+
if: steps.version_check.outputs.should_publish == 'true'
|
|
116
|
+
run: pnpm run test
|
|
117
|
+
|
|
118
|
+
- name: Build
|
|
119
|
+
if: steps.version_check.outputs.should_publish == 'true'
|
|
120
|
+
run: pnpm run build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Swap so `Build` runs first, `Run tests` second — same two `if:` conditions, unchanged, one on each
|
|
124
|
+
step. Nothing else in the file (`Check out code`, `Set up Node`, `Decide whether to publish`,
|
|
125
|
+
`Set up pnpm`, `Show version from package.json`, `Install dependencies (pnpm)`,
|
|
126
|
+
`Ensure recent npm for trusted publishing`, `Publish to npm`) moves or changes.
|
|
127
|
+
|
|
128
|
+
**Do-not-touch (repeat of the Envelope's non-goals, stated here for the implementer):**
|
|
129
|
+
`tests/packageTreeShaking.test.js`, the `sideEffects` array / `src/theme/fonts.js` (DX-2, already
|
|
130
|
+
landed and reviewed), the `on: push: paths` trigger list, `package.json`'s version (stays `2.41.2`).
|
|
131
|
+
This WO touches exactly one file.
|
|
132
|
+
|
|
133
|
+
**Progress contract:** `PLAN: …` naming the single edit, `PROGRESS: [1/1] swap Build/Run-tests order
|
|
134
|
+
in publish.yml … done`, then `RESULT: DONE`.
|
|
135
|
+
|
|
136
|
+
## Preamble
|
|
137
|
+
|
|
138
|
+
The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there is
|
|
139
|
+
no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and
|
|
140
|
+
the app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
|
|
141
|
+
beyond the one named swap. Do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md`** — the register
|
|
142
|
+
row and the review verdicts are the orchestrator's alone. Do NOT `git add`/`commit`/`push`, and do NOT
|
|
143
|
+
dispatch any workflow — leave every change uncommitted in the working tree for the orchestrator's
|
|
144
|
+
independent review. This WO's "Required tests to WRITE" is explicitly **none** — do not write or run
|
|
145
|
+
any test; there is nothing to verify locally that the orchestrator's own read of the diff and the
|
|
146
|
+
reviewer's pass don't already cover.
|
|
147
|
+
|
|
148
|
+
Narrate continuously: a `PLAN: <step1> | …` line up front, then a single-line
|
|
149
|
+
`PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
|
|
150
|
+
completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
|
|
151
|
+
`RESULT: DONE|BLOCKED <reason>`.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## C. Orchestrator only
|
|
156
|
+
|
|
157
|
+
> **STOP — if you are the implementer reading this work order as your own specification, this section is NOT
|
|
158
|
+
> addressed to you. Skip it entirely.** It tells the Orchestrator how to invoke you. **You ARE that
|
|
159
|
+
> invocation — do NOT shell out to `codex exec`, do NOT spawn reviewers, do NOT edit `WORK_ORDERS.md`, do
|
|
160
|
+
> NOT dispatch any workflow, and do NOT `git add`/`commit`/`push`.**
|
|
161
|
+
|
|
162
|
+
### Execution directive
|
|
163
|
+
|
|
164
|
+
Check `.claude/codex-status.md` first. **No line for the current date means use Codex.** A dated
|
|
165
|
+
`unavailable` line for today means skip the attempt, implement directly in Claude and name the record —
|
|
166
|
+
that flips authorship, which keeps the independent `reviewer` mandatory. Otherwise one probe, outcome
|
|
167
|
+
written back either way.
|
|
168
|
+
|
|
169
|
+
### Review routing
|
|
170
|
+
|
|
171
|
+
Tier 3: **independent `reviewer`** — mandatory even for two lines, because CI is the surface. **No
|
|
172
|
+
`ui_reviewer`** (nothing renders), no `sec_reviewer`. Ask the reviewer to confirm the `if:` conditions
|
|
173
|
+
survived the move on **both** steps — dropping one would make a step run unconditionally, which is the only
|
|
174
|
+
way a two-line reorder can go wrong.
|
|
175
|
+
|
|
176
|
+
### Verification
|
|
177
|
+
|
|
178
|
+
Acceptance criterion 2 is the verification: **read the registry.** A green tile is not a publish, and this
|
|
179
|
+
session has now had the publish status misreported in both directions on the same package — once assumed
|
|
180
|
+
published when it was not, once assumed unpublished when it was. `npm view`, every time.
|
|
181
|
+
|
|
182
|
+
### Register + commit
|
|
183
|
+
|
|
184
|
+
One row, `DX-3`. `done` only with the reviewer's verdict named **and** the registry showing `2.41.2`. Also
|
|
185
|
+
update `DX-2`'s note: it currently reads as fully delivered, and it was not — its publish failed. Leaving
|
|
186
|
+
that uncorrected is how the next person concludes `2.41.2` is available when it is not.
|
|
187
|
+
|
|
188
|
+
### Mini-handover
|
|
189
|
+
|
|
190
|
+
Repo: `ui-core-micha`, branch `main`. WO: `work-orders/DX-3.md`. Move `Build` above `Run tests` in
|
|
191
|
+
`.github/workflows/publish.yml`, keeping the `if: should_publish` condition on both. Then **ask the operator
|
|
192
|
+
to confirm a `publish.yml` dispatch on `main`** — the workflow's own path filter excludes
|
|
193
|
+
`.github/workflows/**`, so landing the fix does not re-trigger it, and `2.41.2` stays unpublished until
|
|
194
|
+
someone dispatches. Acceptance is `npm view` returning `2.41.2`, not a green tile. Tier 3, `reviewer`
|
|
195
|
+
mandatory. Follow `orchestrate-codex`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# I18N-3 — Backfill Swahili in `messagingTranslations` and restore the four-locale shape test
|
|
2
|
+
|
|
3
|
+
**Target repo:** `ui-core-micha` (branch `main`)
|
|
4
|
+
**Tier:** 3 — shared-core, and it re-tightens a test the estate deliberately loosened.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## A. Envelope
|
|
9
|
+
|
|
10
|
+
### Goal
|
|
11
|
+
|
|
12
|
+
`src/i18n/messagingTranslations.ts` carries **116 keys with `de`/`fr`/`en` and zero `sw`**. Every one of the
|
|
13
|
+
other six bundles carries all four locales:
|
|
14
|
+
|
|
15
|
+
| bundle | de | fr | en | sw |
|
|
16
|
+
|---|---|---|---|---|
|
|
17
|
+
| `auth` | ✓ | ✓ | ✓ | ✓ |
|
|
18
|
+
| `charts` | 12 | 12 | 12 | 12 |
|
|
19
|
+
| **`messaging`** | **116** | **116** | **116** | **0** |
|
|
20
|
+
| `notifications` | 30 | 30 | 30 | 30 |
|
|
21
|
+
| `onboarding` | 27 | 27 | 27 | 27 |
|
|
22
|
+
| `sectionNav` | 3 | 3 | 3 | 3 |
|
|
23
|
+
| `userMenu` | 3 | 3 | 3 | 3 |
|
|
24
|
+
|
|
25
|
+
`I18N-1` hit this while writing its aggregate test: the required "every value keeps its `{de,fr,en,sw}` shape"
|
|
26
|
+
assertion failed, the gap was confirmed as pre-existing rather than caused by the merge, and the operator
|
|
27
|
+
scoped the test down to `{de,fr,en}` with the backfill recorded as a follow-up. **The follow-up was never
|
|
28
|
+
written, and a scoped-down test tends to stay scoped down.** That is what this WO closes.
|
|
29
|
+
|
|
30
|
+
### Scope
|
|
31
|
+
|
|
32
|
+
1. **`src/i18n/messagingTranslations.ts`** — add `sw` to all 116 keys.
|
|
33
|
+
2. **`tests/i18nAggregate.test.js`** — restore the shape assertion to `{de,fr,en,sw}` and **remove the comment
|
|
34
|
+
recording the scope-down**, since it no longer describes the state.
|
|
35
|
+
3. Nothing else. No other bundle is touched; they already conform.
|
|
36
|
+
|
|
37
|
+
### The translation quality question, which is this WO's real risk
|
|
38
|
+
|
|
39
|
+
**Machine-translated UI strings that nobody on the team can check are a worse outcome than an honest gap.**
|
|
40
|
+
Messaging strings are user-facing and include the kinds of phrase that go wrong invisibly — actions,
|
|
41
|
+
confirmations, error states, relative time. So:
|
|
42
|
+
|
|
43
|
+
- **Name the source of the translations in the register note.** If they are machine-produced, say so plainly
|
|
44
|
+
rather than shipping them as if reviewed.
|
|
45
|
+
- **Flag any key the translator was unsure of** rather than guessing. A `sw` value that is wrong is harder to
|
|
46
|
+
find later than one that is missing, because the shape test will then pass.
|
|
47
|
+
- **Do not invent terminology.** Where `notifications` or `onboarding` already has a Swahili term for the same
|
|
48
|
+
concept — send, reply, unread, conversation, attachment — **reuse their wording** rather than coining a
|
|
49
|
+
second one. Those bundles are the existing precedent and consistency across them matters more than any
|
|
50
|
+
individual phrasing.
|
|
51
|
+
|
|
52
|
+
### Who this is actually for
|
|
53
|
+
|
|
54
|
+
`sw` is configured as a locale in **hram** and in no other app measured. hram does **not** register
|
|
55
|
+
`messagingTranslations` today, so nothing is currently broken for a user — this is prevention, and the honest
|
|
56
|
+
framing is: it closes an inconsistency and re-arms a test, rather than fixing a live defect.
|
|
57
|
+
|
|
58
|
+
That matters for the effort trade-off, and it is why the quality bar above is not negotiable: there is no
|
|
59
|
+
urgency justifying unreviewed strings.
|
|
60
|
+
|
|
61
|
+
### Non-goals / do not touch
|
|
62
|
+
|
|
63
|
+
- **The other six bundles.** They conform.
|
|
64
|
+
- **The aggregate itself, the directory ratchet, or the key/value fidelity and collision checks.** Only the
|
|
65
|
+
locale-shape assertion changes.
|
|
66
|
+
- **`de`/`fr`/`en` values.** Not a re-translation pass.
|
|
67
|
+
- **Adding an eighth locale**, or any change to how consumers register strings (`I18N-2`'s territory).
|
|
68
|
+
|
|
69
|
+
### Risks
|
|
70
|
+
|
|
71
|
+
- **116 unreviewable strings.** The mitigation is disclosure, not confidence — see above.
|
|
72
|
+
- **Re-tightening the test is the irreversible half.** Once the assertion demands four locales, any future
|
|
73
|
+
bundle addition must supply `sw` or fail CI. That is the intent, and it is worth stating so the next person
|
|
74
|
+
adding a bundle is not surprised.
|
|
75
|
+
- **A push to `main` publishes.** See Part C.
|
|
76
|
+
|
|
77
|
+
### Required tests to WRITE
|
|
78
|
+
|
|
79
|
+
None new — **`tests/i18nAggregate.test.js`'s restored four-locale assertion IS the test**, and re-arming it is
|
|
80
|
+
scope item 2. It will fail until the backfill is complete, which is the correct behaviour and the reason it
|
|
81
|
+
belongs in the same change.
|
|
82
|
+
|
|
83
|
+
Scoped run: `i18nAggregate` plus the messaging export specs. Not the full suite — no dependency or theme
|
|
84
|
+
change.
|
|
85
|
+
|
|
86
|
+
### Parity guardrail
|
|
87
|
+
|
|
88
|
+
No API, behaviour or visual change for `de`/`fr`/`en` consumers. Purely additive.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## B. Implementation map
|
|
93
|
+
|
|
94
|
+
*Filled by the Orchestrator on `git pull` — see `AGENTS.md` → "Work Order".*
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## C. Orchestrator only
|
|
99
|
+
|
|
100
|
+
> **STOP — if you are the implementer reading this work order as your own specification, this section is NOT
|
|
101
|
+
> addressed to you. Skip it entirely.** It tells the Orchestrator how to invoke you. **You ARE that
|
|
102
|
+
> invocation — do NOT shell out to `codex exec`, do NOT spawn reviewers, do NOT edit `WORK_ORDERS.md`, and do
|
|
103
|
+
> NOT `git add`/`commit`/`push`.**
|
|
104
|
+
|
|
105
|
+
### Execution directive
|
|
106
|
+
|
|
107
|
+
Check `.claude/codex-status.md` first. **No line for the current date means use Codex.** A dated `unavailable`
|
|
108
|
+
line for today means skip the attempt, implement directly in Claude and name the record — that flips
|
|
109
|
+
authorship. Otherwise one probe, outcome written back either way.
|
|
110
|
+
|
|
111
|
+
### Review routing
|
|
112
|
+
|
|
113
|
+
Tier 3: **independent `reviewer`** — mandatory. Ask it specifically to check **terminology consistency against
|
|
114
|
+
`notifications` and `onboarding`**, and to confirm that no `de`/`fr`/`en` value was altered in passing. **No
|
|
115
|
+
`ui_reviewer`** — nothing renders. No `sec_reviewer`.
|
|
116
|
+
|
|
117
|
+
Neither reviewer can validate Swahili. **Say that in the note** rather than letting a clean verdict imply the
|
|
118
|
+
translations were checked.
|
|
119
|
+
|
|
120
|
+
### Verification
|
|
121
|
+
|
|
122
|
+
The restored four-locale assertion passing is the verification. Nothing renders, so no gate.
|
|
123
|
+
|
|
124
|
+
**A push to `main` here IS the npm publish** — `publish.yml` triggers on `push: branches: [main]` with `src/**`
|
|
125
|
+
in its paths, and publishes cannot be undone. Additive strings, so a **patch** by scope.
|
|
126
|
+
|
|
127
|
+
### Register + commit
|
|
128
|
+
|
|
129
|
+
One row, `I18N-3`. `done` only with the reviewer and verdict named, **the translation source stated**, and any
|
|
130
|
+
uncertain keys listed. Also update `I18N-1`'s note: its recorded follow-up is now closed, and leaving it open
|
|
131
|
+
there is how the same gap gets rediscovered.
|
|
132
|
+
|
|
133
|
+
### Mini-handover
|
|
134
|
+
|
|
135
|
+
Repo: `ui-core-micha`, branch `main`. WO: `work-orders/I18N-3.md`. 116 `sw` values into
|
|
136
|
+
`messagingTranslations.ts`, then restore `i18nAggregate.test.js` to `{de,fr,en,sw}` and drop the scope-down
|
|
137
|
+
comment. **Reuse `notifications`/`onboarding` terminology; disclose the translation source; flag uncertain
|
|
138
|
+
keys instead of guessing.** No app is broken today — this closes an inconsistency and re-arms a test. Tier 3,
|
|
139
|
+
`reviewer` mandatory. **Pushing to main publishes.** Follow `orchestrate-codex`.
|