Skip to content

feat(producer): plan-time validator — reject system fonts#774

Merged
jrusso1020 merged 1 commit into
mainfrom
feat/producer-plan-validate-no-system-fonts
May 13, 2026
Merged

feat(producer): plan-time validator — reject system fonts#774
jrusso1020 merged 1 commit into
mainfrom
feat/producer-plan-validate-no-system-fonts

Conversation

@jrusso1020
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 commented May 13, 2026

What

Extends packages/producer/src/services/render/planValidation.ts with:

  • validateNoSystemFonts(compiledHtml) — scans font-family: declarations and data-font-family="…" attributes; throws PlanValidationError({ code: "SYSTEM_FONT_USED" }) when the PRIMARY family (first entry in the comma-separated list) resolves to a host-OS / CSS-generic family.
  • parseFontFamilyValue(value) — pure helper that splits a font-family value, strips quotes + whitespace.

Banned primary families: sans-serif, serif, monospace, cursive, fantasy, system-ui, ui-sans-serif, ui-serif, ui-monospace, emoji, math, fangsong, -apple-system, BlinkMacSystemFont.

Why

Part of Phase 2, §5.3 (Banned in distributed mode) and §9.3 (typed non-retryable failures).

Distributed chunk workers run in a Linux container without macOS / Windows system fonts. A composition that declares -apple-system or system-ui as its primary family renders differently between the controller and the worker — distributed retries aren't byte-identical and the workflow is broken. Generic families remain acceptable as CSS fallbacks; only the primary slot is rejected.

