# Monster Function Refactoring Plan

**Date:** 2026-03-10
**Goal:** Break three 300+ line, 50+ branch functions into testable, readable units without changing behavior.

## Overview

Three functions in the jobs system have grown into god-functions that each do 5-8 things. They are hard to test, hard to read, and fragile to modify. Each one can be decomposed via extract-function refactoring into 4-8 smaller functions with clear single responsibilities.

| Function                   | File                  | Lines | Branches | Core Problem                         |
|----------------------------|-----------------------|-------|----------|--------------------------------------|
| `stage_worker()`           | `jobs/runner.py`      | 326   | 58       | Version triage + loop + item processing + result dispatch all in one |
| `get_queue()`              | `jobs/ui/queries.py`  | 321   | 61       | DB fetch + row formatting + stage summary + pipeline + events + markdown assembly |
| `_build_job_group_tab()`   | `jobs/app_ng.py`      | 356   | 51       | UI layout + data loading + 7 nested event handlers + auto-refresh |

**Principles:**
- Pure extract-function: move code into new functions, call them from the original. No logic changes.
- Each extracted function should be independently testable (pure data in, data out where possible).
- Preserve all existing behavior, including edge cases.

---

## 1. `stage_worker()` — `jobs/runner.py:461-787`

### Current Structure

Four distinct phases jammed together:

1. **Semaphore setup** (~17 lines): Resolve shared resource semaphore or create per-stage one.
2. **Version triage** (~67 lines): Snapshot handler version, compare with old, LLM-triage cosmetic vs semantic changes, auto-bless or notify. Already bracketed with `# --- Version triage ---` comments.
3. **Main loop — control flow** (~65 lines): Limit checking, pause detection, resume limit management, semaphore hot-swap, item fetching, eligibility filtering.
4. **Nested `process_one()`** (~130 lines): Item execution, result dispatch (dict with `_fail`/`_priority`/`_cost`/`_spawn_items`, numeric, string), dry-run revert, error classification, stage event waking, fan-in detection.

### Proposed Extractions

| ID | Function | Lines | Risk | Notes |
|----|----------|-------|------|-------|
| E1 | `_resolve_stage_semaphore(stage_cfg, resources)` | 473-485 | Safe | Pure logic, no shared state |
| E2 | `_triage_handler_version(job, stage, handler, version)` | 490-557 | Safe | Already self-contained, opens own DB conn |
| E3 | `_resolve_effective_limit(ctx, job, conn, item_counter)` | 566-596 | Moderate | Mutates item_counter, return `should_stop` bool |
| E4 | `_hot_swap_semaphore(stage_cfg, sem, concurrency, ...)` | 599-607 | Safe | Pure logic |
| E5 | `_dispatch_result(conn, job, stage, item_id, ...)` | 679-752 | Moderate-high | Many side effects, fan-in wake logic |
| E6 | `_wake_downstream_stages(conn, job, stage, item_id, stage_events)` | 727-752 | Safe | Extract from E5, read-only DB + event signaling |

**Coupling:** `process_one()` captures ~12 variables from enclosing scope. Consider a `StageWorkerContext` dataclass to bundle them (like existing `RunContext` pattern).

**After:** `stage_worker` becomes ~80 lines: setup, version triage call, main loop with limit check, item fetch, gather, sleep/backoff.

### Extraction Order

E2 → E1 → E4 → E6 → E5 → E3

---

## 2. `get_queue()` — `jobs/ui/queries.py:199-519`

### Current Structure

Six distinct responsibilities:

1. **Job metadata** (~14 lines): Parse display string, load YAML, get stages.
2. **DB fetch + result merging** (~50 lines): Fetch work_items, batch-fetch results, merge fields.
3. **Column definition** (~7 lines): Convert `detail_cols` config to tuples.
4. **Row formatting** (~78 lines): Per-item: parse data, stage indicator, status, columns, bold for recent. Core complexity.
5. **Stage pipeline summary** (~60 lines): Markdown table with metrics, throughput, ETA, stale, bottleneck.
6. **Ancillary summaries** (~96 lines): Pipeline context, events, error/pause/cost/guard, final markdown.

### Proposed Extractions

