@micha.bigler/ui-core-micha 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  Only notable, user-facing changes. Not every version — see `WORK_ORDERS.md` for the full history.
4
4
 
5
+ ## 3.2.0 — UCM-CHART-15
6
+
7
+ **Breaking visual change: every existing chart using a `size` token — including the default
8
+ `standard` size — becomes 80px taller; `standard` is now 400px.** The chart `size` scale shifts
9
+ up one step and gains two new steps above the old ceiling:
10
+
11
+ | token | before | after |
12
+ |---|---|---|
13
+ | `compact` | 240px | **320px** |
14
+ | `standard` | 320px | **400px** |
15
+ | `tall` | 400px | **480px** |
16
+ | `extra_tall` | — | **560px** (new) |
17
+ | `super_tall` | — | **640px** (new) |
18
+
19
+ This redraws every chart already using a `size` token, in every consumer on `ui-core-micha` `3.x`.
20
+ The 10-unit step is unchanged; every value stays a clean multiple of the theme's 8px spacing unit.
21
+
22
+ **Why:** `tall` was the tallest token available, and hram had already needed the documented
23
+ `height` px escape to get past it — a scale that forces its own escape ends too early.
24
+ `standard`'s old 320px value was a migration guardrail from `UCM-CHART-12` (pinned to the
25
+ pre-existing deployed default so that migration did not itself redraw anything), not a design
26
+ verdict on its own merits; the operator judged the whole scale too cramped on its own merits and
27
+ chose to correct it now, before the fourteen remaining apps still on `2.x` migrate onto it.
28
+
29
+ **This is a breaking change shipped as a minor**, by explicit operator decision (`2026-08-24`),
30
+ against the Expertenchat's recommendation of a major. No consuming app is on `3.x` except hram, so
31
+ the practical blast radius is one application; hram's own adoption and visual verification is a
32
+ separate, deliberate work order (`HRAM-CHT-5`), not automatic from this release.
33
+
34
+ ## 3.1.2 — UCM-CHART-14 follow-up
35
+
36
+ **The census script shipped in `3.1.1` worked; its test suite never ran.** `scripts/chart-api-census.mjs`
37
+ began with `#!/usr/bin/env node`. Node strips a shebang when executing a file directly, so the script
38
+ behaved correctly and its output was trustworthy — but Vite's module runner does not strip it, so any
39
+ test importing the module died at collection with `SyntaxError: Invalid or unexpected token`. The
40
+ suite reported `1 failed | 2 passed` with **0 of its 22 census tests collected**.
41
+
42
+ - Shebang removed; the script is invoked as `node scripts/chart-api-census.mjs <workspace-root>`, which
43
+ is how it was already documented and run. A comment in its place records why: a shebang is invisible
44
+ to direct execution but breaks any loader that pulls the file through an ESM graph, so re-adding one
45
+ fails nothing user-facing and only takes out the tests.
46
+ - No change to the census logic, its output, or anything in the published runtime surface. The 22
47
+ fixture tests were already correct; they now execute (118 passing across the chart suite, up from 96).
48
+
5
49
  ## 3.1.1 — UCM-CHART-14
6
50
 
7
51
  **Corrects a wrong record from `3.1.0`, and a runtime error message that stated the opposite of
