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.
| Field | Type | Meaning |
|---|---|---|
containerVersion | string | Container format version. This reader accepts "fpd-v0" only; an unrecognized version is a typed unsupported-container-version refusal, never a best-effort parse. |
capabilities.required | CapabilityFlag[] | Flags the package genuinely needs to render correctly. A required flag this reader doesn't implement refuses the whole package. |
capabilities.optional | CapabilityFlag[] | Flags a reader may ignore and still render something correct (a derived cache, say). |
heads | Record<HeadName, ContentRef> | Named branches (main, alt-1, …) → the history node (role history) each currently points at. |
rootDocument | ContentRef (role document) | The head document state a dumb loader needs to render. |
preview | ContentRef (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 set — entries 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.
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:
-
Pick your document JSON (a valid
DocV2state) and canonicalize + hash it per Ref grammar → you get aContentRefwithrole: "document"and its digest namesdocuments/<digest>.json. -
Render (or hand-produce) a flat PNG of that document, hash it the same way with
role: "preview"→ it namespreviews/flat.png. -
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": [] } } -
Write one minimal history node under
history/<digest>.jsonforheads.mainto point at (a root node hasparents: []— see History-DAG model). -
Write
index.jsonlisting everyContentRefabove. -
Place
documents/<digest>.json,history/<digest>.json,previews/flat.png,manifest.json, andindex.jsonat the pathspathForRefdictates, either as a directory or zipped (STORED) into.fpd. -
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.
Overview
A content-addressed, history-preserving, LLM-native successor to OpenRaster — and why this repo builds it.
Ref grammar + canonical hashing
The v1:b3:<hex> ref string grammar, the RFC 8785 (JCS) canonicalization profile, and the unknown-fields rule — from @faceless-photolib/canonical-json and @faceless-photolib/content-store.