| ID | Function | Lines | Risk | Notes |
|----|----------|-------|------|-------|
| E1 | `_fetch_queue_items(conn, job_id)` | 219-264 | Safe | Pure DB reads |
| E2 | `_format_queue_row(r, data, ctx)` | 276-354 | Moderate | Bundle shared state into `_QueueRowContext` NamedTuple |
| E3 | `_build_stage_summary_md(conn, job, job_id, ...)` | 362-421 | Safe | Self-contained DB + formatting |
| E4 | `_build_pipeline_summary_md(conn, job, job_id)` | 423-440 | Safe | Trivial |
| E5 | `_build_events_summary_md(events)` | 486-504 | Safe | Pure formatting |
| E6 | `_build_queue_header_md(...)` | 454-517 | Safe | Pure string assembly, many params |

**Also:** Move `STATUS_ICONS` and `EVENT_ICONS` to module level (constants defined inline).

**After:** `get_queue` becomes ~50 lines: resolve job, fetch items, format rows, build summaries, assemble return.

### Extraction Order

E5 → E4 → E1 → E3 → E6 → E2

---

## 3. `_build_job_group_tab()` — `jobs/app_ng.py:991-1347`

### Current Structure

Three responsibility zones:

1. **UI layout** (~120 lines): 7 UI cards/panels (overview grid, stage metrics, pipeline bar, items grid, doctor, events, detail).
2. **Data loading helpers** (~34 lines): `_get_job_meta()`, `_load_items()`, `_since_from_toggle()`, `_refresh_overview()`. Closures capturing UI elements.
3. **Event handlers** (~180 lines): 8 nested functions. Largest: `on_overview_click` (~85 lines).

### Proposed Extractions

| ID | Function | Lines | Risk | Notes |
|----|----------|-------|------|-------|
| E0 | Define `_TabWidgets` dataclass | — | Safe | Holds all UI refs + state + config |
| E1 | `_build_overview_card(label, jobs, ...)` | 1004-1048 | Safe | Pure NiceGUI widget construction |
| E2 | `_build_detail_panels()` | 1050-1111 | Safe | Pure NiceGUI widget construction |
| E3 | `_on_overview_click(e, state, widgets, ...)` | 1188-1273 | Moderate | Captures 15+ UI elements → use `_TabWidgets` |
| E4 | `_on_item_click(e, widgets, state)` | 1305-1336 | Safe | Few captures |
| E5 | `_bind_tab_events(widgets, state, ...)` | 1167-1346 | Moderate | Wiring function, connects everything |

**Coupling:** Every nested function captures UI element references. The `_TabWidgets` dataclass is the key enabler — bundles all widget refs into one passable object.

**After:** `_build_job_group_tab` becomes ~40 lines: create widgets, build overview card, build detail panels, bind events.

### Extraction Order

E0 → E1 → E2 → E4 → E3 → E5

---

## Execution Order (Across Functions)

| Phase | Function             | Why this order?                                                    |
|-------|----------------------|--------------------------------------------------------------------|
| 1     | `get_queue()`        | Lowest risk: pure data formatting, no async, no UI framework       |
| 2     | `stage_worker()`     | Medium risk: async but well-tested via `inv jobs.test`             |
| 3     | `_build_job_group_tab()` | Highest risk: NiceGUI closure patterns, UI state management   |

**Do not interleave.** Complete one function before starting the next.

---

## Testing Strategy

### `get_queue()` extractions
- **Unit tests** for pure formatters: `_build_events_summary_md()`, `_build_queue_header_md()` — simple inputs, string outputs
- **Integration test** for `_fetch_queue_items()`: Use `tmp_path` SQLite fixture from `jobs/tests/test_stages.py`
- **Smoke test:** Run dashboard, click through tabs, verify identical behavior

### `stage_worker()` extractions
- **Unit tests** for `_resolve_stage_semaphore()`, `_resolve_effective_limit()` — mock configs, verify returns
- **Integration test** for `_triage_handler_version()`: Mock LLM call, verify DB writes
- **Integration test** for `_dispatch_result()`: Test each result type path with test DB fixture
- **End-to-end:** `inv jobs.test -j TEST_JOB --stage STAGE --item KEY`

### `_build_job_group_tab()` extractions
- **Layout functions** (`_build_overview_card`, `_build_detail_panels`): Manual smoke test (NiceGUI requires event loop)
- **Event handlers**: Test with mock widget objects once extracted
- **Smoke test:** `python jobs/app_ng.py`, navigate tabs, click jobs, verify all panels

### Regression Safety Net

Before each function's refactoring:
1. `python -m pytest jobs/tests/ -v` — record passing count
2. `inv jobs.test -j` for at least one active job
3. Screenshot dashboard baseline

After each extraction:
1. Re-run same test suite
2. Compare dashboard output for one job
