diff --git a/packages/fallbacks/data-drift.test.ts b/packages/fallbacks/data-drift.test.ts new file mode 100644 index 0000000..b1714e9 --- /dev/null +++ b/packages/fallbacks/data-drift.test.ts @@ -0,0 +1,25 @@ +/** + * `src/data.ts` is generated from `records.json` by `scripts/generate-data.ts`; records.json is the only + * file a reviewer hand-edits. This guards the two against drift: a hand-edit to data.ts, or an edit to + * records.json without re-running `bun run gen:data`, fails here. The generator is pure, so the test + * re-renders the module in memory and byte-compares it to the checked-in file. + */ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { + DATA_PATH, + loadRecords, + renderDataModule, +} from "./scripts/generate-data"; +import { SUBSTITUTION_EVIDENCE } from "./src/index"; + +describe("data.ts is generated from records.json", () => { + test("checked-in src/data.ts matches the generator output (run `bun run gen:data` if this fails)", () => { + const onDisk = readFileSync(DATA_PATH, "utf8"); + expect(onDisk).toBe(renderDataModule(loadRecords())); + }); + + test("the exported rows deep-equal records.json", () => { + expect(SUBSTITUTION_EVIDENCE).toEqual(loadRecords()); + }); +}); diff --git a/packages/fallbacks/fallbacks.test.ts b/packages/fallbacks/fallbacks.test.ts index c815a5e..90c40c7 100644 --- a/packages/fallbacks/fallbacks.test.ts +++ b/packages/fallbacks/fallbacks.test.ts @@ -289,3 +289,58 @@ describe("glyphExceptions projection", () => { ).toHaveLength(1); }); }); + +describe("Cooper Black -> Caprasimo (Regular-only, metric_safe)", () => { + const renderAll = { canRenderFamily: () => true }; + const onlyCaprasimo = { canRenderFamily: (f: string) => f === "Caprasimo" }; + + test("the family resolves to Caprasimo as an exact, line-break-safe Regular substitute", () => { + // Unlike Baskerville -> Bacasime (visual_only, NBSP reflows), Cooper measures 0% across the Latin + // core, so the row is metric_safe with no glyph exceptions. + expect(getRenderableFallback("Cooper Black", renderAll)).toEqual({ + substituteFamily: "Caprasimo", + policyAction: "substitute", + verdict: "metric_safe", + lineBreakSafe: true, + faces: { regular: true, bold: false, italic: false, boldItalic: false }, + evidenceId: "cooper-black", + }); + }); + + test("Regular maps; bold/italic/boldItalic are face_missing (never faux-styled onto Caprasimo)", () => { + expect( + getRenderableFallbackForFace("Cooper Black", "regular", renderAll) + ?.substituteFamily, + ).toBe("Caprasimo"); + for (const face of ["bold", "italic", "boldItalic"] as const) { + expect( + getRenderableFallbackForFace("Cooper Black", face, renderAll), + `Cooper Black ${face}`, + ).toBeNull(); + expect( + getFallbackDecisionForFace("Cooper Black", face, renderAll), + `Cooper Black ${face} decision`, + ).toEqual({ + kind: "face_missing", + substituteFamily: "Caprasimo", + evidenceId: "cooper-black", + }); + } + }); + + test("stays asset-aware: a consumer that does not bundle Caprasimo gets asset_missing, not a render", () => { + // Asset gate: Cooper stays inert until a consumer actually bundles Caprasimo. + expect( + getFallbackDecision("Cooper Black", { canRenderFamily: () => false }), + ).toEqual({ + kind: "asset_missing", + substituteFamily: "Caprasimo", + verdict: "metric_safe", + evidenceId: "cooper-black", + }); + expect( + getRenderableFallbackForFace("Cooper Black", "regular", onlyCaprasimo) + ?.substituteFamily, + ).toBe("Caprasimo"); + }); +}); diff --git a/packages/fallbacks/package.json b/packages/fallbacks/package.json index 9e70df8..6b0fca2 100644 --- a/packages/fallbacks/package.json +++ b/packages/fallbacks/package.json @@ -35,6 +35,7 @@ "directory": "packages/fallbacks" }, "scripts": { + "gen:data": "bun run scripts/generate-data.ts", "build": "tsc -p tsconfig.build.json", "prepack": "bun run build" }, diff --git a/packages/fallbacks/records.json b/packages/fallbacks/records.json new file mode 100644 index 0000000..7614b79 --- /dev/null +++ b/packages/fallbacks/records.json @@ -0,0 +1,660 @@ +[ + { + "evidenceId": "calibri", + "logicalFamily": "Calibri", + "physicalFamily": "Carlito", + "verdict": "metric_safe", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "pass", + "ship": "pass" + }, + "policyAction": "substitute", + "measurementRefs": [ + "calibri__carlito#analytic_advance#2026-06-03", + "calibri__carlito#face_aggregate#2026-06-03" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "cambria", + "logicalFamily": "Cambria", + "physicalFamily": "Caladea", + "verdict": "visual_only", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "not_run", + "ship": "pass" + }, + "policyAction": "substitute", + "measurementRefs": [ + "cambria_regular__caladea#regular#w400#d2f6cad3#analytic_advance#2026-06-04", + "cambria_bold__caladea#bold#w700#74eda4fc#analytic_advance#2026-06-04", + "cambria_italic__caladea#italic#w400#9c968bf6#analytic_advance#2026-06-04", + "cambria_boldItalic__caladea#boldItalic#w700#f47a35ad#analytic_advance#2026-06-04" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0.0002378, + "maxDelta": 0.2310758 + }, + "candidateLicense": "Apache-2.0", + "faceVerdicts": { + "regular": "metric_safe", + "bold": "metric_safe", + "italic": "metric_safe", + "boldItalic": "visual_only" + }, + "glyphExceptions": [ + { + "slot": "boldItalic", + "codepoint": 96, + "advanceDelta": 0.231, + "note": "Caladea Bold Italic grave accent (U+0060) advance diverges ~23% from Cambria; lines containing it reflow. All other glyphs, and the regular/bold/italic faces, are within the direct metric threshold." + } + ] + }, + { + "evidenceId": "arial", + "logicalFamily": "Arial", + "physicalFamily": "Liberation Sans", + "verdict": "metric_safe", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "not_run", + "ship": "pass" + }, + "policyAction": "substitute", + "measurementRefs": ["arial__liberation-sans#analytic_advance#2026-06-03"], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "times-new-roman", + "logicalFamily": "Times New Roman", + "physicalFamily": "Liberation Serif", + "verdict": "metric_safe", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "not_run", + "ship": "pass" + }, + "policyAction": "substitute", + "measurementRefs": [ + "times-new-roman__liberation-serif#analytic_advance#2026-06-03" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "courier-new", + "logicalFamily": "Courier New", + "physicalFamily": "Liberation Mono", + "verdict": "metric_safe", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "not_run", + "ship": "pass" + }, + "policyAction": "substitute", + "measurementRefs": [ + "courier-new__liberation-mono#analytic_advance#2026-06-03" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "georgia", + "logicalFamily": "Georgia", + "physicalFamily": "Gelasio", + "verdict": "near_metric", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "pass", + "ship": "fail" + }, + "policyAction": "substitute", + "measurementRefs": [ + "georgia_regular__gelasio#regular#w400#1543f04d#analytic_advance#2026-06-04", + "georgia_bold__gelasio#bold#w700#5a1b9bd7#analytic_advance#2026-06-04", + "georgia_italic__gelasio#italic#w400#be1243a9#analytic_advance#2026-06-04", + "georgia_boldItalic__gelasio#boldItalic#w700#6f3b3f7a#analytic_advance#2026-06-04", + "georgia_regular__gelasio#regular#w400#1543f04d#live_layout#2026-06-03", + "georgia_bold__gelasio#bold#w700#5a1b9bd7#live_layout#2026-06-03", + "georgia_italic__gelasio#italic#w400#be1243a9#live_layout#2026-06-03", + "georgia_boldItalic__gelasio#boldItalic#w700#6f3b3f7a#live_layout#2026-06-03" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0.0000197, + "maxDelta": 0.0183727 + }, + "candidateLicense": "OFL-1.1", + "faceVerdicts": { + "regular": "metric_safe", + "bold": "metric_safe", + "italic": "near_metric", + "boldItalic": "near_metric" + }, + "glyphExceptions": [ + { + "slot": "italic", + "codepoint": 210, + "advanceDelta": 0.0184, + "note": "Georgia Italic vs Gelasio Italic: accented capital O (U+00D2-D8: O-grave/acute/circumflex/diaeresis/stroke) advance differs ~1.84%. 5 rare glyphs; all other glyphs exact, mean 0%." + }, + { + "slot": "boldItalic", + "codepoint": 204, + "advanceDelta": 0.011, + "note": "Georgia Bold Italic vs Gelasio Bold Italic: accented capital I (U+00CC-CE: I-grave/acute/circumflex) advance differs ~1.10%. 3 rare glyphs; all other glyphs exact, mean ~0%." + } + ] + }, + { + "evidenceId": "arial-narrow", + "logicalFamily": "Arial Narrow", + "physicalFamily": "Liberation Sans Narrow", + "verdict": "visual_only", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "not_run", + "ship": "fail" + }, + "policyAction": "substitute", + "measurementRefs": [ + "arial-narrow_regular__liberation-sans-narrow#regular#w400#546e8957#analytic_advance#2026-06-04", + "arial-narrow_bold__liberation-sans-narrow#bold#w700#8e5eb509#analytic_advance#2026-06-04", + "arial-narrow_italic__liberation-sans-narrow#italic#w400#c5de4127#analytic_advance#2026-06-04", + "arial-narrow_boldItalic__liberation-sans-narrow#boldItalic#w700#57fe1513#analytic_advance#2026-06-04" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0.5 + }, + "candidateLicense": "GPLv2-with-font-exception", + "faceVerdicts": { + "regular": "metric_safe", + "bold": "visual_only", + "italic": "metric_safe", + "boldItalic": "metric_safe" + }, + "glyphExceptions": [ + { + "slot": "bold", + "codepoint": 160, + "advanceDelta": 0.5, + "note": "Arial Narrow Bold no-break space (U+00A0) is double-width (2x the regular space); Liberation Sans Narrow Bold matches the regular space, so lines containing a non-breaking space reflow. All other glyphs, and the regular/italic/boldItalic faces, match within the direct metric threshold." + } + ] + }, + { + "evidenceId": "aptos", + "logicalFamily": "Aptos", + "physicalFamily": null, + "verdict": "no_substitute", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "customer_supplied", + "measurementRefs": ["aptos#top_candidates#2026-06-03"], + "exportRule": "preserve_original_name", + "candidateLicense": null + }, + { + "evidenceId": "consolas", + "logicalFamily": "Consolas", + "physicalFamily": "Inconsolata SemiExpanded", + "verdict": "cell_width_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "not_run", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": [ + "consolas__inconsolata-semiexpanded#analytic_advance#2026-06-03" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0.00035999999999999997, + "maxDelta": 0.00035999999999999997 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "verdana", + "logicalFamily": "Verdana", + "physicalFamily": null, + "verdict": "visual_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": ["verdana#top_candidates#2026-06-03"], + "exportRule": "preserve_original_name", + "candidateLicense": null + }, + { + "evidenceId": "tahoma", + "logicalFamily": "Tahoma", + "physicalFamily": null, + "verdict": "visual_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": ["tahoma#top_candidates#2026-06-03"], + "exportRule": "preserve_original_name", + "candidateLicense": null + }, + { + "evidenceId": "trebuchet-ms", + "logicalFamily": "Trebuchet MS", + "physicalFamily": null, + "verdict": "visual_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": ["trebuchet-ms#top_candidates#2026-06-03"], + "exportRule": "preserve_original_name", + "candidateLicense": null + }, + { + "evidenceId": "comic-sans-ms", + "logicalFamily": "Comic Sans MS", + "physicalFamily": "Comic Neue", + "verdict": "visual_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": [ + "comic-sans-ms__comic-neue#analytic_advance#2026-06-03" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0.1005, + "maxDelta": 0.1419 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "candara", + "logicalFamily": "Candara", + "physicalFamily": null, + "verdict": "visual_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": ["candara#top_candidates#2026-06-03"], + "exportRule": "preserve_original_name", + "candidateLicense": null + }, + { + "evidenceId": "constantia", + "logicalFamily": "Constantia", + "physicalFamily": null, + "verdict": "visual_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": ["constantia#top_candidates#2026-06-03"], + "exportRule": "preserve_original_name", + "candidateLicense": null + }, + { + "evidenceId": "corbel", + "logicalFamily": "Corbel", + "physicalFamily": null, + "verdict": "visual_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": ["corbel#top_candidates#2026-06-03"], + "exportRule": "preserve_original_name", + "candidateLicense": null + }, + { + "evidenceId": "lucida-console", + "logicalFamily": "Lucida Console", + "physicalFamily": "Cousine", + "verdict": "cell_width_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "not_run", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "category_fallback", + "measurementRefs": ["lucida-console__cousine#analytic_advance#2026-06-03"], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0.004050000000000001, + "maxDelta": 0.004050000000000001 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "aptos-display", + "logicalFamily": "Aptos Display", + "physicalFamily": null, + "verdict": "customer_supplied", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "not_run", + "layout": "not_run", + "ship": "fail" + }, + "policyAction": "customer_supplied", + "measurementRefs": [], + "exportRule": "preserve_original_name" + }, + { + "evidenceId": "cambria-math", + "logicalFamily": "Cambria Math", + "physicalFamily": null, + "verdict": "preserve_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "not_run", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "preserve_only", + "measurementRefs": [], + "exportRule": "preserve_original_name" + }, + { + "evidenceId": "helvetica", + "logicalFamily": "Helvetica", + "physicalFamily": "Liberation Sans", + "verdict": "metric_safe", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "not_run", + "metric": "pass", + "layout": "not_run", + "ship": "pass" + }, + "policyAction": "substitute", + "measurementRefs": [ + "helvetica__liberation-sans#analytic_advance#2026-06-03" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "calibri-light", + "logicalFamily": "Calibri Light", + "physicalFamily": "Carlito", + "verdict": "visual_only", + "faces": { + "regular": false, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "not_run", + "metric": "fail", + "layout": "not_run", + "ship": "fail" + }, + "policyAction": "category_fallback", + "measurementRefs": ["calibri-light__carlito#analytic_advance#2026-06-05"], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0.0148, + "maxDelta": 0.066 + }, + "candidateLicense": "OFL-1.1" + }, + { + "evidenceId": "baskerville-old-face", + "logicalFamily": "Baskerville Old Face", + "physicalFamily": "Bacasime Antique", + "verdict": "visual_only", + "faces": { + "regular": true, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "pass", + "metric": "fail", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "substitute", + "measurementRefs": [ + "baskerville-old-face_regular__bacasime-antique#regular#w400#7dac1e5f#analytic_advance#2026-06-05" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0.4915590863952334 + }, + "candidateLicense": "OFL-1.1", + "faceVerdicts": { + "regular": "visual_only" + }, + "glyphExceptions": [ + { + "slot": "regular", + "codepoint": 160, + "advanceDelta": 0.4916, + "note": "Bacasime Antique Regular's no-break space (U+00A0) advance diverges ~49% from Baskerville Old Face; lines containing NBSP reflow. Every other Latin-core glyph is advance-identical, which is why this is visual_only with a single named exception, not near_metric." + } + ] + }, + { + "evidenceId": "cooper-black", + "logicalFamily": "Cooper Black", + "physicalFamily": "Caprasimo", + "verdict": "metric_safe", + "faces": { + "regular": true, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "substitute", + "measurementRefs": [ + "cooper-black_regular__caprasimo#regular#w400#786ab84e#analytic_advance#2026-06-05" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0 + }, + "candidateLicense": "OFL-1.1", + "faceVerdicts": { + "regular": "metric_safe" + } + } +] diff --git a/packages/fallbacks/scripts/generate-data.ts b/packages/fallbacks/scripts/generate-data.ts new file mode 100644 index 0000000..f5824ed --- /dev/null +++ b/packages/fallbacks/scripts/generate-data.ts @@ -0,0 +1,39 @@ +/** + * Generate `src/data.ts` from `records.json`, the reviewed source of truth. + * + * `records.json` is the only file a reviewer edits to add or change a row; `src/data.ts` is a + * derived, biome-ignored TypeScript module (a typed re-export of the same rows). Run `bun run gen:data` + * after editing records.json. The drift test (`data-drift.test.ts`) re-renders in memory and fails if + * the checked-in module is stale, so the two can never silently diverge. + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import type { SubstitutionEvidence } from "../src/types.js"; + +const PKG_DIR = join(import.meta.dir, ".."); +export const RECORDS_PATH = join(PKG_DIR, "records.json"); +export const DATA_PATH = join(PKG_DIR, "src", "data.ts"); + +/** Load the reviewed rows from records.json. */ +export function loadRecords(): SubstitutionEvidence[] { + return JSON.parse( + readFileSync(RECORDS_PATH, "utf8"), + ) as SubstitutionEvidence[]; +} + +/** Render the `src/data.ts` module text for a set of reviewed rows. Pure, so the drift test can diff it. */ +export function renderDataModule( + records: readonly SubstitutionEvidence[], +): string { + const rows = JSON.stringify(records, null, 2); + return `// Generated from records.json by scripts/generate-data.ts. Do not edit by hand. +import type { SubstitutionEvidence } from "./types.js"; + +export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = ${rows}; +`; +} + +if (import.meta.main) { + writeFileSync(DATA_PATH, renderDataModule(loadRecords())); + console.log(`Wrote ${DATA_PATH}`); +} diff --git a/packages/fallbacks/src/data.ts b/packages/fallbacks/src/data.ts index dca6541..4ccc309 100644 --- a/packages/fallbacks/src/data.ts +++ b/packages/fallbacks/src/data.ts @@ -1,4 +1,4 @@ -// Generated from reviewed docfonts evidence. Do not edit by hand. +// Generated from records.json by scripts/generate-data.ts. Do not edit by hand. import type { SubstitutionEvidence } from "./types.js"; export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ @@ -648,5 +648,36 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ "note": "Bacasime Antique Regular's no-break space (U+00A0) advance diverges ~49% from Baskerville Old Face; lines containing NBSP reflow. Every other Latin-core glyph is advance-identical, which is why this is visual_only with a single named exception, not near_metric." } ] + }, + { + "evidenceId": "cooper-black", + "logicalFamily": "Cooper Black", + "physicalFamily": "Caprasimo", + "verdict": "metric_safe", + "faces": { + "regular": true, + "bold": false, + "italic": false, + "boldItalic": false + }, + "gates": { + "static": "pass", + "metric": "pass", + "layout": "not_run", + "ship": "not_run" + }, + "policyAction": "substitute", + "measurementRefs": [ + "cooper-black_regular__caprasimo#regular#w400#786ab84e#analytic_advance#2026-06-05" + ], + "exportRule": "preserve_original_name", + "advance": { + "meanDelta": 0, + "maxDelta": 0 + }, + "candidateLicense": "OFL-1.1", + "faceVerdicts": { + "regular": "metric_safe" + } } ];