# C19 — Bootstrap: blueprint → instance (recursive) **Guarantee.** Construction is a distinct phase, recursive at every level. Each node type has a **factory** `params → sized concrete node` (e.g. `SMA(length)` sizes its ring buffer). A **blueprint** is the param-generic, input-role-generic graph-as-data produced by running a Rust builder (C9); it carries *free* numeric params (declared ranges) and *free* input roles. The **bootstrap** binds `(blueprint + param-set + data bindings + seed)` into a concrete, **frozen instance** — buffers sized, topology fixed. This is precisely the "wiring / graph build" that C7 ("sized at wiring", "topology frozen per sim") and C12 ("params injected at graph build") reference; the same machinery applies recursively up to the harness (C20). A sweep builds many instances from one blueprint; instances are disjoint (C1). This binding is a **compilation**: the param-generic, named blueprint (the source) is lowered to a flat, type-erased **`FlatGraph`** (C23) **wired by raw index, not by name** (`Edge { from, to, slot, from_field }`) — composite boundaries dissolve entirely and field / role names are demoted to **non-load-bearing** debug symbols (exactly as `FieldSpec.name` already is, C8/C23). One narrow exception (#275): a `SourceSpec.role` — the lowered bound-`Role` name — is **load-bearing for source binding**, the key `Harness::run_bound` / `bind_sources` resolve a keyed source supply against; every other flat-graph name (edges, ports, composite boundaries) stays a non-load-bearing debug symbol, and the raw-index positional `run` path carries no role. "No recompile" means no **Rust / cdylib** rebuild (C12/C13: the cdylib loads once); re-deriving an instance per param-set is a cheap **graph re-compilation**, not a code recompile. **Forbids.** Params that change topology (a topology change is a *different* blueprint — Fork A, C7 "frozen"); resizing buffers after bootstrap; running a sim against an un-bootstrapped blueprint. **Why.** Separating the param-generic blueprint from the param-bound instance is what makes one strategy reusable across a whole sweep and lets the optimizer mutate "the 20" by *rebuilding* an instance (cheap; no recompile, C12) instead of rewriting code. Naming the build phase makes the implicit "wiring" of C7/C12 explicit — and naming it a *compilation* makes its successor explicit: the flat graph is the target of behaviour-preserving optimisation (C23). ## Current state **Value-empty recipe.** A blueprint leaf is **value-empty** — `BlueprintNode::Primitive` holds a `PrimitiveBuilder { name, schema, build }` recipe (`crates/aura-core/src/node.rs`), not a built node — and the tuning value lives only in the injected param vector (no baked default), so the blueprint stays a pure param-generic recipe. This realizes C12's "params injected at graph build" and C19's factory `params → sized node` literally. `compile_with_params(&[Scalar])` / `bootstrap_with_params(Vec)` on `Composite` (`crates/aura-engine/src/blueprint.rs`) **build each leaf through its own constructor** — the single sizing/validation gate — from its kind-checked slice *while lowering* (build-then-wire), consuming the vector slot-by-slot in the *same* depth-first walk `param_space()` projects, so the two share one traversal and the value reaches the node at the slot the sweep enumerates. Arity is checked up front (`param_space().len()`); a wrong-kind or wrong-length vector is a typed `CompileError::{ParamKindMismatch, ParamArity}`. The value *domain* (e.g. `length ≥ 1`) stays the constructor's own `assert`; the search-range is the run's (#32/C20, still pending). The lowering is structurally invariant under the injected point, so the **flat graph stays bit-identical** for a given point (C23, the correctness invariant). **The root is the fully-bound composite.** There is no separate `Blueprint` type: the root graph **is** a `Composite`, and `compile` / `compile_with_params` / `bootstrap_with_params` / `param_space` are its methods. What once distinguished the root — its bound data sources — is a property of its input roles: `Role` carries `source: Option` (`crates/aura-engine/src/blueprint.rs`) — `None` = an open interior port wired by the enclosing graph, `Some(kind)` = a bound ingestion feed (C3: sources bind at ingestion only). A composite is **runnable iff every root role is bound**; an open root role is a compile-time `CompileError::UnboundRootRole`. `compile` validates structurally **pre-build** (via `signature()`, no node constructed — an ill-typed wiring is caught before any build closure fires) and emits the `FlatGraph`, which `Harness::bootstrap` consumes (kinds/firing from the carried signatures, buffer depth from `lookbacks()`); the per-flat-node signature travels beside the node, so `bootstrap` needs no built-node `schema()` call. **`FlatGraph` shape.** `FlatGraph { nodes, signatures, sources, edges, taps }` (`crates/aura-engine/src/harness.rs`): `signatures[i]` is the static `NodeSchema` of `nodes[i]` gathered at lowering; `sources` are the lowered bound roles in role-declaration order; `taps` are the declared measurement points (C27), with interior-composite taps hoisted to the root list. **Structural-constant bind (#55).** `PrimitiveBuilder::bind(slot, value)` (`crates/aura-core/src/node.rs`) adds the **third** param category beside the topology factory-arg (C7/C19) and the tuning param (the value-pin): a **structural constant**. Pinning a value in the injected vector leaves the knob **in** `param_space` (a tuning param the sweep varies); `bind` instead **removes** the slot from `param_space` entirely — the knob is gone, not fixed. The discriminator is the deform-vs-tune test: a value whose variation yields another valid point of the *same* strategy is a **tuning param** (stays in `param_space`); a value whose variation *deforms* the strategy into a different one (e.g. the `2` of an "SMA2-entry" bound to its two-candle construction) is a **structural constant** (bound out), so a sweep never enumerates deformed strategies as valid family members. Mechanically `bind` shrinks the builder's declared param surface (`schema.params`) and wraps its build closure to re-splice the constant at its original positional slot; both dock sites already key off `builder.params()`, so the construction layer is byte-unchanged and chained binds reconstruct the correct positional vector (each layer computes its slot index relative to the param list it sees). `bind` resolves the param **name** to a position at **authoring** time (the by-name authoring address space) and the flat graph stays wired by raw index — the name never reaches it (C23 unaffected). The complementary export of a **named frozen** strategy (all/most knobs bound) as a reusable blueprint *value* is deferred (#60); `bind` ships only the per-knob overlay, no registry (C9/C10 intact). **Graph render.** `aura graph` renders no ASCII: the render path is a deterministic JSON **model serializer** (`aura_engine::model_to_json`, `crates/aura-engine/src/graph_model.rs`) plus a self-contained WASM-Graphviz **HTML viewer** (`aura-cli::render::render_html`) that inlines the model and lays it out in the browser — aura ships no layout engine (C9: graph-as-data). This render surface, and the debug-symbol labels it draws, belongs to C9/C22. ## See also - [C7](c07-scalar-soa.md), [C9](c09-fractal-composition.md), [C12](c12-atomic-sim-unit.md) — sized-at-wiring, graph-as-data, params-injected-at-build that this phase names. - [C8](c08-node-contract.md) — the `PrimitiveBuilder` / `NodeSchema` recipe and `FieldSpec.name` as the non-load-bearing precedent. - [C13](c13-hot-reload-frozen-deploy.md) — "no recompile" means no cdylib rebuild. - [C20](c20-strategy-harness.md) — the harness is the recursive top of the bootstrap. - [C22](c22-playground-traces.md) — the graph render / viewer surface. - [C23](c23-graph-compilation.md) — the flat graph as the optimisation target. - [C24](c24-blueprint-data.md) — the deferred named-frozen-blueprint value (#60). > History: [c19-bootstrap.history.md](c19-bootstrap.history.md)