Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion packages/registry/corpus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}$/;
Expand Down Expand Up @@ -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)", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
]
}
]
}
9 changes: 6 additions & 3 deletions packages/registry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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,
];
}

Expand Down
14 changes: 9 additions & 5 deletions scripts/import-corpus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CorpusManifest> {
export async function buildManifest(
src: CorpusSource,
): Promise<CorpusManifest> {
const byFamily = new Map<string, CorpusFamily>();
const seenSha = new Set<string>();

Expand Down Expand Up @@ -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);
});
}
128 changes: 128 additions & 0 deletions scripts/promote-candidates.ts
Original file line number Diff line number Diff line change
@@ -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 <local-open-font-cache-dir>
* (--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<string, string> = {
"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 <font-cache-dir> (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);
});
Loading