Gunnar

mikronova/funnel-core

.nornir/for-idiots.md

<!-- nornir:class: idiot -->
# 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-core` and you've touched
  `nornir`, and therefore `nornir-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.