funnel-core — for beginners
No jargon. What this little library is and why it exists.
The one-sentence version
funnel-core turns a list of things that happened ("someone submitted an idea", "a plan was made", "a task finished") into an answer to a question ("what should I work on next?", "show me the kanban board", "which bugs are still open?").
The mental picture
Think of a diary. Each line is one small fact, written once and never erased:
- rickard had an idea: "add a dark theme"
- we made a plan for it
- the plan has 3 tasks: A, then B, then C
- task A is finished
funnel-core reads the whole diary from the top and rebuilds the current picture in memory. That picture is called a Funnel. You never edit the picture directly — you add a new diary line (an Event) and re-read.
It also refuses lines that don't make sense — a task that depends on itself, the same dependency written twice, or a loop ("A waits for B, B waits for A"). A loop is the nasty one: nothing in it can ever start, so the whole plan would freeze. funnel-core spots the loop the moment you try to add the line that would close it and says no, keeping every plan finish-able.
What you get out of it
Once the diary is read, you can ask for different views of the same facts:
- History — the list of everything submitted, newest first.
- Board — a kanban board with columns (Inbox → Accepted → Proposed → Planned → Dropped), each idea/task sitting in the right column.
- Cards + search — every item as a card, filterable with a search box like
type:bug is:open for:me(the same style GitHub/JIRA use). - "What's next?" — the tasks whose prerequisites are all done, in the right order (task B only shows up once task A is finished).
- Plan-DAG — the boxes-and-arrows diagram the nornir app draws.
- The critical path — the longest run of "this task must finish before that one can start". It's the chain that decides how long the whole plan takes: make it shorter and everything finishes sooner. The library can point right at it. There are two flavours: one counts from the original idea down through its tasks (how deep is the whole plan), and a dependency-only one that ignores the idea and counts just the "task waits on task" hops (how deep is the actual work). The work-only chain is always the same length or shorter.
- Slack — for every task, how many "rounds" late it could start without making the whole plan finish later. A task with zero slack is on a longest chain — it's a bottleneck you can't afford to delay; the library lists all of them. A task with slack 2 has two rounds of wiggle room a scheduler can use.
- Bottlenecks — "which task, if I finished it next, would free up the most other work?" The library counts, for every box, how many other boxes are stuck waiting behind it (directly or further down the line) and ranks them, biggest first. The top one is where a human should push. A surprising-but-true rule falls out: a task that feeds one long chain beats a task that feeds three dead-end leaves — because the long chain has more work stalled behind it, even though the other task points at more arrows right now. So "unblocks the most" isn't just "has the most arrows".
- What changed — hand the library the old version of a plan and it tells you the difference to the new one: which tasks were added or removed, which dependency arrows appeared or disappeared, and which surviving tasks changed (renamed, finished, or re-wired). If nothing changed, the difference is empty. This is what a "re-plan" screen shows so you can see a revision at a glance.
- The growth story — read the diary line by line and the library tells you, for every box and arrow in the diagram, which line it first showed up on. Since the diary only ever adds things (never erases), the diagram only ever grows. That's exactly what you need to play the diagram building itself like a little movie — boxes popping in in order — and to drag a slider back through history to see how the plan looked at any earlier moment. It also gives you a simple running count ("3 boxes so far, then 4, then 4 with an arrow…") for a progress line. There are two ways to get this movie. The batch way reads the whole diary at once — perfect when you open an old plan and want the finished movie or a history slider. The live way is for watching it happen right now: you hand the library one new diary line each time it's written and it tells you just the boxes that popped in on that line — no re-reading the whole diary every time. Both ways produce the exact same movie frame-for-frame (the library checks this), so a screen can start with the batch movie and then keep growing it live line by line.
Joining the plan to the actual code
Here's a thing the diary alone can't tell you: which files/crates does this plan actually touch?
Picture two diagrams side by side. On the left, the plan: boxes like "write the importer", "fix the crash". On the right, the code: boxes like funnel-core, nornir, nornir-viz, with arrows showing which one needs which. Now imagine drawing a string from a plan box across to a code box: "this task touches that crate." That string is a wire, and the pile of them is the wiring.
Once the strings are there you get two useful answers:
- Blast radius. Pick a plan box, follow its strings to the code boxes, then keep walking the code arrows backwards — to everything that depends on those boxes. That's everything that might break. Touch
funnel-coreand you've touchednornir, and thereforenornir-viz, and therefore the app. - Who owns this crate? Go the other way: a crate with lots of changes and no string attached to any plan box is work nobody planned.
The part that matters most: admitting what isn't wired
It would be very easy — and very dishonest — for the library to look at a plan box with no string attached and cheerfully report "affects nothing". Empty because there's genuinely nothing downstream, and empty because nobody wired it up, are completely different answers that look identical.
So the library refuses to conflate them. Every plan box is listed as either mapped (has a string) or unmapped (doesn't), with a reason for the second:
- no wire — nobody ever connected it. Just a gap.
- only proposed — the computer guessed a connection but no human has said yes. A guess is not a fact, so it doesn't count yet.
- only rejected — someone looked at the guesses and threw them all away. Needs a fresh one.
- dangling — there is a confirmed string, but it's tied to a crate that doesn't exist (renamed, typo). This is the nastiest one, because at a glance it looks fine.
And there's one row the library shouts about: a task marked finished that was never wired to anything. That means work landed in code the plan can't point at.
Strings are never deleted, by the way — the diary never erases. Rejecting a connection is just a new line saying "no, not that one", which is how the computer remembers not to suggest it again.
Why it's built this way
Because the answer is always computed fresh from the facts, the same diary always gives the same answer — no surprises, nothing hidden, and it's trivial to test. And because it holds none of the heavy machinery (no database, no graphics), both the big nornir app and the ordning app can use the exact same rules for free.