Files
Aura/docs/design/contracts/c09-fractal-composition.md
T
claude 8688a60ded docs(ledger): split the design ledger into an INDEX map, per-contract live files, and history sidecars
The single-file ledger had grown to 2968 lines / ~42k tokens, mixing
current design law with accreted history: 59 cycle-stamped realization
blocks, 18 [HISTORY] passages, 22 supersession markers, and the C10 /
C22 / C24 reframe sagas layered several supersessions deep. A
code-grounding audit (31 agents, adversarially verified) confirmed 11
defects stated as current truth: stale crate homes from the C28 #288
roster split (cost nodes, PositionManagement, PositionEvent, Session),
the renamed InputSpec->PortSpec, the pre-#241 project model in C16 and
the open-threads section, a stale HarnessKind retirement deferral in
C24, and three C28-internal inconsistencies.

New shape, per the ailang precedent:

- INDEX.md stays the sole addressable entry point: foundation, external
  components, a C-id-keyed contract map (one line per contract), and
  only the genuinely open architectural threads.
- contracts/cNN-<slug>.md carries each contract's current truth only:
  Guarantee / Forbids / Why with ratified refinements integrated, plus
  a code-anchored Current state. All confirmed defects are fixed here;
  crate anchors were re-verified against the tree.
- contracts/cNN-<slug>.history.md (18 sidecars) and INDEX.history.md
  preserve every superseded block verbatim, stamps and issue refs
  intact, under a frozen-record banner. Nothing was deleted: superseded
  design intent remains an addressable working-tree artifact, off the
  per-cycle audit walk.
- Ledger discipline is now stated in INDEX.md: live files are edited in
  place at cycle close, superseded text moves verbatim to the sidecar,
  and a supersession marker in a live file is itself an audit finding.

Every contract file was verified against its old text by an independent
zero-loss pass (statement-by-statement) plus a code-accuracy spot check;
C-ids and contract titles are unchanged, so existing C-id citations in
code, tests, and issues resolve as before.
2026-07-21 16:40:36 +02:00

89 lines
6.0 KiB
Markdown

# C9 — Fractal, acyclic composition
**Guarantee.** A composite is an **authoring-level** `Node`: it declares the same
interface (typed inputs + ≤1 output, C8), is wireable wherever a node is, wires an
interior sub-graph, and exposes one output **port** carrying a **record of 1..K
re-exported fields** — signal, combined signal, and (with execution) strategy are
the same abstraction, nestable arbitrarily. A composite is **not a runtime
object**: the bootstrap compiles it away by **inlining** its sub-graph into the one
flat instance (C19/C23), so "nestable arbitrarily" and "graph-as-data" hold at the
**blueprint (source)** level while the running graph is the flat, index-wired
`FlatGraph`. The dataflow graph is a DAG; the only feedback path is an explicit
delay/state node (the RTL "register", C1). Wiring is written in Rust (builder API,
C17); the built graph is introspectable runtime data and, per C24,
**bidirectional** — a blueprint is not only *emitted* as data but is itself a
serializable data value with a load path (data → blueprint → `FlatGraph`), so
topology is a value the World generates, stores, and reproduces. The Rust builder
stays the primary human/LLM authoring surface; the data form is the machine surface
the World owns.
**Forbids.** Implicit dataflow cycles (combinational loops); special-casing
"signal-of-signals" as separate mechanics; a composite surviving as a runtime
sub-engine (a `Box<dyn Node>` driving a nested sub-loop); a blueprint whose
`param_space()` name projection is non-injective — a path-qualified knob name that
repeats, since it then has no distinct by-name address (C12/C19).
**Why.** Self-application of one contract gives unlimited composition with no
adapter zoo. Acyclicity keeps the synchronous reactive model well-defined; forcing
feedback through a visible delay node keeps per-cycle determinism intact (C1) and
the one legitimate feedback path explicit. Graph-as-data enables visualization,
freezing, and re-parameterization for sweeps, and — round-tripping both ways (C24)
— lets the World own topology as content it serializes, loads, and generates rather
than as Rust baked into the binary. Inlining, not a runtime sub-object, is what
makes the composite boundary free: it keeps the interior transparent to the
cross-graph optimiser (C23) and adds no runtime sub-loop the flat model does not
need; the boundary dissolves into the raw index wiring the run loop already
consumes, and names stay non-load-bearing (C23).
## Current state
A composite is `Composite` (`aura-engine::blueprint`), a reusable sub-graph fragment
that is never `eval`'d. It holds interior items, interior edges (local indices),
`input_roles: Vec<Role>`, and `output: Vec<OutField>`.
- **Multi-output record.** `output` is `Vec<OutField { node, field, name }>` — each
entry a **named projection** of one interior `(node, output-field)`. A consumer
selects which re-exported field it reads via `Edge::from_field`, the same binding
that already selects a leaf record's columns (C8). This is the same arity C8
grants a leaf (OHLCV = one port, 5 columns): a multi-line indicator (MACD =
macd/signal/histogram) is one record of K fields, one port, one row per `eval`. A
strategy composite is the K=1 case (one bias field, C10).
- **Named boundary.** `input_roles` is `Vec<Role { name, targets }>` — role `r`
fans its source value into the interior `targets`. Inputs and outputs are ordered,
positionally-indexed named projections of interior handles.
- **Per-node param path.** A node's surface param name flows from its own **instance
name**: every node carries a name (default = its lowercased type label, override
via `Node::named`, `aura-core::node`), and `param_space()`
(`aura-engine::blueprint`) is uniformly `<node>.<param>` at every level including
the root. A same-type fan-in is distinguished by naming the colliding legs — the
same single act that qualifies their param paths.
- **Param-namespace injectivity.** A blueprint compiles only if its `param_space()`
name projection is injective. A duplicated path is a knob no binding can select
alone (C12/C19), rejected as `CompileError::DuplicateParamPath` carrying the
offending path; the cure is `.named(...)` on the colliding same-type siblings. One
structural check, `check_param_namespace_injective` (`aura-engine::blueprint`),
runs before name resolution in `compile_with_params` and both binders. Paramless
interchangeable same-name sources stay legal (no path, no duplicate).
- **Names dropped at lowering.** Output, role, and node names are non-load-bearing
(C23): they live at the blueprint boundary and in render, and are dropped at
inline — `ItemLowering::Composite` carries `output: Vec<(usize, usize)>` (raw index
pairs) and `roles: Vec<Vec<Target>>`, so the flat graph is name-free and wired by
raw index.
- **Bidirectional data form.** The emit half is `model_to_json`
(`aura-engine::graph_model`); the load path is realised —
`aura-engine::blueprint_from_json` (`aura-engine::blueprint_serde`, #155) parses
the data form back to a blueprint that compiles to a `FlatGraph` (C24).
## See also
- [C1](c01-determinism.md) — determinism, acyclic dataflow, the delay/state register.
- [C8](c08-node-contract.md) — the node contract: ≤1 output, record columns, `Edge::from_field`.
- [C10](c10-bias-r-cost.md) — the strategy as a K=1 bias composite; the cost-model graph.
- [C12](c12-atomic-sim-unit.md) — binding by name; why a duplicated knob path is unbindable.
- [C17](c17-authoring-surface.md) — the Rust builder as the human/LLM authoring surface.
- [C19](c19-bootstrap.md) — bootstrap: blueprint → instance; construction-phase checks.
- [C23](c23-graph-compilation.md) — names non-load-bearing; the cross-graph optimiser; inlining.
- [C24](c24-blueprint-data.md) — the blueprint as a serializable, World-owned data value.
- [C28](c28-stratification.md) — the layer ladder: construction phase vs the run loop.
> History: [c09-fractal-composition.history.md](c09-fractal-composition.history.md)