A palette names the block types a host makes available: a map from a
block's type_name to the module implementing StatifierBlocks.BlockType
for it (ADR-0002 decision 2).
It is a caller-supplied value, nothing more. A palette is built once for an editing or compiling operation and passed explicitly into whatever needs it - document validation, the editor's session state, the compiler - the same way any other value is threaded through a pipeline. Nothing in this package holds a palette across operations, and no cadence beyond "one value per operation" is implied: a caller that wants the same set of types for its next operation builds (or reuses) the value again, deliberately.
It is explicitly not:
- an application-configuration lookup keyed by block type
- a table of shared entries reachable by name from anywhere in the process tree
- a lookup registered under a well-known process name
- anything wired up automatically when this package (or a host application) starts
Any of those would make two hosts sharing one runtime step on each
other's block types - the multi-tenant property this design exists to
keep. Two StatifierBlocks.Palette values built with different modules
under the same type_name in the same running system resolve
independently; neither can see or clobber the other.
Every consumer that walks a document and resolves blocks against a
palette carries the case where a block's type_name has no entry as an
ordinary pattern-matched arm, not as an exception - fetch/2 never
raises, so there is nothing to rescue.
Summary
Functions
The core.* structural vocabulary as a palette (ADR-0002 decision 10).
The type_name => module map behind core/0, for a host merging the
core vocabulary with its own entries
Resolves a type_name to its module. Total; never raises (ADR-0002
decision 3). Map.fetch/2 rather than a sentinel default, so a palette
that genuinely maps a name to nil stays distinguishable from a name no
entry carries.
Builds a palette from a type_name => module map. Defaults to an empty
palette.
Resolves block through palette and, if needed, migrates its config in
memory (ADR-0002 decision 8).
Types
@type t() :: %StatifierBlocks.Palette{ assignability: module() | nil, types: %{optional(StatifierBlocks.Block.type_name()) => module()} }
Functions
@spec core() :: t()
The core.* structural vocabulary as a palette (ADR-0002 decision 10).
Seven entries, described in StatifierBlocks.Core. They are ordinary
palette entries with no privileged path anywhere in this package - a
palette without them is as valid as a palette with them, and a host that
wants only some of them builds a map with only those.
Palette.core()
#=> %StatifierBlocks.Palette{types: %{"core.sequence" => ..., ...}}
@spec core_types() :: %{optional(StatifierBlocks.Block.type_name()) => module()}
The type_name => module map behind core/0, for a host merging the
core vocabulary with its own entries:
Palette.new(Map.merge(Palette.core_types(), %{"myapp.authorize" => MyApp.Blocks.Authorize}))A host entry sharing a name with a core one wins, because that is what
Map.merge/2 does and a palette is just a value: nothing in this package
reserves the core. prefix, and a host deliberately swapping in its own
core.wait is doing something this design allows on purpose.
@spec fetch(t(), StatifierBlocks.Block.type_name()) :: {:ok, module()} | {:error, {:unknown_block_type, StatifierBlocks.Block.type_name()}}
Resolves a type_name to its module. Total; never raises (ADR-0002
decision 3). Map.fetch/2 rather than a sentinel default, so a palette
that genuinely maps a name to nil stays distinguishable from a name no
entry carries.
@spec new( %{optional(StatifierBlocks.Block.type_name()) => module()}, keyword() ) :: t()
Builds a palette from a type_name => module map. Defaults to an empty
palette.
Options: :assignability, a module implementing
StatifierBlocks.Assignability.Relation (ADR-0003 decision 6). Defaults
to nil, meaning the palette declares no widening relation - new(types)
and new(types, assignability: nil) are the same palette.
@spec resolve(t(), StatifierBlocks.Block.t()) :: {:ok, module(), StatifierBlocks.Block.t()} | {:error, {:unknown_block_type, StatifierBlocks.Block.type_name()}} | {:error, {:block_type_too_new, StatifierBlocks.Block.id(), pos_integer()}} | {:error, {:migration_failed, StatifierBlocks.Block.id(), term()}}
Resolves block through palette and, if needed, migrates its config in
memory (ADR-0002 decision 8).
Four distinguishable outcomes, checked in this order:
- the block's
typehas no entry inpalette->{:error, {:unknown_block_type, type}} block.type_version == module.current_version()->{:ok, module, block},blockreturned exactly as givenblock.type_version > module.current_version()->{:error, {:block_type_too_new, block.id, block.type_version}}. Hard error, never a best-effort read: the code is older than the data, and guessing is how a rollback corrupts documentsblock.type_version < module.current_version()->module.migrate_config/2is called once, straight from the stored version to current (never a version-by-version ladder). A successful migration rewrites onlyblock.configon the returned struct; a failing one, or a module that does not exportmigrate_config/2at all, becomes{:error, {:migration_failed, block.id, reason}}
The migrated config is applied to the returned struct only - resolve/2
never calls Document.to_json/1, from_json/1, or anything else that
could persist. Persisting a migration is the caller's decision. The
returned block's type_version is left as stored, never bumped to
current_version(), so the result can never be mistaken for a block that
was migrated on disk.
resolve/2 takes one block, not a document - it never walks a document;
the caller owns the walk and what to do with a per-block failure.