Reject cyclic graphs in graph build — DAG invariant 5 unenforced #161

Closed
opened 2026-06-29 22:15:33 +02:00 by Brummel · 1 comment
Owner

The construction op-script service (aura graph build, #157, cycle 0088) accepts a combinational dataflow cycle and reports success, emitting a non-DAG blueprint. This violates domain-invariant 5 / C9: "The graph is a DAG; the only feedback path is an explicit delay/state node (the RTL register)."

Repro (verified 2026-06-29 against target/debug/aura at HEAD)

Fixture fieldtests/cycle-0088-construction-op-script/c0088_3m_two_cycle.json:

[
  {"op": "source", "role": "price", "kind": "F64"},
  {"op": "add", "type": "Sub", "as": "a"},
  {"op": "add", "type": "Sub", "as": "b"},
  {"op": "feed", "role": "price", "into": ["a.rhs", "b.rhs"]},
  {"op": "connect", "from": "a.value", "to": "b.lhs"},
  {"op": "connect", "from": "b.value", "to": "a.lhs"},
  {"op": "expose", "from": "a.value", "as": "out"}
]
aura graph build < c0088_3m_two_cycle.json ; echo $?
# emits edges [{from:0,to:1,slot:0},{from:1,to:0,slot:0}] -- a 2-cycle a<->b -- and prints EXIT 0

A self-loop (fast.value -> fast.series, edge {from:0,to:0}) is likewise accepted.

Cause

The construction gate set (eager: name-resolution, edge kind-match, double-wire, param bind; holistic: interior-slot totality, param-namespace injectivity, root-role boundness) omits acyclicity. A connect that closes a cycle, and a finalize over a cyclic edge set, both pass.

Validity-floor context (decide alongside)

Beyond the cycle, build also exits 0 on two arguably-degenerate inputs the gate set is silent about:

  • an unfed source role -- emitted as "targets":[], a dead role kept in the blueprint (fixture c0088_3i_unfed_role.json).
  • a graph with no expose/output at all -- no output key emitted (fixture c0088_3l_no_expose.json).

The fix scope is a design call: at minimum reject the cycle (clear invariant-5 violation); decide whether the dead-role / output-less cases are intended-valid (an emit-only producer feeding a later bootstrap gate) or should also fail fast. Claim (unverified): acyclicity may be intended as a bootstrap-time (compile-to-FlatGraph) gate, but no CLI path bootstraps an emitted blueprint (see #28), so a CLI consumer currently gets no acyclicity signal at all.

Source: cycle-0088 fieldtest, fieldtests/cycle-0088-construction-op-script/FINDINGS.md. RED-first via the debug skill.

The construction op-script service (`aura graph build`, #157, cycle 0088) accepts a combinational dataflow cycle and reports success, emitting a non-DAG blueprint. This violates domain-invariant 5 / C9: "The graph is a DAG; the only feedback path is an explicit delay/state node (the RTL register)." ## Repro (verified 2026-06-29 against `target/debug/aura` at HEAD) Fixture `fieldtests/cycle-0088-construction-op-script/c0088_3m_two_cycle.json`: ```json [ {"op": "source", "role": "price", "kind": "F64"}, {"op": "add", "type": "Sub", "as": "a"}, {"op": "add", "type": "Sub", "as": "b"}, {"op": "feed", "role": "price", "into": ["a.rhs", "b.rhs"]}, {"op": "connect", "from": "a.value", "to": "b.lhs"}, {"op": "connect", "from": "b.value", "to": "a.lhs"}, {"op": "expose", "from": "a.value", "as": "out"} ] ``` ``` aura graph build < c0088_3m_two_cycle.json ; echo $? # emits edges [{from:0,to:1,slot:0},{from:1,to:0,slot:0}] -- a 2-cycle a<->b -- and prints EXIT 0 ``` A self-loop (`fast.value -> fast.series`, edge `{from:0,to:0}`) is likewise accepted. ## Cause The construction gate set (eager: name-resolution, edge kind-match, double-wire, param bind; holistic: interior-slot totality, param-namespace injectivity, root-role boundness) omits acyclicity. A `connect` that closes a cycle, and a finalize over a cyclic edge set, both pass. ## Validity-floor context (decide alongside) Beyond the cycle, `build` also exits 0 on two arguably-degenerate inputs the gate set is silent about: - an unfed `source` role -- emitted as `"targets":[]`, a dead role kept in the blueprint (fixture `c0088_3i_unfed_role.json`). - a graph with no `expose`/output at all -- no `output` key emitted (fixture `c0088_3l_no_expose.json`). The fix scope is a design call: at minimum reject the cycle (clear invariant-5 violation); decide whether the dead-role / output-less cases are intended-valid (an emit-only producer feeding a later bootstrap gate) or should also fail fast. Claim (unverified): acyclicity may be intended as a bootstrap-time (compile-to-`FlatGraph`) gate, but no CLI path bootstraps an emitted blueprint (see #28), so a CLI consumer currently gets no acyclicity signal at all. Source: cycle-0088 fieldtest, `fieldtests/cycle-0088-construction-op-script/FINDINGS.md`. RED-first via the debug skill.
Brummel added the bug label 2026-06-29 22:15:33 +02:00
Author
Owner

Scope decision (boss run, cycle-0088 fieldtest follow-up)

This fix targets the clear invariant-5 violation; the two validity-floor cases listed above are scoped out, with rationale.

  • Reject dataflow cycles — in scope, fixed now. A combinational cycle (the a.value→b.lhs + b.value→a.lhs 2-cycle, or a self-loop fast.value→fast.series) is a structural violation of invariant 5 / C9: the graph is a DAG, the only feedback path is an explicit delay/state node. This is not degenerate-but-valid — it is flatly wrong, and a fail-fast construction service must reject it. RED-first via debug.
  • Unfed declared source role (targets:[]) and graph with no exposed output — scoped out of this fix, deferred. Both are degenerate but well-formed: they break no invariant. A declared role with no consumer, and an output-less graph, are unusual rather than structurally invalid. Whether build should also reject them is a strictness call best made once the build→consume edge (#28) defines what a valid emitted blueprint must guarantee — at which point the validity floor is stated rather than guessed. Folding a debatable strictness change into an invariant-violation bug fix would conflate the two.

Detection cadence (derived preference): eager, at the connect op that closes the cycle — it is per-op-decidable (does the connect's target node already reach its source node through the edges added so far?), so it yields the sharpest by-identifier error (op N (connect): … closes a cycle), consistent with the surface's eager per-op diagnostics. A holistic topological check at finalize is the fallback if eager reachability proves awkward.

## Scope decision (boss run, cycle-0088 fieldtest follow-up) This fix targets the clear invariant-5 violation; the two validity-floor cases listed above are scoped out, with rationale. - **Reject dataflow cycles** — in scope, fixed now. A combinational cycle (the `a.value→b.lhs` + `b.value→a.lhs` 2-cycle, or a self-loop `fast.value→fast.series`) is a structural violation of invariant 5 / C9: the graph is a DAG, the only feedback path is an explicit delay/state node. This is not degenerate-but-valid — it is flatly wrong, and a fail-fast construction service must reject it. RED-first via debug. - **Unfed declared source role (`targets:[]`)** and **graph with no exposed output** — scoped out of this fix, deferred. Both are degenerate but *well-formed*: they break no invariant. A declared role with no consumer, and an output-less graph, are unusual rather than structurally invalid. Whether `build` should also reject them is a strictness call best made once the build→consume edge (#28) defines what a valid *emitted* blueprint must guarantee — at which point the validity floor is stated rather than guessed. Folding a debatable strictness change into an invariant-violation bug fix would conflate the two. Detection cadence (derived preference): eager, at the `connect` op that closes the cycle — it is per-op-decidable (does the connect's target node already reach its source node through the edges added so far?), so it yields the sharpest by-identifier error (`op N (connect): … closes a cycle`), consistent with the surface's eager per-op diagnostics. A holistic topological check at finalize is the fallback if eager reachability proves awkward.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#161