Skip to content

feat(nodes): add void, an explicit terminal sink for fire-and-forget branches - #50

Merged
senamakel merged 31 commits into
mainfrom
void-node
Aug 13, 2026
Merged

feat(nodes): add void, an explicit terminal sink for fire-and-forget branches#50
senamakel merged 31 commits into
mainfrom
void-node

Conversation

@senamakel

@senamakel senamakel commented Aug 13, 2026

Copy link
Copy Markdown
Member

What

Adds NodeKind::Void (wire name "void") — a terminal sink. It accepts items on main, discards them, and activates nothing. The branch ends there, on purpose.

Why

A branch could always dead-end: a node with no outgoing edges is lowered to the state-graph's END sentinel, which is filtered out of routing and contributes nothing to the next super-step's active set. Wiring nothing to a port has the same effect.

What was missing is the statement. An unwired port reads exactly like a port someone forgot to wire, so an author could not declare "this branch is a side effect; nothing downstream waits on it", and a reviewer could not tell intent from an accident.

One place that ambiguity was resolved against the author: a branch inside a scatter lane that dead-ends is a hard validation error, so fire-and-forget inside a lane was impossible to express at all.

What it is not

  • No concurrency. Work upstream of a void still runs inline in its own super-step; only the result is dropped. For work that should genuinely overlap, that is spawn + TaskRunner.
  • No drain, no cancellation at run end. Abandoning a branch here is exactly what an ungathered spawn ticket already does — which makes spawn → void the explicit spelling of "no gate will ever collect this, and I meant that".

Engine changes: none

