A block document: one tree, one envelope (ADR-0001).
This module is the package's single public entry point over the tree: construction, the shared pre-order walk, and path lookup live here. Canonical encoding, content identity, and structural decoding land in later phases of the same bead.
Summary
Types
"bdoc_" <> uxid.
Derived, never stored. Identifies a position, not a block.
ADR-0001's typespec block: everything validate/1 and from_json/1 can
reject, defined once.
Functions
Every block in document, pre-order, root first.
SHA-256 over to_json/1. Stable document identity.
The path from the root to the block carrying id.
Structural decode. Never consults the block-type registry (ADR-0001
decision 9); unknown type names decode successfully.
Wraps root in a document envelope.
Canonical JSON per ADR-0001 decision 8. Deterministic: sorted object keys,
no insignificant whitespace, empty slots/config/metadata omitted, no
floats.
Checks document against ADR-0001's structural rules: schema version,
envelope shape, per-block shape (id, type, type_version, config, slots),
and document-wide id uniqueness. Never consults a block-type registry -
config is opaque here and type is never resolved against anything.
Types
@type id() :: String.t()
"bdoc_" <> uxid.
@type path() :: [ {StatifierBlocks.Block.id(), StatifierBlocks.Block.slot_name(), non_neg_integer()} ]
Derived, never stored. Identifies a position, not a block.
@type t() :: %StatifierBlocks.Document{ id: id(), metadata: %{optional(String.t()) => StatifierBlocks.Block.json()}, revision: non_neg_integer(), root: StatifierBlocks.Block.t(), schema_version: pos_integer() }
@type validation_error() :: :not_a_block_document | {:unsupported_schema_version, pos_integer()} | {:duplicate_block_id, StatifierBlocks.Block.id()} | {:malformed_block, StatifierBlocks.Block.id() | nil, term()} | {:malformed_envelope, term()}
ADR-0001's typespec block: everything validate/1 and from_json/1 can
reject, defined once.
Functions
@spec blocks(t()) :: [StatifierBlocks.Block.t()]
Every block in document, pre-order, root first.
Within a block, slots are visited in UTF-8-sorted slot-name order so
every consumer of this walk sees one deterministic order regardless of
how the slots map happened to be built. Later phases (encoding,
validation) build on this walk rather than re-deriving their own.
SHA-256 over to_json/1. Stable document identity.
@spec fetch_path(t(), StatifierBlocks.Block.id()) :: {:ok, path()} | :error
The path from the root to the block carrying id.
A path names the {parent block id, slot name, index} steps taken from
the root, so the root's own path is {:ok, []} - it has taken none.
Returns :error when no block in document carries id.
@spec from_json(binary()) :: {:ok, t()} | {:error, validation_error()}
Structural decode. Never consults the block-type registry (ADR-0001
decision 9); unknown type names decode successfully.
Decoding is total and ordered: bytes that are not a JSON object carrying
a "schema_version" key are :not_a_block_document; a recognizable
document that is wrong in a specific way gets an envelope-, block-, or
id-level arm instead. Nothing is rescued to a default and nothing raises.
@spec new( StatifierBlocks.Block.t(), keyword() ) :: t()
Wraps root in a document envelope.
Options: :id (default a freshly minted StatifierBlocks.Id.document/0),
:revision (default 0), :metadata (default %{}). :schema_version
is not an option - decision 7 fixes it at 1 for this ADR's envelope.
Canonical JSON per ADR-0001 decision 8. Deterministic: sorted object keys,
no insignificant whitespace, empty slots/config/metadata omitted, no
floats.
Runs validate/1 first and raises ArgumentError carrying the validation
reason when it fails, so an invalid document can never produce bytes that
claim to be canonical.
@spec validate(t()) :: :ok | {:error, validation_error()}
Checks document against ADR-0001's structural rules: schema version,
envelope shape, per-block shape (id, type, type_version, config, slots),
and document-wide id uniqueness. Never consults a block-type registry -
config is opaque here and type is never resolved against anything.