faceless-photolib

Validator usage

fpd-validate — the standalone package validator, its finding kinds, and exit codes. From @faceless-photolib/package-format.

@faceless-photolib/package-format ships a standalone validator — a library function and a CLI wrapping it — that checks a package without the editor or a renderer: schema validity, digest integrity, reachability closure, the mandatory preview, and the reader-must-ignore/reader-must-reject field classes.

CLI

fpd-validate <package-directory | package.fpd>

fpd-validate is the package's declared bin (installing @faceless-photolib/package-format puts it on your PATH). Inside this repo, run it against the built package from the repo root:

pnpm --filter @faceless-photolib/package-format build
node packages/package-format/bin/fpd-validate.mjs <target>

Exit codes are the standard three-way CLI contract:

CodeMeaning
0Valid. Any warnings are printed first, then a valid: N reachable ref(s) checked, M warning(s) summary.
1Invalid. Every error and warning is printed, then an invalid: N error(s), M warning(s) summary.
2Usage or I/O error (wrong argument count, target doesn't exist, unreadable directory/zip).

The CLI accepts exactly one argument (a directory or a .fpd file); anything else is a usage error, never a best-effort guess at what you meant.

Library

import { validatePackage } from "@faceless-photolib/package-format";

const report = await validatePackage(files); // PackageFiles — see Package anatomy
// { kind: "valid", warnings, checkedRefs } | { kind: "invalid", errors, warnings }

validatePackage runs against the logical file set (PackageFiles, a ReadonlyMap<string, Uint8Array>) — directory and .fpd forms validate identically because they're loaded into the same shape first.

What gets checked, and every finding kind

The validator collects every finding in one pass rather than stopping at the first problem, so a hand-built package (see Package anatomy) gets one complete report to fix from instead of a fix-one-rerun loop.

FindingSeverityMeaning
missing-fileerrorA required layout path (manifest, index, a referenced document/history/blob) is absent.
malformed-jsonerrorA file that should be JSON doesn't parse.
manifest-invalid / index-invaliderrormanifest.json/index.json fails its Zod schema; issues names the offending field(s).
unsupported-container-versionerrormanifest.containerVersion isn't one this reader understands.
unsupported-required-capabilityerrorA capabilities.required flag this reader doesn't implement.
critical-fielderrorAn unknown field whose key begins with ! — reader-must-reject (see Package anatomy).
role-mismatcherrorA ref's declared role disagrees with where it's used.
integrity-errorerrorA blob's actual bytes don't hash to the digest its path/ref claims — content was modified without updating its address.
preview-staleerrorpreviews/flat.png's bytes don't match manifest.preview's digest.
preview-missingerrorThe mandatory flat preview is absent.
missing-contenterrorA reachable ref has no payload file in the package.
index-incompleteerrorA ref reachable from the manifest roots is absent from index.json — the index must be the full closure.
invalid-document / invalid-history-nodeerrorA documents/<hash>.json or history/<hash>.json file fails its schema.
closure-errorerrorThe reachability walk itself failed (e.g. a malformed embedded ref).
index-unreachable-entrywarningAn index.json entry the manifest roots can no longer reach — GC garbage, not corruption.
index-entry-mismatchwarningAn index entry's role/mediaType disagrees with what the closure walk found for that digest.
foreign-filewarningA file outside the layout grammar entirely — readers ignore it, but the validator flags it so you notice stray files.

Severity is derived from the finding's kind, never stored as a separate field a producer could get out of sync with the kind — index-unreachable-entry, index-entry-mismatch, and foreign-file are the only three warning kinds; every other finding is an error.

Directory ↔ .fpd equivalence

Because the validator operates on the logical PackageFiles shape, zipping a valid directory package into .fpd (entries STORED, per Package anatomy) and reopening it validates identically — every ref resolves to the same bytes and the manifest hash is unchanged. There is no "validate the directory form more strictly than the zip form" split.

Corpus samples

The .fpd fixtures below — the minimal valid package and the forward-compatible unknown-fields case — are served through the same /api/samples/<id> proxy every other demo on this site uses; no direct storage URL ever appears in this page.

Samples — format-validator
Run checks in Test Lab

Preparing sample list…