.orchestration/plan-27a-diagnostic.md
Date: 2026-09-06
Issue: #3377
Revision: 2a4a1583b (origin/main at run start)
Scope: Phase A, steps 1–2 only. No resolver, runtime behavior, or remote-source behavior changed.
HEAD matched origin/main; git diff origin/main...HEAD was empty.packages/fonts/src/webfontlist.json. Its web font records point at both Google Fonts static assets and jsDelivr assets; “Google blocked” is not an offline proof.apps/web/src/components/typography/font-display.tsx and calls FontFace.load() against each catalog preview URL.apps/web/src/features/resume/export/pdf-document.tsx → @reactive-resume/pdf/browser; registration is packages/pdf/src/hooks/use-register-fonts.ts.apps/server/src/http/resume-pdf.ts → createResumePdfDownload; Playwright browser routing cannot observe that process’s outbound font fetches.tests/e2e/fixtures/offline-fonts.ts seeds one disposable resume after sample creation. It writes the same text into basics and summary, hides the picture, selects IBM Plex Serif 400/700 for body and heading, and marks the row public for the server-PDF surface.
The exact markers are versioned as offline-font-scripts-v1:
| Marker | Script or coverage |
|---|---|
Latin punctuation • — “quotes” € | Latin plus General Punctuation and currency |
简体中文 | Han / Simplified Chinese |
العربية | Arabic |
עברית | Hebrew |
ไทย | Thai |
Emoji 🚀 | Emoji |
tests/e2e/specs/offline-fonts.spec.ts is opt-in (OFFLINE_FONT_DIAGNOSTIC=1) so the normal PR E2E suite does not become network-dependent. Each surface creates a new browser context with persisted auth state, disabled service workers, and no prior browser cache. Every non-same-origin request is aborted and recorded as { hostname, path }; query strings, fragments, headers, bodies, tokens, and full URLs never enter diagnostic output. Reports are attached as JSON and emitted with the same sanitized shape.
The four surfaces are separate tests:
FontFace preview loads.Builder/browser-PDF reports keep PDF text extraction as a separate textLayerMarkers signal; it does not prove visible glyph outlines. Raster evidence attaches a rendered PNG and per-marker crop metrics, failing for blank or tofu-like visible crops. Blocked browser font requests classify browser surfaces as network-error. The server report deliberately says server-outbound-requests-unobservable-from-playwright; its cold-network gate remains unresolved because server outbound capture and verifiable restart identity require external host-level controls.
Build and database setup follow tests/e2e/README.md. Run each surface in a separately restarted production server process so module-level PDF font registration state cannot leak between controls:
OFFLINE_FONT_DIAGNOSTIC=1 OFFLINE_FONT_DIAGNOSTIC_SERVER_RESTARTED=1 \
pnpm exec playwright test tests/e2e/specs/offline-fonts.spec.ts --grep "picker preview"
Stop and restart the production server before repeating the command with builder PDF, browser PDF, and server PDF grep patterns. The environment used for this change had no built apps/server/dist or apps/web/dist, no running PostgreSQL instance, and no production server to restart, so the cold E2E matrix was not run. This is an explicit infrastructure blocker, not a pass claim. The test records serverRestartFlag only as caller input and labels it non-proof; it does not claim a completed cold-network gate.
The current Playwright route guard cannot impose host-level egress denial on Node.js running the server. A genuinely cold server test therefore needs a separately restarted server plus host-level egress capture/deny (for example, a controlled network namespace or an approved outbound proxy). Do not infer server network behavior from an empty browser request list.
This is a proposal, not an asset download. It intentionally contains only the primary family and glyph fallbacks required by the fixture and current PDF fallback map, not the full catalog.
{
"schemaVersion": "offline-fonts-v1",
"mode": "local-only",
"assetRoot": "/fonts/offline/v1",
"families": {
"IBM Plex Serif": {
"normal": { "400": "ibm-plex-serif/400.ttf", "700": "ibm-plex-serif/700.ttf" },
"italic": { "400": "ibm-plex-serif/400-italic.ttf", "700": "ibm-plex-serif/700-italic.ttf" },
"preview": "ibm-plex-serif/preview.ttf"
},
"IBM Plex Sans": {
"normal": { "400": "ibm-plex-sans/400.ttf", "700": "ibm-plex-sans/700.ttf" },
"italic": { "400": "ibm-plex-sans/400-italic.ttf", "700": "ibm-plex-sans/700-italic.ttf" },
"preview": "ibm-plex-sans/preview.ttf"
},
"Noto Serif": {
"normal": { "400": "noto-serif/400.ttf", "700": "noto-serif/700.ttf" },
"italic": { "400": "noto-serif/400-italic.ttf", "700": "noto-serif/700-italic.ttf" },
"preview": "noto-serif/preview.ttf"
},
"Noto Sans": {
"normal": { "400": "noto-sans/400.ttf", "700": "noto-sans/700.ttf" },
"italic": { "400": "noto-sans/400-italic.ttf", "700": "noto-sans/700-italic.ttf" },
"preview": "noto-sans/preview.ttf"
},
"Noto Sans SC": { "normal": { "400": "noto-sans-sc/400.ttf", "700": "noto-sans-sc/700.ttf" }, "italic": "reuse-normal", "preview": "noto-sans-sc/preview.ttf" },
"Noto Serif SC": { "normal": { "400": "noto-serif-sc/400.ttf", "700": "noto-serif-sc/700.ttf" }, "italic": "reuse-normal", "preview": "noto-serif-sc/preview.ttf" },
"Noto Sans TC": { "normal": { "400": "noto-sans-tc/400.ttf", "700": "noto-sans-tc/700.ttf" }, "italic": "reuse-normal", "preview": "noto-sans-tc/preview.ttf" },
"Noto Serif TC": { "normal": { "400": "noto-serif-tc/400.ttf", "700": "noto-serif-tc/700.ttf" }, "italic": "reuse-normal", "preview": "noto-serif-tc/preview.ttf" },
"Noto Sans JP": { "normal": { "400": "noto-sans-jp/400.ttf", "700": "noto-sans-jp/700.ttf" }, "italic": "reuse-normal", "preview": "noto-sans-jp/preview.ttf" },
"Noto Serif JP": { "normal": { "400": "noto-serif-jp/400.ttf", "700": "noto-serif-jp/700.ttf" }, "italic": "reuse-normal", "preview": "noto-serif-jp/preview.ttf" },
"Noto Sans KR": { "normal": { "400": "noto-sans-kr/400.ttf", "700": "noto-sans-kr/700.ttf" }, "italic": "reuse-normal", "preview": "noto-sans-kr/preview.ttf" },
"Noto Serif KR": { "normal": { "400": "noto-serif-kr/400.ttf", "700": "noto-serif-kr/700.ttf" }, "italic": "reuse-normal", "preview": "noto-serif-kr/preview.ttf" },
"Noto Sans Arabic": { "normal": { "400": "noto-sans-arabic/400.ttf", "700": "noto-sans-arabic/700.ttf" }, "italic": "reuse-normal", "preview": "noto-sans-arabic/preview.ttf" },
"Noto Naskh Arabic": { "normal": { "400": "noto-naskh-arabic/400.ttf", "700": "noto-naskh-arabic/700.ttf" }, "italic": "reuse-normal", "preview": "noto-naskh-arabic/preview.ttf" },
"Noto Sans Hebrew": { "normal": { "400": "noto-sans-hebrew/400.ttf", "700": "noto-sans-hebrew/700.ttf" }, "italic": "reuse-normal", "preview": "noto-sans-hebrew/preview.ttf" },
"Noto Sans Thai": { "normal": { "400": "noto-sans-thai/400.ttf", "700": "noto-sans-thai/700.ttf" }, "italic": "reuse-normal", "preview": "noto-sans-thai/preview.ttf" },
"Noto Emoji": { "normal": { "400": "noto-emoji/400.ttf", "700": "noto-emoji/700.ttf" }, "italic": "reuse-normal", "preview": "noto-emoji/preview.ttf" }
}
}
Sizes are Content-Length bytes from a HEAD request to the exact current catalog assets on 2026-09-06. Responses reported Content-Encoding: gzip; these are compressed transfer-size estimates, not a claim about the eventual on-disk representation. Preview paths are aliases to the selected 400 face and add no extra bytes when stored once. Primary sources: IBM Plex LICENSE.txt, Noto core LICENSE, Noto CJK Sans LICENSE, and Noto Emoji font LICENSE.
| Family | Style/weights in proposal | Current catalog source version | License | Script/fallback role | Gzip bytes (selected files) | Build owner; runtime owners |
|---|---|---|---|---|---|---|
| IBM Plex Serif | normal 400/700; italic 400/700 | fonts.gstatic.com/s/ibmplexserif/v20 | OFL 1.1, Reserved Font Name Plex | Primary serif; Latin and punctuation stack | 294,717 | packages/fonts; apps/web FontDisplay; packages/pdf registration |
| IBM Plex Sans | normal 400/700; italic 400/700 | fonts.gstatic.com/s/ibmplexsans/v23 | OFL 1.1, Reserved Font Name Plex | Primary sans | 435,469 | packages/fonts; apps/web FontDisplay; packages/pdf registration |
| Noto Serif | normal 400/700; italic 400/700 | fonts.gstatic.com/s/notoserif/v33 | OFL 1.1 | Serif punctuation fallback | 1,055,120 | packages/fonts; packages/pdf fallback registration |
| Noto Sans | normal 400/700; italic 400/700 | fonts.gstatic.com/s/notosans/v42 | OFL 1.1 | Sans punctuation fallback | 1,236,259 | packages/fonts; packages/pdf fallback registration |
| Noto Sans SC | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notosanssc/v40 | OFL 1.1 (Noto CJK) | Simplified Han; CJK fallback | 12,766,416 | packages/fonts; packages/pdf CJK fallback |
| Noto Serif SC | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notoserifsc/v35 | OFL 1.1 (Noto CJK) | Simplified Han serif fallback | 17,350,185 | packages/fonts; packages/pdf CJK fallback |
| Noto Sans TC | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notosanstc/v39 | OFL 1.1 (Noto CJK) | Traditional Han fallback | 8,628,278 | packages/fonts; packages/pdf CJK fallback |
| Noto Serif TC | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notoseriftc/v36 | OFL 1.1 (Noto CJK) | Traditional Han serif fallback | 11,804,923 | packages/fonts; packages/pdf CJK fallback |
| Noto Sans JP | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notosansjp/v56 | OFL 1.1 (Noto CJK) | Kana and Japanese Han fallback | 6,383,035 | packages/fonts; packages/pdf CJK fallback |
| Noto Serif JP | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notoserifjp/v33 | OFL 1.1 (Noto CJK) | Kana and Japanese Han serif fallback | 8,685,862 | packages/fonts; packages/pdf CJK fallback |
| Noto Sans KR | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notosanskr/v39 | OFL 1.1 (Noto CJK) | Hangul and Korean Han fallback | 6,102,888 | packages/fonts; packages/pdf CJK fallback |
| Noto Serif KR | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notoserifkr/v31 | OFL 1.1 (Noto CJK) | Hangul and Korean Han serif fallback | 11,113,442 | packages/fonts; packages/pdf CJK fallback |
| Noto Sans Arabic | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notosansarabic/v33 | OFL 1.1 | Arabic sans fallback | 178,455 | packages/fonts; packages/pdf script fallback |
| Noto Naskh Arabic | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notonaskharabic/v44 | OFL 1.1 | Arabic serif fallback | 190,924 | packages/fonts; packages/pdf script fallback |
| Noto Sans Hebrew | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notosanshebrew/v50 | OFL 1.1 | Hebrew fallback for both serif/sans slots | 55,707 | packages/fonts; packages/pdf script fallback |
| Noto Sans Thai | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notosansthai/v29 | OFL 1.1 | Thai fallback for both serif/sans slots | 55,173 | packages/fonts; packages/pdf script fallback |
| Noto Emoji | normal 400/700; italic reuses normal | fonts.gstatic.com/s/notoemoji/v62 | OFL 1.1 for font files; assets/tools have separate licenses | Emoji outline fallback; verify renderer support | 1,153,847 | packages/fonts; packages/pdf script fallback |
Estimated transfer size for all rows and listed styles: 87,490,700 bytes (~83.44 MiB). This confirms why a full-catalog bundle is out of scope. A later implementation should subset by declared glyph requirements or make the administrator choose fallback families; it must not silently fetch another CDN.
@latest URLs as runtime sources.Plex; modified/subset outputs must follow OFL naming requirements.packages/fonts/tooling); runtime owners (apps/web and packages/pdf) consume only the validated manifest.Local mode must resolve only same-origin administrator-hosted manifest paths. If imported resume data names an unavailable family, show an actionable missing-family error naming the family and required local asset; apply a configured local fallback only when the administrator explicitly supplied one. If a required weight/style/fallback asset is absent, fail the affected preview/export with an actionable diagnostic containing family/style/weight and local path. Never retry Google Fonts, jsDelivr, or any other remote URL in local mode.
Standard PDF families (Helvetica, Courier, Times-Roman) remain file-free. They do not prove that a document containing punctuation, CJK, Arabic, Hebrew, Thai, or emoji is network-free; the script fallback rows remain required.
Completed read-only checks before handoff:
pnpm dlx @tanstack/intent@latest list: no matching local intent skill for this work.pnpm exec biome check tests/e2e/specs/offline-fonts.spec.ts tests/e2e/fixtures/offline-fonts.ts turbo.json: passed.git diff --check: passed.pnpm --filter @reactive-resume/fonts test: passed (55 tests).pnpm --filter @reactive-resume/pdf exec vitest run src/hooks/use-register-fonts.test.ts: passed (35 tests).pnpm exec playwright test tests/e2e/specs/offline-fonts.spec.ts --list: passed (4 diagnostic tests collected).pnpm exec turbo boundaries: passed on fresh rerun (Turbo 2.10.12, 1108 files, no issues).The implementation intentionally stops at diagnostic fixtures and manifest evidence. Shared source resolution, asset hosting, local-mode configuration, and production behavior remain Phase A step 3+ work.