Skip to content

feat(fonts): record resolver stage diagnostics on fail-closed font errors - #5189

Merged
jerrai-bot-heygen merged 6 commits into
mainfrom
jerrai/hyperframes-font_fetch_diagnostics-10072026
Oct 8, 2026
Merged

jerrai-bot-heygen merged 6 commits into
mainfrom
jerrai/hyperframes-font_fetch_diagnostics-10072026

Conversation

@jerrai-bot-heygen

@jerrai-bot-heygen jerrai-bot-heygen commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What

Attach optional structured diagnostics to the per-compile unresolved-font FontFetchError (FONT_FETCH_FAILED), so callers can identify the resolver stage that failed without parsing the error message. Existing error names, codes, messages, familyName and unresolvedFamilies remain unchanged.

This change adds observability only; it does not introduce a new font-selection, fallback or retry policy.

Why

A fail-closed font error identifies the required families that remain unresolved, but not whether the resolver encountered an HTTP rejection, a cached CSS response, no accepted font-face blocks, or a font-asset download failure.

Related work

The branch incorporates the authored-link fallback from #5186, preserving its control flow, warning and result shape. Both authored and default requests are recorded separately. No linked issue.

How

  • Add typed diagnostic records and a per-compile collector in packages/core/src/fonts/fontDiagnostics.ts.
  • Record ordered attempts in deterministicFonts.ts and attach the snapshot only to a newly constructed unresolved-font error. Shared, cached and retry-exhausted errors are not mutated.
  • Record resolver path, authored/default request source, presence of the text parameter, CSS cache disposition, HTTP status, parser counters and font-asset outcome counts. cssStatus: null means no HTTP response was recorded for that attempt.
  • Record each caller's cache disposition before awaiting the shared CSS lookup, including when an optional lookup joins an in-flight request that later rejects.
  • Do not add family names, URLs, CSS, page text or an index into unresolvedFamilies. Diagnostic records use resolver order, while unresolved names use sorted order; individual records cannot be associated with names when multiple families fail.
  • Leave FONT_FETCH_UNAVAILABLE and shared/cached errors without attached diagnostics. A swallowed optional-family failure can still appear in a later per-compile unresolved-font snapshot.

Test plan

  • packages/core/src/fonts/deterministicFonts-diagnostics.test.ts: 30/30 tests passed locally at 82e204e59, including authored/default fallback, unchanged error fields, repeated cache hits and a synchronized in-flight rejection case.
  • Failing-first control: the synchronized rejection test failed on the pre-fix cache-label implementation. The join was checked before releasing the deferred response.
  • Manual testing performed: no live font-provider or render validation for this change.
  • Documentation updated (if applicable): not applicable.
  • Comments follow CONTRIBUTING.md "Comments".

Validation at the published commit

At 82e204e59, all 11 required checks passed, including Build, Typecheck, runtime contract, Windows tests/render, regression and the edit-accuracy gate. The runtime test job actually executed the CLI/core/engine tests; the aggregate Test check is a gate, not a separate test run. Local typecheck was not run.

The main CI run 37711021045 completed with 41 successful jobs, two failed jobs and eight skipped jobs. Skipped jobs were not exercised. The two failures are non-required:

  • Fallow: three reported unused collector members have production callers; two small test-duplication findings are accepted. No suppression or audit-driven restructuring was added.
  • Studio timeline viewport: the default arm exceeded its interaction-p95 budget (60.1 ms, then 58.4 ms, against 58.3 ms). Frame timing and run-count checks passed. No rerun was performed for this revision; neither flakiness nor independence from this change is established.

The merge conflict is resolved and the combined revision received fresh review and tests. This PR is ready for review. The results above describe the completed push-triggered checks; subsequent automatic checks must be assessed separately. This validation is not a live failure reproduction or a claim that a production incident is repaired.

— Jerrai

…esolved error

Attach a stage record to the per-compile fail-closed Unresolved FontFetchError
so callers can log why a family did not resolve. The record is an ordered list
of families, each with required/resolved flags and its fetch attempts (path,
authored/default URL, text param, CSS cache fresh/hit, real CSS HTTP status,
block and regex-match counts, asset totals and non-ok HTTP statuses). It holds
enums, counts and HTTP statuses only: no family names, URLs, CSS or page text.
The error class, name, code, message, familyName and unresolvedFamilies are
unchanged, and shared rejected errors are never mutated.

- Jerrai
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Fallow audit report

Found 5 findings.

Dead code (3)
Severity Rule Location Description
major fallow/unused-class-member packages/core/src/fonts/fontDiagnostics.ts:38 Class member 'FontDiagCollector.beginFamily' is never referenced
major fallow/unused-class-member packages/core/src/fonts/fontDiagnostics.ts:43 Class member 'FontDiagCollector.endFamily' is never referenced
major fallow/unused-class-member packages/core/src/fonts/fontDiagnostics.ts:48 Class member 'FontDiagCollector.startAttempt' is never referenced
Duplication (2)
Severity Rule Location Description
minor fallow/code-duplication packages/core/src/fonts/deterministicFonts-diagnostics.test.ts:208 Code clone group 1 (7 lines, 2 instances)
minor fallow/code-duplication packages/core/src/fonts/deterministicFonts-diagnostics.test.ts:245 Code clone group 1 (7 lines, 2 instances)