How

  • Same regex set as deterministicFonts.extractRequestedFontFamilies (inline styles, <style> blocks, and data-font-family= attributes) so the two surfaces stay in sync.
  • Inlines the banned-families set (deliberate copy of the list in deterministicFonts.ts — the two are different concerns that happen to overlap today; if they diverge in the future, that's a design signal).
  • Case-insensitive matching (SYSTEM-UI → flagged).
  • Error message names the offending family + the full declaration so the user can find the offending stylesheet immediately.

Test plan

  • Unit tests added: planValidation.test.ts gains 14 cases for validateNoSystemFonts:
    • Clean composition with "Inter", -apple-system, sans-serif (primary is Inter) → passes.
    • Composition with no font-family declarations → passes.
    • Banned primary -apple-system, system-ui, sans-serif, ui-monospace (via data-font-family) → throws.
    • Case-insensitive matching (SYSTEM-UI).
    • Fallback acceptance — generic AS FALLBACK is fine.
    • Doc-reference assertion in error message.
    • 5 additional cases for parseFontFamilyValue edge handling.
  • No caller invokes the validator yet — Phase 3's plan() will run it on the compiled HTML before freezing the plan.
  • bun run --cwd packages/producer typecheck clean.
  • bunx oxlint + bunx oxfmt --check clean.

This is part of a stack of 10 PRs; this is PR 9 of 10. Stacked on top of #773.

🤖 Generated with Claude Code

@jrusso1020 jrusso1020 force-pushed the feat/producer-plan-validate-no-gpu branch from a1f7e50 to b2306ea Compare May 13, 2026 01:33
@jrusso1020 jrusso1020 force-pushed the feat/producer-plan-validate-no-system-fonts branch from 8d068be to b13acb6 Compare May 13, 2026 01:33
miguel-heygen
miguel-heygen previously approved these changes May 13, 2026
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

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

Good refactor — extracting iterateFontFamilyDeclarations and parseFontFamilyValue from the existing extractRequestedFontFamilies keeps the regex surfaces in sync between the @font-face injector and the plan-time validator. The "primary family only" check is the right design: font-family: "Inter", -apple-system, sans-serif is the canonical fallback chain and should pass.

The deliberate copy of the banned-families set (noted in the PR description as separate concerns that overlap today) is a reasonable call — keeping the two decoupled avoids a false coupling that would slow down future divergence.

Test coverage is thorough: CSS property, data-attribute, case-insensitivity, generic-as-fallback, and the clean-html happy path.

LGTM.

— Magi

Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

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

Strengths:

  • Extraction of GENERIC_FAMILIES + iterateFontFamilyDeclarations + parseFontFamilyValue from deterministicFonts.ts (deterministicFonts.ts:18-78) gives the validator and the @font-face injector a single source of truth for what counts as a generic family + how to scan declarations. Refactors extractRequestedFontFamilies (deterministicFonts.ts:225-235) onto the shared iterator — that's the right shape for Rule 2 contract consistency. PR description claims the validator "inlines" the banned set (deliberate copy) — actual code is better than the description: it imports from the single source. Worth updating the description.
  • "First entry wins" semantics (planValidation.ts:104) correctly captures CSS font-family fallback semantics — the primary slot is what the browser uses; subsequent entries are fallbacks. Pinned by the font-family: "Inter", -apple-system, sans-serif PASS test and the inverted font-family: -apple-system, BlinkMacSystemFont, "Segoe UI" FAIL test (planValidation.test.ts:130, :165).
  • Case-insensitive matching (planValidation.ts:105 .toLowerCase()) pinned by SYSTEM-UI → flagged test (planValidation.test.ts:184).
  • Tests cover ALL three surfaces (font-family in <style>, font-family inline style=, data-font-family= attribute) — the iterator yields all three, so the validator catches all three.

Findings:

important: deterministicFonts.ts:65-67 iterateFontFamilyDeclarations regex set:

[/font-family\s*:\s*([^;}{]+)[;}]?/gi, "font-family"],
[/data-font-family=["']([^"']+)["']/gi, "data-font-family"],
  • The font-family regex matches in both <style> blocks AND inline style="font-family: …" attributes (the regex is HTML-context-agnostic). Good.
  • But the data-font-family regex requires ["'] quoting. HTML5 allows unquoted attribute values (<h1 data-font-family=Outfit>). Compositions emitted by our own studio always quote, but external HTML pasted into a composition might not. The validator would then silently pass a data-font-family=system-ui — false negative. Not blocking if the studio's compiled HTML is the only input, but worth a comment naming that assumption + an explicit test asserting unquoted attribute values are either rejected or matched.

nit: deterministicFonts.ts:74 for (const match of html.matchAll(regex)) — regex g flag is consumed by matchAll. The two-element sources array runs sequentially so no state leak, but if the array grows, ordering may matter (e.g. an iframe.srcdoc regex that overlaps with font-family). Worth a comment noting the iterator emits in source-array order.

nit: planValidation.ts:111 error message says Distributed chunk workers render in a Linux container and cannot produce byte-identical output for fonts that resolve to host system installations. — but the validator only inspects the primary family. The error message language might confuse a user whose CSS is font-family: -apple-system, "Inter" — they'll wonder why "Inter" isn't being used. Recommend the error message explicitly note "primary (first) family in the declaration is …; subsequent entries are fallbacks and unused unless the primary fails to load."

Verdict: APPROVE
Reasoning: Validator is correctly shaped, shared GENERIC_FAMILIES source is the right call, three-surface coverage solid. The unquoted-attribute false-negative is the strongest concern and is a small follow-up.

— Vai

Part of Phase 2 of the distributed rendering plan (determinism hardening).
See DISTRIBUTED-RENDERING-PLAN.md §5.3 (Banned in distributed mode) and
§9.3 (typed non-retryable failures).

Extends packages/producer/src/services/render/planValidation.ts with:

  - validateNoSystemFonts(compiledHtml) — scans `font-family:` declarations
    and `data-font-family=…` attributes. If the PRIMARY family (first
    entry in the comma-separated list) resolves to a host-OS / CSS-generic
    family, throws PlanValidationError with code SYSTEM_FONT_USED.
  - parseFontFamilyValue(value) — pure helper that splits a font-family
    declaration value, stripping whitespace + quotes.

Banned primary families: sans-serif, serif, monospace, cursive, fantasy,
system-ui, ui-sans-serif, ui-serif, ui-monospace, emoji, math, fangsong,
-apple-system, BlinkMacSystemFont. Mirrors the GENERIC_FAMILIES list in
deterministicFonts.ts (deliberately a separate copy — they're two
different concerns that happen to overlap today).

Generic families remain acceptable as CSS fallbacks; only the primary
slot is rejected. `font-family: "Inter", -apple-system, sans-serif` is
fine; `font-family: -apple-system, BlinkMacSystemFont` is rejected.

No caller invokes the validator yet. Phase 3's `plan()` will run it on
the compiled HTML before freezing the plan, so chunk workers (Linux
containers without macOS / Windows system fonts) never see compositions
that would render differently between the controller and the workers.

In-process behavior is unchanged.

14 unit tests added to packages/producer/src/services/render/
planValidation.test.ts cover: clean compositions, missing font-family,
each banned primary family, data-font-family= surface, case-insensitive
matching, fallback acceptance, and parser edge cases.

This is part of a stack of 10 PRs; this is PR 9 of 10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jrusso1020 jrusso1020 force-pushed the feat/producer-plan-validate-no-system-fonts branch from b13acb6 to a9f574d Compare May 13, 2026 04:36
@jrusso1020 jrusso1020 force-pushed the feat/producer-plan-validate-no-gpu branch from b2306ea to 146ff0f Compare May 13, 2026 04:36
vanceingalls
vanceingalls previously approved these changes May 13, 2026
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

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

Re-review of the new HEAD (a9f574d9) after prior approval at b13acb60 was dismissed. PR diff vs. base is the three-file shape: refactor deterministicFonts.ts to extract GENERIC_FAMILIES + parseFontFamilyValue + iterateFontFamilyDeclarations (shared with the validator), then add validateNoSystemFonts in planValidation.ts plus 122 lines of test.

Strengths

  • deterministicFonts.ts:48iterateFontFamilyDeclarations is the right shared-surface abstraction. Both extractRequestedFontFamilies (existing injector) and validateNoSystemFonts (new) consume the same iterator, so the validator can't drift to a different surface coverage than the injector. The "treats data-font-family= as a valid surface" test (planValidation.test.ts:209) pins that both surfaces participate.
  • planValidation.ts:104 — validates ONLY the FIRST family in each declaration. The "accepts generic families when used only as fallbacks" test (planValidation.test.ts:234) pins that "Inter", -apple-system, sans-serif passes — exactly the canonical fallback chain that would otherwise false-positive.
  • Case-insensitivity test at :222 (SYSTEM-UI vs system-ui) — pins the lowercase-before-lookup contract.

Findings

nitdeterministicFonts.ts:51iterateFontFamilyDeclarations regex font-family\s*:\s*([^;}{]+)[;}]? will also match font-family: declarations inside @font-face blocks. If a composition ever names an @font-face with primary system-ui (@font-face { font-family: "system-ui"; src: url(...) }), the validator would erroneously reject it. Pathological/theoretical — no real composition does this — but worth a code comment noting @font-face declarations are scanned too. Marking nit.

Notes (not blocking)

  • All required CI green.

— Vai

Verdict: APPROVE
Reasoning: Validator shares the surface iterator with the injector (no drift), and the fallback-chain + case-insensitivity branches are pinned by tests. CI green.

Base automatically changed from feat/producer-plan-validate-no-gpu to main May 13, 2026 20:37
@jrusso1020 jrusso1020 dismissed stale reviews from vanceingalls and miguel-heygen May 13, 2026 20:37

The base branch was changed.

@jrusso1020 jrusso1020 merged commit 6fd402d into main May 13, 2026
48 of 52 checks passed
@jrusso1020 jrusso1020 deleted the feat/producer-plan-validate-no-system-fonts branch May 13, 2026 21:32
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.

3 participants