@micha.bigler/ui-core-micha 3.1.2 → 3.2.1

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,53 @@
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.1 — UCM-CHART-16
6
+
7
+ **A horizontal-axis chart with a title, or a large-enough tick font, could render every tick
8
+ label blank.** The x-axis tick band reserved a flat, font-size-independent floor; MUI's own
9
+ internal fit check (`shortenLabels`, against `tickLabelsMaxHeight`) blanks every tick label the
10
+ moment the real rendered text is even slightly taller than that floor leaves room for once MUI's
11
+ own tick/gap overhead — and, when a title shares the same band, its own gap too — is subtracted.
12
+ Correctly positioned tick marks with zero visible label text is the exact, easy-to-misread
13
+ signature; two hram consumers had independently patched around it with the same empirically
14
+ measured `height: 56` before this was traced to its shared-core cause.
15
+
16
+ The band is now derived from the actual tick font size (and whether an axis title shares it)
17
+ instead of the flat floor, matching MUI's real fit check rather than only its generic default.
18
+ Patch, not minor: this repairs existing behaviour — the SAME size tokens render at the SAME
19
+ heights (`UCM-CHART-15` is untouched) — the only change is the flat tick band growing by a few
20
+ pixels on charts that were previously either mis-sized or silently blanking their labels. The
21
+ rotated-tick path (`xLabels: "angled"`) is unaffected.
22
+
23
+ ## 3.2.0 — UCM-CHART-15
24
+
25
+ **Breaking visual change: every existing chart using a `size` token — including the default
26
+ `standard` size — becomes 80px taller; `standard` is now 400px.** The chart `size` scale shifts
27
+ up one step and gains two new steps above the old ceiling:
28
+
29
+ | token | before | after |
30
+ |---|---|---|
31
+ | `compact` | 240px | **320px** |
32
+ | `standard` | 320px | **400px** |
33
+ | `tall` | 400px | **480px** |
34
+ | `extra_tall` | — | **560px** (new) |
35
+ | `super_tall` | — | **640px** (new) |
36
+
37
+ This redraws every chart already using a `size` token, in every consumer on `ui-core-micha` `3.x`.
38
+ The 10-unit step is unchanged; every value stays a clean multiple of the theme's 8px spacing unit.
39
+
40
+ **Why:** `tall` was the tallest token available, and hram had already needed the documented
41
+ `height` px escape to get past it — a scale that forces its own escape ends too early.
42
+ `standard`'s old 320px value was a migration guardrail from `UCM-CHART-12` (pinned to the
43
+ pre-existing deployed default so that migration did not itself redraw anything), not a design
44
+ verdict on its own merits; the operator judged the whole scale too cramped on its own merits and
45
+ chose to correct it now, before the fourteen remaining apps still on `2.x` migrate onto it.
46
+
47
+ **This is a breaking change shipped as a minor**, by explicit operator decision (`2026-08-24`),
48
+ against the Expertenchat's recommendation of a major. No consuming app is on `3.x` except hram, so
49
+ the practical blast radius is one application; hram's own adoption and visual verification is a
50
+ separate, deliberate work order (`HRAM-CHT-5`), not automatic from this release.
51
+
5
52
  ## 3.1.2 — UCM-CHART-14 follow-up
6
53
 
7
54
  **The census script shipped in `3.1.1` worked; its test suite never ran.** `scripts/chart-api-census.mjs`
package/WORK_ORDERS.md CHANGED
@@ -27,6 +27,8 @@ 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-16 | The x-axis band starves MUI's tick-fit check, and two consumers have patched around it | A chart with an axis title **and** horizontal tick labels must render its tick labels without a consumer hand-setting `xAxis[].height`. **Defect:** on the horizontal path `resolveXAxisGeometry` reserves one combined band from `TICK_BAND_BASE_PX` + `AXIS_TITLE_BAND_PX`; once a title is also taken from that budget, MUI's own fit check (`shortenLabels` against `tickLabelsMaxHeight`) is left **just under one line of 12 px text** and blanks **every** label regardless of content. Distinctive and easy to misread: correctly positioned tick marks, zero non-empty label texts -- looks like a data or formatter problem and is neither. **Evidence: two consumers, same workaround, same magic number**, independently, in two different WOs -- hram `StructuralReachabilityPanel.jsx:386` (`HRAM-RES-32`) and `AccessGapScatterPanel.jsx:212` (`HRAM-RES-39`), both `height: 56`. RES-32's own comment already diagnosed it and called it *"a pre-existing, latent shared-core near-miss"*, then patched locally because a shared-core fix was outside that WO; RES-39 read the MUI source, reached the same cause and the same 56, and patched locally again. **The third titled-horizontal-axis chart will hit it too**, and the package is meanwhile violating its own stated Rule 1 (*"nothing outside the resolver does chart arithmetic"*) in two places. Fix: correct the reservation, and **derive** it from the known tick font size and title band rather than adding a second magic number that will be wrong at a different font size. Full order [`work-orders/UCM-CHART-16.md`](work-orders/UCM-CHART-16.md) | 2026-08-25 | done | `9830266` | **Tier 3** -- shared-core. `reviewer` + `ui_reviewer` (the plot-area change across consumers); no `sec_reviewer`. **The risk that decides this WO: a mocked prop-assertion test cannot catch it.** The file already records the same trap for a neighbouring bug (*"a mocked prop-assertion test cannot catch this, it only reproduces on an actual MUI scale computation"*) -- a test asserting the computed band's number passes whatever the number is. **Test 1 must observe rendered label text**, or it is decoration. Also: raising the band changes every titled-horizontal-axis chart's plot area, on top of `UCM-CHART-15`'s 80 px shift -- and fixing it too generously wastes vertical space everywhere, so the near-miss margin is the budget, not a number rounded up to be safe. Release patch unless the plot-area change warrants minor; say which and why in the CHANGELOG. Consumer cleanup (removing hram's two workarounds) is `HRAM-CHT-6`, after this ships. **Third occurrence, 2026-08-25, and it changes the fix:** hram's `AccessibilityPanel` in `Urban/Peri-urban/Rural x Division` mode -- five band categories, five rendered bar groups, **no tick labels at all**, same signature. But that chart has **no `xAxisLabel`**, so the title is not competing for the band; what it has is `tickLabelStyle: { fontSize: 13 }`, one pixel above the 12 px the base band was sized against. **So the trigger is not "a title takes the room" -- it is that the band is a near-miss at all**, and anything consuming a little more pushes it over. A fixed `height: 56` patched into a third consumer would work at 13 px and fail at 14. That is exactly why scope item 2 requires the resolver to **derive** the requirement from the tick font size it already knows, instead of adopting the constant two consumers measured empirically at 12 px. Signature-matched, not yet instrumented -- confirm the mechanism on this third case, since it is the one that distinguishes a derived fix from a bigger constant. **Implementation:** `.claude/models.local.json` has no local override (defaults apply) -- `implementation.runtime` is `claude`/`sonnet`, implemented directly by the Orchestrator, no Codex dispatch. **The defect could not be confirmed from source reading alone** (jsdom does not implement real SVG text measurement, `getBBox`, so a rendered test would trivially pass regardless of correctness): reproduced and calibrated against the real, unmocked `BarChart` in this repo's own dev harness (Chrome, DM Sans, via `dev/entries.jsx`, reverted before commit) -- confirmed the defect reproduces exactly as described, confirmed it does NOT reproduce under MUI's default Roboto stack (explains why it was font-dependent and missed until a consumer used a taller font), then binary-searched the real minimal `xAxis[].height` at ten font-size/title combinations (11-18px) to derive `TICK_TEXT_HEIGHT_FACTOR=1.3`, `TICK_BAND_MUI_OVERHEAD_PX=9`, `TICK_BAND_TITLE_EXTRA_PX=2` -- matched every measured threshold within 1px, never under. Third occurrence (no title, `tickLabelStyle.fontSize:13`) confirmed via the same harness. 180 affected chart tests green (78 `chartDefaults.test.js` + 2 new rendered tests in `chartAxisTickBand.test.jsx`, which install a calibrated `SVGElement.prototype.getBBox` mock since jsdom's real implementation always measures 0x0 + 100 across `BarChart`/`LineChart`/`ScatterChart`/`TimeSeriesChart`/`ChartFrame`), `tsc -p tsconfig.build.json` clean. Both new resolver tests and both new rendered tests independently confirmed to fail against the pre-fix resolver (temporarily reverted, reran, restored) -- test 1 must observe rendered label text, not a computed number, per the WO's own risk section; both requirements met. Version bumped `3.2.0` -> `3.2.1` (patch, per the WO's own classification -- repairs existing behaviour, `UCM-CHART-15`'s heights untouched). **Review: codex/gpt-5.6-luna (`reviewer`) · 3 raised · 1 accepted · worst accepted: defect · chosen** (id not divisible by 5, no rotation configured). `reviewer` findings: (1) ACCEPTED -- the Rule-2 guardrail test only asserted `xTitleBand === 0`, which passes whether or not the title margin leaked into `xAxisBand` instead; fixed to assert the actual band/height values too. (2) REJECTED, out of scope -- a caller-supplied `xAxis[].tickSize` override isn't a documented/supported customization point of this package (grep confirms zero call sites, in this repo or its own tests/docs, ever set it); adding speculative handling for an unused knob is the "second magic number" pattern this WO exists to avoid. (3) REJECTED, out of scope -- a custom/multiline title font size overflowing the fixed `AXIS_TITLE_BAND_PX=20` reservation is a pre-existing `UCM-CHART-12`-era limitation, unchanged and unworsened by this WO. **`ui_reviewer`: codex/gpt-5.6-luna dispatch produced NO USABLE VERDICT** -- ran for an extended period repeatedly re-executing `vitest`/`git` against the live working tree (including applying then reverting a debug `console.log` in the new test file -- confirmed harmless via `git diff`/content check, but a review process should never write to the shared tree) and terminated with exit 0 but no findings/verdict text. Treated as unusable per "a review never lapses because a provider is down" -- fell back to `review_fallback` (`claude`/`sonnet`), spawned read-only, explicitly instructed not to run tests or mutate anything: **no findings**, confirmed the fix is byte-identical to the pre-fix band at the package's own default tick font (zero growth for unaffected charts), the CHANGELOG communicates the visual consequence plainly, and patch is the right classification. **Flagged for awareness, not fixed here:** the codex `ui_reviewer` runaway-without-verdict pattern is worth watching for future Tier-3 dispatches -- consider adding an explicit "review only, do not modify files or run mutating commands" line to review prompts. |
31
+ | 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
32
  | 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
33
  | 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
34
  | 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 +98,7 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
96
98
  | 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 `<text>` 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
99
  | 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
100
  | 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, `c2bb86b` | `3627da5` **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. | 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):
101
+ | 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
102
  ```
101
103
  fitness-monitor:
102
104
  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);
@@ -144,6 +144,28 @@ export const PACKAGE_DEFAULT_MARGIN = {
144
144
  // PACKAGE_DEFAULT_MARGIN breathing-room, applied here for the same reason (a hard 0px gap reads
145
145
  // as clipped even when nothing is actually cut off).
146
146
  const ROTATED_TICK_CLEARANCE_PX = 8;
147
+ // UCM-CHART-16: the FLAT (angle 0) tick band used to be the unconditional TICK_BAND_BASE_PX
148
+ // regardless of the actual tick font size -- fine for MUI's own generic default, but MUI's
149
+ // internal fit check (`shortenLabels`, against `tickLabelsMaxHeight`) blanks EVERY tick label the
150
+ // moment the real rendered text is even slightly taller than what that flat floor leaves room
151
+ // for once MUI's own tick/gap overhead (and, when an axis title shares the band, its own gap) is
152
+ // subtracted. `TICK_BAND_BASE_PX` alone was a near-miss with essentially no margin at any font
153
+ // size above whatever MUI itself implicitly assumed -- two hram consumers independently patched
154
+ // around it with the same empirically-measured `height: 56`, and a third, titleless case broke at
155
+ // a tick font just 1px above the untitled default.
156
+ //
157
+ // These three constants are DERIVED, not guessed: binary-searched against the real, unmocked
158
+ // `BarChart` rendered in this repo's own dev harness (`dev/entries.jsx`'s `chart-probe` entry,
159
+ // Chrome, this package's own DM Sans default -- the taller of the two fonts checked; MUI's Roboto
160
+ // fallback needs visibly less and was never the trigger) at tick font sizes 11-18px, with and
161
+ // without an axis title:
162
+ // required flat tickBand ~= ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
163
+ // required tickBand+title ~= that, plus TICK_BAND_TITLE_EXTRA_PX, plus AXIS_TITLE_BAND_PX
164
+ // matched every measured threshold exactly or over by at most 1px, never under, across ten
165
+ // font-size/title combinations -- see UCM-CHART-16's work order for the full measurement table.
166
+ const TICK_TEXT_HEIGHT_FACTOR = 1.3;
167
+ const TICK_BAND_MUI_OVERHEAD_PX = 9;
168
+ const TICK_BAND_TITLE_EXTRA_PX = 2;
147
169
  // THEME-9: MUI X-Charts' own default y-axis width is a flat 45px (ticks) +
148
170
  // 20px (label, if present) = 65px regardless of what the ticks actually say
149
171
  // -- confirmed against @mui/x-charts' own DEFAULT_AXIS_SIZE_WIDTH /
@@ -280,13 +302,18 @@ export function defaultNumericTickFormatter(locale) {
280
302
  ? new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value)
281
303
  : new Intl.NumberFormat(locale).format(value));
282
304
  }
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 };
305
+ // UCM-CHART-15: named size tokens, resolved through the theme's 8px spacing unit
306
+ // (`src/theme/tokens.js`'s `spacing: 8`). Through UCM-CHART-12/14, `standard` stayed pinned to
307
+ // the pre-existing deployed default (`TimeSeriesChart`'s old `CHART_HEIGHT = 320`) so that
308
+ // migration did not also silently redraw every already-shipped default-sized chart -- that pin
309
+ // was a migration guardrail, not a design verdict. The migration it protected is complete, and
310
+ // the operator judged the whole scale too cramped (`tall` had already forced hram to use the
311
+ // `height` px escape to get past the old ceiling), so this WO shifts every token up one step and
312
+ // adds two steps above the old `tall`, chosen rather than inherited. Documented plainly in
313
+ // `docs/CHART-LAYOUT.md` -- these are visible product decisions, not internal plumbing.
314
+ export const CHART_SIZE_SPACING_UNITS = {
315
+ compact: 40, standard: 50, tall: 60, extra_tall: 70, super_tall: 80,
316
+ };
290
317
  // UCM-CHART-12 Rule 2: every band is its own measured content, or zero -- no band is ever a
291
318
  // constant taken on faith. These two ARE constants, but only for the "content is present" case;
292
319
  // each one collapses to 0 the moment its content is absent (`resolveXAxisGeometry`,
@@ -368,8 +395,18 @@ function resolveXAxisGeometry(axis, xLabelsMode, defaultLineHeight, measureTextW
368
395
  const needsRotation = dataCount > AUTO_ROTATE_TICK_COUNT && labelWidthPx > AUTO_ROTATE_LABEL_WIDTH_PX;
369
396
  angle = needsRotation ? -45 : 0;
370
397
  } // "horizontal" always stays at angle 0
371
- if (angle === 0)
372
- return { tickBand: TICK_BAND_BASE_PX, angle: 0 };
398
+ if (angle === 0) {
399
+ // UCM-CHART-16: derive the flat band from the actual tick font size instead of the
400
+ // unconditional floor -- `axis?.label` is the SAME signal `resolveChartLayout` uses to decide
401
+ // whether an axis title also shares this band (`xTitleBand`), so a titled axis reserves the
402
+ // extra room its own gap costs, precisely where the near-miss was.
403
+ const hasTitle = Boolean(axis === null || axis === void 0 ? void 0 : axis.label);
404
+ const flatTickBand = fontSize > 0
405
+ ? Math.ceil(fontSize * TICK_TEXT_HEIGHT_FACTOR) + TICK_BAND_MUI_OVERHEAD_PX
406
+ + (hasTitle ? TICK_BAND_TITLE_EXTRA_PX : 0)
407
+ : TICK_BAND_BASE_PX;
408
+ return { tickBand: Math.max(TICK_BAND_BASE_PX, flatTickBand), angle: 0 };
409
+ }
373
410
  // Rotation is a visual instruction independent of whether a font size is knowable here -- an
374
411
  // "angled" axis still rotates even when `fontSize` can't be resolved (no theme fallback given),
375
412
  // it just can't project a tighter band than the floor without one.
@@ -388,10 +425,10 @@ function resolveChartHeightPx(size, height, spacing) {
388
425
  return height;
389
426
  const units = CHART_SIZE_SPACING_UNITS[size];
390
427
  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.');
428
+ throw new Error(`[ui-core-micha] Unknown chart size="${size}". Use "compact" | "standard" | "tall" | `
429
+ + '"extra_tall" | "super_tall", or pass height (in px) for the documented escape.');
393
430
  }
394
- // MUI's `theme.spacing(n)` returns a CSS length STRING ("240px"), not a number -- the chart
431
+ // MUI's `theme.spacing(n)` returns a CSS length STRING ("320px"), not a number -- the chart
395
432
  // height feeds straight into MUI X-Charts' own numeric `height` prop, so it must be unwrapped
396
433
  // back to a px number via the same `cssLengthToPixels` the rest of this file already uses.
397
434
  return typeof spacing === 'function' ? cssLengthToPixels(spacing(units)) : units * 8;
@@ -15,13 +15,15 @@ chartHeight === plotHeight + xAxisBand + xTitleBand + legendBand
15
15
  ```
16
16
 
17
17
  - `xAxisBand` — the tick row. Zero when the axis has no visible tick content (an empty `data`
18
- array, or every formatted tick is a blank string). Non-zero otherwise: a flat baseline for
19
- horizontal labels, or a measured/estimated projection for rotated ones (same measurement
20
- mechanism as before — a real DOM measurement when available, a per-glyph estimate otherwise).
21
- The flat baseline (25px) and the title addition (20px) are MUI X-Charts' own
22
- `DEFAULT_AXIS_SIZE_HEIGHT`/`AXIS_LABEL_DEFAULT_HEIGHT` constants, not a re-estimate — the plain
23
- unrotated/untitled case renders pixel-identical to before this WO, even though the resolver now
24
- sets `axis.height` explicitly where MUI used to fill it in implicitly.
18
+ array, or every formatted tick is a blank string). Non-zero otherwise: for horizontal labels, a
19
+ floor of 25px (MUI X-Charts' own `DEFAULT_AXIS_SIZE_HEIGHT`) scaled up from the actual tick font
20
+ size once it needs more room than that floor leaves MUI's own internal fit check — a flat floor
21
+ alone was a near-miss with essentially no margin at any font size above whatever MUI itself
22
+ implicitly assumed (`UCM-CHART-16`); or a measured/estimated projection for rotated ones (same
23
+ measurement mechanism as before — a real DOM measurement when available, a per-glyph estimate
24
+ otherwise). The title addition (20px, `AXIS_LABEL_DEFAULT_HEIGHT`) grows by a further few pixels
25
+ when an axis title shares the tick row's band, matching the extra gap MUI itself reserves
26
+ between the two.
25
27
  - `xTitleBand` — the axis title. Zero unless the x-axis carries a `label`.
26
28
  - `legendBand` — one row for the legend. Zero when `hideLegend` is set.
27
29
  - `plotHeight` is the residual: `chartHeight` minus the three bands above.
@@ -70,14 +72,19 @@ Tokens resolve through the theme's spacing scale (`theme.spacing`, MUI's 8px uni
70
72
 
71
73
  | Token | Spacing units | Height |
72
74
  |---|---|---|
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`.
75
+ | `compact` | 40 | 320px |
76
+ | `standard` | 50 | 400px |
77
+ | `tall` | 60 | 480px |
78
+ | `extra_tall` | 70 | 560px |
79
+ | `super_tall` | 80 | 640px |
80
+
81
+ **UCM-CHART-15** shifted the whole scale up one step and added `extra_tall`/`super_tall` above the
82
+ old ceiling — `tall` (400px) had been the largest token, and hram had already needed the `height`
83
+ px escape to get past it. `standard`'s old pin to 320px (`TimeSeriesChart`'s historical
84
+ `CHART_HEIGHT`) was a migration guardrail for CHART-12, not a design verdict; that migration is
85
+ long complete, so the scale is now a chosen set of five values rather than one inherited number.
86
+ `height` (a raw px number) survives as a **documented escape** for a justified special case; prefer
87
+ `size`.
81
88
 
82
89
  **Four call sites lose responsive height with `aspect` gone**: `hram/AccessGapScatterPanel.jsx`
83
90
  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.2",
3
+ "version": "3.2.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "sideEffects": [
@@ -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