Skip to content
Closed
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ Built by the team behind [SuperDoc](https://github.com/superdoc-dev/superdoc). S

- `packages/fallbacks` - runtime fallback decisions and lookup helpers.

## Source backlog

`packages/fallbacks/sources.json` tracks open-font projects we may measure next. It is public
provenance only, not a recommendation and not shipped in the npm package. The first entries cover TeX
Gyre, the maintained open extension of the URW++ Base 35 set: Termes, Heros, Bonum, Schola, Pagella,
Adventor, Cursor, and Chorus. A candidate only becomes fallback data after measurement and review.

## API

- `getRenderableFallback` - returns the open family to render, or `null` when none is renderable.
Expand Down
1 change: 1 addition & 0 deletions packages/fallbacks/pack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe("publish tarball hygiene", () => {
/data\/measurements/,
/data\/corpus/,
/records\.json/,
/sources\.json/,
/tsconfig/,
];
for (const f of files)
Expand Down
82 changes: 82 additions & 0 deletions packages/fallbacks/sources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
[
{
"sourceId": "tex-gyre-termes",
"family": "TeX Gyre Termes",
"project": "TeX Gyre",
"licenseFamily": "GUST-FL",
"upstream": "https://www.gust.org.pl/projects/e-foundry/tex-gyre/termes",
"retrieval": "OTF release from the GUST e-foundry page; extends the URW++ Base 35 core set. No account required.",
"targetFamilies": ["Times New Roman", "Times"],
"status": "backlog"
},
{
"sourceId": "tex-gyre-heros",
"family": "TeX Gyre Heros",
"project": "TeX Gyre",
"licenseFamily": "GUST-FL",
"upstream": "https://www.gust.org.pl/projects/e-foundry/tex-gyre/heros",
"retrieval": "OTF release from the GUST e-foundry page; extends the URW++ Base 35 core set. No account required.",
"targetFamilies": ["Arial", "Helvetica"],
"status": "backlog"
},
{
"sourceId": "tex-gyre-bonum",
"family": "TeX Gyre Bonum",
"project": "TeX Gyre",
"licenseFamily": "GUST-FL",
"upstream": "https://www.gust.org.pl/projects/e-foundry/tex-gyre/bonum",
"retrieval": "OTF release from the GUST e-foundry page; extends the URW++ Base 35 core set. No account required.",
"targetFamilies": ["Bookman Old Style", "ITC Bookman"],
"status": "backlog"
},
{
"sourceId": "tex-gyre-schola",
"family": "TeX Gyre Schola",
"project": "TeX Gyre",
"licenseFamily": "GUST-FL",
"upstream": "https://www.gust.org.pl/projects/e-foundry/tex-gyre/schola",
"retrieval": "OTF release from the GUST e-foundry page; extends the URW++ Base 35 core set. No account required.",
"targetFamilies": ["Century Schoolbook", "New Century Schoolbook"],
"status": "backlog"
},
{
"sourceId": "tex-gyre-pagella",
"family": "TeX Gyre Pagella",
"project": "TeX Gyre",
"licenseFamily": "GUST-FL",
"upstream": "https://www.gust.org.pl/projects/e-foundry/tex-gyre/pagella",
"retrieval": "OTF release from the GUST e-foundry page; extends the URW++ Base 35 core set. No account required.",
"targetFamilies": ["Palatino Linotype", "Book Antiqua"],
"status": "backlog"
},
{
"sourceId": "tex-gyre-adventor",
"family": "TeX Gyre Adventor",
"project": "TeX Gyre",
"licenseFamily": "GUST-FL",
"upstream": "https://www.gust.org.pl/projects/e-foundry/tex-gyre/adventor",
"retrieval": "OTF release from the GUST e-foundry page; extends the URW++ Base 35 core set. No account required.",
"targetFamilies": ["Century Gothic", "ITC Avant Garde Gothic"],
"status": "backlog"
},
{
"sourceId": "tex-gyre-cursor",
"family": "TeX Gyre Cursor",
"project": "TeX Gyre",
"licenseFamily": "GUST-FL",
"upstream": "https://www.gust.org.pl/projects/e-foundry/tex-gyre/cursor",
"retrieval": "OTF release from the GUST e-foundry page; extends the URW++ Base 35 core set. Monospaced. No account required.",
"targetFamilies": ["Courier New", "Courier"],
"status": "backlog"
},
{
"sourceId": "tex-gyre-chorus",
"family": "TeX Gyre Chorus",
"project": "TeX Gyre",
"licenseFamily": "GUST-FL",
"upstream": "https://www.gust.org.pl/projects/e-foundry/tex-gyre/chorus",
"retrieval": "OTF release from the GUST e-foundry page; extends the URW++ Base 35 core set. Chancery/script. No account required.",
"targetFamilies": ["Monotype Corsiva", "ITC Zapf Chancery"],
"status": "backlog"
}
]
152 changes: 152 additions & 0 deletions packages/fallbacks/sources.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Guards for the source backlog. It records open-font candidates, not fallback recommendations.
*/
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import {
LICENSE_FAMILIES,
loadSources,
SOURCES_PATH,
type SourceCandidate,
} from "./sources";

const joined = (...parts: string[]) => parts.join("");

const ALLOWED_KEYS = new Set<keyof SourceCandidate>([
"sourceId",
"family",
"project",
"licenseFamily",
"upstream",
"retrieval",
"targetFamilies",
"status",
]);

// Fields that mark a row as a measured verdict. The backlog must never carry them: a font earns these
// only after a bakeoff, by moving to records.json.
const VERDICT_FIELDS = [
"verdict",
"faceVerdicts",
"physicalFamily",
"policyAction",
"measurementRefs",
"gates",
"advance",
"exportRule",
"candidateLicense",
"faces",
"evidenceId",
"logicalFamily",
"lineBreakSafe",
];

const STATUSES = new Set(["backlog", "evaluating"]);
const sources = loadSources();

describe("source backlog schema", () => {
test("the backlog is a non-empty array", () => {
expect(Array.isArray(sources)).toBe(true);
expect(sources.length).toBeGreaterThan(0);
});

test("every entry has exactly the public-safe fields, well-formed", () => {
for (const s of sources) {
const keys = Object.keys(s);
for (const k of keys)
expect(
ALLOWED_KEYS.has(k as keyof SourceCandidate),
`${s.sourceId}: unexpected field "${k}"`,
).toBe(true);
for (const k of ALLOWED_KEYS)
expect(keys.includes(k), `${s.sourceId}: missing field "${k}"`).toBe(
true,
);

expect(
/^[a-z0-9]+(-[a-z0-9]+)*$/.test(s.sourceId),
`bad sourceId: ${s.sourceId}`,
).toBe(true);
expect(s.family.length, `${s.sourceId}: empty family`).toBeGreaterThan(0);
expect(s.project.length, `${s.sourceId}: empty project`).toBeGreaterThan(
0,
);
expect(
s.retrieval.length,
`${s.sourceId}: empty retrieval`,
).toBeGreaterThan(0);
expect(
LICENSE_FAMILIES.includes(s.licenseFamily),
`${s.sourceId}: license ${s.licenseFamily}`,
).toBe(true);
expect(
s.upstream.startsWith("https://"),
`${s.sourceId}: upstream not https`,
).toBe(true);
expect(STATUSES.has(s.status), `${s.sourceId}: status ${s.status}`).toBe(
true,
);

expect(
Array.isArray(s.targetFamilies),
`${s.sourceId}: targetFamilies`,
).toBe(true);
expect(
s.targetFamilies.length,
`${s.sourceId}: no targetFamilies`,
).toBeGreaterThan(0);
for (const c of s.targetFamilies)
expect(
typeof c === "string" && c.length > 0,
`${s.sourceId}: bad cluster`,
).toBe(true);
}
});

test("sourceIds are unique", () => {
const ids = sources.map((s) => s.sourceId);
expect(new Set(ids).size).toBe(ids.length);
});
});

describe("source backlog non-promotion", () => {
test("no entry carries a verdict-shaped field (a backlog is not a recommendation)", () => {
for (const s of sources)
for (const field of VERDICT_FIELDS)
expect(
field in (s as unknown as Record<string, unknown>),
`${s.sourceId}: backlog entry must not carry verdict field "${field}"`,
).toBe(false);
});

test("every entry is in a pre-verdict status", () => {
for (const s of sources) expect(STATUSES.has(s.status)).toBe(true);
});
});

describe("source backlog hygiene", () => {
const raw = readFileSync(SOURCES_PATH, "utf8");

test("the backlog text carries no private paths, binaries, or measurement internals", () => {
const FORBIDDEN = [
joined("/", "Users", "/"),
joined("/", "Applications", "/"),
joined("/", "System", "/"),
joined("C", ":", "\\"),
joined("D", ":", "\\"),
joined("D", "Fonts"),
joined("Microsoft ", "Word"),
"macOS",
joined("or", "acle"),
".ttf",
".otf",
".woff",
".pfb",
".pfa",
];
for (const needle of FORBIDDEN)
expect(raw.includes(needle), `backlog text contains "${needle}"`).toBe(
false,
);
});
});
51 changes: 51 additions & 0 deletions packages/fallbacks/sources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Public source backlog for open-font candidates awaiting measurement.
* These rows are not fallback decisions and are not published in @docfonts/fallbacks.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";

export const SOURCES_PATH = join(import.meta.dir, "sources.json");

/**
* Pre-verdict lifecycle of a candidate source. Both values are explicitly NOT a recommendation:
* `backlog` is recorded but untouched; `evaluating` is mid-bakeoff. A promoted candidate leaves this
* file entirely and becomes a measured row in `records.json`.
*/
export type SourceStatus = "backlog" | "evaluating";

/** License families we accept for an open-font candidate. SPDX ids, plus the GUST Font License (GFL). */
export const LICENSE_FAMILIES = [
"OFL-1.1",
"Apache-2.0",
"GUST-FL",
"AGPL-3.0",
] as const;
export type LicenseFamily = (typeof LICENSE_FAMILIES)[number];

/**
* One open-font family recorded as a substitute candidate awaiting measurement.
*/
export interface SourceCandidate {
/** stable backlog id, e.g. "tex-gyre-bonum". */
sourceId: string;
/** the open-font family name as released, e.g. "TeX Gyre Bonum". */
family: string;
/** the upstream project the family belongs to, e.g. "TeX Gyre". */
project: string;
/** license family of the candidate (see {@link LICENSE_FAMILIES}). */
licenseFamily: LicenseFamily;
/** public upstream URL the font is retrieved from (https). */
upstream: string;
/** public-safe note on how to obtain the font. No local paths, no binaries. */
retrieval: string;
/** document font families this candidate might be measured against. Targets, not verdicts. */
targetFamilies: string[];
/** pre-verdict lifecycle state; never a recommendation. */
status: SourceStatus;
}

/** Load the reviewed source backlog from sources.json. */
export function loadSources(): SourceCandidate[] {
return JSON.parse(readFileSync(SOURCES_PATH, "utf8")) as SourceCandidate[];
}
Loading