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

6.0 KiB

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 — determinism, acyclic dataflow, the delay/state register.
  • C8 — the node contract: ≤1 output, record columns, Edge::from_field.
  • C10 — the strategy as a K=1 bias composite; the cost-model graph.
  • C12 — binding by name; why a duplicated knob path is unbindable.
  • C17 — the Rust builder as the human/LLM authoring surface.
  • C19 — bootstrap: blueprint → instance; construction-phase checks.
  • C23 — names non-load-bearing; the cross-graph optimiser; inlining.
  • C24 — the blueprint as a serializable, World-owned data value.
  • C28 — the layer ladder: construction phase vs the run loop.

History: c09-fractal-composition.history.md