From ffcb14d21e988ceeb6f8c0ec115502252eb2bc8c Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Fri, 5 Jun 2026 09:52:57 -0300 Subject: [PATCH] feat(registry): discovery->reviewed promotion path (Viga) Adds the one path that turns a discovery font into a REVIEWED corpus entry: a license-text-hashed CorpusManifest the registry's verdicts are allowed to rest on. scripts/promote-candidates.ts promotes an allow-listed family (Viga only) from the discovery snapshot - it reads the font bytes from the local cache and verifies them against the discovery fileSha256, fetches + hashes the EXACT upstream OFL.txt, parses facts, and reuses import-corpus buildManifest to emit the reviewed manifest. No binaries committed; records.json untouched. Viga (Tahoma's existing top_candidate) is the clean first promotion: static, OFL, single regular face. It passes every reviewed-corpus invariant (license-text hash, unique sha/faceId, parsed metadata) plus a new test that its committed sha matches the discovery snapshot. Naming Viga as Tahoma's public candidate is a SEPARATE editorial decision; this PR only establishes provenance. --- packages/registry/corpus.test.ts | 32 ++++- .../promoted-google-fonts-2026-06-05.json | 76 +++++++++++ packages/registry/src/index.ts | 9 +- scripts/import-corpus.ts | 14 +- scripts/promote-candidates.ts | 128 ++++++++++++++++++ 5 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 packages/registry/data/corpus/promoted-google-fonts-2026-06-05.json create mode 100644 scripts/promote-candidates.ts diff --git a/packages/registry/corpus.test.ts b/packages/registry/corpus.test.ts index 2098355..25e53bd 100644 --- a/packages/registry/corpus.test.ts +++ b/packages/registry/corpus.test.ts @@ -5,6 +5,8 @@ * faces, license provenance present, parsed metadata attached, and NO proprietary family ingested. */ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; import { corpusFaces, loadCorpus } from "./src/index"; const HEX64 = /^[0-9a-f]{64}$/; @@ -42,7 +44,35 @@ describe("loadCorpus (open-font corpus manifests)", () => { "regular", ]); expect(lsn?.license).toBe("GPLv2-with-font-exception"); - expect(faces.length).toBe(28); // 20 ship-set + 4 LSN + 4 Gelasio instances + expect(faces.length).toBe(29); // 20 ship-set + 4 LSN + 4 Gelasio instances + 1 promoted (Viga) + }); + + test("a promoted candidate is reviewed: sha matches discovery + license text is hashed", () => { + const promoted = manifests.find( + (m) => m.corpusId === "promoted-google-fonts-2026-06-05", + ); + const viga = promoted?.families.find((f) => f.family === "Viga"); + expect(viga).toBeTruthy(); + expect(viga?.license).toBe("OFL-1.1"); + // the reviewed tier's promise: an exact, hashed license text (discovery never carries this). + expect(viga?.licenseTextSha256).toMatch(/^[0-9a-f]{64}$/); + expect(viga?.faces).toHaveLength(1); + // the promoted face is the SAME bytes the discovery snapshot points at (verified at import). + const discovery = JSON.parse( + readFileSync( + join( + import.meta.dir, + "data", + "discovery", + "google-fonts-all-files-2026-06-04.json", + ), + "utf8", + ), + ) as { faces: { family: string; styleKey: string; fileSha256: string }[] }; + const disc = discovery.faces.find( + (f) => f.family === "Viga" && f.styleKey === "regular", + ); + expect(viga?.faces[0].fileSha256).toBe(disc?.fileSha256); }); test("Gelasio instances carry full variable-instance provenance (instancedFrom)", () => { diff --git a/packages/registry/data/corpus/promoted-google-fonts-2026-06-05.json b/packages/registry/data/corpus/promoted-google-fonts-2026-06-05.json new file mode 100644 index 0000000..485fedf --- /dev/null +++ b/packages/registry/data/corpus/promoted-google-fonts-2026-06-05.json @@ -0,0 +1,76 @@ +{ + "corpusId": "promoted-google-fonts-2026-06-05", + "source": "Google Fonts (promoted from discovery, reviewed)", + "sourceUrl": "https://github.com/google/fonts/tree/main", + "retrievedDate": "2026-06-05", + "families": [ + { + "family": "Viga", + "license": "OFL-1.1", + "licenseSource": "google/fonts + fetched upstream license text", + "licenseTextSha256": "b67eed9578defb59cc4ff4b58428fb816786e499e84cdeafca54723b055c169e", + "licenseUrl": "https://openfontlicense.org/open-font-license-official-text/", + "sourceUrl": "https://github.com/google/fonts/tree/main/ofl/viga", + "faces": [ + { + "candidateFaceId": "viga#regular#w400#730ad2a3", + "family": "Viga", + "styleKey": "regular", + "weight": 400, + "style": "normal", + "fileName": "Viga-Regular.ttf", + "fileSha256": "730ad2a38691ce19ee5aa9dd9ec84ae8737b2084b06eef13435a4facad9b063c", + "metadata": { + "fileSha256": "730ad2a38691ce19ee5aa9dd9ec84ae8737b2084b06eef13435a4facad9b063c", + "sfntVersion": "1.0", + "names": { + "family": "Viga", + "subfamily": "Regular", + "fullName": "Viga-Regular", + "postscriptName": "Viga-Regular" + }, + "face": { + "weightClass": 400, + "widthClass": 5, + "italic": false, + "bold": false, + "styleKey": "regular" + }, + "metrics": { + "unitsPerEm": 1000, + "hhea": { + "ascent": 1015, + "descent": -329, + "lineGap": 0 + }, + "typo": { + "ascent": 1015, + "descent": -329, + "lineGap": 0 + }, + "win": { + "ascent": 1015, + "descent": 329 + }, + "useTypoMetrics": false, + "fixedPitch": false + }, + "coverage": { + "cmapKind": "unicode", + "latinCoreRatio": 1, + "missingLatinCore": [] + }, + "classification": { + "category": "sans", + "condensed": false + }, + "embedding": { + "fsTypeValue": 0, + "fsTypeSummary": "installable" + } + } + } + ] + } + ] +} diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 629e69e..a7bf65d 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -48,6 +48,7 @@ export type { import shipSetCorpus from "../data/corpus/current-ship-set-2026-06-03.json"; import futureDirectCandidatesCorpus from "../data/corpus/future-direct-candidates-2026-06-03.json"; import generatedInstancesCorpus from "../data/corpus/generated-instances-2026-06-03.json"; +import promotedGoogleFontsCorpus from "../data/corpus/promoted-google-fonts-2026-06-05.json"; import recordsData from "../data/registry/records.json"; import type { CorpusManifest, EvidenceRecord } from "./types"; @@ -61,15 +62,17 @@ export function loadRecords(): EvidenceRecord[] { } /** - * The open-font CORPUS manifests (provenance + parsed facts + hashes; no binaries). GENERATED by - * scripts/import-corpus.ts from SuperDoc ship-set / open-font packets. The durable join key from an - * EvidenceRecord candidate to a corpus face is fileSha256. One manifest per source. + * The open-font CORPUS manifests (provenance + parsed facts + hashes; no binaries). The REVIEWED tier: + * generated by scripts/import-corpus.ts (ship-set / open-font packets) and scripts/promote-candidates.ts + * (allow-listed promotions from the discovery snapshot, license-text-hashed). The durable join key from + * an EvidenceRecord candidate to a corpus face is fileSha256. One manifest per source. */ export function loadCorpus(): CorpusManifest[] { return [ shipSetCorpus as unknown as CorpusManifest, futureDirectCandidatesCorpus as unknown as CorpusManifest, generatedInstancesCorpus as unknown as CorpusManifest, + promotedGoogleFontsCorpus as unknown as CorpusManifest, ]; } diff --git a/scripts/import-corpus.ts b/scripts/import-corpus.ts index 1f0ccac..cdde6b4 100644 --- a/scripts/import-corpus.ts +++ b/scripts/import-corpus.ts @@ -69,7 +69,9 @@ const slug = (s: string) => const STYLE_ORDER = ["regular", "bold", "italic", "boldItalic", "other"]; /** Source-agnostic pipeline: normalized faces -> validated CorpusManifest. */ -async function buildManifest(src: CorpusSource): Promise { +export async function buildManifest( + src: CorpusSource, +): Promise { const byFamily = new Map(); const seenSha = new Set(); @@ -175,7 +177,9 @@ async function main() { ); } -main().catch((e) => { - console.error(e instanceof Error ? e.message : e); - process.exit(1); -}); +if (import.meta.main) { + main().catch((e) => { + console.error(e instanceof Error ? e.message : e); + process.exit(1); + }); +} diff --git a/scripts/promote-candidates.ts b/scripts/promote-candidates.ts new file mode 100644 index 0000000..579463b --- /dev/null +++ b/scripts/promote-candidates.ts @@ -0,0 +1,128 @@ +#!/usr/bin/env bun +/** + * promote-candidates.ts - PROMOTE an allow-listed open font from the discovery snapshot into the + * REVIEWED corpus (a license-text-hashed CorpusManifest). This is the one path that turns "we know + * this font exists" (discovery) into "this font has exact, reviewed provenance" - the tier a verdict + * is allowed to rest on. + * + * It does NOT touch records.json. Naming a promoted font as a public substitute is a SEPARATE, + * editorial step; this only establishes provenance. Allow-list only: nothing is promoted implicitly. + * + * For each allow-listed family it: resolves the discovery face, reads the font bytes from the local + * cache (verifying them against the discovery fileSha256), and fetches + hashes the EXACT upstream + * license text (e.g. ofl/viga/OFL.txt). No binaries are committed - only the reviewed manifest (sha + + * license-text hash + public URLs). Reuses scripts/import-corpus.ts buildManifest for the assembly. + * + * Run: bun run scripts/promote-candidates.ts --cache + * (--cache may also come from DOCFONTS_FONT_CACHE; needs network to fetch the license texts) + */ +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import type { DiscoverySnapshot } from "@docfonts/registry"; +import type { CorpusSource, RawCorpusFace } from "./corpus-sources/types"; +import { buildManifest } from "./import-corpus"; + +// Allow-list: explicit families only. Static, single-source, license-fetchable. (Variable fonts get a +// deliberate instancing pass later - see gelasio-instances.) Add a family here to promote it. +const ALLOW_LIST = ["Viga"]; + +const CORPUS_ID = "promoted-google-fonts-2026-06-05"; +const RETRIEVED_DATE = "2026-06-05"; +const RAW_BASE = "https://raw.githubusercontent.com/google/fonts/main"; +const TREE_BASE = "https://github.com/google/fonts/tree/main"; +const LICENSE_URL: Record = { + "OFL-1.1": "https://openfontlicense.org/open-font-license-official-text/", + "Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0", + "Ubuntu Font License": "https://ubuntu.com/legal/font-licence", +}; + +const SNAPSHOT = join( + import.meta.dir, + "..", + "packages", + "registry", + "data", + "discovery", + "google-fonts-all-files-2026-06-04.json", +); +const OUT = join( + import.meta.dir, + "..", + "packages", + "registry", + "data", + "corpus", + `${CORPUS_ID}.json`, +); + +const arg = (name: string) => { + const i = process.argv.indexOf(`--${name}`); + return i >= 0 ? process.argv[i + 1] : undefined; +}; + +async function main() { + const cache = arg("cache") ?? process.env.DOCFONTS_FONT_CACHE; + if (!cache) { + throw new Error( + "usage: bun run scripts/promote-candidates.ts --cache (or set DOCFONTS_FONT_CACHE)", + ); + } + const snapshot = JSON.parse( + readFileSync(SNAPSHOT, "utf8"), + ) as DiscoverySnapshot; + + // Pre-fetch everything (bytes + license texts) so the sync CorpusSource the driver consumes is ready. + const raw: RawCorpusFace[] = []; + for (const family of ALLOW_LIST) { + const faces = snapshot.faces.filter((f) => f.family === family); + if (!faces.length) + throw new Error( + `[promote] ${family} not found in the discovery snapshot`, + ); + // One license text per family (the upstream OFL/LICENSE that sits in the family's source dir). + const dir = faces[0].repoPath.split("/").slice(0, -1).join("/"); // e.g. "ofl/viga" + const licenseName = + faces[0].license === "Apache-2.0" ? "LICENSE.txt" : "OFL.txt"; + const licRes = await fetch(`${RAW_BASE}/${dir}/${licenseName}`); + if (!licRes.ok) + throw new Error( + `[promote] license fetch failed for ${family}: ${licRes.status}`, + ); + const licenseTextBytes = new Uint8Array(await licRes.arrayBuffer()); + for (const face of faces) { + const bytes = new Uint8Array( + readFileSync(join(cache, face.repoPath.replaceAll("/", "__"))), + ); + raw.push({ + family, + fileName: face.fileName, + bytes, + expectedSha256: face.fileSha256, // driver verifies bytes against the discovery hash + license: face.license, + licenseTextBytes, + licenseUrl: LICENSE_URL[face.license] ?? "", + sourceUrl: `${TREE_BASE}/${dir}`, + }); + } + } + + const src: CorpusSource = { + corpusId: CORPUS_ID, + source: "Google Fonts (promoted from discovery, reviewed)", + sourceUrl: TREE_BASE, + retrievedDate: RETRIEVED_DATE, + licenseSource: "google/fonts + fetched upstream license text", + faces: () => raw, + }; + const manifest = await buildManifest(src); + writeFileSync(OUT, `${JSON.stringify(manifest, null, 2)}\n`); + const faceCount = manifest.families.reduce((n, f) => n + f.faces.length, 0); + console.log( + `[promote] wrote ${CORPUS_ID}: ${manifest.families.length} families, ${faceCount} faces (${ALLOW_LIST.join(", ")}).`, + ); +} + +main().catch((e) => { + console.error(e instanceof Error ? e.message : e); + process.exit(1); +});