Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
127 commits
Select commit Hold shift + click to select a range
99cf008
feat(ward-flow): Ward Flow prototype baseline — routes, model, spec a…
BigSimmo Aug 18, 2026
f047863
feat(ward-flow): add the injectable clock the model will run on
BigSimmo Aug 18, 2026
b2f7cf3
feat(ward-flow): define the phase 1 domain types
BigSimmo Aug 18, 2026
3cf1e89
feat(ward-flow): add eligibility gates with destination-only authoris…
BigSimmo Aug 18, 2026
20bdbc8
feat(ward-flow): model sites with emergency departments and units
BigSimmo Aug 18, 2026
e378b70
fix(ward-flow): replace unused @ts-expect-error with explicit cast in…
BigSimmo Aug 18, 2026
1425b38
feat(ward-flow): build movements at realistic metro pressure
BigSimmo Aug 18, 2026
b2388cd
refactor(ward-flow): migrate every route onto the phase 1 model
BigSimmo Aug 18, 2026
f060ae7
fix(ward-flow): address Task 6 review round 1 — honesty, non-ordinal …
BigSimmo Aug 18, 2026
cba50d8
fix(ward-flow): address Task 6 review round 2 — cover elapsedLabel, f…
BigSimmo Aug 18, 2026
6adab9a
docs(ward-flow): correct ADR 1 and reconcile the glossary with the ph…
BigSimmo Aug 18, 2026
24ddf25
docs(ward-flow): add the durable phase handoff
BigSimmo Aug 18, 2026
787029a
fix(ward-flow): repair whole-branch review defects across Phase 1 mod…
BigSimmo Aug 18, 2026
de7b92f
docs(ward-flow): record the Phase 1 closing state and parked rulings
BigSimmo Aug 18, 2026
8c33cd1
docs(ward-flow): add the Phase 2 plan and the fresh-session kickoff b…
BigSimmo Aug 18, 2026
00a21bc
docs(ward-flow): add the complete context document
BigSimmo Aug 18, 2026
3dbdd5e
feat(ward-flow): define the operational score with no urgency component
BigSimmo Aug 18, 2026
d1b2a3c
style(ward-flow): apply prettier to the ward management console
BigSimmo Aug 18, 2026
f2b98d8
fix(ward-flow): correct the operational score's fraction, transport a…
BigSimmo Aug 18, 2026
ef4e62c
feat(ward-flow): derive per-department pressure from open movements
BigSimmo Aug 18, 2026
992c3e6
fix(ward-flow): make edPressure injectable and close three unfalsifia…
BigSimmo Aug 18, 2026
ec2e594
feat(ward-flow): add the coordinator screen shell and five regions
BigSimmo Aug 18, 2026
015079f
fix(ward-flow): address Task 3 review — lint, broken journeys, overfl…
BigSimmo Aug 18, 2026
d7f5b76
feat(ward-flow): add the emergency department pressure strip
BigSimmo Aug 18, 2026
b3d8fd2
fix(ward-flow): address Task 4 review — order/filter proof, a11y name…
BigSimmo Aug 18, 2026
7b97608
feat(ward-flow): add the priority queue, tier first and score within …
BigSimmo Aug 18, 2026
3d46d35
fix(ward-flow): address Task 5 review — unpin ordering/score/breach p…
BigSimmo Aug 18, 2026
90ed4af
feat(ward-flow): add the flow diagram, departments to units
BigSimmo Aug 18, 2026
19b0ca0
fix(ward-flow): address Task 6 review — never show an ineligible unit…
BigSimmo Aug 18, 2026
b8bd43d
feat(ward-flow): add the explainable shortlist with verdict-stating g…
BigSimmo Aug 18, 2026
65e5bc0
fix(ward-flow): pin gate ordering, decline count, and suggestion elig…
BigSimmo Aug 19, 2026
2eed009
feat(ward-flow): add the exceptions drawer and the phone form
BigSimmo Aug 19, 2026
06cc4d2
fix(ward-flow): close the exceptions-drawer review gaps
BigSimmo Aug 19, 2026
19e9e32
refactor(ward-flow): retire Constellation, move mode nav into the rail
BigSimmo Aug 19, 2026
04cf7bc
fix(ward-flow): pin rail tap targets to 3rem, remove orphaned console…
BigSimmo Aug 19, 2026
52b001a
fix(ward-flow): close the whole-branch review findings on the coordin…
BigSimmo Aug 19, 2026
3789eea
fix(ward-flow): require an explicit candidate before Override, not ju…
BigSimmo Aug 19, 2026
0ccebee
docs(ward-flow): add the Phase 3 role-screens design spec
BigSimmo Aug 19, 2026
657e033
docs(ward-flow): revise the Phase 3 spec after adversarial review
BigSimmo Aug 19, 2026
7f373e8
docs(ward-flow): correct the context document and extend the Phase 3 …
BigSimmo Aug 19, 2026
9bb836e
docs(ward-flow): add the Phase 3 implementation plan
BigSimmo Aug 19, 2026
700075a
docs(ward-flow): encode the 1A/3B rule — the form follows the examina…
BigSimmo Aug 19, 2026
cf75150
docs(ward-flow): give voluntary-on-locked-ward its own distinct flag
BigSimmo Aug 19, 2026
fbd9a86
docs(ward-flow): correct three pre-flight defects in the Phase 3 plan
BigSimmo Aug 19, 2026
f3b1f74
feat(ward-flow): add the Phase 3 model fields and the out-of-catchmen…
BigSimmo Aug 19, 2026
39042cd
fix(ward-flow): give the privacy guard real strings to inspect and a …
BigSimmo Aug 19, 2026
3b76b09
feat(ward-flow): add the pure state reducer and its refusals
BigSimmo Aug 19, 2026
e7faa7b
test(ward-flow): pin six previously-uncovered reducer branches, name …
BigSimmo Aug 19, 2026
f01a4f8
test(ward-flow): pin the phase 3 state invariants
BigSimmo Aug 19, 2026
cbdd47f
test(ward-flow): fix round 1 — ground the phase 3 invariants in walk-…
BigSimmo Aug 19, 2026
e2b72a3
docs(ward-flow): reconcile the Phase 3 plan with the contracts as built
BigSimmo Aug 19, 2026
0612fdf
feat(ward-flow): add the state provider and the ticking clock
BigSimmo Aug 19, 2026
2d59219
fix(ward-flow): derive every 3B deadline from its own examination
BigSimmo Aug 19, 2026
9ae3342
fix(ward-flow): unwrap the midnight rollover so the live clock cannot…
BigSimmo Aug 19, 2026
4d36099
feat(ward-flow): make the coordinator screen live and refer rather th…
BigSimmo Aug 19, 2026
868853b
fix(ward-flow): never claim a referral succeeded when the reducer ref…
BigSimmo Aug 19, 2026
af90428
refactor(ward-flow): every route reads one source of truth
BigSimmo Aug 19, 2026
b5caa53
fix(ward-flow): read the live clock instead of the frozen fixture epoch
BigSimmo Aug 19, 2026
18f5773
test(ward-flow): guard the whole class of frozen-clock reads, not one…
BigSimmo Aug 19, 2026
f3ebd8c
docs(ward-flow): add the Phase 3 session handover and a committed led…
BigSimmo Aug 20, 2026
c8f7b22
test(ward-flow): declare-list the NOW_ANCHOR guard and derive QueueVi…
BigSimmo Aug 20, 2026
845b7d4
test(ward-flow): widen the NOW_ANCHOR allow-list guard to the whole s…
BigSimmo Aug 20, 2026
f4963f2
test(ward-flow): replace the NOW_ANCHOR comment/string scanner with a…
BigSimmo Aug 20, 2026
5f5c011
docs(ward-flow): refresh the Phase 3 handover and ledger after Task 6…
BigSimmo Aug 20, 2026
2d8200a
fix(ward-flow): delete the fabricated Form 3B legal deadline
BigSimmo Aug 21, 2026
f1e32dc
test(ward-flow): repin WF-017 by id and quarantine ED_ACCESS_TARGET_M…
BigSimmo Aug 21, 2026
496039d
perf(tests): share a single src walk/read across ward-flow-single-sou…
BigSimmo Aug 21, 2026
74a174a
docs(ward-flow): rewrite the Phase 3 handover and refresh the ledger …
BigSimmo Aug 21, 2026
72b0145
docs(ward-flow): add the complete cross-phase ledger and a durable co…
BigSimmo Aug 21, 2026
19ae5c6
fix(ward-flow): drop two unused imports and format the workspace copy…
BigSimmo Aug 21, 2026
8f0c3cc
docs(ward-flow): correct the handover after the push, and retract an …
BigSimmo Aug 21, 2026
15bce2e
docs(ward-flow): name the strongest explanation for the emptied node_…
BigSimmo Aug 21, 2026
a75c508
docs(ward-flow): browser gate verified green at HEAD, 24 passed
BigSimmo Aug 21, 2026
ee82faa
docs(ward-flow): record session 3 pre-flight findings and rulings R35…
BigSimmo Aug 21, 2026
adbe329
feat(ward-flow): pin the phone referral bar instead of scrolling to it
BigSimmo Aug 21, 2026
cecc953
feat(ward-flow): separate the transport leg from the provider narrative
BigSimmo Aug 21, 2026
3b4bf41
fix(ward-flow): assert the real scroll container, not window.scrollY
BigSimmo Aug 21, 2026
d819ad9
fix(ward-flow): surface the voluntary-on-locked warning on the flow d…
BigSimmo Aug 21, 2026
88de392
docs(ward-flow): record rulings R45-R53 and the four resolved pre-fli…
BigSimmo Aug 21, 2026
171adb6
feat(ward-flow): add the ward screen
BigSimmo Aug 21, 2026
51ed0e3
feat(ward-flow): add the transport officer phone screen
BigSimmo Aug 22, 2026
1349c21
fix(ward-flow): give every in-transit patient the collection its stag…
BigSimmo Aug 22, 2026
b2e0a92
feat(ward-flow): rewrite transport as the coordinator's live tracker
BigSimmo Aug 22, 2026
dc5daff
docs(ward-flow): record Tasks 8-10, the fixture coherence fix, and ru…
BigSimmo Aug 22, 2026
66c4f7b
feat(ward-flow): add the emergency department screen with both clocks
BigSimmo Aug 22, 2026
954c153
fix(ward-flow): stop five patients being ready to hand over to a ward…
BigSimmo Aug 22, 2026
f08abf3
fix(ward-flow): raise the ED access target from 4h to 24h per clinician
BigSimmo Aug 22, 2026
2affc37
fix(ward-flow): rank a patient confirmed to need a bed above one nobo…
BigSimmo Aug 22, 2026
1e7501f
docs(ward-flow): record Task 11, both clinical changes, and rulings R…
BigSimmo Aug 22, 2026
9168160
feat(ward-flow): add the role switcher and prove the loop end to end
BigSimmo Aug 22, 2026
1813124
docs(ward-flow): consolidate all 73 Phase 3 rulings into one owner-fa…
BigSimmo Aug 22, 2026
e8f9805
fix(ward-flow): model Form 1A expiry on new referrals
BigSimmo Aug 22, 2026
57e3374
fix(ward-flow): attach examination forms to non-voluntary referrals
BigSimmo Aug 22, 2026
a5716d8
fix(ward-flow): derive candidates and inbox items from live state
BigSimmo Aug 22, 2026
1a6f163
fix(ward-flow): surface expired bed holds in the action inbox
BigSimmo Aug 22, 2026
8fd1a6d
fix(ward-flow): use live unit candidates in patient workspace
BigSimmo Aug 22, 2026
a431ead
fix(ward-flow): use live unit candidates in network workspace
BigSimmo Aug 22, 2026
5cf78df
fix(ward-flow): use live unit candidates in flow diagram
BigSimmo Aug 22, 2026
db6e8f9
fix(ward-flow): use live unit candidates in shortlist
BigSimmo Aug 22, 2026
5ae4fbf
test(ward-flow): cover live capacity and expired bed holds
BigSimmo Aug 22, 2026
2c9c56e
test(ward-flow): cover new non-voluntary referrals
BigSimmo Aug 22, 2026
73a6a93
fix(ward-flow): stop the board claiming referrals and withdrawals tha…
BigSimmo Aug 22, 2026
421d9a6
fix(ward-flow): read live unit capacity instead of the frozen fixture
BigSimmo Aug 22, 2026
0234c87
fix(ward-flow): delete the deprecated frozen-capacity eligibleCandida…
BigSimmo Aug 22, 2026
3cc986f
fix(ward-flow): give a raised referral a Form 1A so its examination c…
BigSimmo Aug 22, 2026
13002dc
fix(ward-flow): two small honesty fixes -- real referral count, right…
BigSimmo Aug 22, 2026
113548c
fix(ward-flow): announce a refusal on the collapsed exceptions trigge…
BigSimmo Aug 22, 2026
efc9462
fix(ward-flow): wire spec sec11 escalation to a real control on the s…
BigSimmo Aug 22, 2026
2db08e0
fix(ward-flow): build the jump-forward clock and scenario reset (I3)
BigSimmo Aug 22, 2026
23a3ee0
docs(ward-flow): rewrite the handover for a diverged branch mid-recon…
BigSimmo Aug 22, 2026
6e1300f
fix(ward-flow): address three P1 review findings
claude Aug 22, 2026
663a9f4
chore(ledger): record Run PR sweep review for PR #2289
claude Aug 22, 2026
9c217c8
fix(ward-flow): drop the fabricated Form 1A legal deadline entirely
BigSimmo Aug 22, 2026
d784b33
Merge main into codex/ward-management-design
BigSimmo Aug 22, 2026
57bde16
chore(ledger): record main-merge review for PR #2289
BigSimmo Aug 22, 2026
f59b6dc
Merge the diverged ward-management branch, resolving per file with in…
BigSimmo Aug 22, 2026
cef2f3e
fix(ci): reconcile Ward Flow drift checks
BigSimmo Aug 22, 2026
b37be73
docs(ledger): record CI repair validation
BigSimmo Aug 22, 2026
680d2b0
fix(ward-flow): keep main a11y and restore constellation redirect
cursoragent Aug 22, 2026
dcfd32d
Merge remote-tracking branch 'origin/codex/ward-management-design' in…
BigSimmo Aug 22, 2026
34b282f
fix(ward-flow): drop a third fabricated statutory figure and a broken…
BigSimmo Aug 24, 2026
12f17b1
style(ward-flow): apply Prettier to the four files the push guard fla…
BigSimmo Aug 24, 2026
0a1eb56
test(ward-flow): give the mode-loop journey a budget that fits its work
BigSimmo Aug 24, 2026
5188cae
Merge origin/main into codex/ward-management-design
BigSimmo Aug 24, 2026
3179908
Merge branch 'main' into codex/ward-management-design
BigSimmo Aug 24, 2026
574bb19
docs(ward-flow): repair three broken references the merge exposed
BigSimmo Aug 24, 2026
3d7637f
Merge remote-tracking branch 'origin/codex/ward-management-design' in…
BigSimmo Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/design-system/adoption-manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -1564,7 +1564,8 @@
"tests/ui-formulation.spec.ts",
"tests/ui-smoke.spec.ts",
"tests/ui-v2-components.dom.test.tsx",
"tests/ui-ward-coordinator.spec.ts"
"tests/ui-ward-coordinator.spec.ts",
"tests/ui-ward-roles.spec.ts"
],
"baseline": {
"targetLayer": "v2",
Expand Down
516 changes: 239 additions & 277 deletions docs/ward-flow-phase-3-handover.md

Large diffs are not rendered by default.

458 changes: 458 additions & 0 deletions docs/ward-flow-phase-3-workspace/whole-branch-review.md

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
Expand Up@@ -36,10 +36,13 @@ const PHONE_DIAGRAM_MEDIA_QUERY = "(max-width: 48rem)";
export function CoordinatorScreen() {
// Task 5: the screen's props stop being derived from the frozen `wardMovements` fixture and
// `NOW_ANCHOR` constant and start coming from the shared provider (`WardFlowProvider`, already
// wrapping every `/ward-management` route via `src/app/ward-management/layout.tsx`). `units`
// is not destructured here — nothing this screen renders yet reads live unit state, and an
// unused destructured value would be dead weight rather than real wiring.
const { movements, rejections, now, dispatch, focusMovementId, setFocusMovementId } = useWardFlow();
// wrapping every `/ward-management` route via `src/app/ward-management/layout.tsx`).
//
// Whole-branch review Critical 1: `units` IS now destructured and threaded into `FlowDiagram`
// and `ShortlistPanel` below. It was deliberately left out here on the original claim that
// "nothing this screen renders yet reads live unit state" — false: both child components read
// unit capacity, and both were doing it from the frozen `ward-sites.ts` fixture instead.
const { movements, units, rejections, now, dispatch, focusMovementId, setFocusMovementId } = useWardFlow();
// Task 12: seeded from the shared `focusMovementId` (not always `undefined`) so a coordinator
// who switched away to answer a referral as another role and switches back finds the same
// patient still selected — this screen remounts on every route change (it is a route
Expand DownExpand Up@@ -189,6 +192,7 @@ export function CoordinatorScreen() {
<FlowDiagram
movement={selectedMovement}
now={now}
units={units}
selectedUnitId={selectedUnitId}
onSelectUnit={(unitId) => setSelectedUnitId((current) => (current === unitId ? undefined : unitId))}
/>
Expand All@@ -203,6 +207,7 @@ export function CoordinatorScreen() {
<ShortlistPanel
movement={selectedMovement}
now={now}
units={units}
selectedUnitId={selectedUnitId}
onSelectUnit={(unitId) => setSelectedUnitId((current) => (current === unitId ? undefined : unitId))}
dispatch={dispatch}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -547,7 +547,7 @@
}

/*
* `eligibleCandidates` sorts eligible-first but never filters, so a shortlisted unit can still
* `eligibleCandidatesAmong` sorts eligible-first but never filters, so a shortlisted unit can still
* fail a gate (Critical 1 — already declined, wrong security tier, stale capacity, ...). This
* line must never look like the eligible route above: dashed, and danger-toned rather than
* accent-toned, so the arrow itself does not read as an endorsement of a route the data denies.
Expand DownExpand Up@@ -1453,6 +1453,25 @@
line-height: var(--co-leading-body);
}

/*
* Whole-branch review I2: a recorded escalation is a real, persistent fact — the network really
* was exhausted for this movement at this instant — never a success message, so it deliberately
* does not reuse `.shortlistConfirmationRecord`'s success tokens above. `--warning-*`, not
* `--danger-*`: an escalation is a recorded operational fact a coordinator is expected to act on
* next, not a refusal or an error state.
*/
.shortlistEscalationRecord {
margin: 0 0 var(--co-space-8);
border: 0.0625rem solid var(--warning-border);
border-radius: var(--radius-md);
background: var(--warning-bg);
padding: var(--co-space-8) var(--co-space-10);
color: var(--warning-text);
font-size: var(--text-2xs);
font-weight: 650;
line-height: var(--co-leading-body);
}

.shortlistActionRow {
display: flex;
flex-wrap: wrap;
Expand DownExpand Up@@ -1596,6 +1615,30 @@
font-weight: 700;
}

/*
* Whole-branch review I4: a separate, danger-toned badge for refused transitions — never merged
* into `.exceptionsToggleCount` above, which is `items.length`, a different fact (the action
* inbox). Rendered only while `rejections.length > 0` (see exception-drawer.tsx), so the trigger
* carries no extra chrome until the first refusal and then never goes quiet again — the same
* "present the moment the fact is real" rule `.refusalsSection` already applies to the panel
* beneath it. Danger tokens, not the neutral accent above: a refusal is the reducer stating an
* action did NOT happen, which is a sharper fact than an open work-list item.
*/
.exceptionsToggleRefusalCount {
display: inline-flex;
min-height: var(--co-space-20);
align-items: center;
gap: var(--co-space-6);
border: 0.0625rem solid var(--danger-border);
border-radius: var(--radius-pill, 9999px);
background: var(--danger-bg);
padding: 0 var(--co-space-8);
color: var(--danger-text);
font-size: var(--text-3xs);
font-weight: 700;
white-space: nowrap;
}

.exceptionsDrawer[data-open="true"] .placeholder {
margin-top: var(--co-space-8);
}
Expand Down
25 changes: 24 additions & 1 deletion src/components/ward-management/coordinator/exception-drawer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,30 @@ export function ExceptionDrawer({ items, rejections, open, onToggle, onSelectMov
<button type="button" className={styles.exceptionsToggle} aria-expanded={open} onClick={onToggle}>
{open ? <ChevronDown aria-hidden="true" /> : <ChevronUp aria-hidden="true" />}
<span>Exceptions</span>
<span className={styles.exceptionsToggleCount}>{items.length}</span>
<span className={styles.exceptionsToggleCount} data-testid="ward-exceptions-toggle-count">
{items.length}
</span>
{/* Whole-branch review I4: the count above is `items.length` — the action inbox only —
so a refused transition (HOLD_BED on a ward with zero allocatable beds, in the
review's own Proof 2) was invisible on a closed drawer: nothing about the collapsed
trigger changed when a refusal was filed. Spec §7.4 calls the refusals surface
PERSISTENT, "not a toast that vanishes" — a badge that only appears once the count is
genuinely non-zero, and that stays wherever this drawer is rendered (never inside the
`open` branch below), is the smallest honest fix: it renders nothing extra until the
first refusal, then never goes silent again. A second, distinctly toned badge rather
than folding this into the count above — an action-inbox item and a refusal are
different facts a coordinator responds to differently, and merging their counts would
recreate exactly the "one number, two meanings" defect Ruling 3's own comment on
`exceptionsToggleCount` was written to prevent. */}
{rejections.length > 0 ? (
<span
className={styles.exceptionsToggleRefusalCount}
data-testid="ward-exceptions-toggle-refusal-count"
title={`${rejections.length} refused action${rejections.length === 1 ? "" : "s"}`}
>
{rejections.length} refused
</span>
) : null}
</button>
{open ? (
<section className={styles.exceptionsPanel} aria-label="Exceptions">
Expand Down
43 changes: 25 additions & 18 deletions src/components/ward-management/coordinator/flow-diagram.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,27 +6,27 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
import type { Instant } from "@/components/ward-management/ward-clock";
import {
candidateReason,
eligibleCandidates,
eligibleCandidatesAmong,
restrictionNotice,
unitCapacity,
wardServiceOrder,
} from "@/components/ward-management/ward-derivations";
import { useWardFlow } from "@/components/ward-management/ward-flow-provider";
import { PARALLEL_REFERRAL_CAP, type Movement, type Unit } from "@/components/ward-management/ward-model";
import { edPressure } from "@/components/ward-management/ward-pressure";
import { allUnits, siteByCode, unitById } from "@/components/ward-management/ward-sites";
import { siteByCode } from "@/components/ward-management/ward-sites";

import styles from "./coordinator.module.css";

type FlowDiagramProps = {
movement: Movement | undefined;
now: Instant;
units: Unit[];
selectedUnitId: string | undefined;
onSelectUnit: (unitId: string) => void;
};

type Point = { x: number; y: number };
type ShortlistCandidate = ReturnType<typeof eligibleCandidates>[number];
type ShortlistCandidate = ReturnType<typeof eligibleCandidatesAmong>[number];
type Connector =
| { id: string; path: string; kind: "demand" }
| { id: string; path: string; kind: "route"; eligible: boolean }
Expand DownExpand Up@@ -66,16 +66,20 @@ function recordedDestinationIds(movement: Movement | undefined): Set<string> {
*
* Two further corrections from the whole-branch review:
*
* Critical 1 -- the word "nearest" is gone. `eligibleCandidates` ranks cohort-matching units
* Critical 1 -- the word "nearest" is gone. `eligibleCandidatesAmong` ranks cohort-matching units
* eligible-first and breaks ties on array order; the model holds no distance data at all, so a
* proximity claim here was simply false.
*
* Important 3 -- a movement that ALREADY has a recorded destination is not looking for three of
* them. WF-004 sits at stage `bed_held` with a bed held at BTY Adult Secure, and this line read
* "WF-004 -- 3 eligible destinations". The recorded fact now leads; the candidate count follows
* as secondary context, because a coordinator may still be considering alternatives.
*
* Whole-branch review Critical 1: `units` is the caller's live provider `units`, never
* `ward-sites.ts`'s `unitById`/`allUnits` — a name lookup here still must reflect a unit that
* exists in the live world the same as any capacity figure would.
*/
function hubStatusText(movement: Movement | undefined, shortlist: ShortlistCandidate[]) {
function hubStatusText(movement: Movement | undefined, shortlist: ShortlistCandidate[], units: Unit[]) {
if (!movement) return "Select a movement from the priority queue to route it";

// "Other" means other than the units already recorded against this movement -- a candidate that
Expand All@@ -85,14 +89,14 @@ function hubStatusText(movement: Movement | undefined, shortlist: ShortlistCandi
const candidateTail = otherCount === 0 ? "" : `; ${otherCount} other candidate${otherCount === 1 ? "" : "s"} shown`;

if (movement.acceptedUnitId) {
const accepted = unitById(movement.acceptedUnitId);
const accepted = units.find((unit) => unit.id === movement.acceptedUnitId);
return accepted
? `${movement.id} — accepted destination: ${accepted.name}${candidateTail}`
: `${movement.id} — an accepted destination is recorded but could not be resolved`;
}
if (movement.referredUnitIds.length > 0) {
const names = movement.referredUnitIds
.map((id) => unitById(id)?.name)
.map((id) => units.find((unit) => unit.id === id)?.name)
.filter((name): name is string => Boolean(name));
return names.length === movement.referredUnitIds.length && names.length > 0
? `${movement.id} — outstanding referral${names.length === 1 ? "" : "s"}: ${names.join(", ")}${candidateTail}`
Expand DownExpand Up@@ -120,7 +124,7 @@ function hubStatusText(movement: Movement | undefined, shortlist: ShortlistCandi
* is already recorded against; with nothing selected, this renders the network with nothing routed
* rather than a guessed selection (ruling: display less rather than something plausible).
*
* `eligibleCandidates` sorts eligible-first but never filters -- it can and does return units
* `eligibleCandidatesAmong` sorts eligible-first but never filters -- it can and does return units
* that fail a gate (already declined the movement, wrong security tier, stale capacity, ...).
* Every shortlisted node therefore carries its own verdict (`data-eligible`, plus
* `candidateReason` rendered as real text) and an ineligible route is drawn visually distinct
Expand All@@ -133,12 +137,11 @@ function hubStatusText(movement: Movement | undefined, shortlist: ShortlistCandi
* and reruns on a `ResizeObserver` plus a window resize listener, so the diagram survives a
* resize rather than only ever being screenshotted once.
*/
export function FlowDiagram({ movement, now, selectedUnitId, onSelectUnit }: FlowDiagramProps) {
const { units } = useWardFlow();
export function FlowDiagram({ movement, now, units, selectedUnitId, onSelectUnit }: FlowDiagramProps) {
const pressure = useMemo(() => edPressure(now), [now]);
const shortlist = useMemo(
() => (movement ? eligibleCandidates(movement, now, PARALLEL_REFERRAL_CAP, units) : []),
[movement, now, units],
() => (movement ? eligibleCandidatesAmong(movement, units, now, PARALLEL_REFERRAL_CAP) : []),
[movement, units, now],
);
const shortlistByUnitId = useMemo(
() => new Map(shortlist.map((candidate) => [candidate.unit.id, candidate])),
Expand All@@ -151,20 +154,24 @@ export function FlowDiagram({ movement, now, selectedUnitId, onSelectUnit }: Flo
// group rather than guessing one -- conservative failure, not a crash. `unplacedUnits` below
// catches exactly that case so the unit still renders (as an explicit anomaly) rather than
// silently vanishing from the board (review Minor 6).
//
// Whole-branch review Critical 1: grouped from the caller's live `units`, never `allUnits()` —
// every unit NODE on this board (its bed grid, via `unitCapacity` in `UnitNode` below) must
// move the instant a ward confirms new capacity, not only at first paint.
const serviceGroups = useMemo(
() =>
wardServiceOrder
.map((service) => ({
service,
units: allUnits().filter((unit) => siteByCode(unit.siteCode)?.service === service),
units: units.filter((unit) => siteByCode(unit.siteCode)?.service === service),
}))
.filter((group) => group.units.length > 0),
[],
[units],
);
const unplacedUnits = useMemo(() => {
const grouped = new Set(serviceGroups.flatMap((group) => group.units.map((unit) => unit.id)));
return allUnits().filter((unit) => !grouped.has(unit.id));
}, [serviceGroups]);
return units.filter((unit) => !grouped.has(unit.id));
}, [serviceGroups, units]);

const canvasRef = useRef<HTMLDivElement | null>(null);
const hubRef = useRef<HTMLDivElement | null>(null);
Expand DownExpand Up@@ -380,7 +387,7 @@ export function FlowDiagram({ movement, now, selectedUnitId, onSelectUnit }: Flo
<div className={styles.diagramHub} ref={hubRef}>
<Network aria-hidden="true" />
<strong>Statewide flow hub</strong>
<span>{hubStatusText(movement, shortlist)}</span>
<span>{hubStatusText(movement, shortlist, units)}</span>
</div>

<div className={styles.diagramUnitsColumn}>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,8 +81,12 @@ export function PriorityQueue({ movements, now, selectedId, onSelect, filterEdId
// breached" — the factor list is scoped to Task 7's expandable shortlist, not this
// row, but a breached statutory deadline is the one thing this row must never let a
// coordinator miss, so it always renders here regardless of what Task 7 later shows.
// A form with no `dueAt` (Task 6A: a Form 3B honestly carries none) is never breached —
// `undefined` must never reach `clockState`'s arithmetic.
// A form with no `dueAt` is never breached — `undefined` must never reach
// `clockState`'s arithmetic. As of the 2026-08-23 product-owner correction, neither a
// Form 1A nor a Form 3B carries one any longer (Task 6A first established this for 3B;
// see `LegalForm`'s doc comment in ward-model.ts) — only the transport/transfer forms
// (4A/4C) still do, and none of those are due in the past on today's fixture, so
// `legalBreached` below is false today.
const legalDueAt = movement.legalForm?.dueAt;
const legalBreached = legalDueAt !== undefined && clockState(legalDueAt, now) === "breached";
const legalFactor = factors.find((factor) => factor.label === "Statutory timing");
Expand Down
Loading
Loading