From e5b7f379be44e9ccaa3d3252d6f865f17f9e14c7 Mon Sep 17 00:00:00 2001 From: mecattaf Date: Thu, 17 Sep 2026 07:13:05 +0200 Subject: [PATCH 1/4] fonts: press the Anthropic suite and pin it to the NAS M.2 The Anthropic brand faces captured in ~/colors (Sans, Serif, Mono, Anthropicons; the anthropic.com "Web" 26.043.1 variable cuts, 14 files pinned by sha256) become the fleet's own font suite, installed exactly the way the Apple faces are: one 0444 tarball per package on nas:/mnt/fast/fonts/anthropic/, requireFile-pinned in pkgs/anthropic-{mono-nerd,ui,webfonts}.nix, seeded nightly by update-center-seed. No font byte enters this repo (.gitignore now rejects them). pkgs/fontbuilder is the press, a self-contained package that ships the recipe and no bytes (`nix run .#fontbuilder -- `, ~4 min): instance the variable Mono to 12 statics, normalize names and style bits, completeness-merge Greek/Cyrillic/math/braille from JetBrains Mono, DejaVu Sans Mono and Maple Mono NF (every grafted glyph carries its donor as a name suffix), ligaturize from Fira Code 3.001, re-seat each ligature against Anthropic's own strokes, Nerd- patch last (--complete --mono --makegroups 4, 3.5.1), restore the ten PUA glyphs the patcher overwrites, and canonicalize head+FFTM so two runs are byte-identical (--verify-repro proved it on all 12+5+7 artifacts and the three tarballs). The Sans/Serif stay variable (wght+opsz) with the doubled-"Web" names, the missing italic bit on the variable row, the Serif wght-300 default and the STAT " Italic" doubling repaired; the webfonts land under share/webfonts via home.packages and a fontconfig glob reject, because fontconfig here indexes woff2 and would contend with the TTFs. Measured on the pressed faces: 13,440-13,444 codepoints per terminal face, every advance 1200, braille 256/256, calt 136 lookups, nameID16 "AnthropicMono Nerd Font Mono" (28 of 31 chars), fc-scan spacing 100 on all 12 (the Light Italic source instanced to 90 before the phantom-delta repair). Mono tarball 2,571,132 B. Acceptance suite in pkgs/fontbuilder/tests (kitty's own resolver, shaping from the exact face, coverage, alignment, archive shape, no live fontconfig side effects) passes on this press. update-center-seed.nix now carries full archive paths (two vendor dirs), an eval-time font-vs-font GC-root-name guard and a shell-time font-vs-node guard (a tarball named tally.tar.zst used to clobber seeds/tally silently). Docs: docs/fonts/{README,anthropic-suite,nas-and-bootstrap}.md and pkgs/fontbuilder/README.md. Personal use only, per Tom's ruling; the faces have no published licence and are never redistributed. This commit installs the packages only; the consumers (kitty, fontconfig generics, GTK) move in the next commit, after the review window. Co-Authored-By: Claude Fable 5.1 --- .gitignore | 11 + docs/fonts/README.md | 265 ++++ docs/fonts/anthropic-suite.md | 1212 +++++++++++++++++++ docs/fonts/nas-and-bootstrap.md | 456 +++++++ flake.nix | 38 +- home/home.nix | 30 + home/update-center-seed.nix | 82 +- modules/common.nix | 45 +- pkgs/anthropic-mono-nerd.nix | 61 + pkgs/anthropic-ui.nix | 61 + pkgs/anthropic-webfonts.nix | 71 ++ pkgs/fontbuilder/README.md | 248 ++++ pkgs/fontbuilder/data/anthropic-fonts.css | 184 +++ pkgs/fontbuilder/data/merge-blocks.json | 164 +++ pkgs/fontbuilder/data/sources.sha256 | 14 + pkgs/fontbuilder/default.nix | 149 +++ pkgs/fontbuilder/fontbuilder.py | 466 +++++++ pkgs/fontbuilder/lib/canonicalize.py | 42 + pkgs/fontbuilder/lib/icon_map.py | 120 ++ pkgs/fontbuilder/lib/icons.py | 109 ++ pkgs/fontbuilder/lib/instance.py | 21 + pkgs/fontbuilder/lib/ligature_align.py | 161 +++ pkgs/fontbuilder/lib/ligaturize.sh | 31 + pkgs/fontbuilder/lib/manifest.py | 135 +++ pkgs/fontbuilder/lib/merge.py | 267 ++++ pkgs/fontbuilder/lib/nerdpatch.sh | 45 + pkgs/fontbuilder/lib/normalize.py | 98 ++ pkgs/fontbuilder/lib/normalize_vf.py | 271 +++++ pkgs/fontbuilder/lib/package.py | 165 +++ pkgs/fontbuilder/lib/regraft_pua.py | 65 + pkgs/fontbuilder/lib/repair_source.py | 77 ++ pkgs/fontbuilder/lib/specimen.py | 348 ++++++ pkgs/fontbuilder/lib/verify_sources.py | 58 + pkgs/fontbuilder/lib/webfonts.py | 103 ++ pkgs/fontbuilder/nerd-font-patcher.nix | 27 + pkgs/fontbuilder/specimen/anthropic-demo.sh | 59 + pkgs/fontbuilder/specimen/assert_ov.py | 45 + pkgs/fontbuilder/specimen/review-window.sh | 113 ++ pkgs/fontbuilder/specimen/specimen.html.in | 28 + pkgs/fontbuilder/tests/a12.py | 158 +++ pkgs/fontbuilder/tests/accept.py | 1013 ++++++++++++++++ pkgs/fontbuilder/tests/coverage.py | 31 + pkgs/fontbuilder/tests/fonts.conf.in | 58 + pkgs/fontbuilder/tests/geometry.py | 133 ++ pkgs/fontbuilder/tests/resolve.py | 388 ++++++ pkgs/fontbuilder/tests/shape.py | 216 ++++ 46 files changed, 7924 insertions(+), 18 deletions(-) create mode 100644 docs/fonts/README.md create mode 100644 docs/fonts/anthropic-suite.md create mode 100644 docs/fonts/nas-and-bootstrap.md create mode 100644 pkgs/anthropic-mono-nerd.nix create mode 100644 pkgs/anthropic-ui.nix create mode 100644 pkgs/anthropic-webfonts.nix create mode 100644 pkgs/fontbuilder/README.md create mode 100644 pkgs/fontbuilder/data/anthropic-fonts.css create mode 100644 pkgs/fontbuilder/data/merge-blocks.json create mode 100644 pkgs/fontbuilder/data/sources.sha256 create mode 100644 pkgs/fontbuilder/default.nix create mode 100644 pkgs/fontbuilder/fontbuilder.py create mode 100644 pkgs/fontbuilder/lib/canonicalize.py create mode 100644 pkgs/fontbuilder/lib/icon_map.py create mode 100644 pkgs/fontbuilder/lib/icons.py create mode 100644 pkgs/fontbuilder/lib/instance.py create mode 100644 pkgs/fontbuilder/lib/ligature_align.py create mode 100755 pkgs/fontbuilder/lib/ligaturize.sh create mode 100644 pkgs/fontbuilder/lib/manifest.py create mode 100644 pkgs/fontbuilder/lib/merge.py create mode 100755 pkgs/fontbuilder/lib/nerdpatch.sh create mode 100644 pkgs/fontbuilder/lib/normalize.py create mode 100644 pkgs/fontbuilder/lib/normalize_vf.py create mode 100644 pkgs/fontbuilder/lib/package.py create mode 100644 pkgs/fontbuilder/lib/regraft_pua.py create mode 100644 pkgs/fontbuilder/lib/repair_source.py create mode 100644 pkgs/fontbuilder/lib/specimen.py create mode 100644 pkgs/fontbuilder/lib/verify_sources.py create mode 100644 pkgs/fontbuilder/lib/webfonts.py create mode 100644 pkgs/fontbuilder/nerd-font-patcher.nix create mode 100755 pkgs/fontbuilder/specimen/anthropic-demo.sh create mode 100644 pkgs/fontbuilder/specimen/assert_ov.py create mode 100755 pkgs/fontbuilder/specimen/review-window.sh create mode 100644 pkgs/fontbuilder/specimen/specimen.html.in create mode 100644 pkgs/fontbuilder/tests/a12.py create mode 100644 pkgs/fontbuilder/tests/accept.py create mode 100644 pkgs/fontbuilder/tests/coverage.py create mode 100644 pkgs/fontbuilder/tests/fonts.conf.in create mode 100644 pkgs/fontbuilder/tests/geometry.py create mode 100644 pkgs/fontbuilder/tests/resolve.py create mode 100644 pkgs/fontbuilder/tests/shape.py diff --git a/.gitignore b/.gitignore index f1199783c..729c6903b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,14 @@ home/dot_config/cliamp/cliamp.sock home/dot_config/cliamp/cliamp.sock.pid home/dot_config/cliamp/history.toml home/dot_config/cliamp/resume.json + +# Font binaries never enter this repo (2026-09-17). The Anthropic faces are +# unlicensed brand trade dress and live only on the NAS M.2; pkgs/fontbuilder +# is the press and ships no bytes. nerd-font-patcher writes its output into the +# CURRENT DIRECTORY under some flags (`--cell '?'` does not query, it patches), +# which is how a 440 KB patched TTF once landed in this repo root. +*.ttf +*.otf +*.woff +*.woff2 +*.tar.zst diff --git a/docs/fonts/README.md b/docs/fonts/README.md new file mode 100644 index 000000000..fa69feccb --- /dev/null +++ b/docs/fonts/README.md @@ -0,0 +1,265 @@ +# The Anthropic font suite + +Pressed on 2026-09-17 by `pkgs/fontbuilder` from the brand faces captured in +`~/colors/waves/capture`, stored as three tarballs on the fleet's NAS M.2, pinned by +sha256, and installed on every host through `modules/common.nix`. No font binary is in +this repository and none is ever downloaded. + +| page | what it holds | +|---|---| +| this file | what is installed, the standing rules, the daily-driver notes, how to check and how to revert | +| [`anthropic-suite.md`](anthropic-suite.md) | the measurements: sources, defects, names, donors, ligature alignment, PUA, CSS, verification transcript | +| [`nas-and-bootstrap.md`](nas-and-bootstrap.md) | the operations runbook: NAS layout, tarball rules, the seed refactor, the two-switch bootstrap, recovery | +| [`../../pkgs/fontbuilder/README.md`](../../pkgs/fontbuilder/README.md) | the press manual: stages, forbidden flags, reproducibility, runtimes, exit codes | + +--- + +## What is installed + +| package | family (fontconfig) | files | reaches disk via | consumer | +|---|---|---|---|---| +| `anthropic-mono-nerd` | `AnthropicMono Nerd Font Mono` | 12 statics `AnthropicMonoNerdFontMono- +

ligatures against literal hyphens and equals

+
x = 1 / x == 1 / x -= 1 / x --- 1 / --flag --> => != <= >= |> :: __
+

box art

+
┌─┬─┐ ├─┼─┤ └─┴─┘
+

blocks and braille (U+2800 is the BLANK cell - a tofu here is the E6 bug)

+
█▓▒░ ⣿⥿⠀⣀
+

donor seam - Greek/Cyrillic/math beside Latin

+
Latin Привет λ ∑ 𝔸 Latin
+

weights x slopes

+
Light        Light Italic
+
Regular      Italic
+
SemiBold     SemiBold Italic
+
ExtraBold    ExtraBold Italic
diff --git a/pkgs/fontbuilder/tests/a12.py b/pkgs/fontbuilder/tests/a12.py new file mode 100644 index 000000000..70060ff58 --- /dev/null +++ b/pkgs/fontbuilder/tests/a12.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""A12 - the Serif fvar-default move, as a TOLERANCE test over a GRID. + + tests/a12.py + +v1's wording ("bbox-identical to the same instances taken from the untouched +source") is FALSE and this file is the refutation, lifted from +$P3/refute-C8/a12_grid.py. Moving an fvar default NECESSARILY re-rounds the +base glyf outlines (432 of 747 glyphs rewritten) and rebases gvar (7,688 -> +10,359 tuples). What is preserved is fidelity within a measured tolerance: + + control point <= 2 units at 2000 upm (worst observed 1.5015) + RAW glyph bbox <= 1 unit (worst observed 0.5000) + advance <= 1 unit (worst exactly -1 u on 93 + glyph-instances, concentrated at + wght 600) + +THREE IMPLEMENTATION RULES, EACH FROM A MEASURED FALSE FAILURE: + + 1. The grid MUST include off-default opsz AND off-endpoint wght. v1's 3x2 + grid is exactly why this was missed: at opsz=16 the raw bboxes ARE + identical at wght 300/400/800, so a literal v1 implementation PASSES while + 283 glyphs have moved. The grid here is wght {300,400,500,600,700,800} x + opsz {16,24,32,48} = 24 probes. + + 2. NEVER compare `tuple(round(x) for x in bounds)`. Banker's rounding flips + at half-integer coordinates (77.5 vs 77.49683 -> 78 vs 77) and produced + 742 false failures. Compare the RAW floats against a tolerance. + + 3. The advance tolerance is 1 unit, not 0. + +Also: do NOT assert the STAT ElidedFallbackNameID STRING against the source. +S9 deliberately resets it from `Text Regular` to `Regular`. Assert only that +it is non-zero. And compare fvar instances by RESOLVED STRING, never by +nameID: S9 reassigns nameIDs (unshare -> re-point -> strip -> gc). +""" +from __future__ import annotations + +import os +import sys + +from fontTools.pens.boundsPen import BoundsPen +from fontTools.pens.recordingPen import DecomposingRecordingPen +from fontTools.ttLib import TTFont +from fontTools.varLib.instancer import instantiateVariableFont as inst + +PT_TOL, BB_TOL, ADV_TOL = 2.0, 1.0, 1 +WGHT = (300, 400, 500, 600, 700, 800) +OPSZ = (16, 24, 32, 48) +WANT_AXES = [("wght", 300, 400, 800), ("opsz", 16, 16, 48)] + +# where the repaired Serif may live, most specific first. Running on the S2b +# output (before normalize) is preferred because then the name-table +# assertions are exact; on the shipped face they are string comparisons. +CANDIDATES = [ + os.path.join("work", "s2b", "AnthropicSerif-Roman.ttf"), + os.path.join("work", "serif-s2b", "AnthropicSerif-Roman.ttf"), + os.path.join("work", "AnthropicSerif-Roman-s2b.ttf"), + os.path.join("desktop", "AnthropicSerif-Roman.ttf"), +] + + +def instance_strings(f): + n = f["name"] + return [(tuple(sorted(i.coordinates.items())), n.getDebugName(i.subfamilyNameID)) + for i in f["fvar"].instances] + + +def main(argv): + if len(argv) != 3: + print("usage: a12.py ", file=sys.stderr) + return 2 + capture, out = os.path.abspath(argv[1]), os.path.abspath(argv[2]) + src = os.path.join(capture, "fonts-ttf", "AnthropicSerif-Roman-Web.ttf") + dst = None + for rel in CANDIDATES: + p = os.path.join(out, rel) + if os.path.exists(p): + dst = p + break + fails = [] + if not os.path.exists(src): + print("FAIL A12: no untouched Serif source at %s" % src) + return 1 + if dst is None: + print("FAIL A12: no repaired Serif in the out-dir (looked for %s)" % CANDIDATES) + return 1 + print("A12 source %s" % src) + print("A12 under test %s" % dst) + + f = TTFont(dst, recalcTimestamp=False) + s = TTFont(src, recalcTimestamp=False) + + ax = [(a.axisTag, a.minValue, a.defaultValue, a.maxValue) for a in f["fvar"].axes] + if ax != WANT_AXES: + fails.append("fvar axes %s != %s" % (ax, WANT_AXES)) + a_i, s_i = instance_strings(f), instance_strings(s) + if a_i != s_i: + bad = [(x, y) for x, y in zip(a_i, s_i) if x != y] + fails.append("named instances differ (coords, resolved subfamily string): %s" % bad[:4]) + if len(f["fvar"].instances) != 12: + fails.append("%d named instances, expected 12" % len(f["fvar"].instances)) + if f["OS/2"].usWeightClass != 400: + fails.append("usWeightClass %d != 400" % f["OS/2"].usWeightClass) + if set(f["gvar"].variations) != set(s["gvar"].variations): + fails.append("gvar glyph set changed") + st, ss = f["STAT"].table, s["STAT"].table + na = len(st.AxisValueArray.AxisValue) + ns = len(ss.AxisValueArray.AxisValue) + if na != ns: + fails.append("STAT AxisValue count %d != %d" % (na, ns)) + if not st.ElidedFallbackNameID: + # the STRING is deliberately reset by S9 - only non-zero is asserted + fails.append("STAT ElidedFallbackNameID is 0") + + bad = [] + for w in WGHT: + for o in OPSZ: + a = inst(TTFont(src, recalcTimestamp=False), {"wght": w, "opsz": o}, + inplace=True, updateFontNames=False) + b = inst(TTFont(dst, recalcTimestamp=False), {"wght": w, "opsz": o}, + inplace=True, updateFontNames=False) + if a.getGlyphOrder() != b.getGlyphOrder(): + bad.append(("", w, o, "glyph order")) + continue + ga, gb = a.getGlyphSet(), b.getGlyphSet() + for g in a.getGlyphOrder(): + pa, pb = DecomposingRecordingPen(ga), DecomposingRecordingPen(gb) + ga[g].draw(pa) + gb[g].draw(pb) + if [op for op, _ in pa.value] != [op for op, _ in pb.value]: + bad.append((g, w, o, "contour structure")) + continue + for (_, u), (_, v) in zip(pa.value, pb.value): + for p, q in zip(u, v): + if max(abs(p[0] - q[0]), abs(p[1] - q[1])) > PT_TOL: + bad.append((g, w, o, "point > %g u" % PT_TOL)) + if abs(a["hmtx"][g][0] - b["hmtx"][g][0]) > ADV_TOL: + bad.append((g, w, o, "advance > %d u" % ADV_TOL)) + Ba, Bb = BoundsPen(ga), BoundsPen(gb) + ga[g].draw(Ba) + gb[g].draw(Bb) + if (Ba.bounds is None) != (Bb.bounds is None): + bad.append((g, w, o, "bbox none")) + elif Ba.bounds and max(abs(x - y) for x, y in zip(Ba.bounds, Bb.bounds)) > BB_TOL: + # RAW bbox. NEVER round() - see rule 2 in the docstring. + bad.append((g, w, o, "bbox > %g u" % BB_TOL)) + print("A12 grid: %d probes (wght %s x opsz %s), %d glyph-instance failures" + % (len(WGHT) * len(OPSZ), list(WGHT), list(OPSZ), len(bad))) + for x in bad[:10]: + fails.append("grid %s" % (x,)) + for f_ in fails: + print("FAIL A12:", f_) + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/pkgs/fontbuilder/tests/accept.py b/pkgs/fontbuilder/tests/accept.py new file mode 100644 index 000000000..368a2faae --- /dev/null +++ b/pkgs/fontbuilder/tests/accept.py @@ -0,0 +1,1013 @@ +#!/usr/bin/env python3 +"""fontbuilder acceptance suite - the fontTools/fontconfig half of spec 11. + + tests/accept.py [--src ] + +Covers A3 A4 A5 A6 A7 A8 A9 A10 A13 A14 A16 A21. A1/A2 need kitty's own +resolver and live in tests/resolve.py and tests/shape.py; A12 needs the +untouched Serif source and lives in tests/a12.py. + +Writes /verify/accept.json, prints one `FAIL : ...` line per +failure, exits 1 if there is any. Nothing is written outside /verify/. + +THE ISOLATION CONTRACT (spec 11 preamble, D17). Every fc-scan / fc-match / +fc-list here runs with FONTCONFIG_FILE pointing at a config rendered from +tests/fonts.conf.in (private under /verify) AND with +XDG_CACHE_HOME redirected under /verify. FONTCONFIG_FILE alone is not +enough - with only it set, ~/.cache/fontconfig's DIRECTORY mtime moved. +`fc-cache` is NEVER invoked. A21 proves both by snapshotting + + find ~/.cache/fontconfig -printf '%T@ %s %p\n' | sort | sha256sum + +without -type f, before and after the whole suite. + +EVERY nameID ASSERTION IS SET MEMBERSHIP (A4). The name table carries TWO +nameID16 records with different strings (`AnthropicMono Nerd Font Mono` and +`AnthropicMono NFM`); a dict keyed by nameID silently keeps only the last and +makes A4 fail on a correct font. +""" +from __future__ import annotations + +import argparse +import collections +import hashlib +import json +import math +import os +import re +import subprocess +import sys +import tarfile + +from fontTools.pens.boundsPen import BoundsPen +from fontTools.ttLib import TTFont + +# -------------------------------------------------------------------------- +# constants measured on the real faces (spec 11 A3/A5/A6/A7/A8/A9/A10) +# -------------------------------------------------------------------------- +ROMAN = ["Light", "Regular", "Medium", "SemiBold", "Bold", "ExtraBold"] +ITALIC = ["LightItalic", "Italic", "MediumItalic", "SemiBoldItalic", "BoldItalic", "ExtraBoldItalic"] +STYLES = ROMAN + ITALIC +WEIGHT = {"Light": 50, "Regular": 80, "Medium": 100, "SemiBold": 180, "Bold": 200, "ExtraBold": 205} +# fc-scan's canonical style string for each face; membership, the row also +# carries the RIBBI alias (`ExtraBold,Regular`). +FC_STYLE = { + "Light": "Light", "Regular": "Regular", "Medium": "Medium", "SemiBold": "SemiBold", + "Bold": "Bold", "ExtraBold": "ExtraBold", "LightItalic": "Light Italic", + "Italic": "Italic", "MediumItalic": "Medium Italic", "SemiBoldItalic": "SemiBold Italic", + "BoldItalic": "Bold Italic", "ExtraBoldItalic": "ExtraBold Italic", +} +LONG_FAMILY = "AnthropicMono Nerd Font Mono" +SHORT_FAMILY = "AnthropicMono NFM" +CELL = 1200 + +# A4. The patcher version is NOT hardcoded. The flake's pinned nixpkgs moved +# from nerd-font-patcher 3.4.0 (what the scouts measured) to 3.5.1, so nameID5 +# now reads ";Nerd Fonts 3.5.1". The shape is asserted with a regex, and the +# EXACT version is taken from /manifest.json tools["nerd-font-patcher"] +# when that key is present - which turns a vacuous shape check into an exact +# one without pinning a number in this file. +NF_STAMP_RE = re.compile(r";Nerd Fonts \d+\.\d+\.\d+") +NF_VERSION_RE = re.compile(r"v(\d+\.\d+\.\d+)") + +# A4. nameID6. Patcher 3.5.1 added FontnameParser._remove_regular, so the +# RIBBI Regular face ships the bare `AnthropicMonoNFM` with no -Regular token, +# while the Regular-weight italic stays `AnthropicMonoNFM-Italic` and every +# other face keeps its -