faceless-photolib

Package anatomy

The .fpd directory layout, manifest and index fields, and the mandatory flat-preview contract — from @faceless-photolib/package-format.

A package is one logical file tree, shipped in two byte-identical envelopes: a plain directory (local editing, git-friendly) and a single-file .fpd (a ZIP of the exact same tree, every entry STORED — level 0, no compression, byte-deterministic, no wall-clock timestamps). Both forms validate and open identically; there is no format-level difference between them, only a packaging difference.

The tree

manifest.json            container version, capability flags, named heads,
                          root document ref, preview ref, retention policy
index.json                every reachable ref (validation closure + GC roots)
documents/<hash>.json     canonical document states (immutable)
history/<hash>.json       history nodes (immutable, opaque-but-schema-valid)
blobs/b3/<hh>/<hash>      opaque payloads (hh = first two hex chars of <hash>)
previews/flat.png         mandatory flat preview (dumb-viewer contract)

<hash> is always a 64-character lowercase hex BLAKE3-256 digest (the same digest that appears in the ref string form, see Ref grammar); blobs/ shards on its first two characters so no directory holds more than 256 subdirectories worth of blobs. This is exactly the path grammar @faceless-photolib/package-format's pathForRef/classifyPath implement and the validator checks against — there is no second, informal layout.

manifest.json

The package's entry point. All refs here are full typed ContentRefs (see Ref grammar) — never a bare hash string.

FieldTypeMeaning
containerVersionstringContainer format version. This reader accepts "fpd-v0" only; an unrecognized version is a typed unsupported-container-version refusal, never a best-effort parse.
capabilities.requiredCapabilityFlag[]Flags the package genuinely needs to render correctly. A required flag this reader doesn't implement refuses the whole package.
capabilities.optionalCapabilityFlag[]Flags a reader may ignore and still render something correct (a derived cache, say).
headsRecord<HeadName, ContentRef>Named branches (main, alt-1, …) → the history node (role history) each currently points at.
rootDocumentContentRef (role document)The head document state a dumb loader needs to render.
previewContentRef (role preview)The mandatory flat raster (see below).
retention{ policy, pinned }policy is "keep-all" or "reachable-only"; pinned is a list of refs that survive GC regardless of reachability.

CapabilityFlag and HeadName are both branded, lowercase-kebab strings (/^[a-z0-9][a-z0-9-]*$/) — not bare string.

Four capability flags are reserved by the design but not implemented by this v0 reader: tiles, custom-container, c2pa, remote-packages. Encountering any of them — required or optional — fires a warnNotImplemented beacon; encountering one of them in required additionally refuses the package with a typed unsupported-required-capability result naming the flag. No package this reader opens ever silently drops a feature it doesn't understand.

index.json

{ "entries": [ /* every reachable ContentRef, one entry per digest */ ] }

The index is both the validation closure (the validator recomputes reachability from the manifest's roots and diffs it against index.json) and the GC root setentries plus the manifest's heads/rootDocument/ preview/retention.pinned are exactly what a mark-sweep GC treats as live.

Unknown fields: preserve-and-hash, and the !-prefix rule

Both manifest.json and index.json (and, by extension, every document and history-node envelope) are .passthrough() objects: a field this reader doesn't recognize is preserved verbatim and participates in hashing — it is never silently stripped, and there is no second "known-fields-only" hash a future writer's extra data could accidentally bypass.

The ignore-vs-reject split is a naming convention, not a schema split:

  • a key beginning with ! is a critical extension — a reader that doesn't understand it MUST reject the package (critical-field);
  • every other unknown key is reader-must-ignore — preserved, hashed, and any refs inside it stay reachable (so its blobs are never garbage collected out from under a future reader that does understand the field).

Mandatory flat preview

Every package carries previews/flat.png: a flattened rendering of the head document state, produced by the real reference pipeline (engine-api + backend-cpu, the same golden renderer documented in /docs/backend-cpu) — never a hand-rolled re-implementation of the pixel math. This is the format's "dumb viewer" contract: a tool that has never heard of manifests, refs, or the history DAG can extract exactly one file and get a correct image. The validator treats a missing preview (preview-missing) or one that no longer matches manifest.preview (preview-stale) as findings — see Validator usage.

Golden fixtures

@faceless-photolib/package-format ships seven golden fixture packages, byte-reproducible via a regen script (any drift fails the fixtures test): minimal single raster layer, a document referencing a missing blob, a package with preserved unknown fields, sibling branches sharing one mask blob, a private/redacted branch, a stale derived preview, and a migrated DocV1 document with the migration recorded as data. They double as the validator's own test corpus and as the "open a golden fixture" live demo content.

Try it: the minimal-raster golden fixture — the smallest valid package, exactly the tree shown above — is in the sample corpus. Opening it loads the real editor, which fetches the .fpd via the sample proxy and hands its bytes to the format worker (unpack → validate → load content, with streamed progress).

Open the sample package in the editor →
Download minimal-raster.fpd (2.7 KB) to inspect it — it passes fpd-validate as-is.

The editor requires WebGPU; a browser without an adapter gets the designed “GPU required” page instead.

Corpus samples: version migration

The migrated-DocV1 fixture mentioned above lives in the sample corpus tagged format-migration, 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-migration
Run checks in Test Lab

Preparing sample list…

Hand-building a minimal package

A minimal, valid package needs exactly: one document state, one preview PNG, a manifest pointing at both, and an index listing every ref the manifest reaches. Concretely, without touching any editor:

  1. Pick your document JSON (a valid DocV2 state) and canonicalize + hash it per Ref grammar → you get a ContentRef with role: "document" and its digest names documents/<digest>.json.

  2. Render (or hand-produce) a flat PNG of that document, hash it the same way with role: "preview" → it names previews/flat.png.

  3. Write manifest.json:

    {
      "containerVersion": "fpd-v0",
      "capabilities": { "required": [], "optional": [] },
      "heads": { "main": { "algo": "b3", "digest": "<history-node-digest>", "mediaType": "application/x-fpd-history+json", "role": "history", "bytes": 512 } },
      "rootDocument": { "algo": "b3", "digest": "<doc-digest>", "mediaType": "application/x-fpd-doc+json", "role": "document", "bytes": 2048 },
      "preview": { "algo": "b3", "digest": "<preview-digest>", "mediaType": "image/png", "role": "preview", "bytes": 40960 },
      "retention": { "policy": "keep-all", "pinned": [] }
    }
  4. Write one minimal history node under history/<digest>.json for heads.main to point at (a root node has parents: [] — see History-DAG model).

  5. Write index.json listing every ContentRef above.

  6. Place documents/<digest>.json, history/<digest>.json, previews/flat.png, manifest.json, and index.json at the paths pathForRef dictates, either as a directory or zipped (STORED) into .fpd.

  7. Run it through the validator (fpd-validate your-package.fpd, see Validator usage) — a hand-built package that passes is definitionally correct, since the validator and the writer share the same schemas.