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..b94a54fe6 --- /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..ac1356c04 --- /dev/null +++ b/pkgs/fontbuilder/tests/a12.py @@ -0,0 +1,164 @@ +#!/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 argparse +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): + ap = argparse.ArgumentParser() + ap.add_argument("capture", help="the READ-ONLY capture dir holding the untouched Serif") + ap.add_argument("out") + ap.add_argument("--src", default=None, + help="accepted and ignored; the capture dir is the first positional here") + ap.add_argument("--allow-partial", action="store_true", + help="accepted and ignored; A12 needs only the one Serif face") + a = ap.parse_args(argv[1:]) + capture, out = os.path.abspath(a.capture), os.path.abspath(a.out) + 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 -