mikronova/funnel-core
README.md
<!-- ⚠ GENERATED by nornir from .nornir/README.md — DO NOT EDIT this file -->
# 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.
<!-- Write any intro prose here. Anything outside the marker blocks below
is preserved verbatim on every render. -->
## The shape
A funnel is a fold of an append-only [`Event`] log into an in-memory [`Funnel`]:
```text
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
<!-- nornir:gen:start:benches -->
**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 | | | | | | | | | | |
<!-- nornir:gen:end:benches -->
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 / integration / doc tests, enumerated from source via syn. -->
<!-- nornir:gen:start: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)_
<!-- nornir:gen:end:tests -->
## Dependency graph
<!-- nornir:gen:start:depgraph -->

<!-- nornir:gen:end:depgraph -->
## License
MIT OR Apache-2.0.