Skip to content

Fonts wiring (TXT1-C Phase B): @font-face, font map, font manifest in the digest, web @font-face and assets - #29

Merged
thejackshelton merged 24 commits into
masterfrom
txt1c-wiring
Sep 30, 2026
Merged

thejackshelton merged 24 commits into
masterfrom
txt1c-wiring

Conversation

@thejackshelton

@thejackshelton thejackshelton commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

What changed

  • @font-face: accepted at the top level of a stylesheet and collected. The configuration takes an optional fonts map: Dragon Sans for sans-serif, Dragon Mono for monospace, or platform.
  • Digest: the font manifest enters it only when a project has fonts.
  • font-family:
    • It is keyed by how it resolves: pinned, declared, platform or unmapped. Ahem keeps font-family:Ahem.
    • An unmapped family is DRAGON_FONT_UNMAPPED_FAMILY on every target, including after var() substitution.
    • A quoted generic such as "sans-serif" names a family and is never pinned, matching Chrome (the font-map.ts fix).
  • Diagnostics: eight new font codes (map invalid, unmapped family, remote URL, local(), unresolved asset, unreadable file, unsupported descriptor, variable font refused) and one warning (descriptor not applied).
  • Variable-font fence: it runs on the faces Chrome would pick.
  • Web output:
    • pinned generics are rewritten;
    • dragon.css starts with the @font-face rules;
    • the font bytes are returned as assets;
    • a project with no fonts gets byte-identical output.
  • Other:
    • four planted faults;
    • Lato is mapped in pinned.json (recaptured) and in the north-star check;
    • docs/api.md gains a Fonts section.

Native targets still refuse any font but Ahem. Real fonts on device are TXT1a-1/2, which follow inline formatting.

What passed

  • pnpm typecheck.
  • The fonts-wiring and fonts tests (115).
  • Font capture --check: byte-identical.
  • north-star:check, run twice: totals unchanged.
  • The full suite on the stacked parity branch: 109 files, 2319 tests.

Changed tests

  • android-target: serif is mapped to platform, because serif alone is now an unmapped family.
  • ua: the import pin.
  • seams: font-face is accepted; the byte pin is intact.
  • wire: the unmapped examples no longer use Lato, which is now mapped.

🤖 Generated with Claude Code

Note

Add font wiring to Dragon compiler: @font-face, font map, digest manifest, and web assets

  • Adds the Phase B font pipeline: projects accept an optional fonts FontMap, @font-face rules are accepted at the top level of stylesheets, and their sources are resolved from snapshot assets during compilation.
  • Font-family values in CSS are resolved against declared and pinned faces. Unmapped families produce the new DRAGON_FONT_UNMAPPED_FAMILY diagnostic, and per-case checks cover variable-font refusals, Ahem conflicts, and invalid family lists.
  • Web output now rewrites pinned generic families, prepends generated @font-face rules, and returns bundled font assets on the ready ArtifactState. The font manifest contributes to the digest.
  • Adds nine font diagnostic codes with catalogue entries in codes.ts, and documents the full font contract in docs/api.md.
  • Behavioral Change: @font-face is the only at-rule accepted at the top level (others are still refused); generic DRAGON_UNSUPPORTED_VALUE is no longer emitted for unmapped font-family values, which now report DRAGON_FONT_UNMAPPED_FAMILY instead; ready artifacts gain a required assets collection; a lone unquoted generic family like sans-serif is now parsed as a family-list value rather than a family value.

Macroscope summarized 397f7d6.