package/WORK_ORDERS.md CHANGED
@@ -27,6 +27,7 @@ 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
+ | UCM-CHART-15 | Shift the chart size scale up one step, and raise the ceiling | `compact` 30->**40** (240->**320 px**), `standard` 40->**50** (320->**400**), `tall` 50->**60** (400->**480**), plus **`extra_tall` 70 (560)** and **`super_tall` 80 (640)**. The 10-unit step is preserved and every value stays a clean multiple of the 8 px spacing unit. **Two pressures, both from real call sites:** the ceiling is too low -- hram already uses the documented `height` escape to get past `tall`, with a comment recording exactly that ("no size token goes taller than `tall`") -- and the whole scale is too cramped, which lands on a value that was never chosen on its merits: `chartDefaults.js:295` says `standard` is "pinned to the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so this migration does not also silently redraw every already-shipped default-sized chart". **That was a migration guardrail, not a design verdict** -- the migration is complete, so what remains is a height inherited rather than decided. Full order [`work-orders/UCM-CHART-15.md`](work-orders/UCM-CHART-15.md) | 2026-08-24 | done | `be2eb6b` | **Tier 3** -- shared-core, by the tiering table regardless of diff size. `reviewer` (enumeration completeness, and whether the test can actually fail) + `ui_reviewer` (does a 10-unit step still read as a coherent scale at five members, is `super_tall` distinguishable from `extra_tall`); no `sec_reviewer`. **Blast radius is one app, and that is the argument for doing it now:** measured 2026-08-24, hram is on **3.0.1 and is the only 3.x consumer**; the other 14 apps are on 2.41.3/2.37.0, where the size tokens do not exist (a 3.0.0 feature, this repo's CHANGELOG marks 3.0.0 "Breaking"). Fourteen apps have yet to migrate, and every one that migrates onto a scale already known to be too cramped must be revisited afterwards. **Version: minor (3.2.0) by operator decision on 2026-08-24, against the Expertenchat's recommendation of major** -- redefining what an existing public token renders is breaking under semver. Recorded because the consequence is real: a minor can arrive through Renovate's weekly run without a human reading it as a visual change, and on this estate that triage is also what opens `develop -> main`. **Two obligations follow and are release gates, not decoration:** the CHANGELOG entry must **lead** with "every existing chart becomes 80 px taller", and the consuming pin bump is coordinated (`HRAM-CHT-5`), never left to the weekly bump. **Most likely defect: a half-updated enumeration** -- the valid-size list is written out in the resolver's error message, `assertRemovedChartProp` in three chart components, three JSDoc blocks and `docs/CHART-LAYOUT.md`; grep the literal `"tall"` before declaring it done. **Implementation:** `.claude/models.local.json` has no local override (defaults apply) -- `implementation.runtime` is `claude`/`sonnet`, implemented directly by the Orchestrator, no Codex dispatch. Grep for the literal `"tall"` confirmed the WO's own enumeration-site list was complete, nothing extra found. 176 affected chart tests green (76 `chartDefaults.test.js` including two new tests hardcoding all five pixel values independently of `CHART_SIZE_SPACING_UNITS`, so they fail against the pre-CHART-15 scale + 100 across `BarChart`/`LineChart`/`ScatterChart`/`TimeSeriesChart`/`ChartFrame`), `tsc -p tsconfig.build.json` clean. Version bumped `3.1.2` -> `3.2.0` (minor, per the WO's operator decision), CHANGELOG entry leads with the redraw. **Review: codex/gpt-5.6-luna (`reviewer` + `ui_reviewer`, concurrent, diff inline) · 2 raised · 2 accepted · worst accepted: nit · chosen** (id not divisible by 5, no rotation). `reviewer`: nit -- a stale `"240px"` example in a `theme.spacing` comment at `chartDefaults.js:416`, unrelated to runtime behaviour (fixed to `"320px"`); confirmed all five tokens consistently enumerated everywhere and that the new tests fail against the old scale. `ui_reviewer`: nit -- the CHANGELOG lead didn't explicitly say "breaking" or name the new `standard` default for a Renovate skimmer (fixed, lead now reads "Breaking visual change: every existing chart using a `size` token -- including the default `standard` size -- becomes 80px taller; `standard` is now 400px"); judged the five-member scale coherent and `extra_tall`/`super_tall` distinguishable. No further reviewer pass run on the two nit fixes (in-scope, no approval needed). |
30
31
  | 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 | done | 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.** **Dispatch:** landing the fix did not re-trigger `publish.yml` (its `paths` filter excludes `.github/workflows/**`) — operator confirmed on request, `gh workflow run publish.yml --ref main` dispatched, run [32032549303](https://github.com/bigler-webapps/ui-core-micha/actions/runs/32032549303) `completed/success`, `tests/packageTreeShaking.test.js` passing (not skipped) in the run order fixed above. **Acceptance verified against the registry, not the tile: `npm view @micha.bigler/ui-core-micha version` → `2.41.2`.** `DX-2`'s deliverable is now actually consumable. **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
32
  | 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
33
  | 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 | done | 30229d6 | 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. Codex implemented the two named files (`messagingTranslations.ts` backfill, `i18nAggregate.test.js` restored to `{de,en,fr,sw}` + stale scope-down comment removed) and correctly **stopped rather than exceed its named 2-file scope** when it found `tests/messagingMsg6.test.jsx:103` had a pre-existing exact `toEqual` hardcoding the old 3-locale shape for `MessagingThread.SHOW_REPLIES_one`, which the additive `sw` property broke. **Orchestrator fixed that one line itself** (added the matching `sw` value to the assertion) as necessary mechanical collateral of the in-scope change, not scope creep. **Translation source: machine-translated by the implementing Codex/GPT model, NOT reviewed by a fluent Swahili speaker** — stated plainly per the WO's disclosure requirement, not implied as checked. Codex flagged its own highest-uncertainty keys: `MessagingComposer.STAGED_FILES`, `MessagingList.MUTE`/`UNMUTE`, the `MessagingReadTicks.*` receipt wording, `MessagingAttachments.*PREVIEW*`, `MessagingReactions.TOGGLE_*`, `MessagingPoll.YOU_VOTED`, `MessagingConfig.DM_POLICY`/`GROUP_ENABLED`/`EVERYONE_POSTS`. **Independent `reviewer` (Sonnet)**, explicitly scoped to structure/parity/mechanics only (cannot and did not attempt to validate Swahili quality — said so): confirmed all 116 keys have exactly `{de,en,fr,sw}` with `sw` non-empty; **0 mismatches** in a scripted byte-comparison of every `de`/`en`/`fr` value against the pre-change file (additive-only, confirmed); **0 placeholder-token mismatches** — every `{{...}}` interpolation token set in `sw` matches its `en` counterpart exactly, including multi-token keys; **terminology-reuse spot-check confirmed real, not merely claimed** — Save→"Hifadhi", Close→"Funga", and the "could not be X: {{message}}" error-phrasing pattern all matched `notifications`/`onboarding` precedent, no coined alternates found for the named glossary concepts; both test-file diffs minimal and correct. Independently ran the scoped suite: **138/138 messaging tests + 4/4 i18nAggregate tests green.** **`I18N-1`'s recorded follow-up is now closed** — see its own Notiz, updated in this commit. Version bumped 2.41.2 → 2.41.3 (patch, additive strings only, per the WO). **A push to `main` publishes** — do not treat the green tile as the acceptance criterion (see `DX-3`); verify `npm view @micha.bigler/ui-core-micha version` returns `2.41.3` after push. |
@@ -96,7 +97,7 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
96
97
  | UCM-CHART-11 | The rotated-tick allowance is now counted once, but estimated too high | CHART-10 removed the double reservation and that holds. What remains is a SINGLE reservation that is too large. Measured in hram 2026-08-22, ucm 2.42.2 installed via the pin, Teaching -> Basic Plots, real data, 1280 px: Ward Metrics and Access Ladder each reserve 131 px of `axis.height` while the rendered tick band is **79 px** -- 52 px unused; Division Metrics 97 vs 56 (41 unused); Cost 68 vs 53 (15). All four are -45 deg and the same preset. The unused band SCALES WITH THE ESTIMATE, not with the rendered text: `rotatedTickMetrics` derives `extraHeight` from `longestFormattedTick x AVERAGE_GLYPH_WIDTH_EM (0.6) x sin(angle)`, a character-count guess that runs consistently above the real extent. **Cost is not the well-behaved case** -- MUI truncates its labels itself (`Ching'a...`), so the estimator sees a short string; nothing about Cost is configured better. | 2026-08-22 | done | 2026-08-22: rotated-tick estimate fix + version bump | Tier 3, shared-core, third proportion change in this series. Full order: [`work-orders/UCM-CHART-11.md`](work-orders/UCM-CHART-11.md). Do NOT revert CHART-10 -- this is the SIZE of the single reservation, not whether there are two. TRAP recorded because I walked into it during the measurement: an axis TITLE also sits below the lowest tick, and `Expert Mode: Simulation Trajectory` showed 53 px there that is entirely title, not waste (`hasAxisTitle: true`, angle 0). The four cards in the table all have `hasAxisTitle: false`, which is why THEIR residual is genuinely unused -- any validating measurement must separate title from empty band or a "fix" will clip the title. **Codex:** `.claude/codex-status.md` already had an `unavailable` line for 2026-08-22 (logged earlier the same day during `UCM-CHART-9`) -- skipped the attempt per the known-unavailable shortcut, implemented directly, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at Tier 3. **Fix, two parts:** (1) real width measurement via a transient shared invisible SVG `&lt;text&gt;` node (`measureTickTextWidthPx`, `getComputedTextLength`) instead of the flat `charCount x 0.6em` guess -- verified live against hram to match the real rendered tick's own measurement to the sub-pixel (91.203125px both ways for "Msolwa Station"); a per-character-width table (`estimateTextWidthPx`) is the declared ESTIMATE fallback for when no DOM is available (confirmed live: jsdom's `getComputedTextLength` throws, so this is what unit tests actually exercise). (2) Direct DOM inspection showed an accurate width alone still left ~38px unused (the old `axis.height = 45 + (projection - fontSize)` combine step double-counts the baseline) -- corrected to `axis.height = max(45, ceil(projection) + 8px clearance)`, i.e. the larger of the un-rotated floor and the real rotated extent, not always both added together. **Reviews:** `reviewer` + `ui_reviewer` ran concurrently (Sonnet, diff inline), converged on distinct real findings, both fixed before commit -- (R1) the combine step's leftover `Math.max(1, ...)` floor (carried over from the old formula) made the true floor 46px while every doc comment/test claimed 45px; changed to `Math.max(0, ...)` and added a test pinning the exact-45 case. (R2/U2, informational, accepted as-is) the estimate fallback and the DOM measurer only account for `fontSize`, not `fontFamily`/`fontWeight`/`letterSpacing` -- no current caller sets those, so no code change; named as a latent gap for a future consumer that does. (U1) only -45deg was live-verified; hram's one real -90deg call site (`RatioMetricsPanel`'s `xAxisAngle=-90`, wired to `VillageMetricsPanel.jsx`) is **not imported/reachable anywhere in the app** (confirmed by grep, dead/unwired code) so it could not be live-checked -- added a deterministic unit test instead, confirming the formula degenerates correctly at -90deg (the line-height term vanishes, textWidth alone drives the projection, no NaN/blow-up) and flagging the live-verification gap honestly rather than silently. **Tests:** affected-area set -- 139/139 green (134 CHART-10 baseline + 5 new: measured-width injection, truncated-string measurement, per-character fallback comparison, exact-floor-at-45, -90deg generalization). **Rendered check -- numeric, on hram with real data** (Teaching -> Basic Plots, `CLI-Kilombero_2026-per_cell-network`, real production-shaped data), swapping the locally-built fixed `dist` into hram's `node_modules` for a live before/after (reverted after measurement -- see below): at 1280px, Ward Metrics 129.6px -> **14.6px** empty; Access Ladder 156.9px -> **14.6px**; Division Metrics 85.2px -> **14.2px**; Cost 92.3px -> **14.6px** (unchanged as expected -- its `axis.height: 60` is caller-fixed, bypasses this helper entirely, confirmed by reading `CostMetricsPanel.jsx`). All four land under the WO's ~15px target. Re-checked at 375px: identical numbers (fixed-pixel chart heights, width-independent), zero clipped x-axis tick labels at either width across all four cards (35/35/5/35 ticks checked). SUBSTITUTION DECLARED: screenshot capture was unavailable this session; this is DOM/computed-style measurement (`getComputedTextLength`/`getBoundingClientRect` against the live rendered SVG), cross-validated against the real rendered tick's own measurement (exact sub-pixel match) rather than taken on faith. **Environment note:** the verification swap briefly touched a concurrent session's idle `npm run dev` process on hram's port 5173 (stopped and restarted with forced dependency re-optimization -- the pin-bump work it supported, commit `ae0fd956`, was already landed and clean, confirmed via `git log`/`git status` before touching it) and, separately, an accidental `cp` overwrite of hram's installed `2.42.2` package file was verified NOT to have corrupted pnpm's global content-addressable store (`pnpm store status`: "Packages in the store are untouched") before being restored from a freshly-downloaded, hash-verified npm tarball. hram's own git tree confirmed untouched throughout (`git status`, only pre-existing unrelated foreign files). Publishes automatically via `publish.yml` on this push (version-increase gate). |
97
98
  | UCM-CHART-12 | One layout model for charts. BREAKING (3.0.0), and the end of this series | Four WOs in a row each corrected ONE term of an equation nobody had written down: CHART-8 (`minHeight` vs `height`), CHART-9 (`ChartFrame` given a fixed height), CHART-10 (allowance added to BOTH `axis.height` and `margin.bottom`), CHART-11 (that allowance ESTIMATED above the rendered text). A fifth was already visible -- `ScatterChart`'s x-axis band stays reserved when the tick labels are empty, 58 px of blank inside hram's AccessGapScatter SVG. **Operator called a stop: no more term-by-term corrections.** Root cause is structural -- no single place owns a chart's composition; five interacting knobs (`minHeight`, `height`, `aspect`, `margin`, `xAxisAngle`) are assembled differently per preset and consumers add their own pixel arithmetic on top. THE MODEL, three rules: (1) one size source -- named `size` tokens through the theme scale, `height` px as a documented escape; `minHeight` and `aspect` REMOVED from the presets, so `resolveChartHeight` is DELETED rather than corrected a fourth time. (2) every band is its own measured content or ZERO -- no constants, no term in two addends; an empty band collapses, which is the fifth defect's fix. (3) consumers pass intent, never pixels -- no `margin` prop, and `xAxisAngle` becomes `xLabels: auto|horizontal|angled` with the model deciding rotation from measured labels and available width. | 2026-08-22 | done | `8142fa9`, `03201d7` | Tier 3, BREAKING, first major in this package's history. Full order: [`work-orders/UCM-CHART-12.md`](work-orders/UCM-CHART-12.md). THE DELIVERABLE IS THE INVARIANT, not the fix: `chartHeight === plot + xAxisBand + xTitleBand + legendBand` and `chartWidth === yAxisBand + plot + rightPad`, asserted per preset over a label matrix (none/short/long/long-and-many/empty-strings). CHART-8, -10 and -11 would EACH have failed it on day one, and the width line also catches the two findings still open elsewhere (a scatter y-axis reserving 72 px for a 33 px label; hram's tornado 220 px gutter). It must not be dropped for expedience. MEASURED SURFACE justifying breaking over continuing: 38 chart-consuming files across five apps, 14 with `minHeight`, 76 % in hram -- a sweep, not a migration programme. OPERATOR DECISIONS 2026-08-22: tokens with a pixel escape; `aspect` removed entirely (accepting that four call sites lose responsive height -- 1x hram AccessGapScatter, 3x fitness-monitor BodyHistoryPage, each converting to a token as a NAMED decision in its own migration WO, not silently); ucm first with apps migrated one at a time; NO compatibility shim, because two coexisting models are the ambiguity that caused this. `ChartFrame` KEEPS its `minHeight` -- CHART-9 established a frame is not a chart, do not sweep the frame's floor away with the presets' prop. TEST-ENVIRONMENT CONSTRAINT from CHART-11: jsdom's `getComputedTextLength` throws, so unit tests must inject a measurement stub and a BROWSER-level check is mandatory, not optional -- otherwise the invariant is only asserted against a fiction. SEQUENCING: hram has RES-28, HRAM-RES-29, FIX-19 and CHT-3 in flight on these files; hram's migration WO sequences after them and this WO must not start hram's sweep. **Codex:** first-hand out-of-credits failure this session on this exact WO's dispatch (`codex exec` returned "ERROR: Your workspace is out of credits" twice, exit 1, no PLAN/PROGRESS lines, no file touched) -- stamped `.claude/codex-status.md` 2026-08-22 and implemented directly in Claude, flipping authorship; `reviewer` + `ui_reviewer` were already mandatory at this WO's Tier 3, so the flip changed no routing. **Review runtime note:** `.claude/models.local.json` (added mid-session, per the new `35fbae3` governance commit) names `review`/`ui_review` as `codex`/`gpt-5.6-luna` -- not consulted before spawning, since this session's context predated that commit; the actual choice (spawned Claude `reviewer`+`ui_reviewer`, Sonnet) matches what the rule's own Codex-unavailable branch would have produced anyway, since Codex was independently confirmed unavailable today (this WO's own dispatch failure, plus the `UCM-CHART-9` entry earlier the same day) -- `review_fallback` is exactly `claude`/`sonnet`. Named here for the record, not as a retroactive justification. **Reviews:** `reviewer` (Sonnet) -- R1 (the height "invariant" was tautological by construction and could not have caught a CHART-10-shaped double-count; FIXED by adding independent per-term assertions -- `xAxis[0].height === xAxisBand + xTitleBand` and `margin` growth over baseline `=== legendBand` -- alongside the sum, in every invariant test case, plus a docblock correction distinguishing what each assertion actually proves), R2 (a `legendPosition.vertical: 'middle'` side-placed legend was still reserving a HEIGHT band, over-reserving height / under-reserving width; FIXED -- side-placed legends now reserve 0 height, new test added), R3 (dev harness demo's rotation angle silently drifted -25° -> -45° migrating off the old `tickLabelStyle.angle`-implies-rotation API; FIXED -- restored via `xLabels="angled"` + explicit `tickLabelStyle.angle: -25` override, commented). `ui_reviewer` (Sonnet) -- U1 (every axis, even the plain unrotated/untitled case, now gets an explicit `axis.height` where MUI used to fill in its own default -- verified against `node_modules/@mui/x-charts/constants`: `DEFAULT_AXIS_SIZE_HEIGHT=25`/`AXIS_LABEL_DEFAULT_HEIGHT=20` match `TICK_BAND_BASE_PX`/`AXIS_TITLE_BAND_PX` exactly, so no rendering changes for that case; comment + doc note added to make this provable, not just asserted), U2 (docs/CHART-LAYOUT.md's `margin` removal was prose-only, no before/after snippet; FIXED, snippet added), U4 (the no-`spacing`-fn fallback duplicates the 8px unit as a literal; left as a documented P3, all four presets always pass `theme.spacing` in practice). Both reviews' findings fixed; no open findings after fixes. **Tests:** affected-area set -- `chartDefaults`, `ChartFrame`, all four presets, `TimeSeriesChart` -- 152/152 green (151 + 1 new side-legend case). Full repo suite run once as an additional (non-required) sanity check: 566/566 green in isolation (one `packageTreeShaking` test timed out under concurrent-run machine load in the first pass, confirmed unrelated to this diff and passing cleanly alone). **Browser-level check, on this repo's own dev harness** (`dev/entries.jsx` via `ui-core-micha-dev`, `pnpm vite`) -- **hram itself could NOT be used**: hram is still pinned to `2.41.3` and every existing chart call site there still passes the now-removed `minHeight`/`aspect`, so a direct pin swap would throw immediately on nearly every chart (the dev-mode `assertRemovedChartProp` error) rather than render anything measurable; a real hram-with-real-data check is deferred to hram's own migration WO by design (Non-goals: "no consumer migration here"). Instead, verified against this repo's own updated specimens (BarChart, LineChart linear-x-axis, ScatterChart access-shape -- all three exercise real-shaped data, legends, and titled axes) at 1280px: DOM measurement (`getBoundingClientRect` on `.MuiChartsSurface-root`/`.MuiChartsAxis-bottom`) matched the hand-computed model EXACTLY on all three -- `xAxisHeight = 45px` (= `TICK_BAND_BASE_PX 25 + AXIS_TITLE_BAND_PX 20`, every specimen has an axis label) and `gapBelowXAxis = 40px` (= `PACKAGE_DEFAULT_MARGIN.bottom 8 + LEGEND_BAND_PX 32`, every specimen shows a legend). At 375px: chart height stayed correctly pinned (no responsive-height regression), all 22 tick labels on the LineChart specimen remained present with zero overflow past the SVG surface edge (`overflowingTickTexts: []`). SUBSTITUTION DECLARED, consistent with this series' established pattern: screenshot capture was unavailable this session (Browser pane would not composite frames) -- DOM/computed-style measurement only. One PRE-EXISTING, UNRELATED console error observed on the ScatterChart specimens (`<rect> attribute width: A negative value is not valid ("-4")`, from the marker slot's square-shape rendering) -- confirmed via `git diff` that this WO's changes never touch that code path; not investigated further, out of scope. Publishes automatically via `publish.yml` on this push (version-increase gate, `3.0.0 > 2.42.3`). **SECOND REVIEW ROUND, operator-requested, 2026-08-22:** operator asked for a `codex exec` pass over the already-landed diff, per `.claude/models.local.json` (`review`/`ui_review`: `codex`/`gpt-5.6-luna`) -- confirmed Codex available today first (`.claude/codex-status.md` had a fresh `available` line at the top of the log). Invoked directly via Bash with both mandatory flags and `-m gpt-5.6-luna`; first attempt failed (`Argument list too long` -- the 1900-line diff inlined as a CLI argument exceeded the OS limit), retried successfully via stdin. Verdict: changes requested, 6 findings, 4 P1 -- all real, none a repeat of round one. **(1)** `resolveChartLayout` only ever accounted for `sizedYAxis[0]`'s width, ignoring a secondary (right-positioned) y-axis entirely (`TimeSeriesChart`'s `axis: 'secondary'` feature) -- FIXED: `secondaryYAxisBand` now tracked separately, folded into `margin.right`, width invariant extended to four terms; unmarked axes default to the left per MUI's own rule. **(2)** `xLabels="auto"` decided rotation from a raw character-count threshold, never the measured label width the function already computes for the rotated-projection case -- FIXED: the SAME measurement (real DOM when available, per-glyph estimate otherwise) now drives the decision too (`AUTO_ROTATE_LABEL_WIDTH_PX = 50`, replacing the character threshold); the "available width" half of Rule 3's wording stays honestly unmeasured (documented) since no container width exists pre-render. **(3)** `sizeYAxisForContent` floored a y-axis's width at the font size even when every formatted tick was blank (a `valueFormatter` reducing all candidates to `''`) -- FIXED: collapses to a genuine 0 (distinct from the pre-existing "nothing to measure at all" case, which still leaves `width` untouched), unless an axis title is still set, which keeps its own reserved width. **(4)** `slotProps.legend.position` could override `legendPosition` at render time (`withChartSlotDefaults`) while the resolver only ever reserved space for `legendPosition` -- a caller passing both could get a rendered top/side legend against space reserved for bottom, the exact reserved-vs-rendered mismatch this WO exists to close -- FIXED: new exported `resolveLegendPosition` is the one merge both call sites now share; all four presets thread `slotProps` into `resolveChartLayout`. **(5, P2)** a caller-set `xAxis[0].height` bypassed the model's SIZE reservation (an accepted escape) but ALSO silently kept whatever stale `tickLabelStyle.angle` was on that axis, even under `xLabels="horizontal"` -- FIXED: rotation is now written unconditionally from `xLabels`, independent of whether `height` was caller-overridden. **(6, P2, accepted as-is)** invariant coverage is resolver-level, not exhaustively duplicated per preset (ScatterChart/TimeSeriesChart) -- all four presets already wire through the same resolver identically (confirmed by their own passing preset-level tests), so resolver-level coverage was judged proportionate rather than duplicating the full matrix four times. All four P1s and the P2 fixed; 10 new tests added (dual-axis width, measured-width auto rotation incl. two negative cases, blank-y-tick collapse at both `sizeYAxisForContent` and `resolveChartLayout` level, legend-override consistency, angle-survives-caller-height). Re-verified: 162/162 chart tests green, `tsc -p tsconfig.build.json` clean. Per Reviews (`AGENTS.md`): findings fixed, no further reviewer pass without separate approval -- not re-spawned a third time. Version bumped `3.0.0` -> `3.0.1` (patch -- correctness fixes within the same model, not a further breaking change) so `publish.yml`'s gate fires again on this push. |
98
99
  | UCM-CHART-13 | `ChartFrame` accepts three sizing props and applies one. Remove the other two | Measured 2026-08-22 against 3.0.1, prompted by the operator asking why the frame keeps all three: `minHeight` is destructured (`:56`) AND applied (`:138`); `height` (`:57`) is destructured and never applied -- it exists only to feed `warnOnChartFrameHeightMismatch`; `aspect` (`:58`) is destructured and **never read again**, there is no `aspectRatio` in the applied `sx` at all. A grep across all five consuming apps finds **no `<ChartFrame>` passing either one**. The justification offered for keeping them -- `docs/CHART-LAYOUT.md`'s "was never in scope here (UCM-CHART-9)" -- is a statement about CHART-12's BOUNDARY, not a finding that the props are correct; reading a scope note as a design endorsement is the same error this series is made of, a reservation kept because it exists rather than because something needs it. | 2026-08-22 | done | `676af5c` | Tier 3, shared-core API removal, but ZERO measured consumer impact -> minor `3.1.0`. Full order: [`work-orders/UCM-CHART-13.md`](work-orders/UCM-CHART-13.md). `minHeight` STAYS unchanged -- it is the one justified prop (a card with a loading/empty state would collapse; CHART-9 established the floor). `height` and `aspect` are removed, and `warnOnChartFrameHeightMismatch` dies with `height`: **a prop that is accepted and silently ignored is worse than one that errors**, because it lets a caller believe the card was sized when it was not -- the CHART-9 regression in slow motion. Passing either becomes a dev-mode error naming the replacement (`minHeight` for a floor, `size` on the chart inside if the chart's height was meant), matching how CHART-12 treats the presets' removed props. Also corrects `docs/CHART-LAYOUT.md`'s "What stays unchanged" entry, whose wording is what led a later reader (me) to defend all three. Re-run the consumer grep at implementation time rather than trusting the note. **Consumer grep RE-RUN at implementation, per the WO's own instruction (excluding `node_modules`/`dist`/`build` to avoid a 2-minute timeout the first attempt hit):** confirmed zero `<ChartFrame>` call sites passing `height=`/`aspect=` across all five apps. **CORRECTION TO THIS WO'S OWN ENVELOPE, found during implementation:** the measurement table above and this row's own Beschreibung both claim `aspect` "is destructured and never read again... there is no `aspectRatio` in the applied `sx` at all" -- **this is factually wrong.** `git show db335b5:src/components/charts/ChartFrame.jsx` (the commit immediately before this WO's implementation) shows `aspectRatio: aspect` IS in the applied `sx` (was line 139) -- it worked, unlike `height` (genuinely dead, only ever fed a warning). Caught by the second Codex review round (below), not by this WO's own authoring pass. The REMOVAL decision itself still stands unchanged (0 consumers pass it, and a working-but-unused sizing prop that only `ChartFrame` has and the four presets don't is exactly the inconsistency this WO exists to close) -- only the WHY changes: not "dead code," but "unused surface." `ChartFrame.jsx`'s own docblock and `docs/CHART-LAYOUT.md` were corrected to state this accurately rather than repeat the WO's error; Part A itself (Expertenchat-owned) was left untouched, flagged here and to the operator instead of silently edited. **Implementation:** `.claude/models.local.json`'s `implementation.runtime` is now `claude`/`sonnet` (operator fixed the earlier malformed `"gpt-5.6-luna"` value) -- implemented directly, no Codex dispatch. **Reviews, both rounds through `codex exec -m gpt-5.6-luna`** (per `review`/`ui_review` in `.claude/models.local.json`; Codex confirmed available today, `.claude/codex-status.md`), run in parallel background processes, via stdin this time (learned from `UCM-CHART-12`'s round 2: a large diff as a CLI argument can hit the OS `ARG_MAX` limit -- this diff was small enough that it wasn't strictly necessary here, but stdin is now the default approach). `reviewer` -- P1 `height`/`aspect` still named top-level destructured parameters, contrary to the DoD's literal "removed from the signature" (FIXED: captured via a `...legacyProps` rest instead); P2 the shared `assertRemovedChartProp` helper hardcoded "removed in UCM-CHART-12, v3.0.0", misattributing `ChartFrame`'s own, different removal (FIXED: new optional `removedIn` parameter, default preserves the four presets' existing message byte-for-byte, `ChartFrame`'s two call sites pass `'UCM-CHART-13, v3.1.0'`); P2 test coverage only checked the prop name, not the full replacement wording (FIXED: assertions now check for `minHeight`/`size` by name, plus a new test pinning the `UCM-CHART-13, v3.1.0` attribution). `ui_reviewer` -- converged on the same signature/versioning findings, PLUS the aspect-was-actually-applied correction above (the one genuinely new finding). All fixed; 166/166 chart tests green (145 prior + 21 in `ChartFrame.test.jsx`, up from 16 pre-WO), `tsc -p tsconfig.build.json` clean. Version bumped `3.0.1` -> `3.1.0` (minor, per the WO's own classification). **CORRECTED BY `UCM-CHART-14`, 2026-08-22 -- the opening claim of this Notiz is false and is left standing on purpose.** "ZERO measured consumer impact" was wrong: FOUR `ChartFrame`s pass `aspect` (fitness-monitor `BodyHistoryPage.jsx:295,:444`, `EnvironmentPage.jsx:245,:279`), measured by parsing JSX opening tags brace- and string-aware across all 13 app repos. hram is genuinely clean (16 frames, 11 with `minHeight` only, 0 with `aspect`/`height`), so `HRAM-CHT-4` is unaffected. Consequences: the removal was breaking, not a minor -- `3.1.0` shipped it under the wrong semver, and `UCM-CHART-14` deliberately does NOT publish a compensating major (it changes no API, and the dev-mode error already delivers the signal a major would buy); and the same false premise reached the runtime error text, which tells callers `aspect` "was never applied" while this file's own docblock says it WAS applied and worked. Sixth wrong consumer count in this series, all six from greps -- `UCM-CHART-14` ships a parsing census so the number stops being a claim. |
99
- | UCM-CHART-14 | The removal was right, the reason on the record is false | `UCM-CHART-13` removed `height` and `aspect` from `ChartFrame` correctly, but recorded a justification that does not hold and shipped an error message contradicting its own docblock. THREE FINDINGS: **(F1)** "no consumer across the five apps passed it (measured against 3.0.1)" appears in the docblock (`ChartFrame.jsx:29-30`), `docs/CHART-LAYOUT.md` and the commit message -- four fitness-monitor `ChartFrame`s pass `aspect` right now. **(F2)** the runtime message says `aspect` "was never applied to the frame" while the docblock 50 lines above says it WAS wired to `aspectRatio` at 3.0.1 and worked; the four callers who see that error are exactly the four whose cards change shape, and they are told the prop did nothing. **(F3)** `3.1.0` is a minor for removing an applied prop with four live call sites -- breaking, and the minor was chosen BECAUSE of F1; `3.1.0` is published and stays. THE DELIVERABLE IS THE CENSUS, not the three corrected sentences: this is the SIXTH wrong consumer count in the `CHART-*` series and all six came from greps, which cannot see a prop three lines below its component name, an arrow function whose `=>` ends the match, an apostrophe in a comment, or a `{a > b}` in a sibling prop. `scripts/chart-api-census.mjs` parses opening tags (strings, template literals, brace depth, comments), counts only components imported from `@micha.bigler/ui-core-micha`, and reports an unclosable tag as `UNPARSED` rather than as clean. Sequencing: `FM-CHART-1` is mid-flight and owns the four call sites -- do not fix them from here; until it lands the census correctly reports 4. | 2026-08-22 | done | `3627da5` | Tier 3, shared-core, version `3.1.1` (PATCH). **The compensating `4.0.0` was proposed here first and WITHDRAWN on the operator's challenge:** this WO changes no API (prose, one error string, one dev script that never ships), so a major would announce a break it does not contain. No consumer has crossed `3.1.0` -- hram `3.0.1`, fitness-monitor `2.37.0` committed / `3.0.1` in tree, jg-ferien `2.41.1` -- which was the argument FOR a major; what defeats it is that `assertRemovedChartProp` THROWS on first render in dev and names the replacement, so the "read the notes" signal a major buys is already delivered, harder and at the exact call site. CONDITIONAL ON F2: the guard is a safety net only while it tells the truth, so `3.1.1` is defensible because the false "never applied" message is fixed IN it -- defer F2 and the major comes back. Full order: [`work-orders/UCM-CHART-14.md`](work-orders/UCM-CHART-14.md). Does NOT reopen the removal: `height` and `aspect` stay gone, `minHeight` stays, and `13`'s `removedIn` parameterisation of `assertRemovedChartProp` was an improvement over what its own WO asked for (the error now attributes the right WO instead of always blaming `UCM-CHART-12`) and is kept. F2 is ordered FIRST because the fitness-monitor session is reading that message now. Test note: `13`'s assertion `/ChartFrame.*aspect.*size/s` PASSES against the false message, so the new test must assert message content and the absence of "never applied", not just that it throws. Census must stay out of the published package (off `exports`, off `files`, `packageTreeShaking` still green -- that test has flaked under concurrent load before, re-run it alone before believing a failure). **Implementation:** `.claude/models.local.json` names `implementation`: `claude`/`sonnet` -- implemented directly, no Codex dispatch. F2 done first, per the WO's own ordering. **Reviews, two full rounds, both via `codex exec -m gpt-5.6-luna`** (per `.claude/models.local.json`'s `review`/`ui_review`; Codex confirmed available today), `reviewer` + `ui_reviewer` in parallel background each round. **Round 1** converged independently on the same core finding: `scripts/chart-api-census.mjs`'s first version used a three-pass design (a raw `RegExp.exec` tag search with NO string/comment context, then per-tag bounds/attribute scans) that could produce a WRONG, non-UNPARSED answer -- not merely fail to parse -- via two concrete mechanisms both reviewers demonstrated: JSX-looking text inside a string/comment counted as a real element (`// <BarChart aspect />`), and an untracked regex literal's `}`/`>` characters (`data={/[}>]/}`) corrupting brace-depth and closing a tag early. Also found: a nested target component inside another's prop expression was skipped entirely (outer tag's scan jumped straight past it); two of the four original trap fixtures didn't actually exercise their claimed trap (the apostrophe-comment trap sat between two already-closed tags; the nested-braces trap used object keys that weren't even in `TARGET_PROPS`); the census didn't report every element per the DoD (silently dropped clean ones); and "up to 3.1.0" was imprecise for a prop that stopped applying AT 3.1.0 (fixed to "through 3.0.1" in all four places it appeared: `ChartFrame.jsx`, `docs/CHART-LAYOUT.md`, `tests/ChartFrame.test.jsx`, `CHANGELOG.md`). **FIXED:** rewrote the scanner as ONE unified single-pass function sharing string/template/comment/regex/brace-depth state across tag-open detection AND tag-bounds detection (regex/comment tracking added to the attribute-extraction pass too); `formatReport` now lists every element (`(clean)` for ones with no target props, not omitted); both broken fixtures rewritten to actually exercise their trap, four new fixtures added for round-1's findings (string, line comment, block comment, regex literal, nested-component); the `minHeight`-wording test-coverage gap fixed too. **A SECOND, real bug -- found by this WO's OWN verification run against the actual workspace, not by either review round:** the rewritten scanner's regex-heuristic character set (`REGEX_PRECEDING_CHARS`) originally included `<`/`>`, on the theory that a regex can follow a comparison operator -- but `<`/`>` are also EXACTLY what precedes the `/` in every JSX closing tag (`</Something>`). Running the new parser against real hram/fitness-monitor files immediately turned every multi-line `<ChartFrame>...</ChartFrame>` usage into a false `UNPARSED` (7 spurious UNPARSED entries, and fitness-monitor's real 4+4 finding VANISHED) -- caught only because this WO's own philosophy ("ship the parser, measured output instead of a trusted claim") was applied to the parser's own claims, not assumed. Fixed by dropping `<`/`>` from the regex-heuristic set (a genuine `a < /regex/` construct does not occur in this codebase); a dedicated regression fixture shaped exactly like the real file that exposed it (`</Toolbar>` closing tag ahead of the target's own closing tag) is now in the test suite permanently. **No second review round was run** on the post-fix diff (per "Fix in-scope findings; no further reviewer pass without approval") -- the post-fix state was instead verified directly: the full real-workspace census re-run (below) confirms zero `UNPARSED` and the exact fitness-monitor/hram counts this WO's own F1 predicted. **Tests:** 22 census fixtures (up from 15) + 23 `ChartFrame` tests -- 190/190 green across the full chart suite, `tsc -p tsconfig.build.json` clean, `packageTreeShaking` green (re-run alone, not just in the batch). **CENSUS OUTPUT, pasted per the DoD** (`node scripts/chart-api-census.mjs`, run against the corrected parser, zero UNPARSED):
100
+ | UCM-CHART-14 | The removal was right, the reason on the record is false | `UCM-CHART-13` removed `height` and `aspect` from `ChartFrame` correctly, but recorded a justification that does not hold and shipped an error message contradicting its own docblock. THREE FINDINGS: **(F1)** "no consumer across the five apps passed it (measured against 3.0.1)" appears in the docblock (`ChartFrame.jsx:29-30`), `docs/CHART-LAYOUT.md` and the commit message -- four fitness-monitor `ChartFrame`s pass `aspect` right now. **(F2)** the runtime message says `aspect` "was never applied to the frame" while the docblock 50 lines above says it WAS wired to `aspectRatio` at 3.0.1 and worked; the four callers who see that error are exactly the four whose cards change shape, and they are told the prop did nothing. **(F3)** `3.1.0` is a minor for removing an applied prop with four live call sites -- breaking, and the minor was chosen BECAUSE of F1; `3.1.0` is published and stays. THE DELIVERABLE IS THE CENSUS, not the three corrected sentences: this is the SIXTH wrong consumer count in the `CHART-*` series and all six came from greps, which cannot see a prop three lines below its component name, an arrow function whose `=>` ends the match, an apostrophe in a comment, or a `{a > b}` in a sibling prop. `scripts/chart-api-census.mjs` parses opening tags (strings, template literals, brace depth, comments), counts only components imported from `@micha.bigler/ui-core-micha`, and reports an unclosable tag as `UNPARSED` rather than as clean. Sequencing: `FM-CHART-1` is mid-flight and owns the four call sites -- do not fix them from here; until it lands the census correctly reports 4. | 2026-08-22 | done | `3627da5`, `c2bb86b` | Tier 3, shared-core, version `3.1.1` (PATCH). **The compensating `4.0.0` was proposed here first and WITHDRAWN on the operator's challenge:** this WO changes no API (prose, one error string, one dev script that never ships), so a major would announce a break it does not contain. No consumer has crossed `3.1.0` -- hram `3.0.1`, fitness-monitor `2.37.0` committed / `3.0.1` in tree, jg-ferien `2.41.1` -- which was the argument FOR a major; what defeats it is that `assertRemovedChartProp` THROWS on first render in dev and names the replacement, so the "read the notes" signal a major buys is already delivered, harder and at the exact call site. CONDITIONAL ON F2: the guard is a safety net only while it tells the truth, so `3.1.1` is defensible because the false "never applied" message is fixed IN it -- defer F2 and the major comes back. Full order: [`work-orders/UCM-CHART-14.md`](work-orders/UCM-CHART-14.md). Does NOT reopen the removal: `height` and `aspect` stay gone, `minHeight` stays, and `13`'s `removedIn` parameterisation of `assertRemovedChartProp` was an improvement over what its own WO asked for (the error now attributes the right WO instead of always blaming `UCM-CHART-12`) and is kept. F2 is ordered FIRST because the fitness-monitor session is reading that message now. Test note: `13`'s assertion `/ChartFrame.*aspect.*size/s` PASSES against the false message, so the new test must assert message content and the absence of "never applied", not just that it throws. Census must stay out of the published package (off `exports`, off `files`, `packageTreeShaking` still green -- that test has flaked under concurrent load before, re-run it alone before believing a failure). **Implementation:** `.claude/models.local.json` names `implementation`: `claude`/`sonnet` -- implemented directly, no Codex dispatch. F2 done first, per the WO's own ordering. **Reviews, two full rounds, both via `codex exec -m gpt-5.6-luna`** (per `.claude/models.local.json`'s `review`/`ui_review`; Codex confirmed available today), `reviewer` + `ui_reviewer` in parallel background each round. **Round 1** converged independently on the same core finding: `scripts/chart-api-census.mjs`'s first version used a three-pass design (a raw `RegExp.exec` tag search with NO string/comment context, then per-tag bounds/attribute scans) that could produce a WRONG, non-UNPARSED answer -- not merely fail to parse -- via two concrete mechanisms both reviewers demonstrated: JSX-looking text inside a string/comment counted as a real element (`// <BarChart aspect />`), and an untracked regex literal's `}`/`>` characters (`data={/[}>]/}`) corrupting brace-depth and closing a tag early. Also found: a nested target component inside another's prop expression was skipped entirely (outer tag's scan jumped straight past it); two of the four original trap fixtures didn't actually exercise their claimed trap (the apostrophe-comment trap sat between two already-closed tags; the nested-braces trap used object keys that weren't even in `TARGET_PROPS`); the census didn't report every element per the DoD (silently dropped clean ones); and "up to 3.1.0" was imprecise for a prop that stopped applying AT 3.1.0 (fixed to "through 3.0.1" in all four places it appeared: `ChartFrame.jsx`, `docs/CHART-LAYOUT.md`, `tests/ChartFrame.test.jsx`, `CHANGELOG.md`). **FIXED:** rewrote the scanner as ONE unified single-pass function sharing string/template/comment/regex/brace-depth state across tag-open detection AND tag-bounds detection (regex/comment tracking added to the attribute-extraction pass too); `formatReport` now lists every element (`(clean)` for ones with no target props, not omitted); both broken fixtures rewritten to actually exercise their trap, four new fixtures added for round-1's findings (string, line comment, block comment, regex literal, nested-component); the `minHeight`-wording test-coverage gap fixed too. **A SECOND, real bug -- found by this WO's OWN verification run against the actual workspace, not by either review round:** the rewritten scanner's regex-heuristic character set (`REGEX_PRECEDING_CHARS`) originally included `<`/`>`, on the theory that a regex can follow a comparison operator -- but `<`/`>` are also EXACTLY what precedes the `/` in every JSX closing tag (`</Something>`). Running the new parser against real hram/fitness-monitor files immediately turned every multi-line `<ChartFrame>...</ChartFrame>` usage into a false `UNPARSED` (7 spurious UNPARSED entries, and fitness-monitor's real 4+4 finding VANISHED) -- caught only because this WO's own philosophy ("ship the parser, measured output instead of a trusted claim") was applied to the parser's own claims, not assumed. Fixed by dropping `<`/`>` from the regex-heuristic set (a genuine `a < /regex/` construct does not occur in this codebase); a dedicated regression fixture shaped exactly like the real file that exposed it (`</Toolbar>` closing tag ahead of the target's own closing tag) is now in the test suite permanently. **No second review round was run** on the post-fix diff (per "Fix in-scope findings; no further reviewer pass without approval") -- the post-fix state was instead verified directly: the full real-workspace census re-run (below) confirms zero `UNPARSED` and the exact fitness-monitor/hram counts this WO's own F1 predicted. **Tests:** 22 census fixtures (up from 15) + 23 `ChartFrame` tests -- 190/190 green across the full chart suite, `tsc -p tsconfig.build.json` clean, `packageTreeShaking` green (re-run alone, not just in the batch). **CENSUS OUTPUT, pasted per the DoD** (`node scripts/chart-api-census.mjs`, run against the corrected parser, zero UNPARSED): **FOLLOW-UP `c2bb86b` (3.1.2), Orchestrator-fixed review finding on the landed 3.1.1.** The census script worked and its output was independently re-verified correct, but `scripts/chart-api-census.mjs` began with `#!/usr/bin/env node` -- Node strips a shebang when executing a file directly, Vite's module runner does not, and `#` is an illegal token there. Every test importing the module died at collection: `SyntaxError: Invalid or unexpected token`, `1 failed / 2 passed`, **0 of the 22 census tests ever collected**. The DoD's central deliverable -- fixture tests for the four traps behind this series' six wrong counts -- was therefore unverified at landing, while the suite still printed mostly-green. The fixtures themselves were correct and needed no change: with the shebang removed all 22 run, 118 passing across the chart suite (up from 96). FAILURE MODE WORTH REMEMBERING: a shebang breaks only ESM-graph loaders, never direct execution, so the script stays trustworthy while its tests silently vanish -- 'N passed' says nothing unless N is compared to N-expected. **Review:** codex/gpt-5.6-luna · 1 raised · 1 accepted · worst accepted: nit (the replacement comment claimed the tests are the only thing that notices -- too absolute, any ESM-graph importer is affected; reworded, and the CHANGELOG line aligned with it). **STILL OPEN, pre-existing, NOT introduced here:** the package has no `files` field and no `.npmignore`, so it publishes 345 files including `tests/` and now `scripts/` -- the WO's 'census stays out of the published package' item is unmet. Introducing a `files` allowlist changes the entire published surface and is its own Tier-3 change, deliberately not folded into a review-finding fix. |
100
101
  ```
101
102
  fitness-monitor:
102
103
  fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:295 <ChartFrame aspect minHeight>
@@ -27,16 +27,17 @@ import { DEFAULT_LEGEND_POSITION, assertRemovedChartProp, defaultNumericTickForm
27
27
  * a label the caller DID set (`withAxisDefaults` only ever falls back onto
28
28
  * an axis without its own `label`).
29
29
  *
30
- * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
31
- * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
32
- * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
30
+ * `size` (UCM-CHART-12, UCM-CHART-15): `"compact" | "standard" | "tall" | "extra_tall" |
31
+ * "super_tall"`, resolved through the theme's spacing scale by `resolveChartLayout`. `height`
32
+ * (px) is the documented escape for a justified special case -- prefer `size`.
33
+ * `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
33
34
  */
34
35
  export function BarChart(_a) {
35
36
  var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, size = 'standard', height, minHeight, aspect, margin, xLabels = 'auto', grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "size", "height", "minHeight", "aspect", "margin", "xLabels", "grid", "hideLegend", "legendPosition", "slotProps"]);
36
37
  const theme = useTheme();
37
38
  const { i18n } = useTranslation();
38
39
  const neutralPalette = useNeutralChartPalette();
39
- assertRemovedChartProp('BarChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
40
+ assertRemovedChartProp('BarChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall" | "extra_tall" | "super_tall", or height for the documented escape.');
40
41
  assertRemovedChartProp('BarChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
41
42
  assertRemovedChartProp('BarChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
42
43
  const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'band', data: [] }, theme.typography.caption.fontSize);
@@ -41,16 +41,17 @@ function FilledMarkElement(_a) {
41
41
  * (`withAxisDefaults` only ever falls back onto an axis without its own
42
42
  * `label`).
43
43
  *
44
- * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
45
- * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
46
- * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
44
+ * `size` (UCM-CHART-12, UCM-CHART-15): `"compact" | "standard" | "tall" | "extra_tall" |
45
+ * "super_tall"`, resolved through the theme's spacing scale by `resolveChartLayout`. `height`
46
+ * (px) is the documented escape for a justified special case -- prefer `size`.
47
+ * `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
47
48
  */
48
49
  export function LineChart(_a) {
49
50
  var { series = [], xAxisLabel, yAxisLabel, xAxis, yAxis, palette, size = 'standard', height, minHeight, aspect, margin, xLabels = 'auto', grid, hideLegend = series.length <= 1, legendPosition = DEFAULT_LEGEND_POSITION, slots, slotProps } = _a, chartProps = __rest(_a, ["series", "xAxisLabel", "yAxisLabel", "xAxis", "yAxis", "palette", "size", "height", "minHeight", "aspect", "margin", "xLabels", "grid", "hideLegend", "legendPosition", "slots", "slotProps"]);
50
51
  const theme = useTheme();
51
52
  const { i18n } = useTranslation();
52
53
  const neutralPalette = useNeutralChartPalette();
53
- assertRemovedChartProp('LineChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
54
+ assertRemovedChartProp('LineChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall" | "extra_tall" | "super_tall", or height for the documented escape.');
54
55
  assertRemovedChartProp('LineChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
55
56
  assertRemovedChartProp('LineChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
56
57
  const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'point', data: [] }, theme.typography.caption.fontSize);
@@ -118,9 +118,10 @@ export function ScatterReferenceLine({ from, to, color, dashed, strokeWidth = 1.
118
118
  * Reference geometry (a computed curve, a straight line, either with an optional label) is
119
119
  * composed as `children` using `ScatterReferenceCurve`/`ScatterReferenceLine` above.
120
120
  *
121
- * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
122
- * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
123
- * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
121
+ * `size` (UCM-CHART-12, UCM-CHART-15): `"compact" | "standard" | "tall" | "extra_tall" |
122
+ * "super_tall"`, resolved through the theme's spacing scale by `resolveChartLayout`. `height`
123
+ * (px) is the documented escape for a justified special case -- prefer `size`.
124
+ * `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
124
125
  *
125
126
  * Tested magnitude: a few hundred marks (hram's largest cloud is ~300 points). Not verified,
126
127
  * and not implied to hold, at thousands+.
@@ -140,7 +141,7 @@ export function ScatterChart(_a) {
140
141
  const theme = useTheme();
141
142
  const { i18n } = useTranslation();
142
143
  const neutralPalette = useNeutralChartPalette();
143
- assertRemovedChartProp('ScatterChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
144
+ assertRemovedChartProp('ScatterChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall" | "extra_tall" | "super_tall", or height for the documented escape.');
144
145
  assertRemovedChartProp('ScatterChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
145
146
  assertRemovedChartProp('ScatterChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
146
147
  const labelledXAxis = withAxisDefaults(xAxis, xAxisLabel, { scaleType: 'linear', valueFormatter: defaultNumericTickFormatter(i18n.language) }, theme.typography.caption.fontSize);
@@ -280,13 +280,18 @@ export function defaultNumericTickFormatter(locale) {
280
280
  ? new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)
281
281
  : new Intl.NumberFormat(locale).format(value));
282
282
  }
283
- // UCM-CHART-12: named size tokens, resolved through the theme's 8px spacing unit
284
- // (`src/theme/tokens.js`'s `spacing: 8`). `standard` is pinned to the pre-existing deployed
285
- // default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so this migration does not also
286
- // silently redraw every already-shipped default-sized chart; `compact`/`tall` are proportional.
287
- // Documented plainly in `docs/CHART-LAYOUT.md` -- these are visible product decisions, not
288
- // internal plumbing.
289
- export const CHART_SIZE_SPACING_UNITS = { compact: 30, standard: 40, tall: 50 };
283
+ // UCM-CHART-15: named size tokens, resolved through the theme's 8px spacing unit
284
+ // (`src/theme/tokens.js`'s `spacing: 8`). Through UCM-CHART-12/14, `standard` stayed pinned to
285
+ // the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so that
286
+ // migration did not also silently redraw every already-shipped default-sized chart -- that pin
287
+ // was a migration guardrail, not a design verdict. The migration it protected is complete, and
288
+ // the operator judged the whole scale too cramped (`tall` had already forced hram to use the
289
+ // `height` px escape to get past the old ceiling), so this WO shifts every token up one step and
290
+ // adds two steps above the old `tall`, chosen rather than inherited. Documented plainly in
291
+ // `docs/CHART-LAYOUT.md` -- these are visible product decisions, not internal plumbing.
292
+ export const CHART_SIZE_SPACING_UNITS = {
293
+ compact: 40, standard: 50, tall: 60, extra_tall: 70, super_tall: 80,
294
+ };
290
295
  // UCM-CHART-12 Rule 2: every band is its own measured content, or zero -- no band is ever a
291
296
  // constant taken on faith. These two ARE constants, but only for the "content is present" case;
292
297
  // each one collapses to 0 the moment its content is absent (`resolveXAxisGeometry`,
@@ -388,10 +393,10 @@ function resolveChartHeightPx(size, height, spacing) {
388
393
  return height;
389
394
  const units = CHART_SIZE_SPACING_UNITS[size];
390
395
  if (units == null) {
391
- throw new Error(`[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall", `
392
- + 'or pass height (in px) for the documented escape.');
396
+ throw new Error(`[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall" | `
397
+ + '"extra_tall" | "super_tall", or pass height (in px) for the documented escape.');
393
398
  }
394
- // MUI's `theme.spacing(n)` returns a CSS length STRING ("240px"), not a number -- the chart
399
+ // MUI's `theme.spacing(n)` returns a CSS length STRING ("320px"), not a number -- the chart
395
400
  // height feeds straight into MUI X-Charts' own numeric `height` prop, so it must be unwrapped
396
401
  // back to a px number via the same `cssLengthToPixels` the rest of this file already uses.
397
402
  return typeof spacing === 'function' ? cssLengthToPixels(spacing(units)) : units * 8;
@@ -70,14 +70,19 @@ Tokens resolve through the theme's spacing scale (`theme.spacing`, MUI's 8px uni
70
70
 
71
71
  | Token | Spacing units | Height |
72
72
  |---|---|---|
73
- | `compact` | 30 | 240px |
74
- | `standard` | 40 | 320px |
75
- | `tall` | 50 | 400px |
76
-
77
- `standard` is pinned to the exact number every current consumer already renders at today
78
- (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) — this migration does not silently redraw an
79
- existing default-sized chart. `height` (a raw px number) survives as a **documented escape** for a
80
- justified special case; prefer `size`.
73
+ | `compact` | 40 | 320px |
74
+ | `standard` | 50 | 400px |
75
+ | `tall` | 60 | 480px |
76
+ | `extra_tall` | 70 | 560px |
77
+ | `super_tall` | 80 | 640px |
78
+
79
+ **UCM-CHART-15** shifted the whole scale up one step and added `extra_tall`/`super_tall` above the
80
+ old ceiling — `tall` (400px) had been the largest token, and hram had already needed the `height`
81
+ px escape to get past it. `standard`'s old pin to 320px (`TimeSeriesChart`'s historical
82
+ `CHART_HEIGHT`) was a migration guardrail for CHART-12, not a design verdict; that migration is
83
+ long complete, so the scale is now a chosen set of five values rather than one inherited number.
84
+ `height` (a raw px number) survives as a **documented escape** for a justified special case; prefer
85
+ `size`.
81
86
 
82
87
  **Four call sites lose responsive height with `aspect` gone**: `hram/AccessGapScatterPanel.jsx`
83
88
  and three in `fitness-monitor/BodyHistoryPage.jsx` (its `960/380` ratios were a design decision,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "sideEffects": [
@@ -1,4 +1,12 @@
1
- #!/usr/bin/env node
1
+ // NO SHEBANG. Run this with an explicit interpreter: `node scripts/chart-api-census.mjs
2
+ // <workspace-root>`, which is how it is documented and how it is run.
3
+ //
4
+ // A `#!` line here is invisible to direct execution -- Node strips it -- but breaks anything
5
+ // that loads this file through an ESM graph rather than running it: Vite's module runner
6
+ // (hence vitest) rejects `#` as an illegal token, and other bundlers/parsers may too.
7
+ // UCM-CHART-14 shipped with one. The script kept working and its output stayed correct, so
8
+ // nothing user-facing complained; what broke was its own test suite, silently reduced to
9
+ // 0 collected tests (`SyntaxError: Invalid or unexpected token`). Do not re-add it.
2
10
  // UCM-CHART-14: a parser, not a grep. This is the whole point of this file -- five wrong consumer
3
11
  // counts in this series (see work-orders/UCM-CHART-14.md) all came from a grep pattern that could
4
12
  // not see JSX structure: a prop several lines below the component name, an arrow function's `=>`
@@ -24,9 +24,10 @@ import {
24
24
  * a label the caller DID set (`withAxisDefaults` only ever falls back onto
25
25
  * an axis without its own `label`).
26
26
  *
27
- * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
28
- * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
29
- * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
27
+ * `size` (UCM-CHART-12, UCM-CHART-15): `"compact" | "standard" | "tall" | "extra_tall" |
28
+ * "super_tall"`, resolved through the theme's spacing scale by `resolveChartLayout`. `height`
29
+ * (px) is the documented escape for a justified special case -- prefer `size`.
30
+ * `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
30
31
  */
31
32
  export function BarChart({
32
33
  series = [],
@@ -51,7 +52,7 @@ export function BarChart({
51
52
  const { i18n } = useTranslation();
52
53
  const neutralPalette = useNeutralChartPalette();
53
54
 
54
- assertRemovedChartProp('BarChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
55
+ assertRemovedChartProp('BarChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall" | "extra_tall" | "super_tall", or height for the documented escape.');
55
56
  assertRemovedChartProp('BarChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
56
57
  assertRemovedChartProp('BarChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
57
58
 
@@ -41,9 +41,10 @@ function FilledMarkElement({ color, style, ...props }) {
41
41
  * (`withAxisDefaults` only ever falls back onto an axis without its own
42
42
  * `label`).
43
43
  *
44
- * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
45
- * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
46
- * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
44
+ * `size` (UCM-CHART-12, UCM-CHART-15): `"compact" | "standard" | "tall" | "extra_tall" |
45
+ * "super_tall"`, resolved through the theme's spacing scale by `resolveChartLayout`. `height`
46
+ * (px) is the documented escape for a justified special case -- prefer `size`.
47
+ * `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
47
48
  */
48
49
  export function LineChart({
49
50
  series = [],
@@ -69,7 +70,7 @@ export function LineChart({
69
70
  const { i18n } = useTranslation();
70
71
  const neutralPalette = useNeutralChartPalette();
71
72
 
72
- assertRemovedChartProp('LineChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
73
+ assertRemovedChartProp('LineChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall" | "extra_tall" | "super_tall", or height for the documented escape.');
73
74
  assertRemovedChartProp('LineChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
74
75
  assertRemovedChartProp('LineChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
75
76
 
@@ -152,9 +152,10 @@ export function ScatterReferenceLine({
152
152
  * Reference geometry (a computed curve, a straight line, either with an optional label) is
153
153
  * composed as `children` using `ScatterReferenceCurve`/`ScatterReferenceLine` above.
154
154
  *
155
- * `size` (UCM-CHART-12): `"compact" | "standard" | "tall"`, resolved through the theme's
156
- * spacing scale by `resolveChartLayout`. `height` (px) is the documented escape for a justified
157
- * special case -- prefer `size`. `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
155
+ * `size` (UCM-CHART-12, UCM-CHART-15): `"compact" | "standard" | "tall" | "extra_tall" |
156
+ * "super_tall"`, resolved through the theme's spacing scale by `resolveChartLayout`. `height`
157
+ * (px) is the documented escape for a justified special case -- prefer `size`.
158
+ * `minHeight`/`aspect`/`margin` are gone (see docs/CHART-LAYOUT.md).
158
159
  *
159
160
  * Tested magnitude: a few hundred marks (hram's largest cloud is ~300 points). Not verified,
160
161
  * and not implied to hold, at thousands+.
@@ -195,7 +196,7 @@ export function ScatterChart({
195
196
  const { i18n } = useTranslation();
196
197
  const neutralPalette = useNeutralChartPalette();
197
198
 
198
- assertRemovedChartProp('ScatterChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall", or height for the documented escape.');
199
+ assertRemovedChartProp('ScatterChart', 'minHeight', minHeight, 'Use size="compact" | "standard" | "tall" | "extra_tall" | "super_tall", or height for the documented escape.');
199
200
  assertRemovedChartProp('ScatterChart', 'aspect', aspect, 'Removed with no replacement -- pick a size token; the chart no longer tracks width.');
200
201
  assertRemovedChartProp('ScatterChart', 'margin', margin, 'Removed -- the layout model owns margins completely.');
201
202
 
@@ -291,13 +291,18 @@ export function defaultNumericTickFormatter(locale) {
291
291
  : new Intl.NumberFormat(locale).format(value));
292
292
  }
293
293
 
294
- // UCM-CHART-12: named size tokens, resolved through the theme's 8px spacing unit
295
- // (`src/theme/tokens.js`'s `spacing: 8`). `standard` is pinned to the pre-existing deployed
296
- // default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so this migration does not also
297
- // silently redraw every already-shipped default-sized chart; `compact`/`tall` are proportional.
298
- // Documented plainly in `docs/CHART-LAYOUT.md` -- these are visible product decisions, not
299
- // internal plumbing.
300
- export const CHART_SIZE_SPACING_UNITS = { compact: 30, standard: 40, tall: 50 };
294
+ // UCM-CHART-15: named size tokens, resolved through the theme's 8px spacing unit
295
+ // (`src/theme/tokens.js`'s `spacing: 8`). Through UCM-CHART-12/14, `standard` stayed pinned to
296
+ // the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so that
297
+ // migration did not also silently redraw every already-shipped default-sized chart -- that pin
298
+ // was a migration guardrail, not a design verdict. The migration it protected is complete, and
299
+ // the operator judged the whole scale too cramped (`tall` had already forced hram to use the
300
+ // `height` px escape to get past the old ceiling), so this WO shifts every token up one step and
301
+ // adds two steps above the old `tall`, chosen rather than inherited. Documented plainly in
302
+ // `docs/CHART-LAYOUT.md` -- these are visible product decisions, not internal plumbing.
303
+ export const CHART_SIZE_SPACING_UNITS = {
304
+ compact: 40, standard: 50, tall: 60, extra_tall: 70, super_tall: 80,
305
+ };
301
306
 
302
307
  // UCM-CHART-12 Rule 2: every band is its own measured content, or zero -- no band is ever a
303
308
  // constant taken on faith. These two ARE constants, but only for the "content is present" case;
@@ -404,11 +409,11 @@ function resolveChartHeightPx(size, height, spacing) {
404
409
  const units = CHART_SIZE_SPACING_UNITS[size];
405
410
  if (units == null) {
406
411
  throw new Error(
407
- `[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall", `
408
- + 'or pass height (in px) for the documented escape.',
412
+ `[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall" | `
413
+ + '"extra_tall" | "super_tall", or pass height (in px) for the documented escape.',
409
414
  );
410
415
  }
411
- // MUI's `theme.spacing(n)` returns a CSS length STRING ("240px"), not a number -- the chart
416
+ // MUI's `theme.spacing(n)` returns a CSS length STRING ("320px"), not a number -- the chart
412
417
  // height feeds straight into MUI X-Charts' own numeric `height` prop, so it must be unwrapped
413
418
  // back to a px number via the same `cssLengthToPixels` the rest of this file already uses.
414
419
  return typeof spacing === 'function' ? cssLengthToPixels(spacing(units)) : units * 8;
@@ -31,10 +31,10 @@ describe('BarChart preset', () => {
31
31
  legend: { position: { vertical: 'bottom', horizontal: 'start' } },
32
32
  });
33
33
  expect(props.hideLegend).toBe(true);
34
- // UCM-CHART-12: size defaults to "standard" (320px, the historical default) -- the wrapper
35
- // always carries an explicit height now, width stays responsive.
36
- expect(props.height).toBe(320);
37
- expect(window.getComputedStyle(screen.getByTestId('bar-chart-container')).height).toBe('320px');
34
+ // UCM-CHART-12/UCM-CHART-15: size defaults to "standard" (400px) -- the wrapper always
35
+ // carries an explicit height now, width stays responsive.
36
+ expect(props.height).toBe(400);
37
+ expect(window.getComputedStyle(screen.getByTestId('bar-chart-container')).height).toBe('400px');
38
38
  });
39
39
 
40
40
  it.each([
@@ -78,11 +78,13 @@ describe('BarChart preset', () => {
78
78
  // UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
79
79
  // deleted `minHeight`/`aspect` CHART-8 trio -- see the removed-prop assertions below for the
80
80
  // migration side of this.
81
- describe('size/height resolution (UCM-CHART-12)', () => {
81
+ describe('size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
82
82
  it.each([
83
- ['compact', 240],
84
- ['standard', 320],
85
- ['tall', 400],
83
+ ['compact', 320],
84
+ ['standard', 400],
85
+ ['tall', 480],
86
+ ['extra_tall', 560],
87
+ ['super_tall', 640],
86
88
  ])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
87
89
  renderChart({ series: [{ data: [1] }], size });
88
90
  expect(chartSpy.mock.calls.at(-1)[0].height).toBe(px);
@@ -36,7 +36,7 @@ describe('LineChart preset', () => {
36
36
 
37
37
  // UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
38
38
  // deleted `minHeight`/`aspect` CHART-8 trio.
39
- describe('size/height resolution (UCM-CHART-12)', () => {
39
+ describe('size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
40
40
  function renderLine(props) {
41
41
  return render(
42
42
  <ThemeProvider theme={createTheme()}>
@@ -48,9 +48,11 @@ describe('LineChart preset', () => {
48
48
  afterEach(cleanup);
49
49
 
50
50
  it.each([
51
- ['compact', 240],
52
- ['standard', 320],
53
- ['tall', 400],
51
+ ['compact', 320],
52
+ ['standard', 400],
53
+ ['tall', 480],
54
+ ['extra_tall', 560],
55
+ ['super_tall', 640],
54
56
  ])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
55
57
  renderLine({ size });
56
58
  expect(chartSpy.mock.calls.at(-1)[0].height).toBe(px);
@@ -235,13 +235,15 @@ describe('ScatterReferenceCurve / ScatterReferenceLine (THEME-10)', () => {
235
235
 
236
236
  // UCM-CHART-12: `size`/`height` resolution, wired through the actual component. Replaces the
237
237
  // deleted `minHeight`/`aspect` CHART-8 trio.
238
- describe('ScatterChart size/height resolution (UCM-CHART-12)', () => {
238
+ describe('ScatterChart size/height resolution (UCM-CHART-12, UCM-CHART-15)', () => {
239
239
  afterEach(() => { cleanup(); vi.clearAllMocks(); });
240
240
 
241
241
  it.each([
242
- ['compact', 240],
243
- ['standard', 320],
244
- ['tall', 400],
242
+ ['compact', 320],
243
+ ['standard', 400],
244
+ ['tall', 480],
245
+ ['extra_tall', 560],
246
+ ['super_tall', 640],
245
247
  ])('resolves size="%s" to %ipx, on both the chart height and the wrapper', (size, px) => {
246
248
  renderScatter({ size });
247
249
  expect(scatterSpy.mock.calls.at(-1)[0].height).toBe(px);
@@ -115,8 +115,8 @@ describe('chart chrome defaults', () => {
115
115
  expect(timeSeriesProps.grid).toEqual({ horizontal: true });
116
116
  expect(timeSeriesProps.skipAnimation).toBe(true);
117
117
  // UCM-CHART-12: TimeSeriesChart's inner BarChart now takes size="standard" instead of a
118
- // fixed height prop -- the resolved height must still land on the historical 320px default.
119
- expect(timeSeriesProps.height).toBe(320);
118
+ // fixed height prop. UCM-CHART-15 shifted "standard" from 320px to 400px.
119
+ expect(timeSeriesProps.height).toBe(400);
120
120
  });
121
121
 
122
122
  it('keeps caller tick font, grid, showMark, and legendPosition values instead of the defaults', () => {
@@ -551,15 +551,22 @@ describe('resolveChartLayout composition invariant (UCM-CHART-12)', () => {
551
551
  });
552
552
  });
553
553
 
554
- describe('resolveChartLayout size tokens and height escape (UCM-CHART-12)', () => {
554
+ describe('resolveChartLayout size tokens and height escape (UCM-CHART-12, UCM-CHART-15)', () => {
555
555
  it.each(Object.entries(CHART_SIZE_SPACING_UNITS))('resolves size="%s" through the theme spacing scale', (size, units) => {
556
556
  const layout = resolveChartLayout({ size, spacing: theme.spacing });
557
557
  expect(layout.chartHeight).toBe(units * 8);
558
558
  });
559
559
 
560
- it('pins "standard" to the historical 320px default', () => {
561
- const layout = resolveChartLayout({ size: 'standard', spacing: theme.spacing });
562
- expect(layout.chartHeight).toBe(320);
560
+ // UCM-CHART-15: hardcoded independently of CHART_SIZE_SPACING_UNITS -- the test above computes
561
+ // its expectation from the same source under test and would pass unchanged against either the
562
+ // old or the new scale. This one is written to fail against the pre-CHART-15 values (240/320/400).
563
+ it('resolves the five size tokens to their documented pixel heights', () => {
564
+ const px = Object.fromEntries(['compact', 'standard', 'tall', 'extra_tall', 'super_tall'].map(
565
+ (size) => [size, resolveChartLayout({ size, spacing: theme.spacing }).chartHeight],
566
+ ));
567
+ expect(px).toEqual({
568
+ compact: 320, standard: 400, tall: 480, extra_tall: 560, super_tall: 640,
569
+ });
563
570
  });
564
571
 
565
572
  it('lets height override the size token entirely', () => {
@@ -567,13 +574,23 @@ describe('resolveChartLayout size tokens and height escape (UCM-CHART-12)', () =
567
574
  expect(layout.chartHeight).toBe(111);
568
575
  });
569
576
 
570
- it('throws on an unknown size token', () => {
577
+ // UCM-CHART-15: the height escape still wins for a token added in this WO, not only a
578
+ // pre-existing one.
579
+ it('lets height override a token that has no call site yet', () => {
580
+ const layout = resolveChartLayout({ size: 'super_tall', height: 111, spacing: theme.spacing });
581
+ expect(layout.chartHeight).toBe(111);
582
+ });
583
+
584
+ it('throws on an unknown size token, naming all five valid tokens', () => {
571
585
  expect(() => resolveChartLayout({ size: 'huge', spacing: theme.spacing })).toThrow(/Unknown chart size/);
586
+ expect(() => resolveChartLayout({ size: 'huge', spacing: theme.spacing })).toThrow(
587
+ /"compact" \| "standard" \| "tall" \| "extra_tall" \| "super_tall"/,
588
+ );
572
589
  });
573
590
 
574
591
  it('falls back to a numeric 8px unit when no spacing function is given', () => {
575
592
  const layout = resolveChartLayout({ size: 'standard' });
576
- expect(layout.chartHeight).toBe(320);
593
+ expect(layout.chartHeight).toBe(400);
577
594
  });
578
595
  });
579
596
 
@@ -0,0 +1,187 @@
1
+ # UCM-CHART-15 — Shift the chart size scale up one step, and raise the ceiling
2
+
3
+ ## Part A — Envelope
4
+
5
+ *Authored by the Expertenchat. Authoritative WHAT/WHY.*
6
+
7
+ ### Goal
8
+
9
+ The named chart sizes become one step taller, and the scale gains two steps above `tall`.
10
+
11
+ | token | today | new |
12
+ |---|---|---|
13
+ | `compact` | 30 → 240 px | **40 → 320 px** |
14
+ | `standard` | 40 → 320 px | **50 → 400 px** |
15
+ | `tall` | 50 → 400 px | **60 → 480 px** |
16
+ | `extra_tall` | — | **70 → 560 px** |
17
+ | `super_tall` | — | **80 → 640 px** |
18
+
19
+ The 10-unit step is preserved throughout, and every value stays a clean multiple of the theme's 8 px
20
+ spacing unit.
21
+
22
+ ### Why
23
+
24
+ Two independent pressures, both from real call sites rather than speculation.
25
+
26
+ **The ceiling is too low.** `tall` (400 px) is the largest token, and hram has already had to use the
27
+ documented `height` escape to get past it — `StructuralReachabilityPanel` sits at `height={480}` with
28
+ a comment recording exactly this: *"no size token goes taller than `tall`"*. An escape used because
29
+ the scale ends is a scale that ends too early.
30
+
31
+ **The whole scale is too cramped.** The operator's judgement, on existing plots and not only the new
32
+ ones. That judgement lands on a value that was never chosen on its merits: the comment at
33
+ `src/components/charts/chartDefaults.js:295` says `standard` is
34
+
35
+ > "pinned to the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so this
36
+ > migration does not also silently redraw every already-shipped default-sized chart"
37
+
38
+ That was a **migration guardrail, not a design verdict** — 320 was kept because it was the previous
39
+ value. The migration it protected is complete, so the pin has served its purpose and what remains is
40
+ a height inherited rather than decided.
41
+
42
+ ### The blast radius is one app, and that is why now
43
+
44
+ Measured across the workspace on 2026-08-24:
45
+
46
+ ```
47
+ hram 3.0.1 <- the only 3.x consumer
48
+ 14 other apps 2.41.3 / 2.37.0
49
+ ```
50
+
51
+ The size tokens are a **3.0.0 feature** (this repo's own CHANGELOG marks 3.0.0 "Breaking": `size`
52
+ replaced `minHeight`/`height`/`aspect`). On 2.x the scale does not exist. So no chart outside hram
53
+ changes, and the verification surface is one application.
54
+
55
+ **That inverts the usual caution into urgency:** fourteen apps are still to migrate onto 3.x. Every
56
+ app that migrates onto a scale already known to be too cramped has to be revisited afterwards.
57
+ Correcting the scale before they migrate is strictly cheaper than after.
58
+
59
+ ### Version: minor, by operator decision — and what that obliges
60
+
61
+ Redefining what an existing public token renders is breaking under semver, and the Expertenchat
62
+ recommended a major. **The operator decided minor on 2026-08-24; that decision stands and this WO
63
+ implements it.**
64
+
65
+ The consequence has to be carried somewhere, so it is carried here: a minor version can arrive
66
+ through automation. Renovate's weekly run bumps minors without a human reading the diff, and on this
67
+ estate that triage is also the vehicle that opens `develop → main`. A silent minor could therefore
68
+ make every hram chart 80 px taller in a PR nobody read as a visual change.
69
+
70
+ Two obligations follow, and they are not optional decoration:
71
+
72
+ 1. **The CHANGELOG entry must lead with the redraw**, in the plainest possible words: every existing
73
+ `compact`/`standard`/`tall` chart becomes 80 px taller. Not a bullet at the bottom — the first
74
+ line of the entry.
75
+ 2. **Release and consumption are coordinated**, not left to the weekly bump. `hram`'s pin bump and
76
+ its own verification are `HRAM-CHT-5` and land deliberately.
77
+
78
+ ### Scope
79
+
80
+ **`src/components/charts/chartDefaults.js`**
81
+
82
+ 1. `CHART_SIZE_SPACING_UNITS` becomes `{ compact: 40, standard: 50, tall: 60, extra_tall: 70,
83
+ super_tall: 80 }`.
84
+ 2. **Rewrite the pin comment at `:294-299`, do not delete it.** It explains why 320 was there; without
85
+ a successor the next reader sees an unexplained shift. The new text should say what the old pin
86
+ was for, that its migration is complete, and that the scale is now chosen rather than inherited.
87
+
88
+ **The enumeration sites — all of them.** The valid-size list is written out in more places than the
89
+ one that matters, and a half-updated enumeration is this change's most likely defect:
90
+
91
+ 3. `resolveChartHeightPx`'s error message (`:407`) — `"compact" | "standard" | "tall"`.
92
+ 4. `assertRemovedChartProp`'s guidance string in **each** chart component that carries it
93
+ (`LineChart.jsx:72`, `BarChart.jsx:54`, and the same call in `ScatterChart`).
94
+ 5. The `size` JSDoc in **each** preset (`LineChart.jsx:44`, `BarChart.jsx:27`, `ScatterChart.jsx:155`).
95
+ 6. `docs/CHART-LAYOUT.md`.
96
+ 7. The CHANGELOG entry described above.
97
+
98
+ Grep for the literal `"tall"` before declaring this done; the list above is what was found on
99
+ 2026-08-24 and is not guaranteed complete.
100
+
101
+ ### Non-goals / do not touch
102
+
103
+ - `resolveChartLayout`'s geometry, the axis-band resolution, margins, `xLabels`, rotation logic.
104
+ - The `height` px escape — it stays exactly as documented.
105
+ - `ChartFrame`'s `minHeight` semantics.
106
+ - Any consuming app. hram's adoption is `HRAM-CHT-5`.
107
+ - Adding a sixth step. If 640 px proves too small later, the scale extends by another 10 units; do
108
+ not pre-add one now.
109
+
110
+ ### Risks
111
+
112
+ - **A half-updated enumeration.** Adding the tokens to the resolver but not to the error message
113
+ produces a runtime error that lists only three valid values while five work — which reads as a bug
114
+ in the caller's code, not in this package.
115
+ - **A test that asserts the token names but not their values** would pass against both the old and
116
+ new scale and prove nothing. See test 1.
117
+ - The two new tokens have **no call site yet** in this repo. That is intended — `HRAM-CHT-5` adopts
118
+ them — but it means nothing here exercises them beyond the resolver test.
119
+
120
+ ### Tests to write
121
+
122
+ 1. `resolveChartHeightPx` returns **320 / 400 / 480 / 560 / 640** for the five tokens — asserting the
123
+ pixel values, not just that a number comes back. Written so it fails against the current scale.
124
+ 2. An unknown size still throws, and the message names **all five** valid tokens.
125
+ 3. The `height` escape still wins over `size`, for a new token as well as an old one.
126
+ 4. Existing chart-layout tests that hard-code 240/320/400 are updated deliberately, each one seen
127
+ and changed rather than mass-replaced — one of them may be asserting the old value *on purpose*.
128
+
129
+ ---
130
+
131
+ ## Part B — Implementation map
132
+
133
+ `.claude/models.local.json` has no local override, so the default applies: `implementation.runtime`
134
+ is `claude`/`sonnet` — the Orchestrator implemented directly, no Codex dispatch, no preamble/progress
135
+ contract needed.
136
+
137
+ Context package used:
138
+
139
+ - `src/components/charts/chartDefaults.js:294-300` (the pin comment + `CHART_SIZE_SPACING_UNITS`)
140
+ and `:402-419` (`resolveChartHeightPx`, its error message, and the `theme.spacing` unwrap comment).
141
+ - Enumeration sites confirmed by grep for the literal `"tall"` (matched the WO's own 2026-08-24
142
+ list exactly, nothing extra found): `LineChart.jsx:44,72`, `BarChart.jsx:27,54`,
143
+ `ScatterChart.jsx:155,198`, `docs/CHART-LAYOUT.md:66,73-75`.
144
+ - Existing hardcoded-value tests located via grep for `240|320|400` across `tests/*.test.{js,jsx}`:
145
+ `chartDefaults.test.js:119,562,576`, `BarChart.test.jsx:36,83-85`, `LineChart.test.jsx:51-53`,
146
+ `ScatterChart.test.jsx:242-244`. Distinguished from unrelated same-valued `minHeight`/`ChartFrame`
147
+ test fixtures (`chartDefaults.test.js:291,298,308,315,318`, `ChartFrame.test.jsx:294,311`,
148
+ `chart-api-census.test.js:196`), which were left untouched — they assert an arbitrary prop value,
149
+ not a size-token pixel height.
150
+
151
+ ---
152
+
153
+ ## Part C — Orchestrator only
154
+
155
+ > **STOP — everything below addresses the Orchestrator.**
156
+
157
+ **Tier 3 · tests: the four resolver cases plus the updated layout tests**
158
+
159
+ Shared-core — a change *inside* `ui-core-micha` is Tier 3 by the tiering table regardless of diff
160
+ size, and this one redefines rendered output for every consumer that reaches 3.x.
161
+
162
+ - `reviewer` — the enumeration completeness and the test's ability to fail are its brief.
163
+ - `ui_reviewer` — the scale as a design decision: does the 10-unit step still read as a coherent
164
+ scale with five members, and is `super_tall` distinguishable from `extra_tall` in practice.
165
+ - `sec_reviewer` — not run. No auth, no data, no exposure surface.
166
+ - Concurrent, full context.
167
+
168
+ ### Release
169
+
170
+ Minor bump per the operator's decision → **3.2.0** from the current 3.1.1. Follow this repo's release
171
+ procedure. The CHANGELOG obligation in Part A is a release gate, not a nicety: if the entry does not
172
+ lead with "every existing chart becomes 80 px taller", the release is not ready.
173
+
174
+ Do **not** bump hram from here. `HRAM-CHT-5` owns that, and it carries the visual verification.
175
+
176
+ ### Register
177
+
178
+ Row `UCM-CHART-15`, review notes in the required shape, the published version, landing SHA(s) in a
179
+ second commit before the push. Record explicitly that a **major was recommended and minor was chosen
180
+ by the operator** — a future reader tracing a silent redraw should find that decision, not infer it.
181
+
182
+ ### Log
183
+
184
+ - **2026-08-24** — Envelope authored. Operator judged all three existing tokens too cramped and chose
185
+ a full one-step shift over adding a token at the top, accepting the breaking redraw; added
186
+ `super_tall` to the two steps proposed. Version set to minor against the Expertenchat's
187
+ recommendation of major, recorded above with its consequence.