Funnel intake — design + guide (EPIC FUNNEL-INTAKE)
LAW 2 doc for the funnel intake feature: paste an error report or an idea straight into the funnel, and browse the intake history, from BOTH the viz Funnel tab and the
nornir funnelCLI. Errors are first-class funnel items (kinderror), triageable to a plan like any idea. Source:src/funnel/{event.rs,state.rs,history.rs},src/warehouse/funnel.rs(+iceberg_schema::funnel_events), CLInornir funnel idea|error|history, serverFunnel.SubmitIdea/History, vizsrc/viz/funnel_tab.rs(paste-in strip + history side-panel).
Why
A human hits an error or has an idea and wants one place to capture it that turns into tracked work. Before, the funnel only ingested "ideas" via funnel submit and had no way to browse what had been submitted. EPIC FUNNEL-INTAKE closes the loop:
- FI1 paste-in — a viz textbox (and
nornir funnel idea/funnel error) to paste a raw error report OR an idea straight in. - FI2 history — browse submitted errors/ideas newest-first, filter by kind/status, open one to see its triage→plan lineage (viz list +
nornir funnel history). - FI3 errors are first-class — an error is a funnel item of kind
error; it triages → a plan node exactly like an idea (Henrik's bug report → an error → a plan).
Item kind: idea | error
A funnel intake item carries an ItemKind — idea (default) or error. It rides the IdeaSubmitted event and the in-memory Idea, and persists as a new optional column item_kind (field id 28) on the event-sourced funnel_events Iceberg table. Optional + defaulted means it is migration-safe: every pre-FI row (no value) replays as an idea, and a server table created before FI is auto-evolved forward (Iceberg add-column) on the first FI write — see warehouse-schema.md § On-append auto-evolution.
Derived history status
History is a pure projection over the replayed funnel (funnel::history) — never stored, always derived:
plan exists for item ⇒ planned (the loop closed: intake → tracked work)
else triage == accept ⇒ accepted
else triage == drop ⇒ dropped
else (no triage) ⇒ untriaged
Items sort newest submission first; the projection plus its kind/status filters back the CLI, the Funnel.History RPC, and the viz history list, so the three never disagree (one inject-assert test pins the derivation).
CLI (parity with the viz)
# FI1 — paste in
nornir funnel idea "add a dark theme" # an idea (alias of `submit`)
nornir funnel error - < panic_report.txt # an error from stdin
nornir funnel error "panic at foo.rs:42" # an error inline
# text precedence: --file <path> → positional arg (`-` = stdin) → stdin pipe
# FI2 — browse history (newest-first), JSON by default
nornir funnel history # everything
nornir funnel history --kind error # only error reports
nornir funnel history --status untriaged --table # human table
nornir funnel history --kind idea --status planned # combined filter
funnel idea/funnel error print the new item id (e.g. i-007). history emits a JSON array of {id, item_kind, text, source, submitted_at, status, plan_ids} (or a compact table with --table).
Viz (Funnel tab)
- Paste-in strip (top): an
💡 idea/🐞 errortoggle + a multiline textbox + Submit. Submitting records the item (localStoreor theFunnel.SubmitIdeaRPC with the kind), clears the box, and refreshes. - History side-panel (right): newest-first list, kind/status filter chips, error rows tinted red; click an item to expand its source, submission time, and its triage→plan lineage (the plan ids refining it).
- Introspection (LAW 6):
state_json()exposes anintakeblock (is_error,text_len, laststatus) and ahistoryblock (count,error_count,filter_kind,filter_status,open, and theitemsarray) — "see what the user sees" as readable data.
FI3 — error → plan, same as an idea
An error item is just an Idea with item_kind = Error; it has the same id space (i-NNN) and is triaged + planned with the existing verbs:
ID=$(nornir funnel error - < report.txt) # capture
nornir funnel plan "$ID" "fix the panic" # → a plan node like any idea
Data + control flow
intake (FI1) history (FI2)
├─ viz paste strip (💡 idea/🐞 error) ─┐ ┌─▶ viz history side-panel
└─ nornir funnel idea|error ──────────┤ ├─▶ nornir funnel history
▼ ├─▶ Funnel.History RPC
IdeaSubmitted{item_kind} ──▶ funnel_events ──▶ funnel::history
(+ item_kind col 28) (derive status + lineage)
edges:
viz paste strip → IdeaSubmitted
nornir funnel CLI → IdeaSubmitted
IdeaSubmitted → funnel_events (warehouse)
funnel_events → funnel::history
funnel::history → viz history side-panel / nornir funnel history / Funnel.History RPC
IdeaSubmitted ⋯⋯⋯▶ funnel_events (kind=error: triage → plan)
Tests (inject-assert)
funnel::history::tests::history_derives_kind_status_and_filters— inject an idea, an error, and an idea-with-plan; assert kind/status/lineage + filters.warehouse::funnel::tests::roundtrip_all_event_kinds— bothitem_kinds round-trip through Iceberg.warehouse::funnel::tests::stale_27col_funnel_table_evolves_and_kind_round_trips— a pre-FI 27-col table evolves to 28 on the first error append, kind reads back.viz::funnel_tab::tests::intake_idea_and_error_show_in_history_and_filter— submit an idea + an error through the tab; assertstate_jsonhistory (newest-first, error kinded/counted) and the kind filter.