Skip to main content

Plan Compilation

Source of truth: src/qtg/compiler/ This document describes the compilation pipeline that converts a movement plan template into an executable request. It covers graph validation, topological ordering, binding resolution, snapshot creation, and deterministic hashing.

Table of Contents

  1. Overview: what is Plan Compilation
  2. Compilation pipeline
  3. Graph validation (validate.py)
  4. Binding resolution (resolve.py)
  5. Compiled Plan Snapshot (snapshot.py)
  6. Deterministic Hash (hash.py)
  7. Frontier node computation
  8. Valid graphs vs invalid graphs
  9. Completion Policy validation
  10. v3.0 executability decision

Overview: what is Plan Compilation

Plan Compilation is the process of combining the abstract node/edge graph defined by a template with the input_params provided by the user, producing an immutable snapshot in which all information required for approval and execution has been finalized. A compiled plan guarantees the following:
  • Graph integrity: DAG (directed acyclic graph) property, no duplicate nodes
  • Bindings finalized: the executor/signer to use for each node is decided
  • Hash fixed: the plan at approval time and the plan at execution time can be verified as identical
  • Topological order: the execution order of nodes is deterministic

Compilation pipeline

Public API

Source: src/qtg/compiler/__init__.py

Step-by-step call order


Graph validation (validate.py)

Source: src/qtg/compiler/validate.py

validate_template_graph()

Input:
  • nodes — the list of nodes defined by the template. Each node must contain at least node_key, node_kind, action_type, executor_selector, and has_side_effect.
  • edges — connections between nodes. Each edge contains from, to, and edge_type, optionally with condition_expr.
  • completion_policy — completion policy (optional). The completion_assurance field is required.
Returns: a GraphValidationResult dataclass.

Validation rules

Graph validation is performed in the following order.

1. Duplicate node_key check

If two or more nodes share the same node_key, validation fails immediately.

1-A. Dots prohibited in node_key

Because $ref:node_key.field syntax splits node and field on the first ., a node_key cannot contain ..

2. Minimum node count check

An empty graph is not allowed.

3. Edge reference integrity

Validation fails if an edge’s from/to references a non-existent node.

4. Self-loop prohibition

A node cannot have an edge to itself.

5. DAG validation (cycle detection)

Topological ordering is performed using Kahn’s algorithm. If the resulting ordering contains fewer nodes than the total node count, a cycle exists.
Deterministic ordering guarantee: nodes with the same indegree are ordered by sorted(), and outbound edges are also ordered by (to, edge_type). This ensures the same topological order is produced for the same input.

6. Config $ref validation

Immediately after topological order generation, each node’s top-level config string values are scanned for $ref:node_key.field.
  • A malformed expression raises GraphValidationError.
  • A reference to a node missing from the template raises GraphValidationError.
  • A reference to a node that is not a DAG ancestor of the current node raises GraphValidationError.
At runtime a synthetic namespaced ref map is built from every completed node’s provider_refs, but those keys are not included in the provider_context that the executor sees.

7. Completion Policy validation

When completion_policy is provided, additional validation is performed (see the Completion Policy validation section below).

Graph Shape classification

After validation passes, the graph shape is classified:

Topological ordering

The topological_order produced by Kahn’s algorithm provides a deterministic order for node execution:
  • Predecessor nodes come first
  • Nodes at the same level are ordered alphabetically by node_key
  • Edges are also ordered by (to, edge_type)
This order is stored in GraphValidationResult.topological_order as a list[str] (list of node keys).

Binding resolution (application.services.binding)

Source: src/qtg/application/services/binding.py

Current behavior

At movement creation time, executor_selector / signer_selector are resolved to concrete bindings against the DB registry. Executor selector contract:
  • action_type required
  • provider optional
  • chain_family optional
Signer selector contract:
  • chain_family required
  • profile optional
Resolution rules:
  • active registry rows are candidates
  • every non-null selector field must match
  • 0 matches -> BindingResolutionError
  • 2 or more matches -> AmbiguousBindingError
Output: node_key -> {"executor": concrete binding, "signer": concrete binding | None} Example:

Compiled Plan Snapshot (snapshot.py)

Source: src/qtg/compiler/snapshot.py

Snapshot structure

Full schema of the returned dict:

Field descriptions

Immutability guarantee

After creation, this snapshot is not modified. It is persisted to the DB at MovementRequest creation and is referenced throughout approval and execution. The compiled_plan_hash can be used to verify whether it has been tampered with.

