Status: implemented reference Date: 2026-06-28
Related documents:
Boundary
Docket.Graph is the canonical editable graph document.
Docket.Runtime.Graph is the internal executable materialization. The compiler
is the only supported path between them.
The compiler owns:
- canonicalization and deterministic graph identity
- publish/run validation and graph-attached diagnostics
- node configuration-schema materialization
- public-to-runtime lowering
- lowered graph invariant checks
Graph editing, durable storage, node execution, run transitions, authorization, and UI projection remain outside the compiler.
Public API
Docket.Graph.verify(graph, opts \\ [])
Docket.Graph.Compiler.verify(graph, opts \\ [])
Docket.Graph.Compiler.compile(graph, opts \\ [])Return shapes:
verify(graph, opts)
:: {:ok, Docket.Graph.t()}
| {:error, Docket.Graph.t()}
compile(graph, opts)
:: {:ok, Docket.Runtime.Graph.t()}
| {:error, Docket.Graph.t()}verify/2 and compile/2 run the same pipeline, including lowering and
lowered-result validation when public validation has no blocking diagnostic.
verify/2 returns the authored graph with a fresh diagnostic list. compile/2
returns the runtime graph on success.
Supported options:
:profileaccepts:publishor:run; both use the same rules in v0.1.:max_superstepssupplies the runtime cycle bound when the graph does not declare a"max_supersteps"policy.
Representable graph invalidity returns diagnostics. Wrong argument types and invalid compiler options raise because they are caller errors outside the graph data model.
Canonical Identity And Publication
Compiler ingest removes transient diagnostics, normalizes the graph through the private durable codec, and validates the exact versioned deterministic ETF bytes. The SHA-256 digest of the effective bytes is the graph hash.
Publication snapshots each node module's config_schema/0 once, materializes
configuration defaults into an effective graph document, hashes that document,
and stores it through the configured backend. Execution fetches the exact
effective graph pinned by Docket.GraphRef and compiles it without reapplying
publication defaults.
This separation makes hashes independent of transient diagnostics and prevents changes in module-provided defaults from changing an already-published graph.
Pipeline
The implemented pipeline is:
- Canonicalize the authored or effective graph and validate durable encoding.
- Fetch node configuration schemas once for the compile.
- Materialize publication defaults when compiling an authored graph for publication.
- Validate graph records, references, topology, node contracts, policies, guards, schemas, reducers, outputs, and cycles.
- Lower the effective graph to
Docket.Runtime.Graph. - Validate runtime IDs, subscriptions, generated channels, output projections, and lowering maps.
The implementation is split across:
Docket.Graph.Compiler.CanonicalDocket.Graph.Compiler.NodeContractsDocket.Graph.Compiler.ValidationDocket.Graph.Compiler.LoweringDocket.Graph.Compiler.RuntimeValidationDocket.Graph.Compiler.Diagnostics
These modules are internal. Docket.Graph.Compiler is the public compiler
facade.
Validation Contract
Graph And IDs
schema_versionmust be supported.- Graph, field, output, node, and edge IDs use the public binary ID syntax.
"$start"and"$finish"are reserved edge endpoints.- Inputs and state fields share a namespace; outputs may mirror a source ID.
- Graph semantic content must be encodable by the durable codec.
Editing helpers reject malformed operations early, but compile validates the whole loaded document because graphs may come from durable storage, imports, or manual construction.
Fields, Outputs, And Reducers
- Inputs and state fields require valid
Docket.Schemavalues. - A state field without a reducer uses
Docket.Reducer.last_value(). - Supported reducer types are
:last_value,:first_value,:append,:merge,:sum, and:union. - Reducer/schema pairings and reducer options are validated.
- Defaults must satisfy the field schema. Accumulating reducers receive their natural zero when no explicit default is present.
- Outputs project a declared input or state field and inherit its schema unless a compatible schema is supplied.
Concrete run input and node writes are runtime data, so their values are validated by the runtime rather than by graph compilation.
Nodes
- Every runnable node has a module implementation using
call/3. - The module must export
config_schema/0andcall/3. config_schema/0must return a validDocket.Schema.- Configuration is validated against that schema.
- Node policies validate the implemented
"timeout_ms"and"retry"shapes."on_error"remains reserved and is rejected.
Exceptions, exits, and malformed returns from config_schema/0 become
node-scoped diagnostics rather than compiler crashes.
Edges, Guards, And Topology
- An edge source is
"$start", a node ID, or a non-empty unique list of node IDs. Its target is a node ID or"$finish". - Multi-source edges use barrier semantics.
- Branch groups are node-local metadata over outgoing edge IDs.
- Guards are durable
Docket.Guardexpressions with resolvable field references. - Runnable nodes must be reachable from
"$start". - Quiescence is a valid terminal condition; an explicit
"$finish"edge is not required. - Cycles are allowed when an effective max-supersteps bound exists. Unguarded cycles produce a warning.
Lowering Contract
Runtime IDs are deterministic and namespaced:
node:<node_id>
input:<input_id>
state:<field_id>
edge:<edge_id>
output:<output_id>Inputs and fields lower to last-value runtime channels. The field's reducer controls how committed writes update its value. Each edge lowers to an activation channel; list-form sources use a barrier channel. Runtime nodes carry subscriptions, outgoing public edge IDs, normalized config, and policies.
Docket.Runtime.Graph.Lowering records both directions of the public/runtime ID
mapping plus generated activation-channel and branch metadata. Runtime events,
debug views, and tests use this mapping to refer back to public graph intent.
Runtime edge descriptors remain plain internal maps. They contain the public edge ID, generated channel ID, normalized source list, target, guard, and barrier flag.
Diagnostics And Runtime Errors
Each compiler call replaces stale diagnostics. A successful verification may contain warnings; a failed verification contains at least one error.
Diagnostics use public graph paths and IDs whenever possible. Runtime IDs appear only for lowering invariant failures. Internal exceptions are not exposed as diagnostic messages.
The durable facade normalizes a compiler failure to
%Docket.Error{type: :invalid_graph} and stores the diagnostic list in
error.details.diagnostics. Backend execution vehicles treat an incompatible
stored graph as an abandon/poison condition according to their configured
budget; they do not bypass compiler validation.
Compiler/Runtime Split
Compile-time validation protects static graph shape and implementation contracts. Runtime validation protects input values, node results, reducer application, guard evaluation, retry/timeout behavior, interrupts, and the max-supersteps limit.
The split keeps compilation deterministic and side-effect free apart from the single configuration-schema read per node module. Node execution, storage, network access, and external services are never part of compilation.
Compiled graphs are derived values rather than durable public documents. The PostgreSQL vehicle cache may reuse a compiled graph under its local generation, but cache entries are invalidated independently of the graph's durable content identity.
Verification
Compiler tests cover diagnostics, policies, lowering, generated IDs, lowering
metadata, durable canonicalization, and determinism under
test/docket/graph/compiler/. Compile-and-run behavior is exercised through
the processless Docket.Test runtime. Compiler tests require no backend,
database, network service, or supervisor.