…c (quoted "sans-serif") names a family, never the pinned generic, as Chrome's stated reference P2 shows. The pinned capture's map gains Lato (vendor/fonts/Lato 400 and 700, T036) and four requests (Lato lists, a quoted generic); wire.test.ts compares Dragon's rewrite with every P2 visit, and its unmapped-family examples move off Lato, which is now mapped
…nditions=dragon-internal scripts/capture-font-data.ts; --check byte-identical, the other four captures unchanged)
…l and collected (at-rules.ts, stylesheet.ts); the fonts configuration key with DRAGON_FONT_MAP_INVALID; projectFonts and the font manifest in the digest only when the project has fonts; font-family keyed by resolution kind (featureOf and usedKeys take the font context; Ahem keeps font-family:Ahem); unmapped families refused for every target as DRAGON_FONT_UNMAPPED_FAMILY (not also UNSUPPORTED_VALUE); @font-face and pinned-face issues with their own codes; the variable-font fence at style resolution; the web output rewritten through the map with its @font-face prelude and GeneratedAsset font files; four planted faults; fonts-wiring.test.ts. Retargeted: android-target's font test maps serif to the platform (serif alone is now unmapped); ua.test's web-emitter import pin gains the fonts rewrite imports (still no lowering, layout or matching)
… Regular and Bold and pins sans-serif to Dragon Sans (T036 constraint); the font files enter the snapshot as assets, and the report records the map and asset hashes
…un north-star:check; twice, second run unchanged; per-target error totals unchanged: web 185, ios 219, android 690)
…families, the recommended Dragon Sans and Dragon Mono, the web @font-face prelude and assets, the digest, the resolution-kind support keys, the codes); createProject's fonts key and the ready output's assets; §7.1 the pinned-generic stated reference
…ed families after substitution; the variable-font fence runs on the faces Chrome selects for the text's UA weight and style (userAgentTextFonts: h1 to h6 bold, address italic), so UA-bold text on a variable face is fenced at its wght 700 instance; validateFontMap refuses a pinned entry with keys beyond mode, family and faces. Tests in fonts-wiring.test.ts
…rows from the profile it enforces, so it holds whatever rows the committed web profile has
…keeps the refusal (a style rule: Chrome ignores it; a conditional rule: refused with it)
Comment thread packages/dragon/src/css/at-rules.ts
Comment thread packages/dragon/src/project.ts Outdated
Comment thread packages/dragon/src/css/values.ts
@macroscopeapp

macroscopeapp Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a substantial cross-cutting font capability, including new public configuration and artifact fields, @font-face parsing, font asset emission, digest changes, and altered handling of existing font-family declarations. The scope and unresolved semantic concern warrant human review.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

No code changes detected at 397f7d6. Prior analysis still applies.

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

Comment thread packages/dragon/src/project.ts
… with the project's font context

checkSubstitution called featureOf without the FamilyKeyContext, so a substituted font-family was keyed
font-family:<family-list> and an enforced profile refused a pinned sans-serif. checkComputed now takes the
same context usedKeys gets. Test in fonts-wiring.test.ts (fails before: DRAGON_UNSUPPORTED_VALUE).
…raws the text with

