ADR 0005: an application tier over the checked graph, developed on graph patterns
Status: Accepted Date: 2026-11-05 Related: ADR 0004 (the graph, and its step 4 export, which this ADR widens); the domain flow computational model (layers 1 to 5, leaf operators, phases, and the resource contract); racing-drone ADR 0008 (change the core only with a client in hand); the flow-testing plan (the toy operators and the reference interpreter); racing-drone’s precision sweep (the one design-space tool a mission has today)
Context
Section titled “Context”ADR 0004 made a mission’s stack a checked graph of operators over domains, and steps 1 to 3 built it: the domain and signature types, the checking builder, the CPU stack that runs the checked flow::Graph, and both missions’ baselines as graphs, byte-identical to their slot builds. The flow tests, the trace and trace_compare then checked the graph against a reference interpreter rather than against the slot stack.
All of that work faces inward or downward. Its client so far is the equality gate, and its next client is the domain flow compiler (step 4). Nothing faces a designer: no tool lets an engineer build a graph, run it, characterize it, or optimize it. What exists is per mission and hard-wired:
- Running is one set of executables per baseline (
sitl_<name>,sitl_batch_<name>,replay_<name>), each a separate build target. - Analysis is the mission’s scorer (
score), the log tools, the trace, andflow::canonicalandflow::mermaidfor reading a graph. - Exploration is racing-drone’s precision sweep, which runs B0 in each scalar type of a list. It is one axis, one
Tfor the whole stack, and one mission.
Three properties of the current design keep applications out:
- A graph exists only inside a C++ build. ADR 0004’s amended decision 2 describes the graph in C++, so the compiler checks every edge. A baseline file names what fills the graph and the operators’ parameters, but a change to its topology, or to which operator fills a composite, is a new build target. A tool that proposes graphs, such as an optimizer, would have to generate C++ and rebuild for each candidate.
- The graph carries no cost. The trace records each edge’s value and its simulated time. Nothing records what a firing costs: time, energy, memory, operations, bytes moved. ADR 0004 decision 3 rightly keeps targets and lowering out of anatomy, so a cost on the KPU or a GPU can’t be computed here. Nothing yet carries it back either.
- The
Graphvalue isn’t complete enough to leave the process. The flow-testing plan notes that it doesn’t name each edge’s domain, holds no constant’s value, and lists no input or its trigger. Step 4 needs all three for the compiler, and any tool outside anatomy needs the same three.
The graph must capture the compute
Section titled “The graph must capture the compute”The graph exists so that one description of a capability can be lowered onto a multi-core CPU, a TPU and a KPU (the model’s section 2). Today a node of the graph is an operator’s name and its handlers: the CPU stack calls a C++ function, and what that function computes is hidden inside it. A compiler can’t lower what it can’t see. For each transformation it needs the computation itself: the computational domain (the index space and the dependences between its points), the faces that are its ports, its state, and its structure. From those it derives a parallel kernel for each target, and the bufferization around it.
So the node is where the parallel implementation attaches. On specialized hardware, a transformation runs as a kernel dispatch: a parallel kernel, launched over a set of processing elements. The bufferization that the lowering chooses for that node (each domain’s phases, layouts and transitions: stream in, drain, park, resume) is what the kernel needs around it. A kernel dispatch together with its bufferization is a hardware subroutine: a self-contained unit of parallel work with a declared footprint. The parallel runtime hands each hardware subroutine to the resource manager, the orchestrator that owns the target’s checkerboard of resources (the tiles of processing elements and the memory banks beside them), which allocates a region of the checkerboard, runs the subroutine, and frees the region when the node’s notifier arrives. The CPU stack today is the degenerate case: the dispatch is a direct call, the bufferization is C++ values, and the checkerboard is one core.
The application tier and the lowering need the same graph. An analysis that counts operations, a cost model that maps a node onto hardware, and a compiler that emits its kernel all read the node’s computational domain. A tier built on today’s opaque nodes would have to be rebuilt once the nodes describe their compute.
Where the machinery gets its feedback
Section titled “Where the machinery gets its feedback”There is one more problem, in how the machinery has been developed rather than in what it is. Every feature of the graph machinery so far was pulled by a mission: scout-drone’s s0-gt-pid and racing-drone’s B0 were the clients of steps 2 and 3, and the acceptance test of each step was byte-equality with a mission’s slot stack. That kept the change safe, but it makes a mission the test environment for low-level machinery, and a mission brings a lot of context the machinery doesn’t need: a vehicle, a world and its scenarios, the reflex dynamics, a scorer, seeds, and a log format. When a graph feature misbehaves inside a closed-loop flight, the failure has to be found through all of that.
A mission also exercises little of the model. The domain flow model is about operators over shaped, structured domains, with declared state and with rates. The missions’ graphs today pass records of scalars between composites. The camera frame is an opaque scalar until an operator reads pixels, and the only rate rule is hold, because accumulate and queue “come when a mission needs them” (the flow-graph plan). The flow tests fill part of the gap with toy operators over a time and an unsigned integer. These are exact, but they have no shape, structure, state domain or element type worth the name, and no compute to lower. So the parts of the model that matter most to the compiler (computational domains, matrices and their structure, state carried across firings, constants with values, rates that differ by a ratio, and per-edge element types) have never run.
What would give the machinery direct feedback is a set of small, well-understood graph patterns, each one exercising a few features of the model and one class of parallel kernel, in a test environment with nothing else in it:
| Pattern | Operators | What it exercises | Oracle |
|---|---|---|---|
| MLP | linear, bias, activation, in two or three layers | dense matrix and vector domains with real shapes; weights as constants with values; no state; an element type per edge; elementwise epilogues fused onto a drain (the model’s section 4) | direct evaluation; the operation count in closed form |
| Kalman filter | constant-velocity predict and update | a symmetric covariance; a declared state domain carried across firings; two triggers, predict on a clock and update on a measurement; a bounded measurement with a count | the covariance’s symmetry under the Joseph form; convergence to the steady-state Riccati solution; agreement across element types, as cortex’s kalman_posit test checks for one filter |
| DSP spectral pipeline | ADC, band-pass FIR, window, FFT, magnitude | a FIR delay line as state; complex elements; two rates, the ADC per sample and the FFT per frame of N samples, which needs accumulate; the FFT’s butterflies, whose dependences aren’t uniform | tones at known frequencies in known bins; Parseval’s theorem; the filter’s stop-band attenuation |
| Attitude filter | gyro integration, accelerometer and magnetometer correction | a quaternion domain whose structure is unit norm; integration at the IMU rate and correction at a lower rate | a constant rate integrates to a known rotation; a static attitude converges to gravity; the norm is preserved |
| PID control | PID, with a first-order plant as an operator of the pattern | a carried edge with no matrix (the integrator and the last error); a closed loop through a carried edge; a clock trigger | the plant’s discrete step response in closed form; zero steady-state error with the integral term |
| Convolution | a 2D convolution, with bias and activation | a tensor domain and its faces; a sliding-window computational domain; weights as constants | direct summation; an impulse returns the kernel |
| SOR / SSOR | successive over-relaxation sweeps on a 2D Poisson problem, forward, and forward then backward | a carried edge per sweep; an ordering dependence inside a sweep, lexicographic or red-black, which decides what runs in parallel | convergence to a manufactured solution; the residual’s rate against the known spectral radius at the optimal ω |
| Stencils | 5-point and 7-point Jacobi, and an explicit heat-equation step | halos between tiles when a domain is divided (the model’s section 7); a carried edge per step | an eigenmode decays by its known factor per step |
| QR | a Householder or Givens factorization, and a least-squares solve | a triangular computational domain; an inout matrix | Q is orthogonal, R is upper triangular, QR = A, and the least-squares solution of a known system |
Together they cover the kernel classes the targets must run: dense linear algebra (MLP, Kalman, QR), small stateful recurrences (attitude, PID), sliding windows (convolution, FIR), stencils and iterative solvers (stencils, SOR/SSOR), and transforms (FFT). Each pattern is a few operators, takes its inputs from a driver at declared rates instead of from a simulator, and has an oracle in closed form. A failure points at the machinery, not at a vehicle.
The tools already elsewhere
Section titled “The tools already elsewhere”The design-space tools Branes.AI already has live in other repos, on their own graph formats: graphs characterizes and maps PyTorch FX graphs onto 32+ hardware models; domain_flow’s tools/dse samples hardware and software configurations of a DFG for latency and energy; embodied-ai-architect runs mission-driven system synthesis and analysis. None of them can read a mission’s graph today. Step 4’s export is the first point where they could. The patterns are also graphs those tools already understand: a linear layer, a filter, an FFT, a factorization.
The options considered:
- Leave applications to each mission. Each mission grows its own sweeps, as racing-drone did. Nothing is shared, and every tool is rebuilt for scout-drone and every mission after it.
- Build the applications in anatomy, on the C++ builder, driven by the missions. The tools would be C++ programs that each mission links. A design-space loop over topology would still need a rebuild per candidate, the analysis and optimization code Branes.AI already has, mostly Python, couldn’t be reused, and every feature would be developed through a mission.
- Make the checked graph capture the compute, as a document that leaves anatomy and can come back. Add a cost record, and let applications and the lowering run on the same document, wherever they live. Develop all of it first on graph patterns in anatomy, then bring it to the missions. Anatomy keeps the checks and the CPU stack. This is the one proposed.
Decision
Section titled “Decision”-
The graph machinery is developed on graph patterns, in a workbench in anatomy. The workbench is a driver, a set of patterns and their oracles, with no vehicle, world, simulator or scorer. A driver feeds a pattern’s inputs at their declared rates from synthetic sources (a sequence, a noisy trajectory, a sum of tones, a manufactured right-hand side) and collects its outputs and its trace. The patterns are the nine in the table above: the MLP, the Kalman filter, the DSP spectral pipeline, the attitude filter, PID control, convolution, SOR/SSOR, stencils and QR. A new feature of the machinery (a domain’s structure, a rate rule, a computational domain, the document, the cost record, a lowering) lands with a pattern that uses it and an oracle that checks it. Then it lands in a mission.
- The patterns’ operators are reference implementations, written plainly to be checked, not to be fast. Like the flow tests’ toys, they live in the workbench and its tests, not in the libraries a mission links. So the patterns don’t break the rule that anatomy holds no algorithms a mission flies. A mission’s operators still come from cortex, reflex and synapse.
- The patterns are the machinery’s clients, in the sense of racing-drone ADR 0008. A change to the machinery is accepted on its patterns and their oracles. ADR 0004 decision 7 still applies on top: no change may alter racing-drone’s or scout-drone’s outputs. The missions remain the gate that the machinery doesn’t break them, and the client for the mission interface (external ports, composites, scorer). They stop being the place where machinery is first made to work.
- Every tool in this ADR runs on the patterns first: printing, tracing, the document and its round trip, the cost record, the evaluation and the lowering. A pattern is small enough to read its whole document, trace and cost record by hand.
-
A leaf node describes its compute, and its lowering is a hardware subroutine. A leaf operator declares its computational domain: the index space, and the dependences between its points, as a system of recurrences where they are uniform (domain_flow’s
.surespecification) and as an explicit dependence structure where they aren’t (the FFT’s butterflies, a triangular solve’s sweep order). It also declares the faces that are its ports, its state domains and its structure. Anatomy captures this, checks it against the node’s ports, and carries it in the document. Anatomy never writes the kernel. The lowering (layer 5, outside anatomy, ADR 0004 decision 3) turns each leaf node, or each fused group of them, into a hardware subroutine for a target: a kernel dispatch together with its bufferization. On a multi-core CPU the dispatch is a threaded kernel over tiles of the domain. On a TPU it is a program for the systolic array. On the KPU it is a Domain Flow Program. In each case the subroutine declares its footprint (its processing elements, its buffers and their shapes, its memory), which makes up the node’s share of the region’s resource contract. The parallel runtime submits each hardware subroutine to the target’s resource manager. The resource manager allocates it a region of the checkerboard of resources, runs it, and reclaims the region on the node’s notifier, keeping resident domains held as the model’s section 8 says. The CPU stack stays the reference target: its dispatch is a direct call, and every other target’s results are checked against it. -
The checked graph has an interchange document, and step 4’s export is that document. A graph document is the checked
flow::Graph, plus what it lacks today: each edge’s domain (index set, element type, structure, shape, with symbolic extents bound), each constant’s value, each input with its trigger, and each operator’s declared state domains, parameters and, for a leaf, its computational domain (decision 2). It is versioned and it has a schema. Its identity is a hash of its canonical form, which covers every field that changes what runs, constants’ values and element types included. The composition hash (flow::graph_hash) covers the graph’s structure but not constants’ values, so it stays the identity of a graph’s topology, as the logs and traces use it today, and never identifies a document. Where a consumer must tell two documents apart (the evaluation cache, a cost record, a lowering’s record), it uses the document’s hash. The domain flow compiler reads it, and so does every application. There is one export, not one for the compiler and one for tools. What a lowering produces (each node’s hardware subroutine and the region’s resource contract, for one target) is a separate record keyed by the document’s hash and the target, written by the compiler and never by anatomy. It is kept apart so that the document stays the same across targets. -
A graph document can come back. Anatomy reads a document and builds a stack from it, with the same checks the C++ builder makes, reported as the invariant broken (G1 to G9). A document can change:
- parameters, constants and element types, which a baseline file or a pattern’s configuration can change today only in part;
- which registered operator fills each composite, in a scoreboard graph;
- the whole graph between the external ports, in a research graph or a pattern, from registered operators only.
Every operator is still C++, registered in a build. A document chooses among the operators and wires them. It can’t add one. ADR 0004 decision 6 still holds for missions: a document declares its class, and the checks refuse a scoreboard document that changes a composite’s ports or the connections between composites.
-
A graph host runs a document without a rebuild. It links every operator its build registers, and takes a graph document and its configuration at run time. The workbench has one, which registers the patterns’ operators and runs the driver. Each mission then gets one, which registers the mission’s operators and runs the mission’s harnesses: SITL, batch, replay and score. The C++ graphs stay. A C++ graph is a document its build writes, and the host’s run of that document equals the build’s own run, byte for byte, with no exception for
composition_hash, since they share the graph. That is this ADR’s equality gate, held first on the patterns and then on every mission baseline. Once a lowering exists for a target, the host runs a document on that target through the target’s runtime, with the lowering’s record beside it. -
Anatomy defines the cost record, not the cost. A cost record attaches costs to a document’s firings, edges and hardware subroutines: time, energy, peak memory, operations, and bytes moved, each with its unit, its source (measured, estimated or modelled), and the target and configuration it is for. Anatomy writes measured CPU records itself, from a timing observer on the CPU stack, which needs no change to the stack. Operation counts can also be derived from a leaf’s computational domain, so the patterns check the record: the MLP’s, the FFT’s and QR’s counts are known in closed form, and a record that disagrees with them is wrong. Records for other targets come from outside: from
graphs’ estimators and mappers, and from the domain flow compiler,tools/dseand each target’s runtime once a leaf is lowered. ADR 0004 decision 3 stands: anatomy never computes a target’s cost. -
An evaluation is a deterministic function, run by a graph host. It takes a graph document, its configuration, its inputs (a pattern’s driver, or a mission’s scenario suite and seeds), a target, and optionally cost records. It returns an objective vector: the oracle’s errors for a pattern, or the scorer’s metrics and the success rate for a mission, and each cost aggregated over the run. Its result is keyed by the hashes of the document, the configuration, the inputs and the target, and by the identities of the cost records it was given and the rule that aggregates them, so it can be cached and repeated, and two evaluations that differ in any of them never share a result. It is a command with JSON in and JSON out, so a tool in any language can call it.
-
Optimizers live outside anatomy. A multi-objective loop (generate candidates, evaluate, keep the Pareto front) belongs with the design-space tools, most naturally in
embodied-ai-architect. Anatomy provides what a loop needs: the document, its schema, the checks, the graph hosts and the evaluation. It must not depend on any optimizer. A mission candidate an optimizer finds is a graph document. To join the scoreboard, it is written back as a C++ baseline and goes through the mission’s gates, like any other baseline. -
The first applications run on the patterns, and the first mission application is the precision sweep, generalized. The domain flow model gives each domain its own element type, so a precision choice belongs to an edge, not to a whole stack. The first loop sweeps per-edge element types on the MLP and the Kalman filter. It trades the oracle’s error against measured cost and reports the Pareto front, where both axes can be checked by hand. The same loop then runs on racing-drone’s B0, against the scorer’s metrics. It replaces no gate. If it can’t be built on decisions 3 to 7, they are wrong.
| Step | Repo | What |
|---|---|---|
| 1 | anatomy | The workbench: a driver, and the MLP, Kalman and DSP patterns with their oracles, on the C++ builder and the CPU stack. It adds what they need and the missions haven’t: shaped and structured domains that run, constants with values, complex elements, and the accumulate rate rule. The missions’ outputs are unchanged |
| 2 | anatomy | The computational domain of a leaf (decision 2), declared and checked on the first three patterns. The other six patterns follow, each landing with the feature it exercises: the attitude filter and PID, then convolution, stencils, SOR/SSOR and QR |
| 3 | anatomy | The graph document and its schema, written from the checked graph (ADR 0004 step 4’s export, widened). A test that every document written validates against the schema, carries every field decision 3 lists, and is written byte-identically twice, on the patterns, then on every flow fixture and both missions’ baselines |
| 4 | anatomy | The reader and its checks, and the workbench’s graph host. A round-trip test (document → stack → document is the identity) on the patterns, every flow fixture and both missions’ baselines. For each pattern, the host’s run of the document its build writes is byte-identical to the build’s own run, and the mutation tests refuse the same broken documents the builder refuses |
| 5 | anatomy | The cost record, the CPU timing observer, and the evaluation command, checked against the patterns’ closed-form operation counts. The first per-edge precision loop, on the MLP and the Kalman filter |
| 6 | anatomy, domain_flow, graphs | The patterns’ documents to the domain flow compiler and to graphs’ estimators. The first lowered leaves, as hardware subroutines on a multi-core CPU and then on the KPU, run by a resource manager and checked against the CPU stack’s outputs: the MLP’s linear layer first (ADR 0004 step 4), then a stencil and QR. A TPU follows. The first cost records for a target beyond the CPU stack |
| 7 | scout-drone, racing-drone | Each mission’s graph host, with logs byte-identical to each baseline’s own build on every scenario and the batch |
| 8 | racing-drone, embodied-ai-architect | The per-edge precision loop on B0, against the scorer’s metrics, with its Pareto front |
Consequences
Section titled “Consequences”- The machinery gets fast, local feedback. A pattern test runs in milliseconds, with no simulator, and fails against an oracle a reader can check, so a bug in a domain, a rate rule, a computational domain or the document is found in the machinery that has it.
- The model’s untested parts get clients. Shapes, structure, state domains, computational domains, constants and rates other than
holdare exercised before any mission needs them, and before the compiler depends on them. - The node becomes the unit of parallel work. What a transformation computes is visible to every reader of the graph, and what runs on a target is a hardware subroutine per node, or per fused group. Analysis, cost and lowering share one description, and a new target needs a lowering and a runtime, not a new graph.
- The lowering’s output has a home outside anatomy. Hardware subroutines and resource contracts are records the compiler writes, beside the document, so anatomy never depends on a target, and one document is lowered to every target.
- The missions stop leading the machinery. They adopt a feature once it works on the patterns, and they gate it. A feature only a pattern needs (complex elements, say) can land with no mission using it, which racing-drone ADR 0008’s rule would otherwise forbid. This ADR grants that exception for the graph machinery only, not for the mission interface.
- Anatomy gains reference operators, in the workbench only. They must stay out of the libraries a mission links, so no mission flies a reference implementation. The production operators of the same kind (cortex’s Kalman filters, synapse’s matmuls) are not replaced by them.
- A graph can be built, changed and evaluated without the C++ compiler, as long as the operators it uses are already registered. An optimizer can try thousands of graphs with no rebuild.
- ADR 0004’s step 4 grows. The export has to carry each leaf’s compute and enough for a tool, and come back, not only reach the compiler. It is designed once, for both readers, and tried first on the patterns.
- The checks become two front ends. The C++ builder checks at compile time and when the graph is built, and the document reader checks at load time. They must report the same invariants. The flow tests (the reference interpreter, mutations, meaning-preserving changes) run on both, and a test holds them equal.
- Each host links every operator its build registers. A mission’s build gains a larger binary. The per-baseline targets stay, and remain what flies.
- The schema becomes a contract outside anatomy. Changing it touches every tool that reads it, so it is versioned and changed like the mission interface, with its clients in hand.
- Costs are only as good as their source. Measured CPU times vary by machine and are kept apart from the deterministic log and trace. A record names its source and target, and an evaluation never mixes sources silently.
- The tools in other repos can reach anatomy’s graphs.
graphs,domain_flowandembodied-ai-architectread the patterns first, which they already understand, and then the missions’ graphs.
Open questions
Section titled “Open questions”- Determinism on a parallel target. ADR 0004 decision 5 makes replay exact on the CPU schedule. A parallel kernel’s reductions run in the order its schedule fixes, so a replay on the same target and resource contract can be exact too. Across targets, and on the CPU stack, results agree only to a tolerance the oracle or the scorer sets. Whether a target must also offer a mode that is bit-identical to the CPU stack is open.
- Iteration to convergence. SOR/SSOR and some stencils run until a residual is small enough, which is the model’s open question on conditional execution (section 11). The patterns start with a fixed number of sweeps.
- How far the dependences are described. Uniform recurrences cover the matmul, convolution, stencils and the FIR. The FFT, QR’s Householder updates and a triangular solve need more. Whether the document describes them as recurrences over a transformed index space, or names them as library kernels whose dependence structure the compiler already knows, is open.
- The document’s format. JSON with a JSON Schema is the default, and Pydantic models in
embodied-schemaswould give the Python tools types for free. A binary form may be needed for large constants, such as an MLP’s or a convolution’s weights. - Where a graph host’s run-time stack comes from. The CPU stack records, for each firing, a function instantiated from the binding at build time. A host built from a document needs the same functions, looked up from the registry by operator and handler. The plan for step 4 must show this keeps ADR 0004’s guarantees: no virtual dispatch on a firing, and no allocation once built.
- Bounded shapes and symbolic extents are bound from configuration. Whether an optimizer may vary them (a 4-state filter or a 6-state one, a 256-point FFT or a 1024-point one, a 64 × 64 grid or a 1024 × 1024 one) or only the configuration may, is open. The patterns are where this is first decided.
- Whether an evaluation runs remote, on hardware rather than on the host’s CPU.
embodied-ai-architecthas SSH and Kubernetes back ends. The evaluation command’s interface should not preclude them.
Current state: proposed on 2026-11-05, accepted on 2026-11-07. Step 1 is complete (plan, anatomy#89, guide), released in v0.12.0:
- In the core:
flow::data<D>, the value of a shaped or structured domain, and complex element types (anatomy#91). Also theaccumulaterate rule, read as aflow::window<M, N>(anatomy#93). Without anaccumulateedge, a graph’s canonical form and hash are unchanged. racing-drone’s and scout-drone’s outputs, built against it, are byte-identical to their main’s. - In the workbench (
workbench/, not installed): the driver, its sources and codecs,workbench_run, and the MLP (anatomy#94). Also the MTL5 adapter and the Kalman filter (anatomy#95), and the DSP spectral pipeline (anatomy#96). Each pattern’s stack equals its operators called by hand, bit for bit (W1). It meets its closed-form oracle (W2), and its traces are byte-identical run to run (W3).
Optical flow, ResNet18 and a transformer, with the reusable subgraphs they need, are left to a step of their own (the plan’s “Not in this plan”). Step 2 is planned (plan, anatomy#98): the computational domain of a handler, declared in its binding, checked (K1–K8) and evaluated against the handler, on the first three patterns, then the six others, each with the feature it exercises.