diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1634cc..4f27535 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,9 @@ name: release-fallbacks on: + push: + branches: + - main workflow_dispatch: inputs: dry_run: @@ -8,6 +11,10 @@ on: type: boolean default: true +concurrency: + group: release-fallbacks + cancel-in-progress: false + permissions: contents: write @@ -34,4 +41,4 @@ jobs: --package semantic-release@24 --package @semantic-release/github@12.0.8 --package semantic-release-ai-notes@0.3.0 - semantic-release ${{ inputs.dry_run && '--dry-run' || '' }} + semantic-release ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && '--dry-run' || '' }} diff --git a/README.md b/README.md index 7792dc2..b14f027 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,24 @@ > Document font substitution, measured. -This public repository currently publishes `@docfonts/fallbacks`: a small runtime package that maps -common proprietary document fonts to measured open-font fallbacks. +docfonts publishes `@docfonts/fallbacks`, a small runtime package for document renderers. +It maps common proprietary document fonts to reviewed open-font fallback decisions. -The package ships no font binaries and no proprietary data. It contains a reviewed evidence snapshot, -runtime lookup helpers, and tests that prove the npm tarball excludes research data, local paths, and -oracle-environment labels. +The package ships no font binaries and no proprietary data. It contains a public evidence snapshot, +asset-aware lookup helpers, and tests that prove the npm package only includes supported runtime files. Built by the team behind [SuperDoc](https://github.com/superdoc-dev/superdoc). Standalone and neutral. ## Package -- `packages/fallbacks` - runtime fallback evidence and lookup helpers. +- `packages/fallbacks` - runtime fallback decisions and lookup helpers. -Everything else used to build, measure, or review the evidence stays outside the tracked public tree -until it is ready to be documented and supported as public source. +## API + +- `getRenderableFallback` - returns the open family to render, or `null` when none is renderable. +- `getFallbackDecision` - explains the outcome for UI, diagnostics, and reporting. +- `createFallbackMap` - builds a resolver map from only the font families you can render. +- `normalizeFamilyName` - normalizes map lookup keys. ## Install @@ -39,5 +42,6 @@ bun run build ## Release -`@docfonts/fallbacks` is released from the manual `release-fallbacks` workflow. -Versioning follows Conventional Commits through semantic-release. +`@docfonts/fallbacks` is released by the `release-fallbacks` workflow on every push to `main`. +semantic-release publishes a new version when the merged commits contain a releasable Conventional Commit. +The same workflow can still run manually as a dry run. diff --git a/packages/fallbacks/README.md b/packages/fallbacks/README.md index 059fe62..6c42992 100644 --- a/packages/fallbacks/README.md +++ b/packages/fallbacks/README.md @@ -2,9 +2,9 @@ Document font substitution, measured. -Measured open-font fallbacks for proprietary document fonts (Office / Word / DOCX), as a tiny runtime data package. Map a requested proprietary font to an open one without hand-copying tables, and without routing to a font you do not bundle. +Measured open-font fallbacks for proprietary document fonts. Use it to decide whether a requested document font can render with an open family you actually ship. -It ships no fonts and no proprietary binaries: only the measured evidence - which open family stands in, the verdict, a stable evidence id - including the honest "no open font stands in." +It ships no fonts and no proprietary binaries. It ships decisions: the recommended open family when one exists, the fidelity verdict, and the honest cases where no open family should be used. ## Install @@ -12,47 +12,82 @@ It ships no fonts and no proprietary binaries: only the measured evidence - whic npm install @docfonts/fallbacks ``` -ESM-only (`"type": "module"`); import it, or let your bundler. CommonJS `require()` is not supported. +ESM-only. Use `import`, or let your bundler handle it. CommonJS `require()` is not supported. -## Three things you might want +## Render A Font + +Use `getRenderableFallback` when you need one font family to render now. Pass `canRenderFamily` so docfonts only returns families your app can load. ```ts -// 1. I need a family to render right now (only ever a font you bundle, or null): import { getRenderableFallback } from "@docfonts/fallbacks"; -getRenderableFallback("Helvetica", { canRenderFamily: (f) => bundled.has(f) }); + +const fallback = getRenderableFallback("Helvetica", { + canRenderFamily: (family) => bundledFamilies.has(family), +}); + // { substituteFamily: "Liberation Sans", policyAction: "substitute", verdict: "metric_safe", lineBreakSafe: true, evidenceId: "helvetica" } ``` +The result is `null` when there is nothing renderable from your available assets. Use `getFallbackDecision` when you need to know why. + +## Explain A Decision + +Use `getFallbackDecision` for UI, diagnostics, and reporting. It distinguishes known fonts with no recommended fallback from fonts docfonts has never seen. + ```ts -// 2. I need to explain the decision (UI, diagnostics, reporting) - a discriminated union: import { getFallbackDecision } from "@docfonts/fallbacks"; -getFallbackDecision("Aptos"); // { kind: "customer_supplied", evidenceId: "aptos" } -getFallbackDecision("Tahoma"); // { kind: "no_recommended_fallback", evidenceId: "tahoma" } -getFallbackDecision("Made Up Font"); // { kind: "unknown" } -getFallbackDecision("Georgia", { canRenderFamily: (f) => bundled.has(f) }); -// not bundled -> { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", evidenceId: "georgia" } + +getFallbackDecision("Aptos"); +// { kind: "customer_supplied", evidenceId: "aptos" } + +getFallbackDecision("Tahoma"); +// { kind: "no_recommended_fallback", evidenceId: "tahoma" } + +getFallbackDecision("Made Up Font"); +// { kind: "unknown" } + +getFallbackDecision("Georgia", { + canRenderFamily: (family) => bundledFamilies.has(family), +}); +// { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", evidenceId: "georgia" } ``` -This is the point of the package: a measured "no open font stands in" (Aptos) is a real answer, and it is **not** the same as a font docfonts never heard of (`unknown`). The `kind`s are `fallback`, `asset_missing`, `no_recommended_fallback`, `customer_supplied`, `preserve_only`, `unknown`. +Decision kinds: + +- `fallback` - render the returned `substituteFamily`. +- `asset_missing` - docfonts has a fallback, but your app does not load that family. +- `no_recommended_fallback` - docfonts knows the font but recommends no renderable open family. +- `customer_supplied` - the real font should come from the customer or environment. +- `preserve_only` - keep the original family name. Do not substitute. +- `unknown` - docfonts has no evidence for this family. + +## Create A Resolver Map + +Use `createFallbackMap` when wiring a resolver. `canRenderFamily` is required because a resolver map must never point at fonts you cannot load. ```ts -// 3. I need a resolver map. `canRenderFamily` is required - the map only holds fonts you can render: import { createFallbackMap, normalizeFamilyName } from "@docfonts/fallbacks"; -const map = createFallbackMap({ canRenderFamily: (f) => bundled.has(f) }); + +const map = createFallbackMap({ + canRenderFamily: (family) => bundledFamilies.has(family), +}); + map[normalizeFamilyName("Times New Roman")]; // { substituteFamily: "Liberation Serif", ... } ``` -Keys are normalized (lowercased, quote-stripped); look up with `normalizeFamilyName`. Rows whose family you do not bundle are left out, never routed to a missing asset. +Keys are normalized. Use `normalizeFamilyName` for lookups. Rows whose substitute family is not available are omitted. ## What the fields mean - `substituteFamily` - the open family to render in place of the requested one. -- `policyAction` - what a renderer should do, not a quality claim: `substitute` renders the named open family, `category_fallback` renders a lower-fidelity same-category family. A `substitute` can still be top-level `visual_only` (e.g. Cambria); use `verdict` for fidelity. -- `verdict` - the measured fidelity, from a fixed taxonomy (`metric_safe`, `near_metric`, `cell_width_only`, `visual_only`, ...). The headline rolls up to the worst face. -- `lineBreakSafe` - true when advances preserve line breaks: `metric_safe`, `near_metric`, or monospace `cell_width_only`. Not a claim of an exact clone (`cell_width_only` keeps advances but not glyph shapes); read `verdict` for the tier. +- `policyAction` - what a renderer should do, not a quality claim. Use `verdict` for fidelity. +- `verdict` - the measured fidelity. Examples: `metric_safe`, `near_metric`, `cell_width_only`, `visual_only`. +- `lineBreakSafe` - true when advances preserve line breaks: `metric_safe`, `near_metric`, or monospace `cell_width_only`. - `evidenceId` - the stable id for the reviewed evidence row; look the full row up in `SUBSTITUTION_EVIDENCE`. -The full structured rows are exported as `SUBSTITUTION_EVIDENCE` (faces, per-face verdicts, glyph exceptions) for richer reporting. Face-level routing stays yours: these answer "which family", not "which face". +`cell_width_only` keeps monospace advances stable, but glyph shapes can still differ. A `substitute` can still have a lower-fidelity `verdict` when one face or glyph is qualified. The verdict is the fidelity signal. + +The full structured rows are exported as `SUBSTITUTION_EVIDENCE` for richer reporting, including faces, per-face verdicts, and glyph exceptions. Face-level routing stays yours: these helpers answer "which family", not "which face". ## Provenance diff --git a/packages/fallbacks/pack.test.ts b/packages/fallbacks/pack.test.ts index bc8b562..3f521e0 100644 --- a/packages/fallbacks/pack.test.ts +++ b/packages/fallbacks/pack.test.ts @@ -1,6 +1,6 @@ /** - * Publish-safety: prove the npm tarball ships only the built runtime artifact and no private evidence - * labels, local paths, tests, or source files. + * Publish-safety: prove the npm tarball ships only the built runtime artifact, not local paths, + * measurement environment details, tests, or source files. */ import { describe, expect, test } from "bun:test"; import { execSync } from "node:child_process"; @@ -59,7 +59,7 @@ describe("publish tarball hygiene", () => { expect(pat.test(f), `forbidden file packed: ${f}`).toBe(false); }); - test("the shipped bytes carry no local path or oracle-environment leak", () => { + test("the shipped bytes carry no local paths or measurement environment details", () => { const distDir = join(PKG_DIR, "dist"); expect(existsSync(distDir)).toBe(true); const bytes = readdirSync(distDir) diff --git a/packages/fallbacks/src/types.ts b/packages/fallbacks/src/types.ts index 0faff80..7d25d54 100644 --- a/packages/fallbacks/src/types.ts +++ b/packages/fallbacks/src/types.ts @@ -87,8 +87,8 @@ export interface SubstitutionEvidence { } /** - * A resolved fallback: which open family to render, how it was chosen, and how faithful it is. The full - * structured row stays available via {@link SUBSTITUTION_EVIDENCE} for richer reporting. + * A resolved fallback: which open family to render, how it was chosen, and whether advances preserve + * line breaks. The full structured row stays available via {@link SUBSTITUTION_EVIDENCE} for reporting. */ export interface FontFallback { /** the open family to render in place of the requested font. */