Reject cyclic graphs in graph build — DAG invariant 5 unenforced #161
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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/auraat HEAD)Fixture
fieldtests/cycle-0088-construction-op-script/c0088_3m_two_cycle.json: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
connectthat closes a cycle, and a finalize over a cyclic edge set, both pass.Validity-floor context (decide alongside)
Beyond the cycle,
buildalso exits 0 on two arguably-degenerate inputs the gate set is silent about:sourcerole -- emitted as"targets":[], a dead role kept in the blueprint (fixturec0088_3i_unfed_role.json).expose/output at all -- nooutputkey emitted (fixturec0088_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.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.
a.value→b.lhs+b.value→a.lhs2-cycle, or a self-loopfast.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.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. Whetherbuildshould 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
connectop 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.