checkCaseFonts fenced the best face group of every declared or pinned family in the list, so a later
variable family refused text that an earlier static family draws in full. renderedFaces takes the
primary font (the first bundled family's first face whose unicode-range holds U+0020) and, per code
point, the first face down the list whose unicode-range and cmap hold it; entries without bundled faces
are passed over, so a later family still counts for any character an earlier one might lack. Tabs and
segment breaks count as U+0020 (css-text-3 §4), so a face that maps a control character is not counted
for it. Tests in fonts-wiring.test.ts (both fail before).
…*/ one included, leaves the prelude empty

css-tree 3.2.1 skips every comment before an at-rule prelude (skipSC), so the prelude is null and
acceptFontFace accepts the rule; the reported refusal does not reproduce. This test keeps that true if
the parser changes, and pins that a real prelude with a trailing /*! */ comment is still refused.
…ubtree, as checkFonts does

Chrome never draws that text, so it selects no face for it. The var() font-family check stays per
declaration, like checkFamilies, and still runs there. Audit of the other per-case font and text walks:
checkFonts and checkComputed's bidi, position and UA-font checks already skip hidden subtrees;
checkSubstitution is per declaration like checkValues; usedKeys keys hidden content through its
display-none contexts by design. Test in fonts-wiring.test.ts (fails before).
Conflicts: stylesheet.ts keeps master's preprocessed input, U+0000 report and escape canonicalization
with the branch's fontFaces collector; values.ts familyValue keeps master's serializeString and
decodeName, and tests the lone generic keyword on its decoded, ASCII-folded name (asciiLower).
…-folded, as the stylesheet parser reads property names

parseFontFace compared the descriptor name as written, so f\6fnt-family was an unknown descriptor
(Chrome reads it as font-family, css-syntax-3 §4.3.7). It now decodes the name (css/escapes.ts
decodeName); fonts/css-tokens.ts re-exports css/escapes.ts asciiLower, so the fonts module and the
stylesheet parser fold names with one definition (A-Z only). Tests in fonts-wiring.test.ts: escaped and
upper-case at-rule, descriptor, format() and keyword names (the descriptor case fails before), and
escaped or upper-case generic and declared family names keep their font-family keys.
Comment on lines +107 to +108
if (v.kind === 'family') return serializeString(v.value);
return v.kind === 'other' && v.type === 'family-list' ? v.text : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium css/values.ts:107

Quoted generic families such as font-family: "ui-serif" are pinned or replaced as the mapped generic ui-serif, changing both the selected font and emitted CSS. familyListText sends the single-family value through familySupport without preserving that it was quoted; exclude generic names from resolution when the value is a family, while continuing to resolve bare generic lists.

-  if (v.kind === 'family') return serializeString(v.value);
+  if (v.kind === 'family') {
+    const name = asciiLower(v.value);
+    return (GENERIC_FAMILY_KEYWORDS as readonly string[]).includes(name) ? null : serializeString(v.value);
+  }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/dragon/src/css/values.ts around lines 107-108:

Quoted generic families such as `font-family: "ui-serif"` are pinned or replaced as the mapped generic `ui-serif`, changing both the selected font and emitted CSS. `familyListText` sends the single-family value through `familySupport` without preserving that it was quoted; exclude generic names from resolution when the value is a `family`, while continuing to resolve bare generic lists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chrome 145 treats "ui-serif" and ui-serif as the same family: ui-*, emoji and fangsong are family names to its parser whether quoted or not, and both serialize as ui-serif (new P2 case quoted-ui-serif). So resolving a quoted "ui-serif" like the bare one matches Chrome, and returning null would not. The quoting distinction applies only to the parser generics (sans-serif, serif, monospace, system-ui, -webkit-body, …), where a quoted name was already treated as a family. Checking that class turned up a real hole, fixed in db4673b: a family named like a generic in another case ("SANS-SERIF", "-webkit-body") was written bare in the web output, and bare it re-parses as the generic. Dragon now quotes those names. The Chrome font reference had the same hole and is fixed too, with Chrome-backed P2 cases for quoted, escaped and case-variant generics (a33056f).


Generated by Claude Code

…itten quoted, so it never becomes the generic

Chrome 145 (probed): "SANS-SERIF" is a family, and Chrome's own serialization writes it bare, which re-parses as
the generic sans-serif; "-webkit-body" likewise. rewriteFamilyList and the web emitter's family name now quote a
name that ASCII case-folds to a parser generic (font-map.ts outputFamilyName; web-css.ts uses it in place of its
own list). Quoted "ui-serif" and bare ui-serif stay the same family, as in Chrome, where both serialize ui-serif.

The stated reference (capture-font-reference.ts) had the same hole when it wrote a rewritten list back: it now
quotes such a bare name. P2 gains quoted-monospace, escaped-generic, quoted-escaped-generic,
quoted-upper-then-mono (fails the reference without the fix: Chrome drew Inter, not Noto Sans Mono) and
quoted-ui-serif; a case whose Chrome serialization is lossy carries its authored list.

Tests: fonts-wiring.test.ts (quoted names, escaped generics, var() and the no-fonts emitter; fails before),
wire.test.ts P2 compares Dragon's rewrite of the authored list where Chrome's text is lossy (Chrome's
serialization cannot be the input there), and ua.test.ts's web emitter import pin gains outputFamilyName.
node --conditions=dragon-internal scripts/capture-font-reference.ts, then --check (byte-identical).
Conflict in project.ts: master's profileText (profile snapshots only) is kept beside the branch's analyze,
which takes the fonts configuration key; the digest, moved after parsing on this branch, hashes
profileText(profileFor(...)) of the project's snapshot. Every profile read the branch adds (checkValues,
the contextual check, checkComputed's substitution check, resolved support queries) reads
options.supportProfiles or record.profiles, the snapshot.
…ce, as DRAGON_FONT_UNMAPPED_FAMILY

Keying the substituted font-family with the font context (67c6a3f) made checkSubstitution also refuse it as
DRAGON_UNSUPPORTED_VALUE on font-family:<unmapped>, beside checkCaseFonts' DRAGON_FONT_UNMAPPED_FAMILY; the
Tailwind sweep (#35) caught it on font-sans, font-serif and font-mono. It now skips that key, as checkValues
leaves a direct unmapped family to checkFamilies. Test in fonts-wiring.test.ts (fails before).
pnpm run tw:sweep, twice (the second run changed nothing). font-sans, font-serif and font-mono now refuse with
DRAGON_FONT_UNMAPPED_FAMILY on every target (the sweep has no font map), in place of DRAGON_UNSUPPORTED_VALUE
on the legacy font-family:<family-list> key; nothing else changed.
Comment thread packages/dragon/src/project.ts
Comment thread packages/dragon/src/project.ts
…e scratch pass checks font families

- An @font-face that declares Ahem (in any case: FontFaceCache folds it) names the authored face on web, while
  ios and android draw the bundled Ahem. Text in font-family Ahem is then DRAGON_UNSUPPORTED_FONT on each
  configured native target, and web keys it font-family:<declared>, not the Ahem rows proven with the bundled face.
- A font map entry keyed Ahem, or pinning a family named Ahem, is DRAGON_FONT_MAP_INVALID. Native lowering treats
  no other family specially: every other family is already refused on native.
- The scratch pass over the rules inside an unsupported at-rule now runs checkFamilies too, so an unmapped family
  there is a related entry of the at-rule error. It already ran every other per-declaration check of the main
  pass (checkValues, and checkCases: computed, native font, case font and contextual checks).

Tests in fonts-wiring.test.ts on web, ios and android (each fails before).
@thejackshelton
thejackshelton merged commit 58604a2 into master Sep 30, 2026
4 checks passed
@thejackshelton
thejackshelton deleted the txt1c-wiring branch September 30, 2026 07:35
thejackshelton added a commit that referenced this pull request Sep 30, 2026
#37, showcase group) into MQ-a: pnpm run parity:capture and pnpm run profile:rows to the fixed point (a third run changes nothing); pnpm run parity:dpr-capture; pnpm run layout:vectors; pnpm run layout:dpr-vectors; pnpm run layout:break-vectors; pnpm run parity:break-capture; pnpm run parity:pixel-capture; pnpm run native:gen; pnpm run north-star:check; pnpm run tw:sweep. Generated conflicts were taken from origin/master and regenerated, never hand-merged.
thejackshelton added a commit that referenced this pull request Sep 30, 2026
Part-1 code takes master's reviewed version. Conflicts:
- packages/parity/emitted/**, profiles/*.ts, north-star-check.json: master's files; regenerated in the next commits.
- scripts/capture-font-reference.ts: keeps the import of the shared packages/parity/src/font-reference.ts; master's
  change to the in-page rewrite (quote a bare family that ASCII case-folds to a parser generic, PARSER_GENERICS) is
  ported into font-reference.ts applyFontReference, so the script and the fonts fixtures run the same rewrite.
- packages/parity/src/fixtures.ts: master's showcase group, then fonts.
thejackshelton added a commit that referenced this pull request Sep 30, 2026
…puts

- font-reference.ts pinnedGenerics: a named family entry may pin under its own name as Chrome's FontFaceCache folds it
  (foldFamily, fonts/selection.ts), not only byte-equal; any other family name still throws.
- pipeline.ts inlineFontAssets (webCssOf): every url("fonts/...") of dragon.css must name a web output asset and every
  asset must be named, else it throws; before, a url of another shape was left as written and an unnamed asset passed.
- fonts-run.ts readFontCapture (committedFontAuthored): a committed expected-fonts capture is checked before use (its case,
  direction, viewport, Chrome version, well-formed nodes, and the exact bytes parity:capture writes); before, the JSON was cast.

Tests (fonts-fixtures.test.ts): pinnedGenerics folding and rename refusal; inlineFontAssets both failure directions;
readFontCapture on a wrong case, non-canonical text, a malformed node, no nodes and null.
thejackshelton added a commit that referenced this pull request Sep 30, 2026
…capture, profile:rows, parity:capture, parity:dpr-capture, layout:vectors, layout:dpr-vectors, native:gen, north-star:check, tw:sweep, wpt:run -- --target web, wpt:update-expectations -- --target web

Changed:
- packages/dragon/src/profiles/web.ts: six web rows, font-family:<pinned|declared|platform> at text-in-block ltr and rtl
  (<platform> caveat); no ios or android row changes.
- packages/parity/emitted/**, packages/parity/expected-fonts/emitted/**: header digests only (the digest covers the web profile).
- examples/music-player/dragon/north-star-check.json: the Lato declaration's two web DRAGON_UNSUPPORTED_VALUE rows become two
  DRAGON_UNPROVEN_CONTEXT rows (text-in-flex-item row and column); per-target error totals unchanged.
No change from grammar:gen, the captures (expected, expected-dpr, expected-fonts captures), the vectors, native:gen, tw:sweep or
the WPT web expectations.
thejackshelton added a commit that referenced this pull request Sep 30, 2026
Conflicts: faults.ts keeps both sets of appended plants (the four fonts plants, then blockifySkipped and inlineFlexToBlock); the Tailwind snapshot takes master's side here and is regenerated in the next commit.
thejackshelton added a commit that referenced this pull request Sep 30, 2026
…it, #37) into wm0-horizontal-tb

Only packages/parity/out/lanes.json conflicted; it takes master's side here and is rebuilt by the regeneration and device run that follow.
thejackshelton added a commit that referenced this pull request Sep 30, 2026
…o pr/v1a-engine-values; lanes.json and corpus-dpr.json taken from master and regenerated after
thejackshelton added a commit that referenced this pull request Sep 30, 2026
… sequence of 1c8999d, tw:sweep included): only packages/translate/corpus-dpr.json's digests change (V1 engine over master's 388 cases); tw:sweep changes no outcome
thejackshelton added a commit that referenced this pull request Sep 30, 2026
… parity:lanes -- --run-host --run-device) after merging origin/master (#29, #37, #40): 388 cases; frames, applied, lines and layout-vectors pass on ios and android; device-pixels ios 135, android 182, equal to origin/master's; out/device-failures-{ios,android}.json byte-identical to master's; parity:glyph-b3 passes, fringe 0
thejackshelton added a commit that referenced this pull request Sep 30, 2026
…ring, since f799419) into size-ar

Source conflicts resolved by hand: computed-checks.ts imports exactLayoutRatio beside master's FamilyKeyContext; values.ts keeps
ratioValue and exactLayoutRatio before master's font-context featureOf; faults.ts keeps v1b's two calc faults and master's four
font faults. The Tailwind sweep snapshot takes master's side and is regenerated by tw:sweep in the next commit.
thejackshelton added a commit that referenced this pull request Sep 30, 2026
…) into pr/v1b-compiler-values. faults.ts keeps both sides' faults (fonts and blockification, then V1's sumOrderSwapped and dropExplicitZeroPercent); fixtures.ts keeps master's order with values last (values.test.ts); the glyph-clearance pins in pixel-reference.test.ts are master's plus the values-* cases' own increments (edge +12/+12 at DPR 2, +10/+10 at 3, +11/+9 at 2.625, as in 2c7e72a). Generated conflicts (profiles, emitted CSS, pixel manifest, bottom pins, lanes.json, corpus-dpr.json, the Tailwind snapshot, north-star-check.json) taken from master and regenerated after
thejackshelton added a commit that referenced this pull request Sep 30, 2026
…) (regen sequence: grammar:gen, parity:capture, profile:rows, parity:capture, layout:vectors, parity:dpr-capture, layout:dpr-vectors, layout:break-vectors, parity:break-capture, parity:pixel-capture, native:gen, parity:lanes -- --run-host, node --conditions=dragon-internal examples/music-player/tools/check.ts (pnpm run north-star:check fails on master since #39: fixture-reader imports collectFontFaces from the internal entry), wpt:run --target web, wpt:update-expectations --target web, parity:glyph-b3 -- --write-bottom-pins): profiles regain the values rows, emitted CSS digests, pixel manifest and bottom pins gain the values cases, corpus-dpr.json digests; north star supported declarations 159 -> 165 on web and ios (54.6% -> 56.7%)
thejackshelton added a commit that referenced this pull request Sep 30, 2026
…ough T057 #40, T037 #34, T038 #29/#39, T112 #36); T121 grid G1a and T122 LANE-SPEED cards; HANDOFF.md moves to notes/; active T058, current lanes dispatched
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant