Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit b5d9f7b35cec1d36fb4d90fcd1f22d88e0243819
parent 4132b68c9c04c7df92ddd5b4de3668f06c2cbb10
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 03:33:45 -0400

plans: slice O5 as shipped — umtool brand fonts: Plex Sans for the rail / ledger / scroll / chart fitted by the face's own advances, Plex Mono Bold vendored, the unbranded render byte-identical (334 files, 0 different)

The record: why a table rather than an average (Fira's 0.50 re-measured
at 0.457 em; even it overruns given real text; Plex 0 over in the proof),
what shipped, the provenance of IBMPlexMono-Bold.ttf, the proof sheet
and overrun check, the identity proof with its control, the gates
(test:scripts 185 + 1, umtool build, umtool e2e 175/2/45 with mix.spec
alone 12/12), the bite table, and what is left (Fira's own overruns,
the first scroll head beside DATE, STATE.md and S4's record for the
integration pass).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mplans/release-11.md | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 171 insertions(+), 0 deletions(-)

diff --git a/plans/release-11.md b/plans/release-11.md @@ -663,6 +663,177 @@ Gates on the merged tree, from the worktree root (`o6-gateR.log`, `o6-e2e-editor ENVIRONMENT.md regenerated fails exactly the mention test (1); an undeclared `ARCHILYZER_NEW_KNOB` in the entrypoint fails 1; all-absent ids not refused fails 1. +### Slice O5, as shipped — umtool brand fonts (2026-09-28) + +Branch `r11/umtool-fonts` off `main` `2162db92`, fast-forwarded to `main` `cb9d02b2` (O4, O3, O6 +checkpoint A) before any change; worktree `/home/user/Projects/r11-umtool-fonts`, port block #8 +(umtool e2e 3851, stub 3852). One Opus implementer; scratch files `o5-*` in the job's +`tmp/overnight`. Everything is under `umtool/report-to-video/**`; nothing in `umtool/app/**`. + +**Why.** S4 left two things for later ([`brand-and-themes.md`](brand-and-themes.md), "Slice S4, as +shipped"): the rail, `ledger`, `scroll` and `chart` SVG text stayed Fira Sans under the brand, +because `fit()` truncates against Fira's 0.50 em average; and `render.fontBold` stayed whatever the +manifest said, because no bold Plex Mono was vendored. O5 does both, and an unbranded render stays +byte-identical. + +**The measurement, and why the brand's metric is a table rather than an average.** Nothing +recorded how 0.50 was measured, so O5 fixed a method and checked that it gives Fira's own number: +the mean advance per character (fontTools hmtx; Fira from the system files fontconfig resolves, Plex +Sans from the vendored variable font instanced at wdth 100) over the text these assets draw. The +`fit()` corpus is the five S4 manifests' track labels, log and scroll labels, roster lines, column +heads and quotes, read and never written (`o5-corpus.json`, 144 strings, all from +`quartering-employee-count`, the only one with a rail). + +| face | weight | `fit()` strings | p95 of a string's own mean | every SVG text run | capitals | +|---|---|---|---|---|---| +| Fira Sans | 400 | 0.457 em | 0.492 | 0.463 | 0.549 | +| Fira Sans | 700 | 0.462 | 0.497 | 0.525 | 0.550 | +| IBM Plex Sans | 400 | 0.456 | 0.497 | 0.469 | 0.590 | +| IBM Plex Sans | 700 | 0.479 | 0.521 | 0.577 | 0.613 | + +So 0.50 is Fira's mean plus the "hair" its comment describes, just over its p95. By the same method +Plex Sans Regular gets 0.50 too, and Bold 0.53. **But an average cannot keep the promise the slice +makes, that a branded column never overruns** (`o5-stress.log`). Every real string (87, 77 over 20 +characters) was pushed through every call site's size, budget, weight and letter-spacing: +- **Fira's own 0.50 overruns** the scroll's bold, letter-spaced column head in 40 of 87 runs (up + to 11 %) and the rail's track label once; +- Plex at 0.50 / 0.53 overruns the head in 35 of 87 runs; +- uppercased, every site overruns in both faces at every average tried (0.50 to 0.55). + +None of the manifests' labels comes near a budget as written (the tightest is a wrapped quote line +at 95 %), which is why this never showed. So the preset's metric is **the face's own advances**: +- `face-metrics.mjs` is generated by `fonts/gen-face-metrics.py` (fontTools 4.65.0, instancer) + from the vendored `IBMPlexSans[wdth,wght].ttf`. It holds the advance of all 891 characters the + font maps at wght 400 and 700, wdth 100 (11.5 kB, do-not-edit header, the font's sha256). +- `fit()` keeps the longest prefix whose width, ellipsis, weight and letter-spacing included, is + inside the budget. +- Fira keeps its 0.50 average and its exact character counts. It is the system's font, so a table + of its advances could not be pinned anyway. +- Kerning is ignored. Over the proof's 24 Plex runs, the plain sum was at or above the rendered ink + width of every one (by 0.19 px at the least), so the sum is the conservative side. +- A character Plex does not map (emoji, CJK) counts 1.3 em. + +**What shipped.** +- **`svg-faces.mjs`:** `FIRA_SANS` (`{family, em: 0.5}`), `IBM_PLEX_SANS` (`{family, advances: + {regular, bold}}`, built from the table), `textWidth(text, size, face, {weight, ls})`, and + `FALLBACK_EM`. `brand.mjs` gains `brandSvgFace(render)`: null without a brand, the preset's face + with one. +- **`render-cards.mjs`:** + - `fit(text, size, maxPx, face = FIRA_SANS, run)` and `wrapPx(…, face)` are exported. An + average face runs the old code path verbatim. A measured face fits by width, and `wrapPx` + wraps by width and fits every line, so a lone overlong word is cut too. + - `svgTextIn(face)` sets each asset's family. The rail (chrome, log, tally, roster, the QR + tile), the ledger card, the scroll and the chart take `svgFaceOf(render)`. + - The scroll head fits with `{weight: "bold", ls: 0.6}`, and the ledger's scope pill is the + label's measured width + 22 (Fira: `length × 7.6 + 22`, unchanged). + - `rasterize()` hands rsvg-convert `childOpts(render, …)`, which passes the options through + unchanged without a brand. Without it a branded SVG found no Plex (it is not a system font) + and fell back to another sans, silently. +- **IBM Plex Mono Bold, vendored:** + + | | | + |---|---| + | file | `fonts/IBMPlexMono-Bold.ttf`, unmodified (Reserved Font Name "Plex") | + | source | google/fonts `ofl/ibmplexmono` at `0b58fb37` (`0b58fb370093f9a9f4ff785d94405710b79de67c`), the commit S4 took Regular from; Regular there is byte-identical to the vendored one | + | bytes | 137,784 | + | sha256 | `ac27abd6450a64dd94467580a02fe6235156d5b92f2926ebbc8e7489df64e0be` (git blob `17b406d2`, as upstream lists it) | + | licence | `OFL.txt` already holds Plex's OFL verbatim; unchanged | + + `fonts.conf`'s relative `<dir>` picks it up: under `FONTCONFIG_FILE`, `IBM Plex Mono:bold` now + resolves to it (it resolved to Regular, emboldened, before), and without it the system still + answers Noto. `resolveBrandRender` sets `fontBold` to it beside `fontRegular`, so compose-chrome's + HyperFrames band sets its bold in the brand's mono. The fonts are about 1.5 MB in all. +- **README:** the preset's "owns" list; the "Not branded yet" paragraph goes; the fonts paragraph; + "Why a table, not an average". + +**Shown.** +- **`o5-fit-proof.png`** (+ `.tsv`): every `fit()` call site, three cases each (the site's longest + real label; the longest real string anywhere, a 143-character quote; and the site's label in + capitals), fitted in both faces inside a box exactly its budget wide. Each fitted run is then rendered + **alone** through rsvg-convert and its ink measured: + - **Plex: 24 runs, 9 truncated, 0 over budget, widest 98.9 %.** + - Fira (the unchanged path): 1 over, the scroll head at 152 px in 140. +- **`o5-overrun.py`** computes every `<text>` run's extent from its face's real advances (anchor, + weight and letter-spacing aware) over the SVG sources the renderers write. It flags a run that + leaves its canvas or tally lane, or overlaps another run on its line. **0 problems** in all 27 + SVGs of each set: unbranded, branded before, branded after (1,552–1,554 runs). It catches a + synthetic collision. +- **Branded before/after** (`o5-brandshots.mjs`: the five manifests with `render.brand` and + `endCard: false` set in memory, 19 render calls). Every asset differs, as it should. Two ledger + quotes that Fira wrapped onto a stranded last word (L03 "employees", L06 "now") are one line in + Plex, and the pills hug their labels. + - Looked at: the ledger cards, the rail chrome, the log, the tally strip, the QR tile, the scroll + and the chart. No clipped glyph and no overrun. + - "IMPLIED" clears its delta triangle by 21.4 px (Fira 28), and "≈" and "→" are in Plex. + - `o5-compare.png`: a ledger row, the scroll heads and the chart's end labels, before and after. +- **The branded chapter kicker** (Pango, the `label` face in bold) now draws the real Plex Mono Bold, + where it drew an emboldened Regular before. It is the only change on that card: 276 px inside the + kicker's 189 × 24 box, and it looks the same at 24 px. + +**The byte-identity proof** (S4's harness, copied as `o5-identity.mjs` / `o5-idiff.mjs`, paths +adapted; S4's fixture copied to `o5-fixture`): +- **BEFORE** was rendered on `cb9d02b2` before any change, into `o5-before/`. It covers every card, + footer, rail, ledger, scroll and chart asset of the five manifests in both variants (70 render + calls, the resolved `render` blocks included), plus a full offline fixture build. +- **Control** (the same code twice, `o5-before` vs `o5-before2`): 334 files, 201 raw identical, 133 + identical but for ImageMagick's `tEXt date:*` chunks, 0 different. +- **AFTER, on the final code tip `216533ea`** (`o5-after-2`): **334 files, 201 raw identical, 133 + identical but for the date chunks, 0 different, none missing or extra.** The fixture's cut is the + same 3,380,693 bytes, and the mp4s are raw-identical. The same result on the pre-commit tree + (`o5-after-1`). +- The five manifests' sha256 were the same after every run (`o5-manifests-sha-before.txt`). + +| sha | what | +|---|---| +| `b9933d71` | `umtool:` vendor IBM Plex Mono Bold; `resolveBrandRender` sets `fontBold`; brand tests (the file, the value, fontconfig resolves it) | +| `2223ba07` | `umtool:` Plex Sans for the rail / ledger / scroll / chart under the preset — `svg-faces.mjs`, `face-metrics.mjs` + `fonts/gen-face-metrics.py`, `fit()`/`wrapPx()` take the face, the pill, `rasterize()` gets `childOpts`, `brandSvgFace`; `svg-faces.test.mjs` (7) and a scroll-card render test | +| `b555af1a` | `umtool:` test that the preset's rsvg-convert children get `FONTCONFIG_FILE` (an `RSVG_BIN` stub); fc-match's escaped paths unescaped | +| `216533ea` | `umtool:` README — the preset's faces, "Why a table, not an average", Plex Mono Bold; `svg-faces.mjs`'s header says what was rendered | +| _this_ | `plans:` this record | + +**Gates** (from the worktree root; logs `o5-*.log`): +- **tsc clean** before every commit: 66 s, 32 s, 38 s and 30 s. The `.mjs` files are `allowJs` + without `checkJs`, so it resolves them but does not check them. +- **`test:scripts` 185 + 1 skip** (`main` `cb9d02b2`: 175 + 1; +10 = `svg-faces.test.mjs` 7 + + `brand.test.mjs` 3). Common, editor unit and mcp not run: nothing outside umtool changed. +- **umtool build** (`pnpm --filter umtool run build`): ok, 17 s. +- **umtool e2e, full** (`SONG_DIR=~/reports/quartering-uh-song/data node scripts/worktree.mjs run -- + pnpm --filter umtool run e2e`, on `2223ba07`, no queue wait): **175 passed, 2 failed, 45 skipped, + 4.4 min**, which is release 10's count. The two failures are `mix.spec.ts:166` and `:201`, the + known order-dependent pair; the skips are the song-data capabilities this machine lacks. + **`mix.spec.ts` alone: 12 passed** (26 s), both included. The two later commits are a test, the + README and a comment, so it was not re-run. +- **Changelog:** umtool has none (`umtool/CHANGELOG.md` does not exist), so nothing was added. + +**They bite** (`o5-bite.py`: scratch copies of `report-to-video/`, the worktree untouched; `svg-faces` ++ `brand` tests, 21): + +| variant | result | +|---|---| +| the tip, unmutated (control) | 21/21 | +| `cb9d02b2` (the old code) | both files fail at import (`MONO_BOLD_FONT_FILE`, `fit` not exported) | +| `b9933d71` (bold in, measured face not yet) | both fail at import (`brandSvgFace`) | +| `fit()` counts for every face | 4 fail: Plex inside its budget, measured not counted, wrap, the scroll render | +| the scroll head fitted without its face | 1 fails: the scroll render (`THE QUARTERING · MED…` in Plex) | +| `wrapPx()` counts for every face | 1 fails: wrap | +| `rasterize()` without `childOpts` | 1 fails: the rsvg env test | +| `fontBold` not set | 1 fails: the preset's render | +| `IBMPlexMono-Bold.ttf` removed | 2 fail: the vendored faces, `fonts.conf` resolution | + +**Found and left** +- **Fira's unbranded path can overrun its own columns** given long enough text (the scroll head + above; uppercased text at every site). None of the real manifests' labels comes near a budget, + and the byte-identity rule forbids changing it. If it ever matters, the fix is a Fira table like + Plex's, and that has to be pinned to one Fira build. +- **The scroll's first column head** is fitted to the column pitch (140 px) and ignores the `DATE` + head to its left. "THE QUARTERING" clears `DATE` by 8.4 px of advance in Plex (23 in Fira), and a + head near 140 px would reach it in either face. Not hit by any manifest. +- The 1.3 em fallback for unmapped characters is a bound, not a measurement: nothing rendered an + emoji or CJK label through a fallback font. +- **`plans/STATE.md`** ("New lows", l. 154–155) lists both of this slice's items as open. **S4's + record** in `brand-and-themes.md` (l. 1451–1453, 1541) states them as they were. Neither file is + this slice's to edit, so they are left for the integration pass. + ## Rollout Nothing is rolled out tonight. The morning runbook lists what is owed: the :3001 editor restart,