Generated by fallow.

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 2059 (base branch 2059), smooth 1526 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • crop-none-px-r30-root-z100: tracking 0.04, pressJump 0, drop 40.08, reload 40.08, render 34.74, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.26, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.26, renderKey -, undo true, teleport true

…0072026

Conflict in packages/core/src/fonts/deterministicFonts.ts: kept #5186's authored-zero-faces
default fallback and its stylesheet result/status shapes unchanged; threaded the diagnostics
attempt through every stylesheet fetch (authored and default fallback each get their own).

- Jerrai
…e cssStatus comment

Add generic tests (invented family names) for an authored stylesheet that is rejected or
yields zero faces and falls back to the default request, an authored success with no fallback
attempt, an unchanged transient UNAVAILABLE, and a failed case that records both attempts with
the error contract unchanged. cssStatus doc now reads: no HTTP response was recorded for that
attempt.

- Jerrai
…Unresolved error's url and cause

A second compile of the same failing authored-plus-default page must label both attempts as
cache hits while the first compile's are fresh. The Unresolved error keeps url "" and no
cause, as at main.

- Jerrai
An optional family can join an in-flight CSS lookup that then rejects as UNAVAILABLE. The
rejection is swallowed, but the attempt kept its default cssCache "fresh" because the label was
written only after the await. The label is now recorded on the caller's own attempt before the
await. The shared result and error objects are not touched; retry, fallback and selection are
unchanged.

- Jerrai
The second compile now passes its own AbortSignal; a lookup that awaits a shared promise
registers a listener on it, so the test waits (bounded) until the joiner has reached the optional
family's lookup and asserts the optional URL still has exactly one request before releasing the
deferred 503. The first compile's optional attempt must be fresh and the joiner's a hit, in that
order.

- Jerrai
@jerrai-bot-heygen
jerrai-bot-heygen marked this pull request as ready for review October 8, 2026 01:47

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review at 82e204e5. Approving. The diagnostics are useful, contain only enums, counts and HTTP statuses, and don't change resolver behaviour.

Usefulness.

  • Each failed lookup records which URL it tried: urlSource is authored or default (deterministicFonts.ts:1563-1582).
  • An authored link that returns 400 and then a 400 from the weight-list fallback reads as [{urlSource:"authored",cssStatus:400},{urlSource:"default",cssStatus:400}] under required:true, resolved:false.
  • An authored link with zero faces, followed by a fallback whose asset 404s, reads as blocksTotal:1, regexMatches:0 on the first attempt and nonOk:{"404":1} on the fallback.
  • So the record says which stage failed, without naming the font.

Privacy. FontDiagnostics contains only enum literals, booleans, counts, and Response.status values (fontDiagnostics.ts:1-33). The nonOk keys are String(status). I fed in a hostile family name (containing a URL, text=…, quotes and @font-face), an authored link with a text= parameter, and a family name planted as a secret. None of them appeared in the serialized diagnostics.

Behaviour unchanged. I swapped in main's deterministicFonts.ts and ran the same six scenarios on both versions:

  • authored 400 → default 400;
  • authored 400 → default OK;
  • zero faces → woff2 404;
  • CSS 503;
  • woff2 503;
  • open mode.

Fetch call sequences, error names, codes and messages were identical in all six, so the PR adds no fetches. The only new work is bookkeeping plus one regex count over CSS text the resolver already has.

Tests.

  • deterministicFonts-diagnostics.test.ts passes 30/30.
  • Mutants, all caught (4/4):
Mutant Tests failed
cache label set after the await 1 (the join test)
fallback labelled authored 4
nonOk not counted 2
cssStatus dropped 16

Non-blocking.

  • cssCache: "hit" covers two cases: a result that was already cached, and a joined request that was still in flight. Non-retryable statuses like 400 stay cached for the whole process, so hit + 400 reads as "a sticky 400 from an earlier lookup", which isn't always what happened. Consider calling it shared, or documenting it at fontDiagnostics.ts:14.
  • No structured record is written when the fallback succeeds. Only the existing warn line covers that case. Lookups made through the declared-alias second pass are filed as direct_google attempts under the same family, so you can't tell them apart.
  • cssStatus: null has several meanings: a swallowed error in open mode, a joined lookup that rejected, or a swallowed optional unavailable. A small outcome enum would separate them.
  • snapshot() returns the live array (fontDiagnostics.ts:67-69). Returning a copy would be safer.
  • The Fallow "unused class member" findings are false positives. beginFamily, endFamily and startAttempt are called through the interface-typed options.diag at deterministicFonts.ts:1029, :1047 and :1567.

CI. All required checks are green at this head. In the second same-head run, Render on windows-latest was still in progress when I checked; the first run's passed.

— Rames

@jerrai-bot-heygen
jerrai-bot-heygen added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 0c76e52 Oct 8, 2026
234 of 238 checks passed
@jerrai-bot-heygen
jerrai-bot-heygen deleted the jerrai/hyperframes-font_fetch_diagnostics-10072026 branch October 8, 2026 03:09
@xuanruli xuanruli mentioned this pull request Oct 8, 2026
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.

2 participants