Skip to content

Ward Flow Phase 5 — bed availability becomes a number you can plan against - #2390

Merged
BigSimmo merged 24 commits into
mainfrom
claude/ward-flow-phase-5-p8rwcm
Aug 26, 2026
Merged

Ward Flow Phase 5 — bed availability becomes a number you can plan against#2390
BigSimmo merged 24 commits into
mainfrom
claude/ward-flow-phase-5-p8rwcm

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • A bed release gains a lifecycle. predicted → confirmed → blocked → released, and only a ward may move it. confidence narrows to likely | possible and now means something only while a release is predicted; a confirmed release has no confidence because it is no longer a belief. blocker becomes state-dependent and typed — legal only while blocked. The invariant holds by construction: each transition builds its own object literal, so no code path can produce both fields non-null.
  • Leave beds become their own type. LeaveBed carries nothing about the person on leave, and a usable leave bed is never merged into availability — it is its own figure. It has the two-state life the spec describes: recorded, then ended on return.
  • Five capacity figures, never a sum. Available now · Confirmed today · Predicted today · Held · Leave (usable). availableNow is computed before any release or leave bed is examined, so a prediction is structurally incapable of inflating it — not merely tested against, unable to happen. availableNow and held are copied verbatim from unitCapacity so the number a coordinator acts on cannot drift.
  • The discharge and egress board at /mockups/ward-flow/discharges. Blocked rows first because those are the ones somebody must act on; cards rather than a squeezed table below 40rem; the beyond-tonight exclusion count stated at the foot even when it is zero.
  • A shared freshness stamp on every board — confirmed time and role, the clock time a derived figure was computed, or Never confirmed. Never a blank and never a dash.
  • The coordinator's one permitted action: marking a ward's count refresh-requested. It changes no number, nothing leaves the sandbox, and no message is sent.
  • The blocker list grows to seven, all operational. Guardianship, financial arrangements and family availability stay excluded — each describes the person rather than the bed.
  • Registration of the new route in five places, each of which fails closed: the sidebar nav, two route-contract maps in tests, CI's change-scope map, and the generated repo-awareness snapshot.
  • Documentation travels with the code: the roadmap records Phase 5 as built; docs/ward-flow-complete-ledger.md §5d records what was built, what the screenshots caught, and what is still open.

Binding spec: docs/superpowers/specs/2026-08-26-ward-flow-phase-5-bed-availability-design.md. It is unchanged by this branch.

