Files
Aura/docs/design/contracts/c23-graph-compilation.md
T
claude b18a695531 feat(aura-runner, aura-cli, aura-registry): a run's trace directory is keyed by its own identity
A single run recorded into `runs/traces/<render-name>/`, so a second run of the
same blueprint overwrote the first and could leave a stale tap file beside an
index that no longer listed it. The directory is now
`runs/traces/<render-name>-<id8>/`, where the id is a digest over the run's own
manifest: two runs differing in any identity-bearing input land in two
directories, two runs differing in nothing land in one.

The digest hashes the manifest with its two provenance fields removed and its
parameter vectors merged.

- `commit` (the aura binary's build sha) and `project.commit` (the project
  repo's HEAD plus a `-dirty` marker, re-derived on every invocation) record who
  built or checked out the code, not what the run was. The latter would have
  minted a fresh directory for any uncommitted file anywhere in the project
  worktree — the authoring loop's normal state.
- `params` and `defaults` are merged name-sorted before hashing: how a value was
  supplied is not what the run is, so a no-op `--override` no longer splits one
  run across two directories. The two vectors are disjoint by construction.

`project.dylib_sha256` stays in. It is what the C13 hot-reload comparison
varies — an unchanged blueprint against a rebuilt node crate — which the
campaign leg structurally cannot express, since its axes vary params,
instrument and window, never code.

The manifest is now assembled before the tap bind rather than after the run.
Every input it needs was already resolved at that point, and the one value both
keys the directory and enters the report, so a handle can never name one
identity while the record describes another.

`aura chart <render-name>` no longer names a directory; it now lists the trace
handles beginning with that name instead of refusing blankly.
`TraceStoreError::NameTaken` stops prescribing `--trace`, retired in #319.

C18, C22, C23 and C27 record the new identity, each superseded clause moved
verbatim to its history sidecar; C23 gains its first.

closes #311
2026-07-27 10:52:56 +02:00

5.7 KiB

C23 — Graph compilation and behaviour-preserving optimisation

Guarantee. The bootstrap (C19) is a compilation: it lowers a param-generic, named blueprint (the authoring source — nodes, composites, strategy, harness; C8/C9/C20) into a flat, type-erased FlatGraph — the frozen runnable instance (C7) — wired by raw index, not by name (Edge { from, to, slot, from_field }). Composites are inlined at this step (C9): the composite boundary dissolves entirely (there is no composite in the flat graph). The blueprint's field / input-role names are non-load-bearing — the wiring resolves by index, and names survive at most as informative debug symbols (exactly as FieldSpec.name already is, C8), kept for tracing / rendering (C9 graph-as-data) but carrying no run semantics. (Reconciling with the root composite's own render name, #331: that claim stands unweakened — the name never influences compilation, identity, or execution semantics — but at run time, outside compilation, it prefixes the trace directory (traces/<name>-<id8>/, #311 — the run's own identity digest completes it); that operational role, not any load-bearing weight inside the flat graph, is exactly why the authored-blueprint intakes shape-gate it.) The flat graph is then the target of behaviour-preserving optimisation — any transform that leaves every observable sink trace bit-identical (C1 is the correctness invariant) — on two levels:

  • intra-graph (within one graph): common-subexpression elimination — two identical nodes (same type, same bound params, same input source) merge to one with output fan-out, so fractal composition (C9) costs no redundant compute — and dead-node elimination — a node on no path to a sink is dropped. Pure-consumer sinks (C8, no output) are never CSE candidates, so their out-of-graph side effects are preserved by construction.
  • across the sweep family (over the family of flat graphs one blueprint yields under a param sweep, C12): the sub-graph whose nodes carry no swept param and whose inputs are all sweep-invariant (transitively from the sources — same data window) is loop-invariant w.r.t. the sweep and is computed once; its output is materialised as a recorded stream (C11) shared read-only across the sweep instances (Arc<[T]>, C12). Only the parameter-dependent suffix re-runs per sweep point — loop-invariant code motion over the sweep loop.

The compilation also carries one structural gate that is load-bearing: param-namespace injectivity. check_param_namespace_injective runs over the param_space() name projection before name resolution, reads the boundary name projection — the by-name authoring address space — and rejects a duplicated path (CompileError::DuplicateParamPath). This makes the authoring address space injective without making names load-bearing in the flat graph: node names qualify the param path at construction and are dropped at lowering (the flat graph stays wired by raw index).

Forbids. Any "optimisation" that changes an observable trace (it breaks C1); optimising over a representation that is not graph-as-data — an opaque trait-object interior (the rejected nested-composite reading of C9) cannot be analysed or rewritten across its boundary, so it forecloses both levels.

Why. Determinism (C1) is not merely an audit property; it is the licence for these rewrites — a behaviour-preserving transform is only meaningful because "same input → bit-identical run" makes "same result" decidable, and a sweep-invariant sub-graph is reproducible enough to compute once and share. The flat graph representation (C19) is the necessary condition: only a flat, inspectable graph-as-data — with each node's declared param-ranges (C8) — lets the compiler identify identical sub-expressions, dead nodes, and the sweep-invariant frontier. This is precisely why a composite is an inlining (C9) and not a runtime sub-engine: the flat graph is what makes the whole graph optimisable.

Current state

The flat-graph representation — blueprint → inline / compile → flat instance — is the load-bearing substrate, built first, and is realized: Composite::compile emits the first-class FlatGraph (C19). The per-node param declarations the sweep-level pass presupposes carry name + kind (C8); the swept range is still pending (#32/C20), as is the sweep orchestration itself (C12/C21).

The optimisation passes — intra-graph CSE/DCE and sweep-invariant hoisting — are deferred, behaviour-preserving follow-on work; no pass exists yet (Composite::compile in crates/aura-engine/src/blueprint.rs adds none). Each is gated by a "flat graph with pass ≡ flat graph without pass, bit-identical run" test (C1). The one compilation-time structural check that is live today is the param-namespace injectivity gate above — check_param_namespace_injective, defined in crates/aura-engine/src/blueprint.rs and applied at that file's own compile entry points (compile, compile_with_params, and the bind paths) as well as from construction.rs's finish, raising CompileError::DuplicateParamPath.

See also

  • C1 — the bit-identity correctness invariant that licences every rewrite.
  • C8FieldSpec.name as the non-load-bearing precedent and the per-node param declarations.
  • C9 — inlining vs runtime sub-engine.
  • C11, C12 — recorded-stream sharing and the sweep family.
  • C19 — the flat-graph representation this optimises.

History: c23-graph-compilation.history.md