engine.rs, graph/** and compiler.rs are untouched. Leaf→END lowering plus the END routing filter already give the exact semantics. This is a model + executor + validation + catalog change.

Validation rules

  • A void with any outgoing edge is refused — it would be dead (a leaf lowers to END) or would make the node not a void. The message names the offending targets, sorted for determinism.
  • A void with no incoming edge is refused. There is no general orphan check in the crate and adding one is out of scope, but a void-specific rule is safe (the kind is new, so no existing graph can trip it) and it is the one kind whose orphan is unambiguously meaningless: a node with no effect and no input.
  • on_error: "route" on a void is caught directly, rather than letting MissingErrorRoute fire. An error edge is an outgoing edge, so the author would otherwise be told to add an edge the next rule then rejects — advice with no fixed point.

The scatter-lane relaxation

A lane branch ending in a void is now legal — this is the primary use case, and the one behavioural rule that had to change.

The existing rule exists because a lane activation deliberately never writes the node's top-level slot, so an accidentally dead-ended lane node's output is invisible — a wrong answer rather than a failure. A void makes that invisibility the contract instead of the accident. Side effects along the branch still happen once per lane; only the data is dropped.

The hole stays closed. region_members returns an empty set unless the walk reached a gather, and an empty members already errors and continues — so a scatter with no gather anywhere is still refused, void downstream or not. a_scatter_whose_only_path_ends_in_void_is_still_rejected pins exactly this. Separately, a void can never appear in a gather's from list, since it cannot have an outgoing edge, so it cannot skew a release policy.

Observability

The slot is {items: [], port: null, discarded: N}. Emitting nothing would otherwise be indistinguishable from never having run, since a node that never ran has no slot at all:

slot meaning
absent (null) never activated
discarded: 0 activated, nothing to drop
discarded: 3 dropped three items

discarded counts that activation, not a running total — in a loop body the last iteration's value survives, and in a scatter lane it lands under lanes.<lane>. A cumulative counter was considered and rejected: it would silently mean something different in each context.

Deliberately not included

No lint for a spawn with neither a gate nor a void downstream. validate_all returns Vec<ValidationError> with no severity tier, so there is no warning channel, and making it a hard error would break the documented "fire-and-forget is legal" contract. Noted in the CHANGELOG as a possible future addition.

Config

None. The node's existing name is where the human reason goes ("Fire and forget: audit log") — already required, and unlike a config key it is rendered by visualization. Config is ignored entirely, including = expressions, so the node can emit no binding diagnostics.

Tests

  • Unit (src/nodes/control_flow/void_tests.rs, 5): emits nothing / no port / no control; exact discarded count; zero-input still reports 0; ignores config and raises no diagnostics; pure across repeated activations.
  • Validation (validate_tests_part_04, 10): leaf accepted; outgoing edge rejected; multiple targets named deterministically; no incoming edge rejected; on_error: "route" yields no MissingErrorRoute; stop/continue accepted; execution still rejected (pins void out of the mapping kinds); plus the three scatter-lane cases.
  • E2E (tests/void_node_tests.rs, 6, all timeout-guarded since these failure modes hang rather than fail):
    • fan_out_arm_into_void_does_not_block_the_other_arm
    • a_void_that_never_runs_leaves_no_slot_at_all
    • void_arm_beside_a_merge_does_not_strand_the_barrier
    • loop_body_with_a_void_side_branch_runs_every_iteration — the motivating case
    • spawn_into_void_completes_without_a_gate
    • scatter_lane_with_a_void_side_branch_gathers_all_lanes
  • Smoke: smoke_void with bespoke assertions — smoke_single_node asserts a non-empty items slot, which a void can never satisfy.

Verification

cargo fmt, cargo clippy --all-targets --all-features (clean), cargo test --all-features (39 test binaries, 0 failures), cargo check on default / host-caps / store, and cargo publish --dry-run.

Summary by CodeRabbit

  • New Features
    • Added a void node that intentionally discards incoming items without triggering downstream execution.
    • Supports fire-and-forget workflows, spawned tasks, loops, conditional branches, and scatter lanes.
    • Records the number of discarded items for visibility.
  • Validation
    • Enforces that void nodes have incoming connections and no outgoing connections.
    • Prevents unsupported error routing and invalid scatter configurations.
  • Documentation
    • Updated the node catalog, README, changelog, and usage guidance with void behavior and examples.

senamakel and others added 30 commits August 14, 2026 00:58
Renamed the NodeKind enum variants to use more descriptive names that better reflect their purpose in the model. This improves code readability and makes the intent of each variant clearer without changing any behavior.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The tests for node kind classification were asserting incorrect behavior, expecting certain node types to be classified in ways that did not match the actual implementation. This change updates the test assertions to reflect the correct classification logic, ensuring the tests validate the intended behavior rather than codifying the previous incorrect expectations.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The void node was previously simplified to a no-op, which broke control flow by allowing execution to continue past it. This change restores the original behavior where the void node halts execution, ensuring that downstream nodes are not reached.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The void tests were previously removed but are now restored to ensure that control flow nodes with void return types are properly tested. This re-adds the test cases that verify the expected behavior of void nodes in the control flow graph.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a trailing newline to the module file to comply with POSIX standards and avoid potential tooling warnings.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a trailing newline to the module file to comply with POSIX standards and avoid potential tooling warnings.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a trailing newline to the module file to comply with POSIX standards and avoid potential tooling warnings.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The execution loop previously skipped nodes whose state transition failed, leaving them permanently unexecuted. This change ensures that a failed transition does not prevent the node from being processed, restoring correct execution behavior.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Removed the unused `std::collections::HashMap` import from the visualization module to keep the codebase clean and avoid compiler warnings.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test module was accidentally removed during a previous refactor, which caused the test suite to silently skip all node-related tests. This change restores the module so that the tests are executed again and regressions are caught.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test module was accidentally removed during a previous refactor, which caused the test suite to silently skip all node-related tests. This change restores the module so the tests are executed again.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The validator previously skipped validation when the input value was null, which allowed invalid null values to pass through unchecked. This change reintroduces the null check so that null inputs are properly rejected according to the validation rules.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The validator previously skipped a null check that was required to prevent a panic when processing certain input values. This change re-adds the check so that null values are handled gracefully instead of causing a runtime error.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The scatter validation previously skipped checks when the input collection was empty, allowing invalid scatter configurations to pass through unnoticed. This change re-enables validation for empty inputs so that scatter constraints are enforced consistently regardless of input size.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The scatter validation previously accepted an empty point list, which could lead to undefined behavior downstream when computing bounds or rendering. This change adds an explicit check that at least one point is present, returning a clear validation error instead.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The validator previously skipped a null check that was required to prevent a panic when processing certain input values. This change re-adds the check so that null values are handled gracefully instead of causing a runtime error.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The validator previously skipped a null check that was required to prevent a panic when processing certain input values. This change restores that check so validation behaves correctly and safely handles null inputs.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test file was previously truncated, removing several test cases that validated important edge cases in the validation logic. This change restores those tests to ensure the full suite of validation scenarios is covered again, preventing regressions in behavior that were previously guarded against.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test file was missing several assertions that verify validation behavior for edge cases. This change adds back the checks to ensure the validation logic is properly covered and prevents regressions.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds the contract definition for a new "void" node kind, which serves as an explicit terminal sink that accepts and discards items without running anything downstream. This makes intentional dead-ends distinguishable from accidentally unwired ports, and the contract documents its validation rules, output slot behavior, and interaction with scatter lanes and spawn nodes.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The catalog now includes the "void" node kind, extending the NODE_KINDS array to 21 entries and mapping it to its contract in contract_for. This makes the void node type available for use in the DSL alongside the existing node kinds.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The group_02 contract file was previously emptied, and this change restores its full set of type and trait definitions. This ensures the catalog contracts for this group are complete again, allowing dependent code to compile and function as expected.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The catalog now includes a new "void" node kind, so the test assertions for the total number of node kinds and contracts have been updated from 20 to 21, and the new kind is verified in the list.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add contract tests for the fan-out component to verify its behavior against the expected interface. This ensures the implementation adheres to the defined contract and catches regressions early.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The catalog test module was accidentally dropped during a refactor, leaving the catalog functionality without test coverage. This change restores the tests to ensure catalog behavior is verified and to prevent regressions.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Added tests covering void node behavior, including parsing, rendering, and edge cases for empty and self-closing elements. This ensures void nodes are handled consistently across the codebase.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Added tests covering void node behavior, including parsing, rendering, and edge cases for empty and self-closing elements. This ensures void nodes are handled consistently across the codebase.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add a dedicated smoke test for the void node kind, which emits nothing by design and therefore cannot satisfy the non-empty slot assertion used by the generic single-node test. The new test verifies that a void node runs successfully, produces an empty items array, counts the discarded input, and leaves the port null.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds the `void` node to the catalog table, describing it as a terminal sink that discards input and runs nothing downstream, serving as an explicit dead end for pipelines.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
This change introduces a new wiki page documenting the available nodes, their purpose, and usage. It provides a reference for users to understand the catalog structure and node options.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The changelog entry describes the addition of a `void` node kind, a terminal sink that explicitly discards items on its `main` port and activates nothing. This provides a deliberate way to declare a branch as a side effect with no downstream consumer, distinguishing it from an accidentally unwired port. The entry also covers validation rules that reject `void` nodes with outgoing or missing incoming edges, the relaxation of the scatter-lane dead-end rule to allow lane branches ending in `void`, and the deliberate omission of a lint for `spawn` nodes without a `gate` or `void` downstream to preserve the documented fire-and-forget contract.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f6e86e7-11a8-457c-8011-a71eb82450ab

📥 Commits

Reviewing files that changed from the base of the PR and between 6e2cc15 and 7d03d57.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • README.md
  • src/catalog.rs
  • src/catalog/contracts/group_02.rs
  • src/catalog/contracts/group_03.rs
  • src/catalog_tests.rs
  • src/fan_out_contract_tests.rs
  • src/model/node_kind.rs
  • src/model/node_kind_tests.rs
  • src/nodes/control_flow/mod.rs
  • src/nodes/control_flow/void.rs
  • src/nodes/control_flow/void_tests.rs
  • src/nodes/execution.rs
  • src/nodes/mod_tests.rs
  • src/validate.rs
  • src/validate/scatter.rs
  • src/validate_tests.rs
  • src/validate_tests/validate_tests_part_04_tests.rs
  • src/visualization.rs
  • tests/smoke_all_nodes.rs
  • tests/void_node_tests.rs
  • wiki/Node-Catalog.md

📝 Walkthrough

Walkthrough

Changes

Void terminal node

Layer / File(s) Summary
Void contract and public model
src/model/node_kind.rs, src/catalog.rs, src/catalog/contracts/*, README.md, CHANGELOG.md, wiki/Node-Catalog.md
Adds NodeKind::Void and its catalog contract. The node accepts main input, has no outputs, discards items, and supports intentional fire-and-forget branches.
Void execution and dispatch
src/nodes/control_flow/void.rs, src/nodes/control_flow/mod.rs, src/nodes/execution.rs, src/nodes/mod_tests.rs, src/nodes/control_flow/void_tests.rs
Implements VoidNode. It records discarded counts, emits no items or successor port, ignores configuration, and supports repeated activation.
Void topology and scatter validation
src/validate.rs, src/validate/scatter.rs, src/validate_tests.rs, src/validate_tests/validate_tests_part_04_tests.rs
Validates required incoming edges, rejects outgoing edges and routed errors, and allows scatter lanes to terminate in void while preserving gather requirements.
Workflow behavior coverage
tests/smoke_all_nodes.rs, tests/void_node_tests.rs
Adds smoke and end-to-end coverage for fan-outs, conditions, merges, loops, spawned tasks, and scatter/gather lanes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: 🔵 Low · up to 7d03d

The PR adds an explicit sink that can discard routed errors or background-task tickets, so workflows may complete without surfacing failures from those side effects; owners should ensure mandatory audit, authorization, and notification work is independently tracked. Workflows persisted with the new void kind also require a compatible runtime during rollback. The change is mergeable with this bounded owner awareness.

Sequence Diagram(s)

sequenceDiagram
  participant WorkflowExecutor
  participant VoidNode
  participant NodeMetadata
  WorkflowExecutor->>VoidNode: execute input items
  VoidNode->>NodeMetadata: store discarded count
  VoidNode-->>WorkflowExecutor: return empty output
Loading

Poem

I am a rabbit beside the flow,
Watching discarded items go.
No downstream paths, no tangled thread,
Just counted hops where work has fled.
void keeps every branch well-read.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes adding the void node as an explicit terminal sink for fire-and-forget branches.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@senamakel
senamakel merged commit cd39220 into main Aug 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant