funnel-core
The pure workflow-DAG core of the nornir funnel — events, ids, state, topological readiness, history, the Plan-DAG projection, and the kanban-card board — with no warehouse / iceberg / model / GPU coupling.
Extracted from nornir/src/funnel/ so both nornir (the viz funnel + Plan-DAG
panels) and ordning-funnel (idea → case graduation) share one idea→plan→node
DAG without dragging in nornir's storage stack. Everything here is deterministic:
given the same events + query string the output is bit-identical, so every surface
is golden-testable with no warehouse, no clock and no GPU.
The shape
A funnel is a fold of an append-only [Event] log into an in-memory [Funnel]:
Event log ──apply──▶ Funnel ──┬─▶ history() → HistoryItem[] (intake list)
├─▶ cards_from_funnel() → Card[] → board() → Board (kanban)
├─▶ query_cards(q) → Card[] (YouTrack-style smart query)
├─▶ topo_ready() → NextStep[] ("what should I work on next?")
├─▶ simulate_flow() → FlowSim (time-step the DAG to done)
└─▶ PlanDag::from_funnel → PlanDag (nodes + edges + topo, viz)
Every projection is pure (&Funnel → value) and BTree-ordered, so the CLI, the
gRPC server and the viz all render the same bytes and never disagree.
Benchmarks
v0.1.3 · oden · 32 cores · 2026-07-22
| workload | assemble_ms | assemblies_per_sec | build_ms | cards | cards_per_sec | cards_scanned | cards_scanned_per_sec | critical_path_len | depth_nodes | edge_births | edges | elapsed_ms | events_per_iter | events_per_sec | hits | iters | node_births | nodes | pass_ms | passes_per_sec | plans | replay_ms | speedup_x | steps | stream_ms | timeline_ms |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| funnel-core.board_project | 5200 | 639440 | 1626 | 200 | ||||||||||||||||||||||
| funnel-core.funnel_build | 60.28 | 4400 | 1206 | 10400 | 172531 | 20 | 400 | |||||||||||||||||||
| funnel-core.plan_dag_critical | 12 | 5200 | 9200 | 3086 | 200 | 5200 | 15.43 | 65 | ||||||||||||||||||
| funnel-core.plan_dag_growth | 599 | 10737 | 2803 | 50 | 301 | 602 | 214.74 | |||||||||||||||||||
| funnel-core.plan_dag_growth_stream | 599 | 2683 | 50 | 301 | 47437 | 211.41 | 602 | 224.38 | ||||||||||||||||||
| funnel-core.plan_dag_whole | 17.06 | 59 | 9200 | 1706 | 100 | 5200 | ||||||||||||||||||||
| funnel-core.smart_query | 5200 | 3.07M | 847 | 2403 | 500 |
The board / smart-query projection (cards_from_funnel → board → query_cards)
and PlanDag::from_funnel are the hot paths the funnel UI runs on every render;
they are benched under bench/ (nornir bench run funnel-core) and, for the
Plan-DAG assembly, by nornir's own nornir.plan_dag_assemble arm.
Tests
Unit tests (51)
| file | tests |
|---|---|
src/card.rs | card_projection_maps_facets_and_labels, hashtag_mining, smart_query_operators, smart_query_sort, board_groups_into_fixed_lanes, empty_funnel_is_honest_empty_board, cards_from_funnel_includes_task_nodes, card_state_changed_and_assigned_are_reflected_in_the_projection, smart_query_assignee_and_for |
src/history.rs | history_derives_kind_status_and_filters |
src/mode.rs | tokens_round_trip_and_are_stable, parse_is_forgiving_and_case_insensitive, mode_gate_maps_to_the_right_lane, default_is_auto |
src/plan_dag.rs | assembles_mother_and_dependency_edges, topo_order_puts_dependencies_before_dependents, scoping_to_a_plan_keeps_its_mother_and_children, state_json_is_deterministic_and_shaped, empty_funnel_is_an_empty_graph, slack_grades_tension_and_critical_nodes_are_zero_slack, critical_nodes_is_a_superset_of_the_single_critical_path, bottlenecks_rank_by_downstream_reach_not_local_fanout, layers_group_nodes_by_depth_and_width_is_the_peak, critical_path_deps_is_strictly_shorter_than_the_mother_lineage, critical_path_deps_picks_the_longer_dep_branch_ignoring_mother, depths_layer_dependencies_before_dependents, critical_path_is_the_longest_chain, critical_path_picks_the_longer_of_two_branches, state_json_carries_the_dag_vocabulary, diff_classifies_added_removed_and_changed, identical_revision_diffs_to_empty, growth_stamps_first_appearance_of_each_node_and_edge, growth_skips_rejected_events_and_scopes_like_from_funnel, extend_streams_byte_identical_births_to_the_batch_fold, extend_matches_batch_scoped_and_skips_rejected_events |
src/state.rs | edge_closing_a_cycle_is_rejected, transitive_and_direct_back_edges_are_both_rejected, plan_still_drains_after_a_cycle_edge_is_refused, valid_run_bumps_high_water_and_never_touches_node_status, high_water_is_monotone_not_last_write, unknown_plan_or_node_is_rejected_and_high_water_unchanged, runs_are_retained_in_log_order_with_every_payload_field, duplicate_run_id_is_rejected, the_same_node_id_in_a_second_plan_is_rejected, distinct_ids_still_land_and_same_plan_dup_keeps_its_message, non_seq_ids_are_still_unique_funnel_wide, card_assignment_stays_unambiguous, folding_the_same_log_twice_yields_identical_state, last_write_wins_fields_follow_log_order |
src/topo.rs | all_pending_reaches_all_done_in_chain_length_waves, a_failed_first_node_stalls_its_dependents |
Integration tests (12)
| file | tests |
|---|---|
tests/public_api.rs | full_projection_pipeline_composes_via_reexports, smart_query_operators_compose, topo_ready_and_simulate_flow_via_reexports, plan_dag_projection_via_reexports, wire_tokens_are_stable, dag_wire_tokens_are_stable, idea_public_field_contract, funnel_mode_vocabulary_via_reexports, plan_dag_critical_path_via_reexports, plan_dag_diff_via_reexports, plan_dag_growth_timeline_via_reexports, apply_rejects_invariant_violations |
Doc tests (0)
(none)
Dependency graph
License
MIT OR Apache-2.0.