Deterministic Hash (hash.py)

Source: src/qtg/compiler/hash.py
Two hash functions exist; they serve different purposes.

compute_plan_graph_hash()

Purpose: a template-level graph structure hash. Identical template structures produce identical hashes.
Fields included in the hash: Characteristics: input_params and resolved_bindings are not included. The same template structure produces the same graph hash regardless of input values.

compute_compiled_plan_hash()

Purpose: full hash of a compiled plan for a specific request. Core mechanism for approval integrity verification.
Fields included in the hash: Differences vs compute_plan_graph_hash:
  • input_params and resolved_bindings are included — request-specific
  • Edge condition_expr is excluded — runtime conditions are already finalized in the compiled plan
  • approval_policy is excluded — the approval hash is managed externally

Hash computation method

Both functions follow the same pattern:
Determinism guarantee mechanism:
  1. Node ordering: alphabetical by node_key
  2. Edge ordering: composite key (from, to, edge_type)
  3. JSON normalization: sort_keys=True — dict keys ordered
  4. Fixed separators: separators=(",", ":") — compact JSON with no whitespace
  5. Unicode preserved: ensure_ascii=False — non-ASCII characters such as Hangul are preserved
  6. UTF-8 encoding: hashing is done at the byte level
Hash format: "sha256:" prefix + 64-character hex digest Example: "sha256:a1b2c3d4e5f6..."

Why the hash matters

  1. Approval integrity: verifies that the plan approved by the approver is the same plan being executed
  2. Tamper detection: detects whether the compiled snapshot has been altered in the DB
  3. Audit trail: tracks which plan construction was executed via the hash
  4. ExecutionContext propagation: compiled_plan_hash is propagated to every executor, allowing the executor to verify it is executing as part of the correct plan

Frontier node computation

Source: src/qtg/compiler/validate.py
Purpose: find nodes with no predecessor (root nodes) in the graph. At request creation these nodes start in the READY state. Logic: collect every node that appears as an edge’s to target; nodes not in that set are frontier (root) nodes. Example:
Note: this function is used to compute the initial template-level frontier. Runtime advancement of the current frontier is managed by advance_after_completion().

Valid graphs vs invalid graphs

Valid graph examples

1. Linear (executable on v3.0)

  • shape: LINEAR
  • executable_in_v3_0: True
  • topological_order: ["withdrawal_action", "withdrawal_observe", "deposit_observe"]

2. Split (parallel, not executable on v3.0)

  • shape: SPLIT
  • executable_in_v3_0: False
  • reason: "graph shape 'split' requires graph runtime"

3. Diamond / Hybrid (not executable on v3.0)

  • shape: HYBRID
  • executable_in_v3_0: False

Invalid graph examples

1. Cycle

  • Error: GraphValidationError("template graph must be a DAG")

2. Duplicate node_key

  • Error: GraphValidationError("duplicate node_key detected")

3. Reference to a non-existent node

  • Error: GraphValidationError("edge references unknown to node: nonexistent")

4. Self-loop

  • Error: GraphValidationError("self-loop edges are not allowed")

5. Empty graph

  • Error: GraphValidationError("template must define at least one node")

Completion Policy validation

When completion_policy is provided, _validate_completion_policy() performs additional validation.

CompletionAssurance mapping

Depending on the CompletionAssurance value, a terminal node must belong to a specific “family”:

Node family classification

The mapping table that determines the node family from action_type:

Validation rules

  1. The completion_assurance field must be present.
  2. It must be a valid CompletionAssurance enum value.
  3. At least one terminal node (a node with no outbound edges) must belong to the required family.
  4. Family-specific additional configuration validation:
    • destination_provider_observe / destination_chain_receive_observe: config.match_mode required
    • destination_chain_finality_observe: config.confirmations_required > 0 required

Validation failure examples


v3.0 executability decision

The executable_in_v3_0 field on GraphValidationResult indicates whether the current v3.0 runtime can execute this graph.

Conditions that block execution

Both conditions are appended to non_executable_reasons.

Summary of executable conditions

To execute on v3.0:
  • The graph is LINEAR (single path, no branch/merge)
  • Every edge’s condition_expr is None
If these conditions are not satisfied, template registration/validation still succeeds, but actual request creation and execution are only possible in a later version once the graph runtime is introduced.

Cross-References