From a092c385eaeb39f2b0e430b4d7d6142fa3e0dc69 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 2 Jun 2026 00:53:10 -0400 Subject: [PATCH] fix(fusion_plating_shopfloor): job appearing in every not-yet-started stage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression from the partial-order board: _job_presences emitted a card for any area containing a `ready` step. These recipes seed ALL downstream steps to `ready` at job creation, so a job showed in every future stage at once (e.g. WO-30061 across racking/receiving/plating/inspection) even though no parts had advanced there. Fix: a stage shows ONLY where parts physically are (qty_at_step > 0, which includes the first-active seed) OR where a step is in_progress/ paused. A merely ready/pending future step with no parts no longer shows. Strict sequential progress falls out for free — the qty_at_step seed sits on the lowest-sequence non-terminal step and advances as each completes. Co-Authored-By: Claude Opus 4.8 (1M context) --- fusion_plating/CLAUDE.md | 2 +- .../controllers/plant_kanban.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index 050606b8..679ef1be 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -1841,7 +1841,7 @@ A 50-part job can have parts at several stages at once (10 Masking, 20 Plating, **Durable gotchas (non-obvious):** -1. **The plant kanban emits one card PER (job, stage), keyed by a composite `"{job_id}:{area}"`** — NOT one card per job. `cards` is a dict of composite-key → presence payload; a split job lists its key in several `columns[].card_ids`. See `_job_presences` / `_render_presence` in `plant_kanban.py`. A job with all parts at one stage yields exactly ONE presence (identical to the old board). The PRIMARY presence (active-step column) keeps the full job-level `card_state`; SECONDARY presences derive a simpler state from their own focus step (`_secondary_card_state`). Anything reading the board payload must handle composite keys + multi-column jobs. +1. **The plant kanban emits one card PER (job, stage), keyed by a composite `"{job_id}:{area}"`** — NOT one card per job. `cards` is a dict of composite-key → presence payload; a split job lists its key in several `columns[].card_ids`. See `_job_presences` / `_render_presence` in `plant_kanban.py`. A job with all parts at one stage yields exactly ONE presence (identical to the old board). The PRIMARY presence (active-step column) keeps the full job-level `card_state`; SECONDARY presences derive a simpler state from their own focus step (`_secondary_card_state`). Anything reading the board payload must handle composite keys + multi-column jobs. **A presence is emitted ONLY where parts physically are (`qty_at_step > 0`, incl. the first-active seed) OR a step is `in_progress`/`paused` — NEVER for a merely `ready`/`pending` future step.** These recipes seed EVERY downstream step to `ready` at job creation (not `pending`), so keying presence off `ready` made one job show in every not-yet-started stage at once (WO-30061 bug, fixed 2026-06-02). The old single-card board masked this because `active_step_id` picked just one. Strict sequential progress falls out for free: the `qty_at_step` seed always sits on the lowest-sequence non-terminal step and advances as each completes — so don't add `ready` back to the presence condition. 2. **`fp.job.step` has NO `qty_done` / `qty_scrapped` fields.** Those live on `fp.job`. The Move controller previously read `from_step.qty_done - from_step.qty_scrapped` for "available to move" → always 0 → the partial-move path was effectively dead. The source of truth for "parts parked here" is **`qty_at_step`** (move preview/commit + rack moves all read it now). Never reintroduce `step.qty_done`. diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py b/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py index d5e0e15d..52a3ec22 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py @@ -291,10 +291,20 @@ def _job_presences(job): presences = [] for area, steps in by_area.items(): qty_here = sum((s.qty_at_step or 0) for s in steps) - actionable = any( - s.state in ('in_progress', 'paused', 'ready') for s in steps + # A stage shows ONLY where parts physically are (qty_here > 0 — + # which includes the first-active step's qty_at_step seed) OR where + # a step is actively being worked (in_progress / paused — e.g. + # drained to zero but not yet finished). A merely `ready` / `pending` + # step with NO parts is a FUTURE stage and must NOT show — otherwise + # the job appears in every not-yet-started step at once (these + # recipes seed all downstream steps to `ready`, so 6 ready steps = + # 6 phantom cards; bug on WO-30061). Strict sequential progress + # falls out for free because the qty_at_step seed always sits on the + # lowest-sequence non-terminal step and advances as each completes. + being_worked = any( + s.state in ('in_progress', 'paused') for s in steps ) - if qty_here > 0 or actionable: + if qty_here > 0 or being_worked: presences.append((area, _pick_focus_step(steps), qty_here)) if not presences: