diff --git a/packages/fallbacks/fallbacks.test.ts b/packages/fallbacks/fallbacks.test.ts index bf60236..b7c94d1 100644 --- a/packages/fallbacks/fallbacks.test.ts +++ b/packages/fallbacks/fallbacks.test.ts @@ -184,32 +184,32 @@ describe("face-aware lookups (Regular-only safety)", () => { }); }); - test("a Regular-only substitute is returned for Regular and NULL for bold/italic", () => { - // Baskerville -> Bacasime is Regular-only. The face-safe lookup must not route bold to it. + test("a Regular-only substitute can expose reviewed synthetic faces", () => { expect( getRenderableFallbackForFace("Baskerville Old Face", "regular", renderAll) ?.substituteFamily, ).toBe("Bacasime Antique"); - expect( - getRenderableFallbackForFace("Baskerville Old Face", "bold", renderAll), - ).toBeNull(); - expect( - getRenderableFallbackForFace("Baskerville Old Face", "italic", renderAll), - ).toBeNull(); + for (const face of ["bold", "italic", "boldItalic"] as const) { + expect( + getRenderableFallbackForFace("Baskerville Old Face", face, renderAll), + `Baskerville ${face}`, + ).toMatchObject({ + substituteFamily: "Bacasime Antique", + verdict: "visual_only", + lineBreakSafe: false, + faceSource: { kind: "synthetic", from: "regular" }, + }); + } }); test("an uncovered face is `face_missing`, not `unknown` or null collapse", () => { expect( - getFallbackDecisionForFace( - "Baskerville Old Face", - "boldItalic", - renderAll, - ), + getFallbackDecisionForFace("Arial Black", "bold", renderAll), ).toEqual({ kind: "face_missing", - substituteFamily: "Bacasime Antique", - evidenceId: "baskerville-old-face", - generic: "serif", + substituteFamily: "Archivo Black", + evidenceId: "arial-black", + generic: "sans-serif", }); }); @@ -252,9 +252,9 @@ describe("face-aware lookups (Regular-only safety)", () => { `Calibri Light ${face}`, ).toBe("Carlito"); } - // ...while a genuinely face-scoped Regular-only row still gates bold to face_missing. + // ...while a genuinely uncovered face still gates to face_missing. expect( - getFallbackDecisionForFace("Baskerville Old Face", "bold", { + getFallbackDecisionForFace("Arial Black", "bold", { canRenderFamily: () => true, }).kind, ).toBe("face_missing"); @@ -322,10 +322,12 @@ describe("selected visual fallback rows", () => { ["Arial Black", "Archivo Black", "substitute"], ["Arial Rounded MT Bold", "Ubuntu", "category_fallback"], ["Bookman Old Style", "TeX Gyre Bonum", "substitute"], + ["Brush Script MT", "Oregano Italic", "category_fallback"], ["Century", "C059", "substitute"], ["Comic Sans MS", "Comic Relief", "category_fallback"], ["Garamond", "Cardo", "category_fallback"], ["Gill Sans MT Condensed", "PT Sans Narrow", "category_fallback"], + ["Lucida Console", "Noto Sans Mono", "category_fallback"], ["Tahoma", "Noto Sans", "category_fallback"], ["Trebuchet MS", "PT Sans", "category_fallback"], ] as const; @@ -360,6 +362,14 @@ describe("selected visual fallback rows", () => { faces: { regular: true, bold: true, italic: false, boldItalic: false }, }); + expect( + getRenderableFallbackForFace("Brush Script MT", "bold", renderAll), + ).toMatchObject({ + substituteFamily: "Oregano Italic", + faceSource: { kind: "synthetic", from: "regular" }, + faces: { regular: true, bold: false, italic: false, boldItalic: false }, + }); + expect( getRenderableFallbackForFace( "Gill Sans MT Condensed", @@ -371,6 +381,42 @@ describe("selected visual fallback rows", () => { faceSource: { kind: "synthetic", from: "regular" }, faces: { regular: true, bold: true, italic: false, boldItalic: false }, }); + + expect( + getRenderableFallbackForFace("Lucida Console", "boldItalic", renderAll), + ).toMatchObject({ + substituteFamily: "Noto Sans Mono", + faceSource: { kind: "synthetic", from: "bold" }, + faces: { regular: true, bold: true, italic: false, boldItalic: false }, + }); + }); + + test("Lucida Console keeps cell-width evidence for real Noto Sans Mono faces", () => { + for (const face of ["regular", "bold"] as const) { + expect( + getRenderableFallbackForFace("Lucida Console", face, renderAll), + `Lucida Console ${face}`, + ).toMatchObject({ + substituteFamily: "Noto Sans Mono", + verdict: "cell_width_only", + lineBreakSafe: true, + }); + expect( + getRenderableFallbackForFace("Lucida Console", face, renderAll) + ?.faceSource, + ).toBeUndefined(); + } + + for (const face of ["italic", "boldItalic"] as const) { + expect( + getRenderableFallbackForFace("Lucida Console", face, renderAll), + `Lucida Console ${face}`, + ).toMatchObject({ + substituteFamily: "Noto Sans Mono", + verdict: "visual_only", + lineBreakSafe: false, + }); + } }); }); @@ -436,6 +482,10 @@ describe("advance measurement basis", () => { SUBSTITUTION_EVIDENCE.find((row) => row.evidenceId === "consolas") ?.advance?.basis, ).toBe("monospace_cell"); + expect( + SUBSTITUTION_EVIDENCE.find((row) => row.evidenceId === "lucida-console") + ?.advance?.basis, + ).toBe("monospace_cell"); expect( SUBSTITUTION_EVIDENCE.find((row) => row.evidenceId === "calibri")?.advance ?.basis, diff --git a/packages/fallbacks/records.json b/packages/fallbacks/records.json index a45e4a4..29c43ea 100644 --- a/packages/fallbacks/records.json +++ b/packages/fallbacks/records.json @@ -638,29 +638,48 @@ "evidenceId": "lucida-console", "generic": "monospace", "logicalFamily": "Lucida Console", - "physicalFamily": "Cousine", - "verdict": "cell_width_only", + "physicalFamily": "Noto Sans Mono", + "verdict": "visual_only", "faces": { - "regular": false, - "bold": false, + "regular": true, + "bold": true, "italic": false, "boldItalic": false }, + "faceSources": { + "italic": { + "kind": "synthetic", + "from": "regular" + }, + "boldItalic": { + "kind": "synthetic", + "from": "bold" + } + }, "gates": { - "static": "not_run", - "metric": "not_run", + "static": "pass", + "metric": "pass", "layout": "not_run", - "ship": "not_run" + "ship": "fail" }, "policyAction": "category_fallback", - "measurementRefs": ["lucida-console__cousine#analytic_advance#2026-06-03"], + "measurementRefs": [ + "lucida-console__noto-sans-mono#monospace_cell#analytic_advance#2026-06-09", + "lucida-console__noto-sans-mono#visual_review#2026-06-09" + ], "exportRule": "preserve_original_name", "advance": { "basis": "monospace_cell", - "meanDelta": 0.004050000000000001, - "maxDelta": 0.004050000000000001 + "meanDelta": 0.00254, + "maxDelta": 0.00303 }, - "candidateLicense": "OFL-1.1" + "candidateLicense": "OFL-1.1", + "faceVerdicts": { + "regular": "cell_width_only", + "bold": "cell_width_only", + "italic": "visual_only", + "boldItalic": "visual_only" + } }, { "evidenceId": "gill-sans-mt-condensed", @@ -817,6 +836,20 @@ "italic": false, "boldItalic": false }, + "faceSources": { + "bold": { + "kind": "synthetic", + "from": "regular" + }, + "italic": { + "kind": "synthetic", + "from": "regular" + }, + "boldItalic": { + "kind": "synthetic", + "from": "regular" + } + }, "gates": { "static": "pass", "metric": "fail", @@ -825,7 +858,8 @@ }, "policyAction": "substitute", "measurementRefs": [ - "baskerville-old-face_regular__bacasime-antique#regular#w400#7dac1e5f#analytic_advance#2026-06-05" + "baskerville-old-face_regular__bacasime-antique#regular#w400#7dac1e5f#analytic_advance#2026-06-05", + "baskerville-old-face__bacasime-antique#synthetic_faces#visual_review#2026-06-09" ], "exportRule": "preserve_original_name", "advance": { @@ -835,7 +869,10 @@ }, "candidateLicense": "OFL-1.1", "faceVerdicts": { - "regular": "visual_only" + "regular": "visual_only", + "bold": "visual_only", + "italic": "visual_only", + "boldItalic": "visual_only" }, "glyphExceptions": [ { @@ -846,6 +883,41 @@ } ] }, + { + "evidenceId": "brush-script-mt", + "generic": "serif", + "logicalFamily": "Brush Script MT", + "physicalFamily": "Oregano Italic", + "verdict": "visual_only", + "faces": { + "regular": true, + "bold": false, + "italic": false, + "boldItalic": false + }, + "faceSources": { + "bold": { + "kind": "synthetic", + "from": "regular" + } + }, + "gates": { + "static": "pass", + "metric": "fail", + "layout": "not_run", + "ship": "fail" + }, + "policyAction": "category_fallback", + "measurementRefs": [ + "brush-script-mt__oregano-italic#visual_review#2026-06-09" + ], + "exportRule": "preserve_original_name", + "candidateLicense": "OFL-1.1", + "faceVerdicts": { + "regular": "visual_only", + "bold": "visual_only" + } + }, { "evidenceId": "cooper-black", "generic": "serif", diff --git a/packages/fallbacks/src/data.ts b/packages/fallbacks/src/data.ts index 4e68622..9d03f2d 100644 --- a/packages/fallbacks/src/data.ts +++ b/packages/fallbacks/src/data.ts @@ -665,31 +665,48 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ "evidenceId": "lucida-console", "generic": "monospace", "logicalFamily": "Lucida Console", - "physicalFamily": "Cousine", - "verdict": "cell_width_only", + "physicalFamily": "Noto Sans Mono", + "verdict": "visual_only", "faces": { - "regular": false, - "bold": false, + "regular": true, + "bold": true, "italic": false, "boldItalic": false }, + "faceSources": { + "italic": { + "kind": "synthetic", + "from": "regular" + }, + "boldItalic": { + "kind": "synthetic", + "from": "bold" + } + }, "gates": { - "static": "not_run", - "metric": "not_run", + "static": "pass", + "metric": "pass", "layout": "not_run", - "ship": "not_run" + "ship": "fail" }, "policyAction": "category_fallback", "measurementRefs": [ - "lucida-console__cousine#analytic_advance#2026-06-03" + "lucida-console__noto-sans-mono#monospace_cell#analytic_advance#2026-06-09", + "lucida-console__noto-sans-mono#visual_review#2026-06-09" ], "exportRule": "preserve_original_name", "advance": { "basis": "monospace_cell", - "meanDelta": 0.004050000000000001, - "maxDelta": 0.004050000000000001 + "meanDelta": 0.00254, + "maxDelta": 0.00303 }, - "candidateLicense": "OFL-1.1" + "candidateLicense": "OFL-1.1", + "faceVerdicts": { + "regular": "cell_width_only", + "bold": "cell_width_only", + "italic": "visual_only", + "boldItalic": "visual_only" + } }, { "evidenceId": "gill-sans-mt-condensed", @@ -848,6 +865,20 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ "italic": false, "boldItalic": false }, + "faceSources": { + "bold": { + "kind": "synthetic", + "from": "regular" + }, + "italic": { + "kind": "synthetic", + "from": "regular" + }, + "boldItalic": { + "kind": "synthetic", + "from": "regular" + } + }, "gates": { "static": "pass", "metric": "fail", @@ -856,7 +887,8 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ }, "policyAction": "substitute", "measurementRefs": [ - "baskerville-old-face_regular__bacasime-antique#regular#w400#7dac1e5f#analytic_advance#2026-06-05" + "baskerville-old-face_regular__bacasime-antique#regular#w400#7dac1e5f#analytic_advance#2026-06-05", + "baskerville-old-face__bacasime-antique#synthetic_faces#visual_review#2026-06-09" ], "exportRule": "preserve_original_name", "advance": { @@ -866,7 +898,10 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ }, "candidateLicense": "OFL-1.1", "faceVerdicts": { - "regular": "visual_only" + "regular": "visual_only", + "bold": "visual_only", + "italic": "visual_only", + "boldItalic": "visual_only" }, "glyphExceptions": [ { @@ -877,6 +912,41 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ } ] }, + { + "evidenceId": "brush-script-mt", + "generic": "serif", + "logicalFamily": "Brush Script MT", + "physicalFamily": "Oregano Italic", + "verdict": "visual_only", + "faces": { + "regular": true, + "bold": false, + "italic": false, + "boldItalic": false + }, + "faceSources": { + "bold": { + "kind": "synthetic", + "from": "regular" + } + }, + "gates": { + "static": "pass", + "metric": "fail", + "layout": "not_run", + "ship": "fail" + }, + "policyAction": "category_fallback", + "measurementRefs": [ + "brush-script-mt__oregano-italic#visual_review#2026-06-09" + ], + "exportRule": "preserve_original_name", + "candidateLicense": "OFL-1.1", + "faceVerdicts": { + "regular": "visual_only", + "bold": "visual_only" + } + }, { "evidenceId": "cooper-black", "generic": "serif",