The generic field-registry decode/validate engine: ONE
table-driven walk, parameterized by the registry table an artifact layer
supplies — Blueprint today, Deployment next. The engine knows tables,
not domains: every domain judgment (bounded schemas, signature envelopes,
predicates, extension forms, cross-field rules) arrives as data — checker
functions carried in the table and defined in the owning artifact module —
so the dependency direction is always artifact → engine.
A table is a list of field specs (maps):
%{
name: "member_name", # binary()
required: true, # absence → :missing_required_field
kind: kind(), # structural check below
check: fn value -> :ok | {:error, reason()} end, # optional, per-field
root_hook: fn members -> :ok | {:error, reason()} end, # optional, cross-field
min_items: 1, # array cardinality floor (kind arrays)
max_items: 64, # array cardinality ceiling
unique_by: "id" # array element uniqueness member name
}Kinds: :string :integer :float :boolean :number (either number
tag) {:enum, MapSet.t()} {:array, element_spec} {:object, %{members: [spec()]}} (closed world recursively) :any (any well-formed
tagged value) :custom (the spec's check is the whole judgment). :integer is TAG-STRICT: a
{:float, f} — including a zero-fraction window float — denies
:invalid_type. This is the artifact layer's typing; the wire cannot carry
the tag, so only this layer can see it (the artifact-typing relocation).
Failure precedence is pinned so two implementations pick the same
reason. Per object: member-list well-formedness (pairs, duplicates —
hand-built values only; the decoder already denies these) → :unknown_member
(document order) → :missing_required_field (table order) → :invalid_type
(table order) → :invalid_constraint (table order: enum membership and
check results that are not pass-through reasons of earlier stages) →
:invalid_cardinality (table order: counts and uniqueness over raw keys) →
nested recursion (children in document order; child reasons propagate) →
root_hooks (table order). Array elements are recursed after cardinality,
and uniqueness runs on raw member values so it never depends on element
validity.
Total and never-raising: malformed tagged shapes deny :invalid_type.
The engine validates structure — validation never authorizes an operation.
Summary
Functions
Validate value against table under the pinned precedence. The root
must be an object; every stage is fail-closed and value-free.
Types
@type check() :: (AgentBlueprintProtocol.Json.value() -> :ok | {:error, reason()})
@type reason() :: :unknown_member | :missing_required_field | :invalid_type | :invalid_constraint | :invalid_cardinality | term()
@type root_hook() :: (%{optional(binary()) => AgentBlueprintProtocol.Json.value()} -> :ok | {:error, reason()})
@type spec() :: %{ optional(:name) => binary(), optional(:required) => boolean(), optional(:kind) => kind(), optional(:check) => check(), optional(:root_hook) => root_hook(), optional(:min_items) => non_neg_integer(), optional(:max_items) => pos_integer(), optional(:unique_by) => binary() }
Functions
@spec validate([spec()], AgentBlueprintProtocol.Json.value()) :: :ok | {:error, reason()}
Validate value against table under the pinned precedence. The root
must be an object; every stage is fail-closed and value-free.