Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

mikronova/funnel-core

.nornir/plan-dag-critical-path.md

funnel-core — the mode vocabulary + the Plan-DAG critical path

Two additive extensions to the workflow-DAG core, both pure and deterministic (FC-7). Neither changes an existing type's shape or an existing function's signature — new module, new methods, new re-exports only.

1. The FunnelMode vocabulary (solidifying Idea.mode)

Idea.mode is an Option<String> set by an IdeaModeSet event. It is kept free-form on the wire on purpose: a new mode must never force a schema bump, and every pre-keystone log row (no IdeaModeSet) has to replay as None. That storage contract is fixed — consumers (ordning-funnel, nornir's replay) build Idea field-for-field and the field's absence is exactly what once broke ordning-funnel's field-for-field adoption.

The chosen design keeps the field and adds a typed reading layer:

Storage stays a String; reading is typed. The two contracts are separate on purpose.

2. The Plan-DAG critical path + depth layering

The Plan-DAG already exposes nodes, edges and a topological order. Two things a scheduler and a layout engine both want were missing:

All are computed in one topological pass over the existing edge set — O(V + E), no clock, deterministic (equal-length ties break on id). A cyclic graph yields an empty result, mirroring topo_order's Err. They surface additively in state_json (critical_path, critical_path_len, critical_path_deps, critical_path_deps_len, depths) for the headless robot/viz surface.

i-001 (mother, depth 0)
  └─▶ n-001 (depth 1) ─▶ n-002 (depth 2) ─▶ n-003 (depth 3)   ← critical path (len 3)
      └───────────────── n-001 ─▶ n-002 ─▶ n-003 ─────────────┘  ← critical_path_deps (len 2, mother dropped)

The Dep-only twin drops the mother→child hop and highlights just the task→task chain (len 2 here vs the mixed len 3):

3. The bottleneck ranking (which node unblocks the most work)

critical_path() answers how long the schedule is. A second, orthogonal question a funnel pane must surface is which node a human should clear or parallelise next — the constraint point. The chosen design is a single ranking method over a small value type:

Why a full ranking of every node rather than a filtered "bottleneck set": the head is the actionable constraint, but a scheduler wants downstream as a priority weight on all nodes, and a renderer filters group.is_some() for task-only constraints — so returning the whole ranked vector is strictly more useful and still deterministic. It spans both edge kinds (like critical_path), so "downstream" is everything a node gates. One reverse-topological pass accumulates each node's descendant set — O(V·E) on the cold render path; a cyclic/empty graph yields [], mirroring depths. Surfaces additively in state_json["bottlenecks"].

           ┌─▶ n-002        n-005 ─▶ n-006 ─▶ n-007 ─▶ n-008 ─▶ n-009
i-001 ─▶ n-001 ─▶ n-003     (fan_out 1, downstream 4)  ← ranks ABOVE n-001:
           └─▶ n-004         deep reach beats wide fan-out
       (n-001: fan_out 3, downstream 3)

Tests + bench