Verification

  • npm run testTest Files 3 failed | 873 passed (876) / Tests 3 failed | 10559 passed | 1 skipped (10563)
  • npm run typecheck — clean
  • npm run lint — clean, run with node_modules/.cache/eslint cleared first (the cache is per-file, so a warning caused by another file's change stays invisible otherwise; doing this surfaced a real --max-warnings 0 failure)
  • npx prettier --check on every changed file — clean. Formatting is in none of test, typecheck or lint; five files were unformatted after all three passed
  • Chromium journey tests/ui-ward-discharges.spec.ts1 passed (3.6s), and proven able to fail: mutating CONFIRM_BED_RELEASE to coordinator-only turns it red with the release stuck at "Predicted", then green again on revert
  • Screenshots at 390 / 820 / 1440 across the discharge board, capacity board and ward screen, captured and looked at — twice, once after the first visible change and again after the fixes

Verification not run: npm run verify:pr-local — its constituent gates were run individually and quoted above; the wrapper itself was not invoked.

UI verification not run: npm run verify:ui was not invoked as a wrapper. The new Chromium journey was run directly and is quoted above, and the three changed screens were captured and inspected at 390 / 820 / 1440. CI's own Production UI lane is the authoritative full-matrix run.

No provider-backed gate was run: no verify:release, no eval:*, no check:supabase-project, no test:live, and nothing touching OpenAI, Supabase or a live database.

The three failing unit tests are pre-existing and not this branch's. clinical-hazard-controls, privacy-readiness-contract and rag-plan-package-parity fail identically on clean origin/main, verified by running them in a worktree at that ref. They are untouched here.

Every new test was mutation-tested — the thing it guards was deliberately broken, the test watched to go red, then restored. Where a mutation would have survived, the assertion was strengthened rather than the result accepted.

Risk and rollout

  • Risk: low — confined to /mockups/ward-flow, an administrator-gated developer-hub prototype holding only synthetic data and reachable from no production route. The one existing rendered figure this phase changes is unitCapacity().potential on the capacity board and the ward screen, replaced by the state-aware breakdown; unitCapacity() itself is unchanged and its remaining call sites on the network and flow diagrams keep their current behaviour. The only file outside that prototype is the generated data/repo-awareness-snapshot.json, regenerated by the repo's own tooling to add the new route.
  • Rollback: revert this PR's merge commit. No migration, no schema change, no stored state, no configuration change, and no generated artefact that another system reads at runtime.
  • Provider or production effects: None. The Supabase integration reports it ignored this PR entirely because nothing under supabase/ changed.
  • RAG impact: no retrieval behaviour change — no file under src/lib/rag/, no retrieval RPC, no ranking surface and no golden fixture is touched, and classifyPullRequestFiles reports ragRanking: false.

Clinical Governance Preflight

  • Source-backed claims still require linked source verification before clinical use
  • No patient-identifiable document workflow was introduced or expanded without explicit governance approval
  • Supabase target remains Clinical KB Database (sjrfecxgysukkwxsowpy)
  • Service-role keys and private document access remain server-only
  • Demo/synthetic content remains clearly separated from real clinical sources
  • Source metadata, review status, and outdated/unknown-source behavior remain conservative
  • Deployment classification/TGA SaMD impact was checked when clinical decision-support behavior changed

Evidence for each of the seven, in order:

  1. Unchanged — this prototype makes no source-backed claim and generates no answer.
  2. Not one new fact about any person entered the system. BedRelease and LeaveBed both remain free of anything describing the departing or absent patient — not an identifier, not a reason, not a destination, and per spec D11 not even sex, the one otherwise-permitted attribute. This is asserted structurally against each type's own field set rather than against fixture content, so a future field named patientId would fail it.
  3. Untouched. No file under supabase/ is in this diff.
  4. Untouched. No key handling, no document access path, and no server/client boundary is altered.
  5. Every board carries its synthetic-prototype banner and states it is not a medical device. The whole surface is behind the administrator-gated developer hub and holds only synthetic fixtures.
  6. Every failure degrades toward saying less rather than guessing more: a refused transition produces a visible Rejection rather than a silent no-op; a release expected beyond tonight is reported as excluded rather than dropped; and a unit with no confirmed capacity reads Never confirmed rather than zero, because zero is a claim.
  7. Checked and unchanged. No clinical decision-support behaviour was added — this is a synthetic operational prototype behind an administrator gate, and it advises nothing.

No Mental Health Act figure was invented, cited, paraphrased or inferred anywhere. tests/ward-legal-figure-guard.test.ts sweeps every reachable state for exactly that, and it failed the build when the six new events were added without extending it — its switch must be exhaustive, so the gap could not pass silently. It now covers all six, including every one of the seven blockers and both usable and unusable leave beds, and was proven non-vacuous by emptying one candidate list and watching the traversal assertion name the event that stopped being reached.

Notes

Four defects were found by looking at screenshots that every one of 10,000+ passing tests missed — the project's recorded recurring failure, caught mid-phase this time rather than after later work was built on top:

  1. The discharge board printed each confirming ward twice per row, once plainly and once inside the stamp that already names it.
  2. The capacity board contradicted its own headline — the headline separates confirmed from predicted and excludes beyond-tonight, while the per-unit row three columns away still showed the undifferentiated raw count including the excluded ones.
  3. A raw union value rendered as user-facing text — a release read confirmed in lowercase.
  4. Fixing (2) exposed a fourth: the ward screen still said "Potential 1" for a unit the capacity board now described as "Confirmed 1, Predicted 0", directly above an itemised list showing that release as Confirmed.

Each fix carries an assertion that would catch its return.

One decision reversed and reverted during the session: the day-end boundary went 22:00 → 24:00 → 22:00 at the owner's direction. The spec is byte-identical to main. Worth recording because D5 had written the boundary down "so it can be changed in one place", and that is exactly how it behaved — one named constant, one assertion, and no code shipped at the wrong value.

Automated code review did not run on this PR. CodeRabbit reported its hourly limit and posted no findings; the Cursor approval agent approved on policy grounds and stated that it skipped the bug-finding signal because that bot had not reported when it polled. Neither is a defect review. The deterministic gates carried the load instead, per the accepted owner decision recorded in AGENTS.md.

Two things are owed before Phase 6 builds on this, both recorded in the roadmap:

  1. Spec D14 has still never been checked by a ward clinician. predicted → confirmed → blocked → released is a software model of how a bed comes free. A bed may be confirmed and blocked at once in reality, and "predicted" may compress several distinct real states. It is cheap to change while everything is synthetic, and Phase 6 is built entirely from these numbers — so the cost of it being wrong rises the moment Phase 6 lands. Nothing else in this phase depends on there being exactly four states.
  2. Phases 6 and 7 are worth designing in one conversation (and 8 and 9 in another). Each design conversation carries a large fixed setup cost, and Phase 6 is small and already largely determined by Phase 5's numbers. Each phase still gets its own written specification.

claude added 12 commits August 26, 2026 04:15
Product-owner instruction given during implementation ("keep it simple and
end the day at 2400"). D5 originally argued against midnight on the grounds
that nobody hands over at a calendar boundary; that reasoning is kept in
place as superseded rather than deleted, because it is the argument a future
reader will want to weigh before changing it back.

The behavioural consequence is one-directional and stated in the spec: a
release expected at, say, 23:30 now counts inside "tonight" rather than
being reported as excluded.

D5 anticipated this exact amendment — it recorded the boundary "so it can be
changed in one place". The code keeps that property: one named constant,
END_OF_DAY_MINUTES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
… freshness stamp

Two independent Phase 5 foundations. They are committed together because this
repository's pre-commit documentation-sync hook refuses to regenerate while any
source file is untracked, so splitting them was not available; neither depends
on the other and each can be reverted by its own files.

Capacity derivations (spec D5, D6) — src/components/ward-management/ward-bed-availability.ts:
The rule this module exists to enforce is that nothing predicted,
confirmed-but-unreleased, or on leave is ever added into `availableNow`. A
coordinator must be able to point at that number and say "that is a bed I can
fill this minute", and it must never have been softened by an expectation.
`availableNow` and `held` are copied verbatim from `unitCapacity` rather than
re-derived, so the one number a coordinator acts on cannot drift.

Bands compare the raw instant against named minute constants rather than a
wrapped time of day: an `Instant` is minutes since midnight on the synthetic
operating day, so an instant past the evening-shift end is beyond today even
when its clock time is not. Anything beyond tonight is excluded from every
count AND counted, so a board can say how many it left out — silent truncation
reads as "we counted everything" when we did not.

Freshness stamp (spec D7) — src/components/ward-management/ward-freshness.tsx:
Today only the capacity board states when its data was last true; every other
board looks authoritative and cannot say how old it is, which invites the
reader to assume it is current. Three renderings and no fourth: a confirmed
time with the confirming role, the clock time a derived figure was computed,
or "Never confirmed". Never a blank and never a dash — both are claims this
component must not make.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
…ly ask

Two items. They are committed together because this repository's pre-commit
documentation-sync hook refuses to regenerate while any tracked file is
unstaged; neither depends on the other.

1. The lifecycle transitions (spec D2, D3, D12)
   src/components/ward-management/ward-flow-{events,reducer,provider}.tsx,
   tests/ward-bed-release-lifecycle.test.ts

   Six new events. CONFIRM_BED_RELEASE, BLOCK_BED_RELEASE, RELEASE_BED,
   RECORD_LEAVE_BED and END_LEAVE_BED are ward-only; only
   REQUEST_CAPACITY_REFRESH belongs to the coordinator.

   The point of D2 is that the ward owns its own beds. A hub that lets a
   central coordinator edit a ward's bed state is a hub wards will stop
   trusting, so a coordinator attempting any transition gets a visible
   Rejection rather than a silent no-op, and the one thing it may do changes
   no number at all — it records that somebody asked, with the time and the
   requesting role. Nothing leaves the sandbox and no message is sent.

   Legal transitions and nothing else: predicted to confirmed or blocked;
   confirmed to released or blocked; blocked to confirmed or released;
   released is terminal. Each refusal names both states. BLOCK_BED_RELEASE
   sets confidence null and the given blocker; CONFIRM_BED_RELEASE and
   RELEASE_BED set both to null — so D3's invariant (a blocker exists exactly
   when blocked, a confidence exactly when predicted) holds by construction
   rather than by check.

   RELEASE_BED is the only one of the six that moves a real bed count. A
   released bed is available by D1, and capacityBreakdown's availableNow is
   deliberately blind to bed releases, so the unit's own fields are the only
   route — which is what keeps a merely predicted or confirmed bed from ever
   reaching that number.

   The provider gains leaveBeds and refreshRequests, including in its memo
   dependencies, so the boards reading them cannot go stale.

2. Two test repairs found by review
   tests/ward-freshness.dom.test.tsx, tests/ward-bed-availability.test.ts

   The freshness stamp's "Confirmed" test asserted the confirming role but
   never the time, so rendering the current clock in place of the confirmed
   time would have survived it — a stamp silently claiming a stale figure was
   confirmed just now, which is the failure D7 exists to prevent. The
   assertion now pins the exact time, and both remaining branches were
   mutation-tested.

   Adds the missing case for a blocked release expected beyond tonight: it
   counts as excluded and in no other figure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
…acity figures

Committed together because this repository's pre-commit documentation-sync hook
refuses to regenerate while any source file is unstaged. The three are
independent; each is described separately below.

1. The ward's controls (spec D10, D12)
   src/components/ward-management/ward/ward-screen.tsx, ward.module.css

   The existing flagging panel gains the rest of the lifecycle rather than a
   second panel beside it. Each bed release row offers only the transitions
   legal from its current state, so a released bed offers no controls at all.
   Blocking asks for a reason from the fixed list and refuses without one.

   Leave beds now have the two-state life the spec describes — recorded, then
   ended on return — rendered as rows with an Ended control. Without it
   END_LEAVE_BED existed in the reducer and was reachable from no screen, which
   is both an unfinished feature and dead code.

   A coordinator's refresh request appears here as a visible mark naming the
   time and the asking role. It changes no number; it records that somebody
   asked, which is the phone call that says "is that still right?".

2. The discharge and egress board (spec D9)
   src/components/ward-management/discharges/*, its route, and its nav entry

   Grouped Blocked first, then Confirmed, Predicted, Released today — blocked
   first because those are the rows somebody must act on. On a phone it is
   cards, not a squeezed table: Ward Flow's tables are right at a desk and
   wrong in a corridor.

   The count of releases falling beyond tonight sits at the foot of the board
   and is stated even when it is zero. Silent truncation reads as "we counted
   everything" when we did not.

3. Capacity becomes five numbers, and none of them is a guess added to a fact
   src/components/ward-management/ward-management-modes.tsx, its stylesheet

   Available now, Confirmed today, Predicted today, Held, Leave (usable) — five
   separate figures, never a sum. Available now keeps its meaning exactly: a
   coordinator must be able to point at it and say "that is a bed I can fill
   this minute", and it must never have been softened by an expectation.

   This is the one existing rendered figure the phase changes.
   unitCapacity().potential — a raw count of every release regardless of state
   or timing — is superseded here by the breakdown. It is left in place with a
   comment for its three other call sites, which are outside this phase's scope.

   The coordinator's refresh control is the only thing a coordinator may do to
   a ward's bed data. Nothing leaves the sandbox and no message is sent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
Found by capturing the discharge board, the capacity board and the ward screen
at 390, 820 and 1440 pixels and looking at them, after the first visible change
rather than at the end. All three were structurally invisible.

1. The discharge board printed the confirming role twice on every row, in both
   the desktop table and the phone card: a bare "NUM FSH Adult Secure" line
   immediately above "Confirmed 10:07 · NUM FSH Adult Secure". WardFreshness
   already carries the role, so spec D9's "confirming role, and its freshness
   stamp" is satisfied by the stamp alone. Same class as the Phase 4 sidebar
   that duplicated its own title.

2. The capacity board contradicted its own headline. The headline separates
   confirmed from predicted and excludes anything beyond tonight; the per-unit
   row three columns away still rendered unitCapacity().potential — a raw count
   of every release regardless of state or timing, including the ones the
   headline deliberately excludes. One screen, two incompatible answers, and a
   coordinator reading "Potential 1" could not tell which it was. The row now
   shows Confirmed and Predicted from the same breakdown the headline uses.
   Now, Held, Blocked and Occupied are untouched: those four sum to the unit's
   bed total and that identity is asserted elsewhere.

   unitCapacity() itself is unchanged, as are its other call sites.

3. The ward screen rendered a raw union value as user-facing text, so a release
   read "confirmed" in lowercase where every other status on that screen is
   sentence case. Now goes through a label map beside stageCopy, the convention
   this file already uses.

Each defect gains an assertion that would catch its return, and the first two
were mutation-tested: reintroducing the duplicate line and restoring the
Potential chip each failed the new test before being reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
A fourth defect, visible only once the third was fixed. The capacity board's
per-unit row now reads Confirmed and Predicted; the ward screen for the same
unit still read "Potential 1" — a raw count of every release regardless of
state or timing — directly above an itemised list showing that very release as
Confirmed. One prototype, two vocabularies for the same beds.

The chip row now sources Confirmed, Predicted and Leave from the same
capacityBreakdown the capacity board uses. Ready, held, blocked and occupied
are untouched: those four sum to the unit's bed total and that identity is
asserted elsewhere. The explanatory sentence still states plainly that
confirmed, predicted and leave beds are never counted into them — that is the
phase's central promise and it stays explicit.

unitCapacity() is unchanged, as are its two remaining call sites on the network
and flow diagrams, which this phase does not touch.

Also formats five files that no earlier gate could have caught: formatting is
in none of the test run, the type check or the lint run, so nothing verified
this session would have gone red until CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
The type check refused to build, which is this guard working rather than a
nuisance. candidateEvents' switch must be exhaustive over every event type, so
adding six Phase 5 events without extending this file broke compilation instead
of letting six actions pass through the sweep unchecked.

tests/ward-legal-figure-guard.test.ts walks every reachable state and checks
that nothing rendered anywhere carries a figure or requirement that could have
come from the Mental Health Act. It is the test that keeps this prototype's
hardest promise, and until now confirming, blocking, releasing, recording a
leave bed, ending one, and requesting a refresh were all outside it.

Follows the file's own precedent of one candidate per real domain value rather
than one hard-coded choice: every one of the seven blockers, and both usable
and unusable leave beds. No default case and no cast — the exhaustiveness
requirement is the mechanism that caught this, so it stays.

Proven non-vacuous rather than assumed: emptying one new event's candidate list
turns the traversal assertion red, naming the event that stopped being reached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
…d Phase 5

Test migrations — none weakened, each still guards what it guarded:

  ward-management.test.ts counted 15 column headers; the capacity table gained
  a Coordinator action column and its "FIVE BED STATES" header became "BED
  STATES" because the row no longer shows five. Now 16, and all 16 still carry
  scope="col" — checked rather than assumed.

  ward-bed-release.dom.test.tsx asserted "Potential 1" and "0Potential". Both
  re-pointed at the figures that replaced them. The second one's real subject is
  scoping — that a flag raised for one ward cannot move another ward's numbers,
  asserted against the live capacity row so a reducer bug that never reaches the
  fixture is still caught. That property survived the migration and was proved
  by making FLAG_BED_RELEASE write to every unit and watching it go red.

  Both tests originally chose a blocker before submitting. A blocked release
  counts in neither Confirmed nor Predicted, so leaving that in would have made
  both assertions vacuous against the new figures. They now submit with no
  blocker, which is a plain prediction and does move the numbers under test.

Documentation, travelling with the code it describes rather than in its own
pull request:

  The roadmap records Phase 5 as built, and the two things owed before Phase 6
  builds on it — that spec D14's four-state model has still never been checked
  by a ward clinician, and that Phases 6 and 7 are worth designing in one
  conversation since Phase 6 is small and already largely determined by these
  numbers.

  The project ledger records what was built, the 22:00 to 24:00 and back round
  trip, the four defects the screenshots caught that ten thousand passing tests
  did not, the two gates that hide from every local check, and the three
  pre-existing failures on main that are not this branch's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
formatInstant became unused when the discharge board stopped printing the
confirming role separately from its freshness stamp, which renders its own
time. Lint runs with --max-warnings 0, so this was a build failure rather than
a tidy-up.

Found only after clearing the ESLint cache: the cache is per-file, so a warning
caused by a different file's change stays invisible locally and goes red in CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
Spec Verification: a ward flags a release, confirms it, blocks it with a reason
from the fixed list, then releases it — and the coordinator's capacity board
reflects every step without a reload. The no-reload part is the point; a
journey that reloads between steps proves nothing about the shared derivation.

Registered in both regexes in playwright.config.ts. Only one of them is the
project pattern; the other is the top-level testMatch, and a spec file matching
neither is a file tests/playwright-project-isolation.test.ts fails on. That test
passes, so the registration is real rather than assumed.

Proven load-bearing rather than merely green. Making CONFIRM_BED_RELEASE
coordinator-only turns the journey red with the release stuck showing
"Predicted" — the ward can no longer confirm, and the board correctly refuses to
move. Reverting the role table exactly returns it to passing.

Evidence, read from the "N passed" line and never the exit code, since
run-playwright.mjs exits 0 on failure and on refusal alike:
  1 passed (3.6s)      on correct code
  1 failed             with the role gate mutated
  1 passed (3.6s)      after reverting

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

Next included review available in 3 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 93 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba930d68-af00-44e2-afc7-8deb9f08574d

📥 Commits

Reviewing files that changed from the base of the PR and between 13376f3 and 6d1b4bc.

⛔ Files ignored due to path filters (7)
  • docs/branch-review-records/dc2281cbb115db3be68986c3b2208038eb0962bfb2479c973cf031b30cf5d8f8.record.md is excluded by !docs/**, !**/*.md
  • docs/outstanding-issues-inbox/0257b988-e4dc-460d-9356-662893a7af6c.json is excluded by !docs/**
  • docs/outstanding-issues-inbox/f7c6371d-b92b-4050-9629-9aac5065f0a8.json is excluded by !docs/**
  • docs/site-map.md is excluded by !docs/**, !**/*.md
  • docs/ward-flow-complete-ledger.md is excluded by !docs/**, !**/*.md
  • docs/ward-flow-phase-5-handover.md is excluded by !docs/**, !**/*.md
  • docs/ward-flow-roadmap.md is excluded by !docs/**, !**/*.md
📒 Files selected for processing (47)
  • bundle-budget.json
  • data/outstanding-issues-snapshot.json
  • data/repo-awareness-snapshot.json
  • playwright.config.ts
  • scripts/ci-change-scope.mjs
  • src/app/mockups/ward-flow/discharges/page.tsx
  • src/components/ward-management/coordinator/coordinator-screen.tsx
  • src/components/ward-management/coordinator/coordinator.module.css
  • src/components/ward-management/coordinator/flow-diagram.tsx
  • src/components/ward-management/discharges/discharge-board.tsx
  • src/components/ward-management/discharges/discharges.module.css
  • src/components/ward-management/ward-bed-availability.ts
  • src/components/ward-management/ward-change-reasons.ts
  • src/components/ward-management/ward-derivations.ts
  • src/components/ward-management/ward-flow-events.ts
  • src/components/ward-management/ward-flow-provider.tsx
  • src/components/ward-management/ward-flow-reducer.ts
  • src/components/ward-management/ward-freshness.module.css
  • src/components/ward-management/ward-freshness.tsx
  • src/components/ward-management/ward-management-modes.module.css
  • src/components/ward-management/ward-management-modes.tsx
  • src/components/ward-management/ward-management-network.module.css
  • src/components/ward-management/ward-management-network.tsx
  • src/components/ward-management/ward-model.ts
  • src/components/ward-management/ward-movements.ts
  • src/components/ward-management/ward-nav-icons.ts
  • src/components/ward-management/ward-nav.ts
  • src/components/ward-management/ward/ward-screen.tsx
  • src/components/ward-management/ward/ward.module.css
  • tests/ui-ward-discharges.spec.ts
  • tests/ward-bed-availability-model.test.ts
  • tests/ward-bed-availability.test.ts
  • tests/ward-bed-release-lifecycle.test.ts
  • tests/ward-bed-release.dom.test.tsx
  • tests/ward-capacity-freshness-source.dom.test.tsx
  • tests/ward-capacity-sexmix-release.dom.test.tsx
  • tests/ward-capacity-view.dom.test.tsx
  • tests/ward-discharge-board.dom.test.tsx
  • tests/ward-flow-potential-chip-migration.dom.test.tsx
  • tests/ward-flow-reducer.test.ts
  • tests/ward-freshness.dom.test.tsx
  • tests/ward-landmarks.test.ts
  • tests/ward-legal-figure-guard.test.ts
  • tests/ward-management.test.ts
  • tests/ward-model.test.ts
  • tests/ward-nav.test.ts
  • tests/ward-screen.dom.test.tsx
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ward-flow-phase-5-p8rwcm

Comment @coderabbitai help to get the list of available commands.

@supabase

supabase Bot commented Aug 26, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Static PR checks went red on mockup-spec-parity, which is a third registration
point the phase missed. playwright.config.ts decides which project runs a spec;
scripts/ci-change-scope.mjs decides whether the advisory lane starts at all. A
spec matching the first but not the second would be listed as advisory and then
never triggered by its own edit — the journey would sit there looking covered
and run in neither lane.

The guard states that consequence outright rather than reporting a mismatch, so
the failure named the fix.

Reproduced and proved both ways before pushing: with the pattern the self-test
passes ("Mockup spec parity: 13 advisory specs all match mockupPatterns"), and
reverting it reproduces exactly the CI error, naming
tests/ui-ward-discharges.spec.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
cursor[bot]
cursor Bot previously approved these changes Aug 26, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Approved. Cursor Bugbot was not present after the first check poll, so that signal was skipped; remaining policy and review state did not require human review. No reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

Static PR checks went red on check:repo-awareness-snapshot: the committed
snapshot still described 192 pages and 137 mockup pages, and knew nothing of
/mockups/ward-flow/discharges.

Regenerated with the repo's own tooling (npm run snapshot:repo-awareness),
never by hand. The diff is exactly the new route plus the two counts it moves,
and the commit stamp — nothing else.

This is the fourth place a new route has to be registered this phase, after the
nav entry, the two route-contract maps in tests, and CI's change-scope map.
Each one failed closed rather than letting a half-wired route through, which is
the design working.

Verified before pushing: check:repo-awareness-snapshot now reports "in step
with data/repo-awareness-snapshot.json (193 pages, 441 documents, 2618
reviews)".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
cursor[bot]
cursor Bot approved these changes Aug 26, 2026
cursor[bot]
cursor Bot previously approved these changes Aug 26, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Approved. Cursor Bugbot was not present after the first check poll on the current head, so that signal was skipped; remaining policy and review state did not require human review. No reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

…e-5-p8rwcm

# Conflicts:
#	data/repo-awareness-snapshot.json

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a840fc761

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/components/ward-management/ward-flow-reducer.ts Outdated
Comment thread src/components/ward-management/ward-flow-reducer.ts Outdated
Comment thread src/components/ward-management/ward-flow-reducer.ts Outdated
Comment thread src/components/ward-management/ward-bed-availability.ts Outdated
claude added 4 commits August 26, 2026 07:48
Three defects raised by automated review on PR #2390, each fixed with a
mutation-tested regression test; the fourth finding is rejected with its
reasoning recorded rather than silently dropped.

FLAG_BED_RELEASE carried no expected time, so the reducer stamped
expectedAt with the moment the ward reported the release. releaseBand()
then classified it "now" every time, because expectedAt <= now is
trivially true when the two are equal — the four planning bands worked
only for the hand-authored fixture and never for anything a ward actually
flagged. The ward's flag form now collects an "Expected free" time,
parsed by the same helper the leave-bed form already uses. confirmedAt
stays the moment of the report: they are different facts, and conflating
them was the bug. Neither says anything about the departing patient;
expectedAt is an operational estimate about the bed.

Leave-bed identifiers were derived from the array's length while ending a
leave bed removes an entry, so a third record could be handed the first's
identifier and the removal filter would then delete both. Replaced with a
monotonic sequence. Bed releases were checked for the same flaw and do
not have it — nothing ever removes a release — and that reasoning is now
a comment rather than something to re-derive.

Confirming, blocking and releasing all kept the original confirmedAt, so
a row went on reporting when it was first flagged rather than when its
current state became true, defeating the freshness contract. All three
now restate it. confirmedBy is deliberately untouched: each case already
refuses any acting unit other than the release's own ward.

The fourth finding wanted a released bed to stop banding "now" past the
day boundary. That value feeds the discharge board's excluded count,
whose footer reads "expected beyond tonight" — and a released bed is not
expected at any future time. Applying it would have traded a cosmetic
ordering nit for a board stating something false about a real record.

Also records, with its measurements, that the intermittent role-screen
strict-mode violation in tests/ui-ward-roles.spec.ts reproduces on clean
origin/main on a screen this work never touches, so it is not this
branch's. Nothing was quarantined, skipped or loosened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
The whole-branch review of Phase 5 raised seven defects, none critical and
all of them a screen or a guard claiming something it could not back up.
All seven are fixed, each with a test that was mutation-tested.

The network view and the coordinator flow diagram still showed a
"Potential" chip counting every bed release regardless of state or
timing, so a bed that had already been released was advertised as one
that may become available — no clicks, on seeded data. Both now read the
same Confirmed and Predicted breakdown the capacity board and the ward
screen already use. unitCapacity()'s arithmetic is untouched.

The ward screen's chip said "Leave" while counting only usable leave
beds, so on one seeded unit it read "Leave 0" seven lines above "1 bed
currently on leave". It now says "Leave (usable)", as the spec names it
and as the capacity board already said.

Releasing a bed lowered derived occupancy without touching the sex-mix
figure that every other occupancy-changing event maintains, so the
capacity board's own row contradicted itself three clicks in. Sex is not
added to a bed release and no sex is guessed at: the model cannot know
who left, so the cell now says so in visible words when the two figures
no longer agree.

The blocker field was still an unconstrained string at every layer meant
to constrain it, and the reducer only checked that it was non-empty; both
are now real membership checks against the fixed list. The capacity board
asserted a confirming ward unconditionally, so the "Never confirmed"
floor was unreachable and a ward could be credited with a figure it never
confirmed; it now carries the guarded props the ward screen already had.

Two tests could not fail for what they claimed. A leave bed's only
structural privacy guard inspected one hand-authored fixture object, so
adding an optional sex field would have passed the whole suite; it now
checks every seeded record and one the reducer produced. A freshness
test's pattern accepted both possible outputs; it is pinned to the exact
string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
The immutable review record for the whole-branch review, appended with
the repository's own tool rather than written by hand, so the review
throttle can find it by SHA on a later session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
Two conflicts, both from files that grow by accretion rather than by
anyone disagreeing. The Playwright spec patterns are resolved as a union:
main's Care Plan mockup journey and this branch's discharges journey are
both kept, in both patterns. The generated repository-awareness snapshot
is not resolved by hand — main's version is taken and the repository's own
tool regenerates it, which is the only way that file is ever correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
cursor[bot]
cursor Bot previously approved these changes Aug 26, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Approved. Cursor Bugbot was not present after the first check poll on the current head, so that signal was skipped; remaining policy and review state did not require human review. No reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

cursor[bot]
cursor Bot previously approved these changes Aug 26, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Approved. Cursor Bugbot was not present after the first check poll on the current head, so that signal was skipped; remaining policy and review state did not require human review. No reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

The generated snapshot counts pending inbox requests, and this branch
added two. Regenerated with the repository's own tool rather than edited,
which is the only way that file is ever correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq
@github-actions

Copy link
Copy Markdown
Contributor

CI triage

CI failed on this PR. Automated classification of the 2 failed job(s):

  • Buildneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.
  • PR requiredneeds investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.

Compared with main CI run #14008 (failure).

Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger.

Records what was built, what is proven and by what evidence, what is still
red, and the one decision waiting for a human, so a session with no memory
of this work can resume it exactly.

Deliberately blunt about the unfinished parts: the mockups bundle budget is
0.7 points over its ceiling and needs an owner decision, and the
intermittent duplicate-testid failure in the roles journey is characterised
as an unproven hypothesis rather than a diagnosis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq

Copy link
Copy Markdown
Owner Author

Session handover — Ward Flow Phase 5

The full handover is committed to this branch as docs/ward-flow-phase-5-handover.md (commit ab068439), so it travels with the code and survives this conversation. This comment is the short version; the file has the detail, and the two are consistent.

Resume in sixty seconds

git fetch origin claude/ward-flow-phase-5-p8rwcm
git checkout claude/ward-flow-phase-5-p8rwcm
  • Head: ab068439 — 21 commits ahead of main, 53 files, ~+4400 / −166
  • Phase 5 is functionally complete. Every task in the plan is built, reviewed and pushed, including all four automated code-review findings.
  • One check is still red and is this branch's to fix: Build, on the mockups bundle budget. See below. Nothing else is known-red.

What was built

A bed release gained a lifecycle predicted → confirmed → blocked → released that only a ward may move; leave beds became their own type; a discharge and egress board at /mockups/ward-flow/discharges; five capacity figures that are never summed; a freshness stamp on every board; and the coordinator's one permitted action, marking a ward's count refresh-requested.

The rule the phase exists to hold: nothing predicted, confirmed-but-unreleased, or on leave is ever added into "available now" — and it holds structurally, not by assertion. availableNow is computed before any release or leave bed is examined, so a prediction is incapable of inflating it.

The one thing still broken

check:bundle-budget fails the mockups bucket at +25.7% against a 25% tolerance — over by 0.7 points. Reproduced locally after rm -rf .next && npm run build:

production (what users download, 134 routes): 1661.2 KiB gzip — baseline 1656.0 KiB, within tolerance.
mockups (design scratch, 141 routes):          613.1 KiB gzip — baseline  487.6 KiB, +25.7% (tolerance 25%).

There is no user-facing regression — the production bucket is within tolerance. The failing bucket is chunks reachable only from /mockups/**, which 404 in production; AGENTS.md calls it a repo-hygiene ceiling, not a per-mockup gate. The growth is also not all this branch's: main gained the entire Care Plan prototype during this branch's life, and the comparison build on clean origin/main was started but not finished. That measurement is the first thing to complete.

This is already recorded as outstanding issue #QSHHGK — "nothing schedules a bundle-budget baseline refresh, so accumulated growth fails whichever unrelated PR lands last". Phase 5 is simply the PR that landed last.

AGENTS.md allows two resolutions for this bucket — prune stale mockups, or refresh the baseline deliberately with npm run check:bundle-budget -- --update and say why in the PR body. It does not allow refreshing a production baseline to clear a production failure, which is not the situation here. The decision is the owner's; finishing the origin/main measurement first is what should inform it.

Verification, stated honestly

  • npm run testTests 3 failed | 10559 passed | 1 skipped (10563). The three failures are pre-existing: clinical-hazard-controls, privacy-readiness-contract and rag-plan-package-parity fail identically on clean origin/main, verified in a worktree at that ref. They are untouched here and must not be "fixed" inside this branch.
  • npm run typecheck — clean. npm run lint — clean, after clearing node_modules/.cache/eslint. npx prettier --check on all changed files — clean.
  • tests/ui-ward-discharges.spec.ts1 passed (3.6s), and proven able to fail.
  • Every static-pr sub-check run individually — all pass.
  • Every new test was mutation-tested. Where a mutation would have survived, the assertion was strengthened rather than the result accepted.
  • Provider-backed gates were not run: no verify:release, no eval:*, no check:supabase-project, no test:live.

One known unknown

tests/ui-ward-roles.spec.ts has intermittently failed with getByTestId('ward-unit-screen') resolved to 2 elements, on a failure that moves between tests across runs. Only one WardScreen render site exists and its two matching elements are mutually exclusive by an early return, so this is a timing artefact rather than a duplicate render. The current hypothesis — that the failing runs happened while a helper was editing source and the dev server rebuilt mid-run — is unproven. Section 9 of the handover file states exactly what to run to settle it. No assertion was weakened.

Owed before Phase 6

  1. Spec D14 has never been checked by a ward clinician. The four-state model is a software model of how a bed comes free; a bed may be confirmed and blocked at once in reality. Cheap to change while everything is synthetic, and Phase 6 is built entirely from these numbers.
  2. Design Phases 6 and 7 in one conversation, and 8 and 9 in another. Each phase still gets its own written specification; only the conversation is shared.

A note on review coverage

CodeRabbit hit its hourly allowance repeatedly and its one real attempt aborted when the head changed mid-review. The Cursor agent approved twice, both times stating it skipped the bug-finding signal — that is a policy approval, not a defect review. The four findings that were fixed came from the Codex connector. The deterministic gates carried the load; none was weakened to compensate.


Generated by Claude Code

cursor[bot]
cursor Bot previously approved these changes Aug 26, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Approved. Cursor Bugbot was not present after the first check poll on the current head, so that signal was skipped; remaining policy and review state did not require human review. No reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

Clean origin/main already sits at +24.7% against the mockups ceiling of
25%, inside the check's own drift-warning band. This branch adds one
percentage point, so ~96% of the overage predates it. Records the numbers
and the apportionment rather than leaving an instruction to go and find
them.

Also records that --update rewrites every baseline, production included,
so clearing this the easy way would re-baseline production's own drift as
the new normal. Recommends refreshing the mockups number alone, and leaves
the decision with the owner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHpxMM4ct1fF72RUmsZSFq

Copy link
Copy Markdown
Owner Author

Build: the overage is measured now, and 96% of it predates this branch

Following up my earlier comment with the comparison it said was outstanding. Both figures come from a clean rm -rf .next && npm run build at the named ref — a cached .next makes this check report stale numbers, so neither is a reused measurement.

Ref mockups bucket vs baseline 487.6 KiB
clean origin/main 608.1 KiB +24.7%
this branch (400cedd9) 613.1 KiB +25.7%
ceiling 25%

main is already at +24.7% — three-tenths of a point under the ceiling, and already inside the check's own 15% drift-warning band. Phase 5 adds 5.0 KiB, one percentage point, and is simply the change that tips it over. This is outstanding issue #QSHHGK happening word for word: "accumulated growth fails whichever unrelated PR lands last."

Production is unaffected in any meaningful sense: main 1660.7 KiB, this branch 1661.2 KiB — Phase 5 adds 0.5 KiB, 0.03%. Both sit just above the 1656.0 KiB production baseline, so main carries a little production drift of its own too.

Why I have not just cleared it

Pruning is not available to this branch — the growth is main's, and pruning other people's mockups is well outside this change.

That leaves the sanctioned refresh, and it has a trap worth naming: npm run check:bundle-budget -- --update rewrites every baseline — production, per-route and mockups alike (scripts/check-bundle-budget.mjs: "refresh every baseline"). Running it here would silently re-baseline production's own accumulated drift as the new normal, which is exactly the hiding-a-regression failure that the production bucket's "do not refresh the baseline to clear it" rule exists to prevent.

So the conservative fix is to refresh only mockups.gzipBytes and leave production and per-route baselines untouched — changing precisely the number the evidence justifies and nothing else.

Either way this is a repo-wide config change governing every future pull request, so it is the owner's call rather than an agent's. No measurement makes that automatic. Flagging it and holding rather than deciding it unilaterally.

Full detail, including how to reproduce both numbers, is in docs/ward-flow-phase-5-handover.md §6 on this branch.


Generated by Claude Code

cursor[bot]
cursor Bot previously approved these changes Aug 26, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Cursor Bugbot was not present after the first check poll on the current head, so that signal was skipped; remaining policy and review state did not require human review. No reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@BigSimmo
BigSimmo enabled auto-merge (squash) August 26, 2026 14:07
…efresh only the mockups bundle baseline.

A released record was always banded as now, so advancing the clock by a day still listed it under Released today. The Build failure is the mockups ceiling at +25.7 percent against a 25 percent tolerance; production and per-route baselines stay put, and mockups.gzipBytes moves to the measured 613.1 KiB (627814 bytes).

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Cursor Bugbot was not present after the first check poll on the current head, so that signal was skipped; remaining policy and review state did not require human review. No reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@BigSimmo
BigSimmo merged commit ea5482b into main Aug 26, 2026
65 checks passed
@BigSimmo
BigSimmo deleted the claude/ward-flow-phase-5-p8rwcm branch August 26, 2026 14:32
BigSimmo added a commit that referenced this pull request Aug 26, 2026
Keep main's mockups gzip baseline from #2390 (627814). The PR-local 625306 refresh was only needed against the older 499284 ceiling.

Co-authored-by: Cursor <cursoragent@cursor.com>
BigSimmo added a commit that referenced this pull request Aug 26, 2026
The merge brought in new review records from #2390, so the committed snapshot lagged review_state and would fail Static PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
BigSimmo added a commit that referenced this pull request Aug 26, 2026
The merge brought in new review records from #2390, so the committed snapshot lagged review_state and would fail Static PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants