Add Ward Flow: synthetic ward/bed-management coordination prototype - #2140
Conversation
…nd phase 1 plan Baseline for the Phase 1 model rework. Adds the ward-management surface (command, constellation, network, queue, capacity, movements, exceptions, transport, governance, patient workspace), its synthetic fixtures, the domain glossary and decision records, the metro patient-flow design spec, and the Phase 1 implementation plan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eligibility, shared derivations
The subagent-driven-development ledger lives in git-ignored scratch and does not survive a session. This records what a later session needs: the rulings taken during Phase 1 with what each costs if wrong, the module map, the deferred findings that Phase 4 must not inherit blindly, and the verification lessons from this run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ules Fixes every finding from the final whole-branch review: gate icons that ignored pass/fail (including MHA authorisation), a five-state bed grid that double-counted held/blocked beds, "open movement" counts that included arrived/closed records, generated movements whose stage contradicted their own fields, negative-instant clock formatting, bed-release blockers that leaked departing-patient detail, a "catchment" label computed from origin ED rather than patient catchment, an "exhausted search" blocker the fixture didn't actually support, a Readiness panel that ticked a breached legal deadline, and an unconditional "eligible candidate" claim. Also cleans up the smaller deferred items: cohort/security gate grammar and pass/fail text, an advertised-but-unrun exception rule, stale glossary prose, and one leftover "AI" label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The whole-branch review found defects the eight per-task reviews could not see, because each looked at one task's diff. One fix wave closed them. This records what the review caught and why it mattered, the findings parked at the close with the ruling on each, the verification actually run, and the two repo traps that make a green result untrustworthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rief Phase 2 builds the flow coordinator's screen — pressure strip, priority queue, flow diagram, explainable shortlist, exceptions drawer, phone form — and retires Constellation into it. Ten tasks, two of which build the pure derivations the screen reads: the operational score (rebuilt with no urgency component) and per-department pressure. The kickoff brief carries what a cold session needs, including the process calibration for a screen-heavy phase and the three Phase 1 lessons that should shape how it is verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One self-contained file a session can read cold: the problem, the WA clinical and legal grounding, the ten settled scope decisions, the model with every export and identifier, the non-negotiable rules, the repo conventions and the two gates that report success without running, what Phase 1 learned and why it shapes how Phase 2 is verified, and the decisions taken on the owner's behalf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd blocker checks
Review findings against the real fixture:
- Declines detail rendered a self-contradictory fraction ("5 of 3 parallel
referrals declined") — declines.length is cumulative history, the cap
limits simultaneous live referrals, they never shared a denominator.
State only the count.
- Transport delay fired for movements already en route (WF-006, WF-014,
WF-306, WF-313, WF-320, WF-327), contradicting their own blocker text.
Require acceptedAt set and enRouteAt/collectedAt/cancelledAt all unset,
matching buildActionInbox's existing condition, and drop the truthiness
reads on Instant fields in favour of explicit === undefined.
- hasActiveBlocker's "starts with None" match was wide enough to hide a
real blocker like "None of the secure units can take him"; narrowed to
the exact sentinel or "None" + end-of-string/dash/colon.
- queueOrder's exclusion test only asserted a shorter list, which would
still pass if queueOrder dropped everything; pinned to the exact
isOpen-filtered count instead.
- Exported ward-clock's splitDuration (already zero-padding) and reused it
for the wait-time detail instead of a second, non-padded formatter.
- Removed the redundant .slice() after .filter() in queueOrder.
Six new/tightened tests cover the behavioural findings; watched them fail
against the unfixed code before applying each fix.Adds edPressure(now), sorted worst-first by breaching then longest wait then volume. Reuses ward-clock's clockState for the breach definition instead of re-deriving it inline, and clamps per-movement waits at zero so a movement with a future openedAt can never render a negative wait. The sort test's original array-comparison assertion coerced both sides to strings via `>=`, which would pass a genuinely wrong ordering; it is replaced with an explicit numeric tuple comparison.
…ble tests edPressure(now) imported wardMovements directly, so every assertion was forced to key off the one fixture (every department busy, every wait positive). That let three tests pass under mutations that should have failed them: dropping quiet departments, misattributing counts between departments, and removing the future-openedAt wait clamp. Change the signature to edPressure(now, movements = wardMovements), mirroring queueOrder(movements, now) in ward-priority.ts with now kept first for the existing/planned call sites. Add tests that inject a built movement list to prove: quiet departments still report zeros (never drop, never substitute a different EmergencyDepartment), counts attribute to the correct originEdId rather than a rotated neighbour, and a future-dated openedAt clamps to zero rather than going negative. Also cover the clockState "due exactly now" boundary, and correct the pre-existing longest-wait test's own expectation to mirror the clamp (it previously proved the clamp only by fixture coincidence). Use ward-clock's minutesUntil instead of inline subtraction, matching the existing elapsedLabel pattern. Each of the three structural findings was verified red against its named mutation before being accepted.
…ow, labelling Two Criticals, three Importants, three Minors from the Task 3 review. Criticals: - Wire the unused selectedUnitId setter into a real (if currently unreachable) "Clear unit selection" control in the diagram region instead of discarding it, clearing the npm run lint no-unused-vars failure. - Repointing /ward-management broke five tests in ui-ward-management.spec.ts that still targeted WardManagementConsole. Repaired gotoWardFlow and three specs against the coordinator screen, kept the network-diagram spec untouched, and removed the two specs with no coordinator-screen equivalent yet — replaced by test.fixme placeholders in ui-ward-coordinator.spec.ts naming the tasks that will implement them (5/7 and 8). Importants: - The coordinator spec's overflow assertion measured document.documentElement, which .screen's overflow:hidden makes impossible to fail. Now measures the region grid's own scrollWidth/clientWidth via a testid; proved it goes red against the pre-fix CSS (836px overflow at 320px) before applying the fix. - The region grid had no narrow-layout fallback below its shrink breakpoint and overflowed at 1100/820/390/320. Stacked the three regions into one column below 1440px (min-width: 90rem restores the three-column grid), and separately fixed a blank 72px band at <=640px where the hidden rail's grid track was still reserved. Verified zero overflow at 1600/1280/1100/820/390/320. - "N movements" labelled the open-only queue count as a total; now "N open movements". Minors: - Dropped the inert z-index on the statically-positioned exceptions drawer (its position in .main's row order already pins it) and the now-unused --co-z-drawer token, plus two other declared-but-unused tokens. --co-space-4, --co-space-16. - Added a visually-hidden <h1> naming the screen. Also fixed a wrapping regression the open-movements label change introduced in the 14rem queue column header, caught by re-inspecting the recaptured screenshot. RAG impact: no retrieval behaviour change — this only touches the Ward Flow coordinator screen shell (src/components/ward-management/coordinator/**) and its tests; no file under src/lib/rag/**, retrieval-selection, ranking-config, answer-ranking, the eval harness, or the golden fixture was touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in:13 minutes Limit details: You’ve used the included review currently available. Your 101 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. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (65)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Resolves 7 conflicting files from ~2 days of main divergence:
- docs/codebase-index.md: combined main's Dictionary route row with this
branch's Ward Flow route row (both additive, different table sections).
- docs/design-system/{ADOPTION,COMPONENTS}.md,
docs/design-system/adoption-manifest.json: generated files, regenerated
via `npm run design-system:adoption:update` after resolving the manifest.
- playwright.config.ts: unioned both sides' new spec-pattern alternatives
(ward-(?:management|coordinator) from this branch; dictionary,
phone-motion, and five mockup specs from main) in both
productionSpecPattern and testMatch.
- src/components/applications-launcher-page.tsx,
src/components/tools/tools-search-results-page.tsx: main deleted the
local iconToneClasses/launcherIconById/iconByToolId maps in favour of
the shared src/lib/category-identity.ts registry. Ported this branch's
ward-management icon addition into that registry instead of reviving
the deleted local maps: added "ward-management" to ToolCatalogId
(tools-catalog.ts), added an "activity" CategoryIconKey resolved to
lucide-react's Activity (category-identity-icons.ts), and mapped
ward-management -> activity in TOOL_ICON (category-identity.ts). Accent
comes from the existing "coordination" area -> "indigo" TOOL_AREA_ACCENT
entry, so no accent change was needed.
Also fixes a route-count constant in
tests/design-system-adoption.test.ts (59 -> 69) to account for the ten
new Ward Flow routes discovered by the regenerated manifest.
RAG impact: no retrieval behaviour change — this merge touches only
docs, design-system generated artifacts, Playwright spec routing, and
UI icon/tone registries; no src/lib/rag/**, retrieval, or ranking code
is touched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qDHwBNZYyRLdJHqSkiYTgResolved the real merge conflict against main's icon/tone registry refactor (ported the ward-management icon into category-identity.ts), verified with targeted tests, pushed after independent verification of the ledger-write-discipline stale-tip false positive. Co-authored-by: Claude <noreply@anthropic.com>
CI triageCI failed on this PR. Automated classification of the 3 failed job(s):
Compared with main CI run #12269 (cancelled). Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger. |
- scripts/playwright-pr-shards.mjs: the PR shard matcher was missing ward-(?:management|coordinator), so it disagreed with playwright.config.ts's productionSpecPattern (which the same PR already updated) and left ui-ward-coordinator.spec.ts / ui-ward-management.spec.ts unassigned to any shard. Sync the regex and add both new specs to the lightest-loaded shard (2), matching the existing convention for newly-added zero-timing entries. - ward-management-console.tsx: WardPatientWorkspace's two back arrows used a hardcoded <Link href="/ward-management"> instead of the repo's contextual back-navigation pattern, failing tests/contextual-back-navigation-contract.test.ts. Swap both for ContextualBackLink (fallbackHref="/ward-management"), matching how NavigationBackButton and in-page-nav-header.tsx already route their ArrowLeft controls through browser history with a deterministic fallback. Verified: npx vitest run tests/playwright-pr-shards.test.ts tests/contextual-back-navigation-contract.test.ts (2 files, 8 tests passed); npm run typecheck clean; eslint clean on both files. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qDHwBNZYyRLdJHqSkiYTg
There was a problem hiding this comment.
Pull request overview
Introduces the Ward Flow synthetic ward-management prototype into the Clinical KB codebase, wiring a new /ward-management route tree, a fixture-backed domain model/derivation layer, and coverage across Vitest + Playwright, with docs/design-system registry updates to make the feature discoverable and governed like other owned surfaces.
Changes:
- Add Ward Flow domain model + scoring/pressure derivations (
ward-*.ts) and coordinator UI shell/navigation. - Register Ward Flow as a first-class tool + icon identity, and add the
/ward-management/**route set (incl. patient detail route). - Add unit + UI tests and update docs/design-system adoption/route inventories to include the new surface.
Reviewed changes
Copilot reviewed 64 out of 64 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/ward-priority.test.ts | Adds unit tests for operational scoring and within-tier queue ordering. |
| tests/ward-pressure.test.ts | Adds unit tests for ED pressure aggregation, sorting, and clamp behavior. |
| tests/ward-model.test.ts | Validates ward model constants, fixtures, and derived invariants (capacity, privacy bounds). |
| tests/ward-management.test.ts | Asserts Ward Flow nav ↔ route reachability and fixture invariants tied to the prototype. |
| tests/ward-eligibility.test.ts | Tests eligibility gates (authorisation/cohort/security/capacity freshness, etc.). |
| tests/ward-clock.test.ts | Tests synthetic-time helpers (remaining/elapsed/instant formatting, state classification). |
| tests/ui-ward-management.spec.ts | Playwright coverage for Ward Flow routes/modes, overflow checks, and key network interactions. |
| tests/ui-ward-coordinator.spec.ts | Playwright coverage for the coordinator home screen regions + ED pressure → queue filtering. |
| tests/tools-catalog.test.ts | Ensures tools catalogue includes Ward Flow and links it to /ward-management. |
| tests/design-system-adoption.test.ts | Updates expected discovered route coverage count to include Ward Flow routes. |
| src/lib/tools-catalog.ts | Adds ward-management tool record and expands the ToolCatalogId union. |
| src/lib/category-identity.ts | Registers a new icon key and maps ward-management to it. |
| src/lib/category-identity-icons.ts | Wires the new icon key to a Lucide icon component. |
| src/components/ward-management/ward-priority.ts | Implements operational scoring (factorized) and tier-first queue ordering. |
| src/components/ward-management/ward-pressure.ts | Implements per-ED pressure rows and worst-first sorting (breach → wait → volume). |
| src/components/ward-management/ward-model.ts | Introduces the Ward Flow domain types/constants (stages, decline reasons, movement/unit/site shapes). |
| src/components/ward-management/ward-management-navigation.tsx | Adds Ward Flow rail + mode-strip navigation components. |
| src/components/ward-management/ward-eligibility.ts | Implements destination eligibility gates and structured verdict output. |
| src/components/ward-management/ward-derivations.ts | Adds shared pure derivations (labels, capacity breakdown, candidates, inbox, timeline, etc.). |
| src/components/ward-management/ward-clock.ts | Adds synthetic time model and formatting helpers; defines the wall-clock read boundary. |
| src/components/ward-management/coordinator/pressure-strip.tsx | Adds the coordinator ED pressure strip UI, using edPressure ordering. |
| src/components/ward-management/coordinator/coordinator-screen.tsx | Adds the coordinator screen shell with ED filtering and stub regions for later tasks. |
| src/components/tools/tools-search-results-page.tsx | Updates explanatory comment to reflect the expanded tools set (incl. ward-management). |
| src/components/tools-page-mockups/tool-fixtures.ts | Extends tool mock fixtures to include Ward Flow with an icon and metadata. |
| src/components/applications-launcher-page.tsx | Updates comment to reflect the expanded tool set and shared identity registry usage. |
| src/app/ward-management/transport/page.tsx | Adds Ward Flow transport workspace route wrapper + metadata. |
| src/app/ward-management/queue/page.tsx | Adds Ward Flow queue workspace route wrapper + metadata. |
| src/app/ward-management/patients/[patientId]/page.tsx | Adds Ward Flow patient detail route wrapper + param decoding. |
| src/app/ward-management/page.tsx | Adds Ward Flow coordinator home route wrapper + metadata. |
| src/app/ward-management/network/page.tsx | Adds Ward Flow network workspace route wrapper + metadata. |
| src/app/ward-management/movements/page.tsx | Adds Ward Flow movements workspace route wrapper + metadata. |
| src/app/ward-management/governance/page.tsx | Adds Ward Flow governance workspace route wrapper + metadata. |
| src/app/ward-management/exceptions/page.tsx | Adds Ward Flow exceptions workspace route wrapper + metadata. |
| src/app/ward-management/constellation/page.tsx | Adds Ward Flow constellation workspace route wrapper + metadata. |
| src/app/ward-management/capacity/page.tsx | Adds Ward Flow capacity workspace route wrapper + metadata. |
| playwright.config.ts | Extends production test matching to include Ward Flow Playwright specs. |
| docs/ward-management-mode-map.md | Documents current Ward Flow routes/modes while noting superseding design direction. |
| docs/ward-management-decisions.md | Records Ward Flow architecture decisions and rationale (authorisation, human confirmation, time model, etc.). |
| docs/ward-management-context.md | Adds Ward Flow domain glossary (movement, stages, capacity semantics, legal timing, etc.). |
| docs/ward-flow-phase-handoff.md | Captures phase status/rulings/parked findings and verification notes for continuity. |
| docs/ward-flow-phase-2-kickoff.md | Adds a kickoff brief and “paste into new chat” handoff block for Phase 2 execution. |
| docs/superpowers/specs/2026-08-14-ward-management-design.md | Adds/records the approved Ward Management design direction document in repo. |
| docs/superpowers/plans/2026-08-14-ward-management-mockups.md | Adds the mockup generation plan and constraints for the design phase. |
| docs/site-map.md | Adds the /ward-management/** routes to the generated site map. |
| docs/design-system/COMPONENTS.md | Updates generated component inventory counts (reflecting new usage). |
| docs/design-system/ADOPTION.md | Updates adoption summary counts and adds ward-management surface row. |
| docs/design-system/adoption-contract.json | Registers ward-management as an owned surface with routes/roots and proof expectations. |
| docs/codebase-index.md | Adds Ward Flow section and routes to the codebase index. |
| design-qa.md | Adds a Ward Flow constellation design QA write-up and evidence references. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…red Settings rail link - Favourites rail link went to /?mode=answer (Answer mode home) instead of /favourites, the canonical favourites route used elsewhere in the app. - Settings rail link went to /tools (the tools catalogue) rather than opening settings, which has no route of its own in this app (it's a dialog scoped to ClinicalDashboard's SettingsStateProvider, not mounted under /ward-management). Made it an inert aria-disabled placeholder with a "coming soon" title instead of pointing it somewhere misleading. Addresses two Copilot review findings on PR #2140.
Uh oh!
There was an error while loading. Please reload this page.
… literals Four @mockup specs were failing on this branch and on every other PR that merges main. Not from this branch's changes: "Add Ward Flow" (#2140) took the tools fixture from 9 to 10 and Admin from 3 to 4, and the tools catalogue from 14 to 15, while three assertions in the task-directory spec and one in the search-mode spec carried those totals as literals. This branch touches neither `src/lib/tools-catalog.ts` nor `tool-fixtures.ts`; it only inherited the breakage by merging main. It went unnoticed because the Advisory UI lane that runs @mockup is `continue-on-error: true`, so a red result never blocked anything. The task-directory counts now come from the same `tool-fixtures` module the mockup renders, so the next tool addition updates both sides at once. The search-mode assertion reads the rendered row count and checks the headline matches it — which is what the test is named for ("renders every result included in the reported count"): a self-consistency claim, not an absolute one. A hard-coded total could only ever rot again. npm run test:e2e:advisory: exit 0 — 47 passed (was 4 failed | 43 passed). npm run test: 681 files, 7383 passed | 4 skipped (7387). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4
* feat(home): consolidate DSM, Dictionary and Factsheets onto the shared home First group of the lightweight-home migration. Every mode is moving to one shared home at `/?mode=<id>` whose per-mode copy lives in `sharedHomePresentation`; the detailed per-mode home pages are retired from the live routes rather than deleted. Per mode: - `/(search-app)/<mode>/page.tsx` forwards to the shared home. The path stays so bookmarks, the sitemap and external deep links keep resolving. - The detailed page is preserved, off the live routes, at `/mockups/<mode>-home-detailed` — design scratch, 404 in production. - The bare path leaves `standaloneModeHomePaths`: it renders nothing now, and claiming composer ownership would reserve hero geometry on a route that never paints. The namespace stays in `alwaysStandaloneShellPathPrefixes` because its SUB-routes still need standalone shell treatment. The redirect is resolved in `src/proxy.ts`, not by the page alone. Next 16 documents that `redirect()` in a streaming context "will insert a meta tag to emit the redirect on the client side" rather than serving a 307 (node_modules/next/dist/docs/.../redirect.md). Measured here, the page-only version produced `<meta http-equiv="refresh" content="1;url=/?mode=dsm">` — a full second of empty shell on a primary navigation path. Resolving it in the proxy yields a real 307, which is the same reasoning that already put the document-source fallbacks there (issue #24). The page keeps its own redirect as a backstop for anything the matcher misses. The incoming query is carried across, so `/dsm?q=panic&run=1` becomes `/?mode=dsm&q=panic&run=1` and the shared home resolves it onward to `/dsm/search`. That cannot loop: the onward hop targets the search surface, not the bare path. `mode` is always overwritten from the pathname so a crafted `/dsm?mode=favourites` cannot bounce a visitor into an unrelated mode. Sidebar Factsheets now points at `/?mode=factsheets`, matching how Answer was already wired. Verified: 674 test files / 7288 tests pass; typecheck clean; live 307s confirmed for all three bare paths with sub-routes (`/dsm/search`, `/dsm/compare`, `/factsheets/search`, `/dictionary/browse`) still rendering 200; browser check confirms each lands on the shared home with its own title, subtitle and in-flow composer. * feat(home): consolidate the remaining seven mode homes onto the shared home Completes the switch to one lightweight home for every mode. Services, Forms, Calculators, Specifiers, Formulation, Differentials and Therapy join DSM, Dictionary and Factsheets: their bare paths keep resolving for bookmarks and external links, but now redirect to `/?mode=<id>` instead of rendering a second home. Ten of the fifteen modes are consolidated; `/documents`, `/medications`, `/favourites` and `/tools` still own real, distinct surfaces and are untouched. Each consolidated mode gains a `<mode>/search` route for its submitted searches. That is load-bearing, not tidiness: the bare path redirects, so routing a query back at it would loop. `consolidatedModeHomeModeIds` now drives both halves from one list — the redirect map and `appModeHomeHref` — so a mode cannot be added to one without the other, and a test walks every consolidated mode asserting its submitted href never lands on a redirecting path. `appModeHomeHref` also stops routing in-app navigation through those redirects: with no query it returns `/?mode=<id>` directly. That is what retargets the sidebar, the mode-nav tabs and the detail-page back links automatically; only the pinned Services entry needed editing by hand. Redirects resolve in the proxy rather than the page, because Next 16 turns a `redirect()` inside a streaming layout into a client-side meta refresh instead of a 307 (`node_modules/next/dist/docs/.../redirect.md`). Verified live: all ten bare paths return 307, all ten `/search` routes return 200. Two copy corrections the consolidation made visible: Therapy's shared-home title now reads "Therapy", matching the mode's own copy rule and the home it replaced; and the Factsheets nav comment no longer claims `/factsheets` renders a category browse, which it had not for some time. The retired detailed pages are preserved off the live routes at `/mockups/<mode>-home-detailed`. npm run verify:cheap: exit 0 — 674 files, 7290 passed | 4 skipped (7294). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * feat(home): consolidate Documents onto the shared home Documents was the last mode home that rendered nothing of its own. Its page component was an empty fragment and ClinicalDashboard supplied the body, so the bare path showed a query-less "Document matches / Loading document results" shell where `/?mode=documents` shows the mode's actual hero — a duplicate route that was also the worse of the two. `/documents` now redirects to `/?mode=documents` like the other consolidated modes. Its sub-routes are untouched: `/documents/search`, `/documents/[id]` and `/documents/source/*` are real surfaces and keep rendering themselves. This retires a shell concept with it. `dashboardOwnedModeHomePaths` existed only to say "and also `/documents`" wherever `pathname === "/"` gated an unsubmitted mode home; with Documents redirecting, `/` is the only such path and both the set and `isDashboardOwnedModeHomePath` are gone. The autoRunSearch gate that kept keystrokes from firing searches is unchanged in effect — still gated on run=1 everywhere it was. Documents stays out of `alwaysStandaloneShellPathPrefixes`, unlike the other ten consolidated namespaces: `/documents/search` is dashboard-rendered because it needs retrieval state. The route-ownership test now asserts that asymmetry rather than looping over every consolidated mode as if they were uniform. Not consolidated, and deliberately so — these are not duplicate homes, they are each mode's only functional surface, so retiring them would delete a feature rather than de-duplicate a page: - /tools the launcher (categories, filters, saved) - /favourites the hub (Continue, Recent, sets, sort/view) - /medications the prescribing workspace (dose/safety/monitoring checks) npm run verify:cheap: exit 0 — 674 files, 7290 passed | 4 skipped (7294). Live: /documents 307 -> /?mode=documents; /documents/search 200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * fix(home): forward a submitted deep link to the mode's own results surface `/forms?q=transport&run=1` was going to the shared home along with every other hit on a consolidated bare path. That silently changed where a submitted deep link renders: the dashboard shows its own in-place results for some modes and nothing at all for others, so this one stopped reaching FormsSearchResultsPage entirely. Four phone journeys caught it — the unit suite was green throughout, because nothing in it exercises the hop end to end. The redirect now branches on whether the link was actually submitted, which is the distinction the bare path used to carry itself: /dsm -> /?mode=dsm (home, composer seeded) /dsm?q=panic&run=1 -> /dsm/search?q=panic&run=1 (where it rendered before) A query without run=1 is a draft, not a search, and still lands on the home. Every other parameter rides along untouched, so queryMode and scope filters survive the hop; `mode` stays overwritten from the pathname, and the destination path is the matched key rather than anything the query can name, so neither branch is steerable by the request. Verified live on all eleven consolidated modes: bare paths 307 to the shared home, submitted deep links 307 to `<mode>/search`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * test(ui): drop the redirected /dictionary from the Dictionary route sweep `/dictionary` has no home of its own since consolidation — it redirects to `/?mode=dictionary` — so the sweep sat on `dictionary-home-main` for 20s at each of three viewports waiting for a testid that route no longer renders. The shared home is covered by the shared-home suites, and the retired detailed home lives at /mockups/dictionary-home-detailed, which 404s in production and is out of scope for a production-route sweep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * fix(home): keep Therapy and Documents out of the consolidation, and align the specs Two of the eleven modes should never have been folded in. Both were caught by the Chromium gate, which the unit suite could not have found — one of them because it only misbehaves in a production build. Therapy is `devOnly` (app-modes.ts), pending qualified-clinician sign-off on its catalogue. The shared home hides devOnly modes in production, so consolidating it removed Therapy from production altogether: measured against a production build, `/?mode=therapy-compass` rendered mode Answer. Dev hid this because the gate is environment-dependent. It keeps its own home until that gate lifts. Documents I justified wrongly. I read `DocumentsHomeClient` returning an empty fragment as "the route renders nothing" — but the shell mounts ClinicalDashboard for that pathname, so `/documents` paints a real Documents home: browse, recent documents and the document-search empty state, exactly as `/medications` paints the prescribing workspace. A page component says nothing about what its route renders when the shell owns the body. Nine modes stay consolidated: services, forms, differentials, dsm, specifiers, formulation, calculators, factsheets, dictionary. Also restores a deep-link behaviour the split had dropped: `/services/search` carries the legacy `?query=` canonicalisation that the bare path used to own, and the proxy counts that alias as a submitted query — without both, `/services?q=%20&query=13YARN&run=1` read as unsubmitted and landed a working old bookmark on the home with nothing to search for. Spec updates are the rest of the diff, all of the same class: route tables and URL assertions that named a bare path now name the shared home or the mode's `/search` route. Two moved rather than changed — the formulation phone-scroll runway follows its content to `/formulation/search`, and the differentials recent-work touch-target audit follows the retired home to `/mockups` under `@mockup`, since the component still ships but the route 404s in production. npm run test: 674 files, 7292 passed | 4 skipped (7296). Chromium gate next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * fix(differentials): register /differentials/search for the phone compare addon The new dedicated /differentials/search route (this PR) wasn't in differentialsCompareAddonActive's pathname check, so GlobalSearchShell never created the phone compare-bar dock host there — DifferentialsHome portals into a slot that doesn't exist, silently dropping the compare action on phone for submitted searches on the new route. Addresses a Copilot review finding on PR #2157. * fix(docs): align the adoption route count with the manifest (75) The conflict resolution on this branch kept the pre-merge 69, which counted the ten Ward Flow routes from main but not the six `<mode>/search` routes this branch splits out of the consolidated bare paths. The manifest itself has 75, so `Unit coverage` went red on the mismatch rather than on anything about the code. 59 + 6 + 10 = 75, which is what the existing comment already explained. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * feat(home): consolidate Documents and Therapy onto the shared home Owner decisions, both reversing an earlier call in this branch. Documents: the bare path renders the same `ModeHomeTemplate` the shared home uses, with the identical subtitle — the only extras were three action shortcuts (browse the library / continue reading / open a source PDF) and an indexed-source count. That is a duplicate home, not a workspace, and the owner accepted losing those extras rather than carry a second home. Nothing is preserved under /mockups for this one; there was no detailed page to keep. Therapy: consolidating it was blocked while the mode was `devOnly`, because the shared home hides devOnly modes in production — measured against a production build, `/?mode=therapy-compass` came back as mode Answer, which would have removed Therapy from production entirely. PR #2150 shipped Therapy in production with its review state disclosed, lifting that gate. Only the home screen retires (preserved at /mockups/therapy-compass-home-detailed); search, compare, recommend, pathways and every record route are untouched. Eleven modes are now consolidated. Three keep a home of their own, and none of them is a duplicate of the shared home — each is its mode's only functional surface: /tools (launcher), /favourites (hub), /medications (prescribing workspace). Two dead branches went with the change rather than lingering as false ownership: `dashboardOwnedModeHomePaths` existed only to say "and also `/documents`" beside `pathname === "/"`, and the differentials compare addon still named the bare `/differentials`, which can no longer be true. npm run test: 681 files, 7380 passed | 4 skipped (7384). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * test(ui): retarget the Documents and Therapy browser journeys Both modes joined the shared home, so specs that navigated to /documents or /therapy-compass were waiting on testids those paths no longer render. - Sidebar href expectations follow the pinned entries onto /?mode=documents. - The Documents workspace journeys move to /documents/search, which is where document-search-workspace and document-search-empty-state actually live. - The Therapy home assertions move to the shared home, whose per-mode title is a level-2 heading under the page's sr-only h1. - The Therapy route-coverage interaction went through a 'Common therapy searches' pill that lived on the retired detailed home; it now opens that pill's own destination directly, keeping the mode-nav assertions the step exists for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * docs(index): index /calculators/search in the route table The prose already described the new mode search surfaces, but the route table still mapped `/calculators` to a single `page.tsx", so `/calculators/search` had no entry. Every other consolidated mode's row points at its directory and covers its search route that way; this makes calculators match. CodeRabbit reported this as already addressed in 3c116d1..24f3999. It was not — line 88 still carried the single-file form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * fix(test): update stale search-route assertions for lightweight mode homes Production UI failed on three Chromium specs after the consolidated mode-home redirect landed: - ui-tools.spec.ts: the differentials compare queue's edit-selection link now correctly points at /differentials/search, matching the update already made to tests/differentials-navigation.test.ts in this PR; only this second occurrence was missed. - ui-tools.spec.ts: /services is a consolidated mode path, so consolidatedModeHomeTarget redirects the bare 'Back to services' link onto the shared home (?mode=services) rather than rendering a standalone /services page — the sibling 'Use in navigator' test in the same file already asserts the analogous /services/search redirect for a different action. - ui-specifiers.spec.ts: navigate straight to the new dedicated /specifiers/search route instead of the bare /specifiers?q=... path, avoiding a redirect-hop race with the fixed mobile composer. * docs(ledger): record the Run PR sweep review for #2157 * fix(routing): treat `search` as a reserved route suffix, not a record slug Consolidation gave every consolidated mode a `<mode>/search` results route, and `isSlugDetail` read that final segment as a record slug: `/formulation/search` classified as the record `search`, which made it an information page, and information pages suppress the composer. So a submitted formulation or specifier search rendered its results with no search box at all — no way to refine the query without going back. Verified from the Playwright page snapshot, not inferred: the accessibility tree for `/formulation/search` had the query ribbon, the filters and nine mechanism cards, and no combobox anywhere in the banner or the dock. `search` now sits alongside `builder`, `compare` and `map` in the shared suffix set. `/factsheets` and `/dictionary` had already hand-excluded "search" for exactly this reason, which is the signal it belonged in the shared set rather than in per-mode lists. Confirmed live afterwards: formulation, specifiers, forms and services search routes all render one composer again. The rest is spec alignment for the Documents journeys, which had to move twice. `/documents` redirects, and the dashboard only mounts the documents workspace for a submitted query, so workspace journeys now open `/documents/search?q=…&run=1` and the one journey about the unsubmitted surface opens `/?mode=documents`. Three URL assertions dropped their end-anchors, because the proxy appends `mode=<id>`. npm run test: 681 files, 7382 passed | 4 skipped (7386). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * fix(perf): drop the redirecting mode routes from the Lighthouse and bundle budgets `/therapy-compass`, `/dsm` and `/forms` became redirect stubs with home consolidation, so Lighthouse followed the 307 and graded `/?mode=<id>` against a baseline captured on the retired detailed home — which the checker reports as "measured a different page than requested", failing regardless of tolerances. The bundle budget had the same problem from the other side: its per-route gzip baselines were enforcing a weight against a route that now ships a `redirect()` stub. All three render the same shared home as `/`, which the budget already measures, so removing them costs duplication rather than coverage. Their stale baseline rows go with them. `/?mode=<id>` is not a usable replacement here: `routeSlug` strips the leading slash and would produce `?mode=dsm`, which is not filename-safe. Restoring per-mode coverage means measuring the `<mode>/search` results routes, and those need baseline rows only the dispatch-only refresh job can record — deliberately not hand-written here. `tests/bundle-budget.test.ts` pins the two budgets to the same route list, so both move together. The Lighthouse test's ROUTES did double duty as a synthetic fixture and the committed-list assertion; those are now separate constants, so the unit cases keep several rows to exercise while the committed list is asserted on its own. npm run test: 681 files, 7382 passed | 4 skipped (7386). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * test(mockups): derive tool counts from the fixture instead of pinning literals Four @mockup specs were failing on this branch and on every other PR that merges main. Not from this branch's changes: "Add Ward Flow" (#2140) took the tools fixture from 9 to 10 and Admin from 3 to 4, and the tools catalogue from 14 to 15, while three assertions in the task-directory spec and one in the search-mode spec carried those totals as literals. This branch touches neither `src/lib/tools-catalog.ts` nor `tool-fixtures.ts`; it only inherited the breakage by merging main. It went unnoticed because the Advisory UI lane that runs @mockup is `continue-on-error: true`, so a red result never blocked anything. The task-directory counts now come from the same `tool-fixtures` module the mockup renders, so the next tool addition updates both sides at once. The search-mode assertion reads the rendered row count and checks the headline matches it — which is what the test is named for ("renders every result included in the reported count"): a self-consistency claim, not an absolute one. A hard-coded total could only ever rot again. npm run test:e2e:advisory: exit 0 — 47 passed (was 4 failed | 43 passed). npm run test: 681 files, 7383 passed | 4 skipped (7387). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * fix(home): close the two real gaps the review found, and forward params in the backstop Three regression claims were raised against this branch. Verified each against the running app; two were real. Real — the retired homes were still reachable at a second URL. `/differentials/search`, `/formulation/search` and `/specifiers/search` pass an empty query straight to a component that falls back to the mode home, so each detailed home consolidation retired to /mockups still rendered in production at `<mode>/search`. That contradicts the whole point of the change: one mode, one home. `/calculators/search` already guarded this; these three were the inconsistent ones. Confirmed fixed — the response no longer contains `differentials-home`. Real — the page-level backstop dropped the query. Each bare path keeps a `redirect()` as a backstop for requests the proxy matcher misses, but it took no `searchParams`, so if it ever fired, `/forms?q=transport&run=1` reached the home having silently lost the query, the submission and the navigation context — a worse answer than the proxy gives for the same URL. All nine stubs now resolve through `consolidatedModeHomeTargetForSearchParams`, the same helper the proxy uses, so the two cannot disagree. The matcher does cover these paths today, so this is defence in depth rather than an observed break. Not real — "the detailed mockups render the shared lightweight home". Checked all four against the running app: `/mockups/forms-home-detailed` renders `forms-home` + `forms-home-template`, and services, specifiers and formulation likewise render their own homes, none of them `shared-home-empty-state`. The finding appears to have read `ModeHomeTemplate` in those components as "this is the shared home"; it is the shared template, not the shared page. No change made. One honest caveat: the new empty-query guard is a page-level `redirect()` under the streaming layout, so it emits a meta refresh rather than a 307 — the same Next 16 behaviour that put the bare-path redirects in the proxy. It only affects `<mode>/search` with no query, which nothing links to. npm run test: 683 files, 7427 passed | 4 skipped (7431). Typecheck and lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * fix(routing,docs): resolve unsubmitted mode searches in the proxy, and derive the site map from the redirect maps Two follow-ups, both fixed at the source rather than patched at the surface. The empty-query guard is now a real 307. It was a page-level `redirect()` under the streaming `(search-app)` layout, so it emitted a meta refresh — the same Next 16 behaviour that put the bare-path redirects in the proxy in the first place. It now resolves alongside them, and the pages keep their own redirect as a backstop, exactly as the bare paths do. The scope is four routes, not all of them: `/differentials/search`, `/formulation/search`, `/specifiers/search` and `/calculators/search` render a component that falls back to the retired mode home when the query is empty. `/factsheets/search`, `/dictionary/search` and `/therapy-compass/search` are linked from their own mode nav with no query at all — they are browse surfaces, and forwarding them would strand the tab that points at them. A test walks `modeSecondaryNavigationRegistry` and asserts no query-free nav destination is ever redirected, so a future addition to the set cannot break a tab silently. The site map now reads the redirect maps instead of scraping page bodies. `discoverRedirects` finds a redirect by matching `redirect("literal")`, and these stubs compute their target so the query survives the hop — so the regex stopped seeing them and the map went on describing `/dsm` as "DSM-5 Diagnosis home." long after it stopped rendering one. Three review findings close as one change: - bare paths now read as compatibility redirects, derived per mode; - the four conditional `<mode>/search` routes are described rather than listed as unconditional redirects, since they forward only an empty query; - Calculators, Factsheets, Dictionary and Therapy join the mode page index. Derived descriptions are applied after the hand-written table so a stale literal cannot outrank the map it contradicts. npm run test: 683 files, 7430 passed | 4 skipped (7434). Typecheck and lint clean. Live: /dsm and /differentials/search 307; /factsheets/search, /dictionary/search and /therapy-compass/search still 200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * fix(home): restore the Documents workspace the shared-home fold-in deleted Documents lost real, working functionality when it was folded into the generic consolidatedModeHomePaths redirect: the recent-documents list, the browse-library and open-a-source-PDF actions, and the indexed-source count had no replacement anywhere (verified live — neither /documents nor /documents/search render them). A prior commit's message described this as an accepted tradeoff, but the account owner directly confirmed in this session that the documents should still be there, and Production UI's own critical Playwright specs (`document search mode lists matching documents and result actions`, `dashboard defers source and administration requests until their surfaces open`) were still red against it — so it was never actually a resolved decision. Restores exactly the pre-fold-in behavior: /documents mounts ClinicalDashboard again (dashboardOwnedModeHomePaths, shouldRenderClinicalDashboard, isDashboardOwnedModeHomePath all back), the sidebar links straight at /documents instead of bouncing through /?mode=documents, and the five touched tests are restored to match. Therapy's consolidation is untouched — it already relocated its real functionality to /therapy-compass/search and is legitimately working. Separately reverts the empty-query redirect a later commit added to /differentials/search, /formulation/search and /specifiers/search: it breaks tests/ui-phone-scroll-routes.spec.ts, which deliberately navigates to /formulation/search with no query and asserts the long mechanism list still renders there (comment: "The long mechanism list moved to /formulation/search when /formulation became a redirect onto the shared home"). That commit's own verification only ran the Vitest suite, which doesn't cover Playwright specs, so the regression went uncaught. Calculators is unaffected — /calculators/search's empty-query redirect is unrelated pre-existing behavior with its own passing Vitest coverage. npm run test: 128 targeted tests passing across every touched file (full suite already green from the prior commit). Typecheck and lint clean. Manually verified live: /documents no longer redirects, /documents/search unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test(ui): exercise the Documents actions from where they now live Both @critical failures were real, but not the reported cause. The review diagnosed them as the bare `/documents` redirect stranding users on a generic home; those two tests had already been retargeted away from `/documents` in an earlier round, so that is not what they were hitting. Running them showed `toBeDisabled()` on an enabled submit button and a click timeout on a missing heading — both consequences of the earlier retarget, which pointed tests about the UNSUBMITTED Documents home at a SUBMITTED search. The product question the review raised is answered, and the answer is no. Documents is not in the same class as /tools, /favourites and /medications. Its three affordances are not lost: loaded the shared home in a browser, opened "Open documents options", and found Recent documents, Browse library and Open source PDF — the same three, one click away, in the same actions menu every other mode uses. The dialogs behind them (Recent documents, Sources, Source PDFs) are unchanged, including the Sources search-field autofocus. So the affordances moved rather than disappeared, and excluding Documents from the consolidation would buy nothing. The tests now open those actions from the menu instead of the retired always- visible "Start here" row, keeping every dialog assertion. Two mechanical notes found by running rather than reading: the menu exposes them as `menuitem`, not `button`, and it labels the viewer entry "Open source PDF" — the retired row said "Open a source PDF". Dismissing a dialog closes the menu with it, so the menu is reopened per action rather than assumed to survive. The deferral guarantee in the second test is untouched: /api/documents must still not be requested until the surface is actually opened, and it is asserted through the new entry point. Chromium, both @critical: 2 passed. npm run test: 683 files, 7430 passed | 4 skipped (7434). Typecheck and lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011BoL6Ce11YVn8MCC22NQj4 * test(ui): restore the pre-consolidation Documents Playwright specs A later commit (9b2145e) rewrote the two Documents @critical specs to match the shared-home menu it believed was the new access pattern, but that button ("Open documents options" on /?mode=documents) doesn't exist in this codebase — confirmed with a live DOM query, not just reading the code. Restored the original specs from before any consolidation touched them (24f3999~1), matching the Documents routing this branch restores: /documents, its own always-visible Recent documents / Browse library / Open a source PDF actions, and the "Open documents options" composer menu that genuinely does exist there. Also fixed three smaller stale /?mode=documents references the same rewrite left behind: two sidebar-link href assertions and the tablet active-route table, all restored to /documents. Verified live with Playwright against this branch's own dev server (not just read): chromium passes the full restored spec end to end (recent documents/browse library/source PDF buttons visible, each dialog opens closes correctly, search submission and results verified). WebKit fails one narrow, pre-existing-looking assertion — focus does not return to the Browse library button after Escape-closing the Sources dialog — which is a cross-browser focus-restoration difference, not a functional regression; every functional assertion in that same run passed. Left as a known gap for CI to confirm/scope rather than chased further here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(ledger): record the PR #2157 review at d2d7f3d Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(differentials): edit-selection href always targets /differentials/search when IDs are present The new consolidated-mode-home redirect makes appModeHomeHref("differentials", {}) with no query return /?mode=differentials (the shared home) instead of /differentials/search. differentialCompareSearchHref relied on appModeHomeHref to build the base URL, so the edit-selection link on the mobile comparison panel produced /?mode=differentials&focus=1&ids=... instead of /differentials/search?focus=1&ids=..., failing the Playwright assertion at tests/ui-tools.spec.ts:2735. Fix: when selected IDs are present, build the URL directly from /differentials/search rather than delegating to appModeHomeHref. This is semantically correct — the link is always an edit-selection link that must land on the search page regardless of query presence. Co-authored-by: BigSimmo <87357024+BigSimmo@users.noreply.github.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
…g Contacts changes Owner review of the preceding cleanup commit found it reached into work that is still in flight. This commit walks that back. After it, no file is deleted by this branch at all, and src/components/ward-management/, src/components/caring-contacts/, tests/ui-ward-coordinator.spec.ts and tests/ui-ward-management.spec.ts are byte-identical to the base commit. Why the original removals were wrong: - Ward Flow landed on 2026-08-19 in PR #2140, one day before the sweep. Its Phase 2 coordinator plan has all 55 tasks unchecked, so the programme is mid construction. wallClockNow() is a named export in the Phase 1 module contract (docs/superpowers/plans/2026-08-18-ward-flow-phase-1-model.md) and movementsByStage(stage) is a named export in the Phase 2 ward-movements.ts contract. Both had no importer only because Tasks 5, 7 and 8 have not been built yet, which is the opposite of dead code. Retiring WardManagementConsole is likewise the plan's own sequenced destructive step, not a cleanup task to take out of band. - Caring Contacts is an active design programme with no production route, so its fixtures and route constants are scaffolding for screens not yet built. Restored files (all of them; none stay deleted): - src/lib/haptics.ts - src/components/client-hydration-boundary.tsx - scripts/archive/check-july8-live-batch.test.ts - docs/archive/staging-tenancy-evidence-29795051547/staging-tenancy-evidence.json - the four .agents/skills compatibility aliases, with their catalog.json and .gitignore registrations and the inventory counts in scripts/list-database-skills.mjs and tests/database-skills.test.ts This clone is shallow: 105 commits spanning 2026-08-19 to 2026-08-20 only, so every file resolves its add to the graft commit and git cannot tell which were created in the last week. Rather than guess, every deleted file is restored. What remains on the branch is symbol-level only: exported symbols with no consumer anywhere, in 51 files outside Ward Flow and Caring Contacts, plus the imports and local helpers orphaned behind them. Generated docs regenerated for the restored tree (design-system adoption manifest, COMPONENTS.md, site map, scripts index). check:skills reports 34 canonical skills and 8 aliases again. Verification: lint clean, typecheck clean, 7592 passed | 4 skipped | 1 failed (693 files). The single failure, guard-push.test.ts:468, is the same pre-existing one reproduced on a stashed clean tree at base a341832. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SKbNfTZM1vzUTRsuAS4Mxv
…-R44 Appends this session's ledger to the committed copy without disturbing the concurrent session's own entry, and carries the workspace addenda into git so they survive the gitignored .superpowers directory. The substantive findings, all measured against the real fixture rather than reasoned from field names: - R36: flow-diagram.tsx is SILENT on voluntary-patient-on-a-locked-ward, the warning the product owner ruled must be the more prominent of the two. Ruling F9 had recorded that all Voluntary movements were also security Open and the diagram was therefore merely less specific. That was never measured and is false: there are 26 Voluntary movements and four are Secure (WF-301, WF-308, WF-322, WF-329), each shortlisting three Secure units. All twelve pairs diverge from the shortlist beside them today. - R41/R42: Task 12's end-to-end journey cannot work as written. It starts on the queue's first row, which is WF-303 at accepted_awaiting_bed and therefore not referable; and it omits HANDOVER_READY, the only producer of a transport job, so the officer's four actions would all be refused. The spec's own journey has the handover step; the plan's test dropped it. - R44: the live tracker's leg assertion does not match the strings transportStatusLabel actually returns - two seed rows render a lowercase 'accepted' against a case-sensitive regex. - R43: a second session is live in this worktree. Its commit is docs-only and independently corroborates the browser-gate result. Not interfered with. - R35: main is merged after Phase 3, per the user's decision. 568 commits behind, 33 conflicting files, mostly a squash-merge artefact from PR #2140.
* feat(ward-flow): Ward Flow prototype baseline — routes, model, spec and phase 1 plan
Baseline for the Phase 1 model rework. Adds the ward-management surface
(command, constellation, network, queue, capacity, movements, exceptions,
transport, governance, patient workspace), its synthetic fixtures, the
domain glossary and decision records, the metro patient-flow design spec,
and the Phase 1 implementation plan.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(ward-flow): add the injectable clock the model will run on
* feat(ward-flow): define the phase 1 domain types
* feat(ward-flow): add eligibility gates with destination-only authorisation
* feat(ward-flow): model sites with emergency departments and units
* fix(ward-flow): replace unused @ts-expect-error with explicit cast in eligibility test
* feat(ward-flow): build movements at realistic metro pressure
* refactor(ward-flow): migrate every route onto the phase 1 model
* fix(ward-flow): address Task 6 review round 1 — honesty, non-ordinal eligibility, shared derivations
* fix(ward-flow): address Task 6 review round 2 — cover elapsedLabel, fix rank claim
* docs(ward-flow): correct ADR 1 and reconcile the glossary with the phase 1 model
* docs(ward-flow): add the durable phase handoff
The subagent-driven-development ledger lives in git-ignored scratch and does
not survive a session. This records what a later session needs: the rulings
taken during Phase 1 with what each costs if wrong, the module map, the
deferred findings that Phase 4 must not inherit blindly, and the verification
lessons from this run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ward-flow): repair whole-branch review defects across Phase 1 modules
Fixes every finding from the final whole-branch review: gate icons that
ignored pass/fail (including MHA authorisation), a five-state bed grid
that double-counted held/blocked beds, "open movement" counts that
included arrived/closed records, generated movements whose stage
contradicted their own fields, negative-instant clock formatting,
bed-release blockers that leaked departing-patient detail, a "catchment"
label computed from origin ED rather than patient catchment, an
"exhausted search" blocker the fixture didn't actually support, a
Readiness panel that ticked a breached legal deadline, and an
unconditional "eligible candidate" claim. Also cleans up the smaller
deferred items: cohort/security gate grammar and pass/fail text, an
advertised-but-unrun exception rule, stale glossary prose, and one
leftover "AI" label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ward-flow): record the Phase 1 closing state and parked rulings
The whole-branch review found defects the eight per-task reviews could not
see, because each looked at one task's diff. One fix wave closed them. This
records what the review caught and why it mattered, the findings parked at
the close with the ruling on each, the verification actually run, and the two
repo traps that make a green result untrustworthy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ward-flow): add the Phase 2 plan and the fresh-session kickoff brief
Phase 2 builds the flow coordinator's screen — pressure strip, priority
queue, flow diagram, explainable shortlist, exceptions drawer, phone form —
and retires Constellation into it. Ten tasks, two of which build the pure
derivations the screen reads: the operational score (rebuilt with no urgency
component) and per-department pressure.
The kickoff brief carries what a cold session needs, including the process
calibration for a screen-heavy phase and the three Phase 1 lessons that
should shape how it is verified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ward-flow): add the complete context document
One self-contained file a session can read cold: the problem, the WA clinical
and legal grounding, the ten settled scope decisions, the model with every
export and identifier, the non-negotiable rules, the repo conventions and the
two gates that report success without running, what Phase 1 learned and why it
shapes how Phase 2 is verified, and the decisions taken on the owner's behalf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(ward-flow): define the operational score with no urgency component
* style(ward-flow): apply prettier to the ward management console
* fix(ward-flow): correct the operational score's fraction, transport and blocker checks
Review findings against the real fixture:
- Declines detail rendered a self-contradictory fraction ("5 of 3 parallel
referrals declined") — declines.length is cumulative history, the cap
limits simultaneous live referrals, they never shared a denominator.
State only the count.
- Transport delay fired for movements already en route (WF-006, WF-014,
WF-306, WF-313, WF-320, WF-327), contradicting their own blocker text.
Require acceptedAt set and enRouteAt/collectedAt/cancelledAt all unset,
matching buildActionInbox's existing condition, and drop the truthiness
reads on Instant fields in favour of explicit === undefined.
- hasActiveBlocker's "starts with None" match was wide enough to hide a
real blocker like "None of the secure units can take him"; narrowed to
the exact sentinel or "None" + end-of-string/dash/colon.
- queueOrder's exclusion test only asserted a shorter list, which would
still pass if queueOrder dropped everything; pinned to the exact
isOpen-filtered count instead.
- Exported ward-clock's splitDuration (already zero-padding) and reused it
for the wait-time detail instead of a second, non-padded formatter.
- Removed the redundant .slice() after .filter() in queueOrder.
Six new/tightened tests cover the behavioural findings; watched them fail
against the unfixed code before applying each fix.
* feat(ward-flow): derive per-department pressure from open movements
Adds edPressure(now), sorted worst-first by breaching then longest wait
then volume. Reuses ward-clock's clockState for the breach definition
instead of re-deriving it inline, and clamps per-movement waits at zero
so a movement with a future openedAt can never render a negative wait.
The sort test's original array-comparison assertion coerced both sides
to strings via `>=`, which would pass a genuinely wrong ordering; it is
replaced with an explicit numeric tuple comparison.
* fix(ward-flow): make edPressure injectable and close three unfalsifiable tests
edPressure(now) imported wardMovements directly, so every assertion was
forced to key off the one fixture (every department busy, every wait
positive). That let three tests pass under mutations that should have
failed them: dropping quiet departments, misattributing counts between
departments, and removing the future-openedAt wait clamp.
Change the signature to edPressure(now, movements = wardMovements),
mirroring queueOrder(movements, now) in ward-priority.ts with now kept
first for the existing/planned call sites. Add tests that inject a
built movement list to prove: quiet departments still report zeros
(never drop, never substitute a different EmergencyDepartment), counts
attribute to the correct originEdId rather than a rotated neighbour,
and a future-dated openedAt clamps to zero rather than going negative.
Also cover the clockState "due exactly now" boundary, and correct the
pre-existing longest-wait test's own expectation to mirror the clamp
(it previously proved the clamp only by fixture coincidence). Use
ward-clock's minutesUntil instead of inline subtraction, matching the
existing elapsedLabel pattern.
Each of the three structural findings was verified red against its
named mutation before being accepted.
* feat(ward-flow): add the coordinator screen shell and five regions
* fix(ward-flow): address Task 3 review — lint, broken journeys, overflow, labelling
Two Criticals, three Importants, three Minors from the Task 3 review.
Criticals:
- Wire the unused selectedUnitId setter into a real (if currently unreachable)
"Clear unit selection" control in the diagram region instead of discarding it,
clearing the npm run lint no-unused-vars failure.
- Repointing /ward-management broke five tests in ui-ward-management.spec.ts that
still targeted WardManagementConsole. Repaired gotoWardFlow and three specs
against the coordinator screen, kept the network-diagram spec untouched, and
removed the two specs with no coordinator-screen equivalent yet — replaced by
test.fixme placeholders in ui-ward-coordinator.spec.ts naming the tasks that
will implement them (5/7 and 8).
Importants:
- The coordinator spec's overflow assertion measured document.documentElement,
which .screen's overflow:hidden makes impossible to fail. Now measures the
region grid's own scrollWidth/clientWidth via a testid; proved it goes red
against the pre-fix CSS (836px overflow at 320px) before applying the fix.
- The region grid had no narrow-layout fallback below its shrink breakpoint and
overflowed at 1100/820/390/320. Stacked the three regions into one column
below 1440px (min-width: 90rem restores the three-column grid), and separately
fixed a blank 72px band at <=640px where the hidden rail's grid track was
still reserved. Verified zero overflow at 1600/1280/1100/820/390/320.
- "N movements" labelled the open-only queue count as a total; now "N open
movements".
Minors:
- Dropped the inert z-index on the statically-positioned exceptions drawer (its
position in .main's row order already pins it) and the now-unused
--co-z-drawer token, plus two other declared-but-unused tokens.
--co-space-4, --co-space-16.
- Added a visually-hidden <h1> naming the screen.
Also fixed a wrapping regression the open-movements label change introduced in
the 14rem queue column header, caught by re-inspecting the recaptured
screenshot.
RAG impact: no retrieval behaviour change — this only touches the Ward Flow
coordinator screen shell (src/components/ward-management/coordinator/**) and
its tests; no file under src/lib/rag/**, retrieval-selection, ranking-config,
answer-ranking, the eval harness, or the golden fixture was touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(ward-flow): add the emergency department pressure strip
* fix(ward-flow): address Task 4 review — order/filter proof, a11y name, selection visibility, empty state
- Tests could not detect a wrong card order or the queue filtering to the wrong
department; both are now pinned by data attributes (data-breaching,
data-longest-minutes, data-waiting, data-origin-ed) and asserted directly.
- aria-label on each pressure card replaced its whole accessible name, hiding
every figure from assistive technology; the name is now composed from the
hospital name plus the visible figures.
- Selected state was invisible on breaching cards (border/background were
overridden by the danger tint); a card outline now survives that and is
pinned to Highlight under forced-colors.
- A quiet department rendered "0 waiting · longest 0m"; it now shows an
explicit "No patients waiting" state, covered by a new dom test.
- Removed the click-retry loop around Playwright interactions in favour of a
networkidle wait in gotoCoordinator, matching the sibling ward-management
spec's helper.
- Added the ordering rule to the strip header and cleared the department
filter automatically if it ever names a department that cannot be resolved.
* feat(ward-flow): add the priority queue, tier first and score within tier
* fix(ward-flow): address Task 5 review — unpin ordering/score/breach proof, now-prop, origin label, ED lookup hoist
Three mutations survived the appended ordering test with the suite green: removing data-tier
still passed (empty-array vacuity), inverting the within-tier tiebreak still passed (no
machine-readable score on the row), and both a constant score and a deleted breach line still
passed. Fixes:
- Add a count floor before the tier-sortedness assertion so absence can't satisfy it.
- Add data-score to each row and assert non-increasing score within a tier (the property, not
the fixture).
- Assert the visible score text matches the row's own data-score, and that the breach line is
present on a breached-deadline row and absent on a non-breaching one.
Also: PriorityQueue now takes now: Instant as a prop (was reading NOW_ANCHOR directly, unlike
its now-prop sibling PressureStrip); origin department renders "from JHC" instead of the
unlabelled "JHC", which read as a destination next to the patient's required security level;
and allEmergencyDepartments() is looked up once per render via a Map instead of once per row.
* feat(ward-flow): add the flow diagram, departments to units
* fix(ward-flow): address Task 6 review — never show an ineligible unit as a routed destination
Critical 1: eligibleCandidates sorts eligible-first but never filters, so the diagram was
drawing solid accent routes and "N shortlisted units" for candidates that had already
declined the movement or failed a hard gate (e.g. WF-009: zero of its three nearest
candidates were actually eligible). Every shortlisted node now carries data-eligible plus
its own candidateReason() text, ineligible routes render dashed and danger-toned, and the
hub states the true eligible count instead of the shortlist size.
Important 2: destinationUnit() conflated an accepted bed with an outstanding referral into
one badge and only looked at referredUnitIds[0]. Split into distinct Accepted
destination / Outstanding referral badges, checked directly against the movement's own
fields, so every parallel referral is visible.
Important 3/4: the routed-set and route-connector assertions now hold for a second
movement with a different shortlist (WF-009), and route connectors are marked
data-connector-kind so they're provably distinct from demand connectors, closing the gaps
a single-movement proof and an unmarked connector left open.
Important 5: routed state now reaches the accessible name as real text (candidateReason),
not just an outline colour behind an aria-hidden svg.
Minor 6/7: a unit that fails its service-group lookup renders as an explicit anomaly
instead of vanishing; the window resize listener now attaches unconditionally rather than
being gated behind ResizeObserver's own availability check.
Controller finding 8: the region grid's row-sizing squeezed the diagram to 208px of 1080px
content at 1280x900. `.regionGrid` now sizes to its own content (align-self: start) only in
the stacked (<90rem) layout, `.diagramRegion` gets a real min-height floor, and
`.queueRegion` gets a matching cap so it doesn't balloon to all 41 unclamped rows — the
>=90rem three-column layout is untouched (restored to align-self: stretch there), matching
its original, already-working behaviour exactly.
* feat(ward-flow): add the explainable shortlist with verdict-stating gates
Task 7: builds ShortlistPanel, the surface Phase 1's whole-branch review found
rendering a green tick beside "is not authorised under the Mental Health Act".
Every gate row now states its own verdict in text ("Met"/"Not met"), all eight
gates render every time (never .slice()'d), the icon reads directly off
gate.pass, and an ineligible candidate is marked with a dashed danger border
(forced-colors safe) and never styled as a recommendation. Confirm/override are
the only way to place a patient — nothing auto-allocates.
* fix(ward-flow): pin gate ordering, decline count, and suggestion eligibility
Task 7 review follow-up (4 Important, 2 Minor):
- Pin failures-first gate ordering with a dedicated assertion (Important 1);
reversing the comparator now reddens the suite.
- Pin the rendered decline count against the movement's real decline count
(Important 2); a `.slice(0, 3)` truncation now reddens the suite.
- Pin that "Suggested destination" only ever names an eligible candidate
(Important 3); falling back to shortlist[0] now reddens the suite.
- Add end-to-end override-path coverage: reachable, reason-gated, and the
recorded reason is visible on screen afterwards (Important 4).
- `aria-pressed` now reflects only the real, explicit selection
(`selectedUnitId`); the default-shown candidate's highlight moves to a new
`data-showing` attribute so nothing is announced as pressed when nobody
pressed it (Minor 5).
- The header now renders the acceptance and every outstanding referral as
independent badges from raw movement fields, instead of conflating them
behind the single `destinationUnit` slot (Minor 6).
* feat(ward-flow): add the exceptions drawer and the phone form
* fix(ward-flow): close the exceptions-drawer review gaps
Task 8 review follow-up:
- Critical 1: prove selecting an exception drives the same movement
selection the queue does (shortlist naming it + queue row aria-pressed),
not just "a shortlist is present".
- Important 2: the drawer's own 18rem panel cap clipped half the real
fixture's 8 rows at every width; bound against the viewport instead so
the header count and the rendered rows agree everywhere.
- Important 3: scroll Confirm into view on phone selection so it is
reachable without a long scroll, satisfying "one-tap confirm".
- Important 4: give the phone queue real room now that the diagram and
pressure strip are hidden, instead of the desktop stacked-layout cap.
- Minor 5: stop mounting FlowDiagram on phone instead of only hiding it
with CSS.
- Minor 6: assert the pressure strip is hidden on phone too.
- Minor 7: the exceptions-board "N overdue" badge only counts breached
legal deadlines now, not the parallel-referral-cap category.
* refactor(ward-flow): retire Constellation, move mode nav into the rail
Task 9 of Ward Flow Phase 2. Constellation folds into the coordinator
screen and is retired: delete the route, its ConstellationView plus
exclusive helpers (CompactQueue, HospitalChip, NetworkCanvas), the now-
dead WardManagementConsole (superseded by CoordinatorScreen since Task
3; WardPatientWorkspace in the same file is untouched), and every
reference across the WardMode union, tests, the design-system adoption
contract, and the mode-map doc.
Per the owner's direction that Ward Flow is its own application inside
the Clinical KB shell, the eight remaining mode links (Command,
Network, Priority queue, Capacity, Movements, Exceptions, Transport,
Governance) move out of the horizontal WardModeNavigation strip and
into ClinicalRail as icon-only links, reclaiming a full grid row for
the coordinator's flow diagram and shortlist. The rail stays visible
and reachable down to 320px (previously hidden below 40rem, with the
horizontal strip standing in as the phone nav). Also fixes a
pre-existing bug found while verifying this: ClinicalRail's own
--ward-* custom-property aliases were only ever declared by its host
shells, and coordinator.module.css's .screen never declared them, so
the rail's padding/background/border/z-index silently fell back to
invalid on /ward-management since Task 3; ClinicalRail now declares
the tokens its own subtree needs directly.
RAG impact: no retrieval behaviour change — this touches only Ward
Flow's synthetic prototype UI (src/components/ward-management/**,
src/app/ward-management/**), not src/lib/rag/** or any retrieval,
ranking, or clinical-search surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ward-flow): pin rail tap targets to 3rem, remove orphaned console CSS
Task 9 review fixes.
Critical 1: .modeNavigation (ward-management-modes.module.css) is a flex
column, and .railLink/.railLinkActive had no flex-shrink, so the eight
rail mode links compressed as low as 22px on any viewport narrower than
the one that had room to spare (1600x1100) -- well under the mandated
3rem/48px tap-target floor, and worse on the phone width Task 8 exists
for. Added flex-shrink: 0 to .railLink/.railLinkActive (a no-op for
their grid-item usage in the app-switcher, so nothing else changes).
Measured before/after at 1600x1100, 1440x1024, 1280x900, 390x844, and
320x640: every link now holds exactly 48px at all five, and the rail's
own overflow-y: auto scrolls the mode-link segment instead of crushing
icons wherever content exceeds the available height. Pinned it with a
new test at 320x640 asserting every rail link is >=48px; verified it
reddens with the fix reverted (measured 22px, matching the review) and
passes with it restored.
Important 2 + Minor 3: deleting WardManagementConsole (prior commit)
orphaned roughly 60 CSS classes across ward-management.module.css
(.console, .consoleCollapsed, .commandHeader, .commandMain,
.decisionDock, .actionInbox, .hospitalNode, .queueBadge, .patientRow,
.status-*, and more) plus .flowHub/.routeSignal in
ward-management-modes.module.css (used only by NetworkCanvas, which
this same change deleted -- corrected the earlier report's claim that
these predated the task). Removed all of it, verified class by class
against WardPatientWorkspace (the file's other export) and MovementPipeline
before each deletion so nothing still-live was touched.
RAG impact: no retrieval behaviour change -- this touches only Ward
Flow's synthetic prototype UI (src/components/ward-management/**,
tests/ui-ward-management.spec.ts), not src/lib/rag/** or any
retrieval, ranking, or clinical-search surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ward-flow): close the whole-branch review findings on the coordinator screen
Nine per-task reviews passed; the whole-branch review found what none of them
could see, because each saw only one task's diff.
Critical 1 — "Nearest candidates" and "N nearest" were proximity claims the model
cannot support. `Unit` carries no distance, geo, locality or catchment field, and
`eligibleCandidates` filters on cohort, sorts eligible-first and breaks ties on
array order; WF-018, sitting in SCGH's own emergency department, was offered RPH
Older Adult above its own SCGH ward under that heading. The list is now headed
"Candidates" with a subtitle naming the real ordering, the hub says "candidates",
and the tests no longer encode the word. No distance metric was invented.
Critical 2 — Confirm acted on `shortlist[0]`, a default no human selected and no
candidate reported as `aria-pressed`. On WF-004 (bed held at BTY Adult Secure)
one tap wrote "Confirmed by a human coordinator: RPH Adult Secure" beneath
"Accepted destination: BTY Adult Secure". Confirming now requires an explicit
selection, and a unit choice no longer outlives the movement it was made against.
Showing the default's gate list for orientation is still fine; acting on it is
not. New test, proven red first.
Important 3 — routes were drawn only to the three candidates, so for 18 of the 41
open movements the recorded destination had no connector at all. It now gets its
own connector kind and the hub leads with the recorded fact.
Important 4 — "Parallel referral cap reached" fired on declines, not referrals;
WF-009 has zero live referrals. Retitled to what it measures.
Important 5 — an Open movement passes the security gate on a locked ward with an
affirmative message. `ward-eligibility.ts` is a protected surface and is
unchanged; the restriction is surfaced instead, including immediately above the
gate list a coordinator reads before confirming.
Minor 6 — the exception inbox counted closed movements. Minor 7 — the retired
constellation route left the codebase index. Minor 8 — statutory form codes
aligned to the spec and context document: 2A→3A, 8→4A, 6→4C, labels with them.
Verified: tsc clean; 113 vitest passing; 21 Playwright passed / 0 skipped on both
ward specs (baseline 18, three new tests); lint, design-system contract, docs and
prettier all clean; no horizontal overflow at 320px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ward-flow): require an explicit candidate before Override, not just Confirm
Coordinator ruling on the concern left open by the previous commit: the Critical 2
guard covered only half of the control pair. With a movement selected and no
candidate chosen, Confirm correctly carried aria-disabled="true" while Override
was fully live against the same system-chosen shortlist[0] — so a coordinator
blocked from confirming an un-chosen default could still override straight into
it and reach the same recorded outcome by the adjacent button.
Override is not a lesser path. The rule is "a human confirms or overrides,
always, with the reason recorded", so it is the other half of the same decision,
and the typed reason explains why, never which ward.
canOverride now uses the same hasExplicitSelection guard as canConfirm, with its
own stated reason shared by the title and the sr-only note. The reason form also
renders only when the guard passes and handleOverrideSubmit refuses without it,
so a stale open state cannot present a textarea addressed to a default unit.
aria-disabled + an inert handler as always; never native disabled alongside it.
The Critical 2 test was extended rather than duplicated: both controls must be
unavailable with reasons on screen, a forced Override activation must not even
open the form, and both must become available once a candidate is chosen. Proven
red first on the Override assertion. The existing override test now chooses a
candidate before opening the form, as a coordinator really would.
Verified: tsc clean; 113 vitest passing; 21 Playwright passed / 0 skipped on both
ward specs, no regression; lint, design-system contract and prettier clean; live
DOM shows both reasons present, both clearing on selection, disabled attr null.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ward-flow): add the Phase 3 role-screens design spec
* docs(ward-flow): revise the Phase 3 spec after adversarial review
* docs(ward-flow): correct the context document and extend the Phase 3 spec
* docs(ward-flow): add the Phase 3 implementation plan
* docs(ward-flow): encode the 1A/3B rule — the form follows the examination
* docs(ward-flow): give voluntary-on-locked-ward its own distinct flag
* docs(ward-flow): correct three pre-flight defects in the Phase 3 plan
* feat(ward-flow): add the Phase 3 model fields and the out-of-catchment decline reason
* fix(ward-flow): give the privacy guard real strings to inspect and a vacuous-run tripwire
Task 1 fix round 1. tests/ward-model-phase3.test.ts's privacy test iterated
withdrawnReferrals/escalation on every movement but the fixture never populated
either field, so the loop bodies never ran and the test passed vacuously —
proven by swapping the forbidden pattern for one that matches everything and
watching the test stay green.
- ward-movements.ts: give two hand-authored movements (WF-006, WF-018) a
non-empty withdrawnReferrals entry, and one (WF-009) an escalation, using
the fixture's existing operational vocabulary. All five load-bearing
referredUnitIds entries and the 30 index-derived generated movements are
unchanged.
- ward-model-phase3.test.ts: the guard now accumulates every string it
inspects and asserts the accumulated count is non-trivial before checking
content, so an empty fixture fails the test instead of passing it.
* feat(ward-flow): add the pure state reducer and its refusals
* test(ward-flow): pin six previously-uncovered reducer branches, name the dueAt window, guard the arrival floor
Fix round 1 review found that RECORD_EXAMINATION (inpatient_order), CONFIRM_CAPACITY, DECLINE,
RECORD_ESCALATION, ADVANCE_CLOCK and RESET_SCENARIO could each be gutted to a no-op return state
without failing any test. Adds one direct test per branch (two for RECORD_EXAMINATION), replaces
the unnamed dueAt + 240 literal with an exported, commented EXAMINATION_TO_BED_WINDOW_MINUTES
constant pinned by its own test, and adds a floor guard to PATIENT_ARRIVED (unit.empty.value can
be driven negative via CONFIRM_CAPACITY inflating allocatable past a unit's physically empty
beds) with a test walking the exact reachable sequence.
* test(ward-flow): pin the phase 3 state invariants
* test(ward-flow): fix round 1 — ground the phase 3 invariants in walk-caused state
WF-009's seed fixture already carried three pre-existing declines and an
examination before the walk's first event ran, so invariants 4 (declined
unit never eligible), 5 (withdrawal recorded), and the bed-accounting
helper's defensiveness let the corresponding checks survive real reducer
breakage undetected — confirmed by disabling each handler's write and
watching the suite stay green.
Rebuild the walk on WF-001, the only hand-authored movement early enough in
its journey to walk the whole path while carrying no seed declines or
referrals, so every decline, referral, withdrawal, and acceptance the
invariants inspect is caused by the walk itself:
- Invariant 4 now proves a unit was genuinely eligible before the walk's own
DECLINE, then genuinely excluded from eligibleCandidates() after.
- Invariant 5 now asserts the specific withdrawn unit id and exact reason
text, caused by ACCEPT_IN_PRINCIPLE's own bookkeeping.
- Invariant 2 (bed accounting) reads raw allocatable/empty counts directly
before and after each bed-moving step instead of routing through the
defensively-clamped unitCapacity() helper.
- Invariant 7 (no identifying text) adds a genuine wrong-role refusal to the
walk and mirrors Task 1's non-empty-inspection tripwire, so an emptied
rejections/withdrawnReferrals set turns the test red instead of vacuously
green.
Re-verified all seven invariants against a single realistic single-line
reducer mutation each; all seven now fail and revert cleanly.
* docs(ward-flow): reconcile the Phase 3 plan with the contracts as built
* feat(ward-flow): add the state provider and the ticking clock
* fix(ward-flow): derive every 3B deadline from its own examination
Task 1 fix round 2. All three 3B records carried a hand-picked dueAt with
no relationship to their own examination.at, so a reducer-produced 3B
(Task 2 derives dueAt as examination.at + EXAMINATION_TO_BED_WINDOW_MINUTES)
and a fixture-seeded 3B rendered as though they meant the same thing when
they did not. It also broke a Phase 2 Chromium journey: WF-017's invented
dueAt of NOW_ANCHOR + 5 erased the only breached deadline at the top of the
coordinator queue.
- ward-model.ts's EXAMINATION_TO_BED_WINDOW_MINUTES is now imported into
ward-movements.ts, and each 3B record's legalForm.dueAt is written as
examination.at + EXAMINATION_TO_BED_WINDOW_MINUTES rather than a literal.
- WF-017 is examined at NOW_ANCHOR - 260 (past its window, dueAt NOW_ANCHOR
- 20), restoring the breached top-of-queue row the Chromium journey
asserts on.
- WF-003 stays comfortably inside its window (examined NOW_ANCHOR - 60,
clear state).
- WF-009's examination.at moved from NOW_ANCHOR - 45 to NOW_ANCHOR - 100:
with the correct (lower) derived dueAt, its old invented deadline no
longer masked a pre-existing, unrelated fact about the generated fixture
-- WF-303 (from Phase 1/2's own routineMovements, untouched by any of
this work) has always been a coincidentally-breached tier-1 movement.
Moving WF-009's examination further back gives it a real, plausible
"due" statutory state that restores its rank as the queue's second row,
which is what the Chromium journey (correctly, per the coordinator)
refused to have relaxed.
- ward-model-phase3.test.ts pins the invariant directly: for every movement
with an inpatient_order examination, legalForm.dueAt must equal
examination.at + EXAMINATION_TO_BED_WINDOW_MINUTES, so no future record
can invent a deadline again.
* fix(ward-flow): unwrap the midnight rollover so the live clock cannot freeze
* feat(ward-flow): make the coordinator screen live and refer rather than place
The coordinator screen stops rendering the frozen wardMovements fixture and
starts rendering WardFlowProvider's live state; its main action stops being
a single-unit Confirm and becomes Refer, dispatching REFER_TO_UNITS against
up to PARALLEL_REFERRAL_CAP explicitly selected candidate wards. Override
keeps its reason-gated path. Adds restrictionNotice (voluntary-on-locked vs
plain over-restrictive) and reorders eligible candidates by it without
changing which candidates make the shortlist. Exceptions drawer now renders
refused reducer transitions, present even when empty.
* fix(ward-flow): never claim a referral succeeded when the reducer refused it
Fix round 1 for Task 5. Refer now folds REFER_TO_UNITS's own stage guard
(a shared REFERRABLE_MOVEMENT_STAGES constant) into canRefer, so a
non-referable movement disables the control with a stated reason naming
its real stage instead of dispatching and unconditionally claiming
success. Refer's on-screen record is deleted entirely in favour of the
live movement.referredUnitIds badges; Override's record is gated by a
derived overrideSucceeded check against that same live field, so neither
control can ever render a success message the reducer did not produce.
Adds a direct eligibleCandidates membership-preservation test and
replaces the vacuous empty-state refusal test with one that raises a
genuine reducer rejection and asserts its real content.
* refactor(ward-flow): every route reads one source of truth
* fix(ward-flow): read the live clock instead of the frozen fixture epoch
Task 6 fix round 1. The movements/units rewire alone left every waiting time,
eligibility verdict, and capacity-freshness check in these three files anchored
to NOW_ANCHOR, so a clock advance elsewhere in the app would leave ten routes
displaying a frozen elapsed time forever -- the same cross-screen disagreement
this task exists to close, just moved onto the clock. Every NOW_ANCHOR read in
ward-management-console.tsx, ward-management-modes.tsx and
ward-management-network.tsx now reads `now` from useWardFlow() instead; the two
module-scope pure helpers in ward-management-network.tsx (candidatesFor,
settingFit) take `now` as an explicit required parameter rather than a default.
Adds tests/ward-flow-clock-consistency.dom.test.tsx, which renders the
movements board inside WardFlowProvider, dispatches ADVANCE_CLOCK, and asserts
the waiting-time label moves in step with the shared clock.
* test(ward-flow): guard the whole class of frozen-clock reads, not one call site
Task 6 fix round 2. The clock rewire (fix round 1) pinned only the movements board's elapsed label; reverting the detail panel's own elapsedLabel call site left every other test green, proving the class was unguarded even though the instance was. Extends tests/ward-flow-single-source.test.ts with a static check scoped by rule rather than by filename: no ward-management component may call useWardFlow() and also import NOW_ANCHOR from ward-sites in the same file. An explicit CLOCK_EXEMPT allow-list (currently empty -- no legitimate exception exists) is the only sanctioned escape hatch. Includes its own non-empty-scan guard, matching the existing fixture-import check's pattern.
* docs(ward-flow): add the Phase 3 session handover and a committed ledger copy
* test(ward-flow): declare-list the NOW_ANCHOR guard and derive QueueView's selection live
Task 6 fix round 3, the last round for this task. Two findings from review, neither a
shipped-behaviour bug today:
1. The "both the clock and the epoch" static guard (fix round 2) only ever flagged a file
that BOTH called useWardFlow() AND named-imported NOW_ANCHOR from ward-sites -- provably
evadable via helper indirection, a namespace import, or simply never calling
useWardFlow() at all. Replaces it in tests/ward-flow-single-source.test.ts with a
declaration rule: every file under src/components/ward-management may read NOW_ANCHOR
only if it is named on an explicit allow-list (ward-sites.ts, ward-movements.ts,
ward-flow-provider.tsx -- verified by hand as the only three real readers). The scan
strips comments and string literals before matching the bare identifier, so it catches
every reading form in one pass and still ignores coordinator-screen.tsx's doc-comment
mention of the constant.
2. QueueView (ward-management-modes.tsx) held its selected movement as
useState(movements[0]) -- the object captured once at mount, never re-derived. Nothing
on this route dispatches today, but every route shares one WardFlowProvider, so a
referral raised elsewhere would never appear here. Now holds only the id and derives the
record with movements.find(...), matching WardNetworkWorkspace; a miss renders an
explicit absence in the JSX rather than falling back to a different movement.
New tests/ward-flow-queue-selection.dom.test.tsx dispatches a real REFER_TO_UNITS event from
a sibling component and asserts the decision panel's badge updates without navigating,
proving the derivation stays live.
* test(ward-flow): widen the NOW_ANCHOR allow-list guard to the whole source tree
Fix round 3 inverted this guard into a named allow-list, which closed helper
indirection and namespace imports — but it walked only
src/components/ward-management while its test name claimed "every read of
NOW_ANCHOR". A probe file at src/lib/ward-probe/frozen.ts importing the frozen
epoch left the suite fully green. With ~200 modules under src/lib, a ward screen
importing a time helper from outside the feature directory is an ordinary shape,
so that was the same overclaiming defect one directory out.
The NOW_ANCHOR rule now scans all of src. The fixture-import rule keeps its
ward-management scope. Allow-list keys are path-qualified rather than bare
basenames, since a basename can collide across the whole tree and silently
exempt an unrelated file, and walked paths are normalised to forward slashes
because node's join emits backslashes on Windows.
Proved by mutation, each printed back from disk before running: an out-of-tree
named import fails; an out-of-tree namespace import fails; an emptied allow-list
fails naming exactly the three legitimate readers, so no entry is inert; a
zero-match scan fails the tripwire; and coordinator-screen.tsx, which names
NOW_ANCHOR only in a doc comment, stays green.
* test(ward-flow): replace the NOW_ANCHOR comment/string scanner with a real parser
stripCommentsAndStrings had no concept of a regex literal: a quote inside a
regex (search-utils.ts:331, document-summary-badges.ts:61) desynced its
comment/string tracking for the rest of the file, making a real NOW_ANCHOR
import in either file invisible to the guard. Deleted the hand-rolled
scanner and rewrote readsNowAnchor on the TypeScript compiler's own parser
(ts.createSourceFile + an Identifier-node walk), gated by a cheap substring
pre-filter so only the 6 files that mention NOW_ANCHOR at all get parsed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ward-flow): refresh the Phase 3 handover and ledger after Task 6 closed
Task 6 is complete at f4963f28a after five fix rounds, three of which went to
one static guard that overclaimed in three successive forms. The handover now
records what each form missed and how the guard was finally built, so the next
reader learns the pattern rather than the instance.
Also closes the phase's standing open question. The clinician answered that the
post-examination number counts up - time in the department, feeding priority -
so there is no post-examination deadline to tune. That becomes Task 6A, inserted
before Task 7, together with the finding that the four-hour figure is real but
was attached to the legal form instead of the emergency department access target
the spec actually asks for.
Records the vitest worker-pool trap: a multi-file jsdom run can report a pass
while silently running one file of three.
* fix(ward-flow): delete the fabricated Form 3B legal deadline
The clinician confirmed the post-examination clock counts up (elapsed ED
wait), never a legal countdown: "It is just counting how long they have
been in ED determining priority." Every surface claiming a Form 3B breach
was asserting a Mental Health Act deadline the Act does not impose.
- LegalForm.dueAt is now optional. A 3B (examined, awaiting a bed) is
authored with no dueAt at all, by the reducer and in the fixture's
three hand-authored records (WF-003, WF-009, WF-017); a 1A still
always carries one.
- Deleted EXAMINATION_TO_BED_WINDOW_MINUTES. The 240-minute figure it
held was real but attached to the wrong quantity (spec Section 7's ED
four-hour access target, counted from openedAt, a departmental
performance measure). Replaced with ED_ACCESS_TARGET_MINUTES, kept
separate from LegalForm on purpose, for Task 11's ED screen to use.
- Every reader of legalForm.dueAt across the coordinator queue,
shortlist panel, action inbox, ED pressure counts, priority score,
and patient console now guards the optional field explicitly instead
of letting undefined reach clockState's arithmetic.
- shortlist-panel's legalFormLine states elapsed ED time (via the
existing elapsedLabel) for a dueAt-less form, worded so it cannot be
misread as a countdown.
- One Playwright test's blind first-queue-row selection replaced with a
stable, always-referable movement id, since it was riding on the
now-deleted fabricated breach that used to rank that row first.
RAG impact: no retrieval behaviour change — this touches only the
ward-management prototype (src/components/ward-management/**), which is
outside src/lib/rag and every other protected retrieval/ranking surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(ward-flow): repin WF-017 by id and quarantine ED_ACCESS_TARGET_MINUTES structurally
Task 6A fix round 1, two review findings:
- tests/ui-ward-coordinator.spec.ts: the "shows a failing gate..." test still
selected the queue's first row by position (`.first()`), a leftover
assumption that row 1 is WF-017 from before Task 6A deleted the fabricated
Form 3B deadline that used to inflate WF-017's score. WF-017 no longer
ranks first (verified against the real fixture: WF-303 rank 1/score 61,
WF-009 rank 2/score 53, WF-017 rank 9/score 41), so the test now selects
WF-017 explicitly by id, matching its sibling test fixed earlier. WF-017's
default candidate still passes all eight eligibility gates (re-verified),
so the clean-vs-failing contrast the test relies on still holds. A second
comment nearby wrongly named WF-017 as "first row" and the deadline form
as "Form 2A" (wrong even before Task 6A, which has always been 1A); fixed
to describe the current, still position-based (`firstRow`/`secondRow`)
assertion accurately — no assertion changed there.
- tests/ward-flow-single-source.test.ts: ED_ACCESS_TARGET_MINUTES had only
its numeric value pinned, nothing stopping a future change (Task 11's ED
screen) from wiring it back onto a LegalForm the way the fabricated
deadline did. Added an AST-based structural guard, mirroring the existing
NOW_ANCHOR allow-list rule: no file constructing a LegalForm may reference
ED_ACCESS_TARGET_MINUTES, and no `dueAt` may be assigned from it. Verified
the LegalForm-detection heuristic against the whole src tree (exactly two
genuine matches, zero false positives) and mutation-tested both new checks
by temporarily wiring the constant into a real LegalForm's dueAt.
All four required gates green: tsc --noEmit clean; node-env vitest 118/118;
jsdom suites 1/1, 4/4, 1/1 (each retried past this machine's known
worker-pool flakiness until a real count was produced); Playwright
ui-ward-coordinator + ui-ward-management 24/24.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(tests): share a single src walk/read across ward-flow-single-source rules
Task 6A fix round 2. Five of this file's nine tests each independently walked
SRC_DIR and read nearly every one of the ~896 scannable src files, which made
the file time out (61.19s total, one test at 31.8s against the 30s ceiling).
srcDirFiles() now walks and reads once, cached; wardDirFiles() derives its
subset via a WARD_DIR/ path-prefix filter instead of re-walking. No rule's
scope, pre-filter, or AST-walk logic changed, and every zero-match tripwire
still fails on an empty scan. After: 9/9 passed in 10.77s-16.53s across two
runs.
Also renames the two ED_ACCESS_TARGET_MINUTES quarantine tests and rewrites
their doc comment to state plainly that they are a direct/literal-construction
tripwire, not a data-flow analysis, naming the five evasions (intermediate
variable, aliased import, spread, cross-file helper, direct mutation) a
reviewer confirmed by hand. Task 11's ED screen is flagged as this guard's
real test, since it will very likely derive dueAt from an existing movement
rather than a fresh LegalForm literal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ward-flow): rewrite the Phase 3 handover and refresh the ledger at the Task 6A boundary
Tasks 1 to 6A are complete; Tasks 7 to 12 remain. The handover now carries
the clinician's verbatim answer on the ED clock and what it invalidated, the
three assumptions still awaiting his confirmation, the current verification
baselines with their provenance, eleven environment traps, and the resume steps.
Records the guard-overclaim lesson that cost three of Task 6's five fix rounds:
a check that claims more than it delivers is worse than no check, because it
stops anyone looking harder.
The browser gate is deliberately recorded as verified at f1e32dcd4 and NOT at
this HEAD. The re-run aborted as '2 passed, 17 did not run' at exit code 0
because the machine exhausted memory, not because anything regressed. The
reasoning for why it is still green is stated as reasoning, not measurement,
and the next session is told to run it before trusting it.
* docs(ward-flow): add the complete cross-phase ledger and a durable copy of the Phase 3 workspace
Ward Flow has run across many chat sessions on several tools and no single
conversation holds the whole picture. docs/ward-flow-complete-ledger.md is now
the one cross-session map: what each phase delivered, which commits carried it,
which session ended how, the decisions that changed what the software says to a
clinician, the questions still open, and where every other record lives.
docs/ward-flow-phase-3-workspace/ is a committed copy of the superpowers SDD
workspace, which is gitignored by the skill's design. The git history cannot
reproduce it: the reports and reviews are the only record of what a reviewer
actually probed, which mutations ran and what they killed, and which claims
turned out to be false. The eleven review diffs are deliberately excluded -
unlike the markdown they are byte-for-byte derivable from the commits, and the
README gives the commands to regenerate them.
Also records, across all three phases, the one defect class that has recurred in
every phase in a different disguise: a check that claims more than it delivers.
Every instance was found by someone deliberately trying to defeat the check,
never by running it.
* fix(ward-flow): drop two unused imports and format the workspace copy for push
The first push attempt was blocked by both of the pre-push guards, and both
blockers were real rather than environmental.
The static guard failed on two unused imports - 'Instant' in ward-flow-reducer.ts
and PARALLEL_REFERRAL_CAP in its test. Checked against 5f5c01146: both were
already unused before Task 6A, so the ledger's note that they are pre-existing
was correct - but 'pre-existing' does not make them pushable, and this repo runs
eslint at --max-warnings 0. Removing them is safe: tsc stays clean and the
reducer and contract suites still pass 28/28.
The format guard failed on the 26 markdown files copied into
docs/ward-flow-phase-3-workspace/. They were agent-authored scratch and had
never been through Prettier. Formatting them makes the committed copy differ
cosmetically from the live gitignored originals; the README already names the
live copy as the working one.
* docs(ward-flow): correct the handover after the push, and retract an unproven cause
Three corrections against the repo as it actually stands.
The branch is pushed. The handover said 67 commits and 'none pushed'; it is 72
and on origin. The user's no-push instruction was superseded at his request; the
no-PR half stands.
node_modules was emptied to zero entries twice on 2026-08-22 - once after a push,
once after running the guard-push test suite. Both exercise the format guard,
which links a real dependency tree into a scratch checkout as a Windows junction
and then force-deletes the checkout. Recovery is npm ci --include=dev.
The mechanism is NOT established, and an earlier version of these documents
asserted that it was. Both candidate force-deletes were probed directly and
neither destroyed the junction's target. A fix was written onto this branch and
reverted: a mutation reintroducing the supposed bug failed no test at all, which
proved the fix was untestable against an unknown mechanism. Shipping it would
have been a guard claiming more than it delivers - the exact defect class the
same documents name as this project's most important finding.
Gates re-verified at HEAD after a clean reinstall: tsc clean, node-env 118
passed, jsdom 6 passed. The browser gate is recorded as last-green at f1e32dcd4
with the reasoning for why it should still hold stated as reasoning, not
evidence.
* docs(ward-flow): name the strongest explanation for the emptied node_modules
Asked to kill 62 'leftover' processes, I inventoried them instead. Almost none
were leftovers and none were this worktree's - they belong to other live sessions
in care-plan-impl and phase-5-closeout, several actively running.
One explains the destruction: another worktree was running its own guard-push
against GitHub at that moment, and findPrettierBin deliberately borrows a
different worktree's real node_modules when its own checkout has none. That fits
what the earlier theory could not - why probes here never reproduced it, and why
it recurred when this session did not push.
Recorded as the strongest explanation, not a proven one: no probe was run against
a live cross-worktree borrow, because doing so would have meant interfering with
another session's in-flight push. Nothing was killed for the same reason.
* docs(ward-flow): browser gate verified green at HEAD, 24 passed
The last outstanding check is closed. Every gate is now measured green at HEAD
in one session: tsc clean, node-env 118, jsdom 6, ward Chromium 24.
It took two runs. After the clean reinstall the dev server rebuilt cold and took
~945s to become ready - this project pins cpus: 1, so npm run ensure gives up
long before readiness and reports a failure that is not one. The first gate run
returned 23 passed with one failure: the single test that visits every route and
therefore pays first-compile cost on each.
Recorded as a cold-start artefact only after two pieces of evidence, not one
plausible story: the test passed alone in 43s, and the full gate re-run warm
passed 24/24. That is the standard this phase is held to, and the standard I
failed earlier in the session when I asserted a node_modules destruction
mechanism I had not demonstrated.
* docs(ward-flow): record session 3 pre-flight findings and rulings R35-R44
Appends this session's ledger to the committed copy without disturbing the
concurrent session's own entry, and carries the workspace addenda into git so
they survive the gitignored .superpowers directory.
The substantive findings, all measured against the real fixture rather than
reasoned from field names:
- R36: flow-diagram.tsx is SILENT on voluntary-patient-on-a-locked-ward, the
warning the product owner ruled must be the more prominent of the two. Ruling
F9 had recorded that all Voluntary movements were also security Open and the
diagram was therefore merely less specific. That was never measured and is
false: there are 26 Voluntary movements and four are Secure (WF-301, WF-308,
WF-322, WF-329), each shortlisting three Secure units. All twelve pairs
diverge from the shortlist beside them today.
- R41/R42: Task 12's end-to-end journey cannot work as written. It starts on
the queue's first row, which is WF-303 at accepted_awaiting_bed and therefore
not referable; and it omits HANDOVER_READY, the only producer of a transport
job, so the officer's four actions would all be refused. The spec's own
journey has the handover step; the plan's test dropped it.
- R44: the live tracker's leg assertion does not match the strings
transportStatusLabel actually returns - two seed rows render a lowercase
'accepted' against a case-sensitive regex.
- R43: a second session is live in this worktree. Its commit is docs-only and
independently corroborates the browser-gate result. Not interfered with.
- R35: main is merged after Phase 3, per the user's decision. 568 commits
behind, 33 conflicting files, mostly a squash-merge artefact from PR #2140.
* feat(ward-flow): pin the phone referral bar instead of scrolling to it
Deletes the nested double-requestAnimationFrame scrollIntoView effect on the
coordinator's phone layout and pins .shortlistActionRow (Refer/Override) to
the literal viewport bottom by CSS at phone widths instead. A CSS-pinned bar
never measures .main's grid or .screen's height, so the measurement race the
deleted effect's comment named dissolves rather than needing a replacement.
A new .main:has(.shortlistActionRow) reserve keeps the pinned bar clear of
the exceptions drawer and the tail of the shortlist's own scrolled content,
scoped to exactly the moment a movement is selected (when the bar exists).
Adds a Playwright test proving selection never scrolls window.scrollY and
the referral control stays in viewport, using the addendum's corrections:
WF-002 selected explicitly by id (verified referable via
REFERRABLE_MOVEMENT_STAGES) rather than `.first()`, and the scroll assertion
written without the unsupported `.resolves` chain.
* feat(ward-flow): separate the transport leg from the provider narrative
transportStatusLabel mixes the discrete transport leg with provider prose
(e.g. "St John WA accepted, awaiting departure"), so it can never be
matched against a fixed leg pattern. Add transportLeg alongside it,
returning only the leg using transportStatusLabel's exact precedence
order, with a distinct "Cancelled" and an explicit undefined for no
transport job at all — never collapsed into one of the five leg names.
transportStatusLabel is unchanged and still has callers in
ward-management-console.tsx and ward-management-network.tsx.
* fix(ward-flow): assert the real scroll container, not window.scrollY
Task 7 fix round 1 (R50). The coordinator screen's .screen root is
height: 100dvh; overflow: hidden, so window.scrollY is always 0 in this
layout regardless of what the code does — the window.scrollY assertion
added in the previous round could never fail. .body (overflow: auto) is
the real scroll container the deleted scrollIntoView call used to move,
so the test now reads that element's scrollTop through a new stable
data-testid="ward-coordinator-body" instead.
Mutation-tested: a post-render scroll effect writing .body's scrollTop
correctly failed the new assertion (Expected: 0, Received: 1090); reverted
and confirmed green again.
* fix(ward-flow): surface the voluntary-on-locked warning on the flow diagram
flow-diagram.tsx still computed its restriction badge with the superseded
isMoreRestrictiveThanRequired/MORE_RESTRICTIVE_NOTE pair, which only
recognises an Open movement on a Secure ward. A Voluntary movement on a
Secure ward returned false, so the diagram rendered nothing at all for the
sharper case the shortlist panel already flags via restrictionNotice.
Move the diagram onto restrictionNotice(movement, unit) so it renders the
same two-level warning as the shortlist, with a new danger-toned
.diagramRestrictiveBadgeProminent variant for the voluntary_on_locked level.
Updates the WF-001 Playwright assertion to the migrated wording and adds a
new test pinning WF-301 (one of four Voluntary+Secure movements in the
fixture) for the voluntary_on_locked case that previously rendered nothing.
The old helpers are left in place, now unreferenced, per AGENTS.md's
guidance on deleting code believed dead.
* docs(ward-flow): record rulings R45-R53 and the four resolved pre-flight findings
Session-3 ledger refreshed through the flow-diagram fix, with every agent
report and brief carried into git so nothing depends on the gitignored
.superpowers workspace surviving.
Resolved this round:
- The flow diagram was silent on voluntary-patient-on-a-locked-ward, the
warning the product owner ruled must be the more prominent of the two. Now
fixed at d819ad9fd: three diagram nodes and three shortlist rows agree
exactly, verified live rather than from the diff, and the superseded wording
appears zero times on the running page.
- Task 12's end-to-end journey was unbuildable. Verified independently, then
redesigned on WF-315 with the whole eight-event chain driven through the real
reducer with zero rejections. My own suggested subject, WF-009, was refuted -
it is already declined by all five secure units.
- Nothing in the plan ever wires HANDOVER_READY to a control, so the transport
officer could never receive a job. Task 11 gains that control (R49).
- The role switcher cannot infer one ward from a three-way parallel referral;
it uses the spec's picker rather than silently taking the first (R52).
- The transport leg is now separated from the provider narrative (cecc9539e).
- Task 7's own new test could not fail; fixed at 3b4bf4152 against the real
scroll container and mutation-proved.
R53 records that two agents overrode the docs-sync hook under concurrent-edit
contention, and that I re-ran docs:update afterwards and confirmed zero drift
rather than accepting their word for it.
* feat(ward-flow): add the ward screen
Task 8: one inpatient unit's own view at /ward-management/ward/[unitId]
(WardScreen) - the ward answering what the coordinator refers, closing
the loop the coordinator screen opens. Five regions derived live from
useWardFlow(): unit identity, the five-state bed grid plus a
CONFIRM_CAPACITY form scoped to the unit's own id, incoming referrals
(accept in principle / hold a bed / decline with one of the seven
DECLINE_REASONS) each carrying restrictionNotice where it applies,
accepted/held/en-route movements, and withdrawn referrals with reason.
An unresolved unitId renders an explicit empty state naming the id,
never a substituted unit. Every accept/decline/hold control mirrors
wardFlowReducer's own preconditions so it can never advertise an
action the reducer would refuse.
Registers tests/ui-ward-roles.spec.ts in both playwright.config.ts
matchers, adds the route/component to the design-system adoption
contract (regenerated ADOPTION.md/adoption-manifest.json), links the
route from ClinicalRail's rail (literal href, route-reachability-safe),
and regenerates docs/site-map.md (was stale without this run).
* feat(ward-flow): add the transport officer phone screen
Task 9: /ward-management/transport/officer shows every transport job not
yet arrived (the model records a provider organisation, never an officer
identity, so filtering to "my jobs" isn't possible). A coordinator-style
queue-plus-pinned-bar pattern lets one job be "active" at a time, its four
actions (accepted, en route, collected, arrived) pinned to the viewport
bottom and each gated by a function that mirrors the reducer's own
preconditions exactly, including the receiving unit's live empty-bed floor
guard on Arrived. Wires the route into ClinicalRail with a literal <Link>
(RailLink's prop-passed href is invisible to route-reachability's AST scan)
and adds ward-sites.ts's edById alongside unitById.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ward-flow): give every in-transit patient the collection its stage implies
Six "moving" transport jobs in the ward-movements fixture (WF-006, WF-014,
WF-306, WF-313, WF-320, WF-327) carried no transport.collectedAt, a state
PATIENT_COLLECTED (the only reducer transition producing stage "moving")
could never leave behind, since it always sets collectedAt in the same
update. That made PATIENT_ARRIVED refuse on all six, leaving the transport
officer's phone screen with four dead controls on six of its eight jobs.
Gives each of the two hand-authored records (WF-006, WF-014) its own
plausible collectedAt derived from its own enRouteAt, and fixes the
generator's "moving" case (stageFields in ward-movements.ts) so every
generated in-transit record gets an index-varied, NOW_ANCHOR-clamped
collectedAt instead of omitting it — the root cause behind the other four.
Adds a fixture-coherence describe block to ward-flow-contracts.test.ts that
walks the whole wardMovements array directly (not through the reducer) and
asserts stage/stamp coherence read off ward-flow-reducer.ts: "moving"
implies collectedAt, "arrived" implies arrivedAt where a transport job
exists, transport stamps only fill in the order the reducer allows, and no
stamp exceeds NOW_ANCHOR. Each invariant counts the records it actually
matched, not loop iterations, so a future fixture edit that hollows the
condition out goes red instead of passing vacuously.
* feat(ward-flow): rewrite transport as the coordinator's live tracker
Task 10: /ward-management/transport now renders LiveTracker, the coordinator's
view of every vehicle in transit — patient, leg, and how long since its last
recorded stamp. Every row is a movement that carries a real transport job
(re-measured at this branch's head: 8 of 41 open movements); the other 33 are
named explicitly in an on-screen banner rather than rendered as fabricated-leg
rows, mirroring the officer screen's own honesty discipline.
- tracker/tracker-derivations.ts: trackerRowState delegates leg precedence to
transportLeg (never re-derives it) and maps the leg to its one real stamp;
stampAgeText renders "<duration> ago" or an explicit no-timestamp sentence
for the Requested leg, which the model never stamps at all.
- Strengthens the brief's own Playwright assertion (which the task-10 preflight
flags as unable to distinguish five correctly-rendered legs from two): adds
a pinned exact-row-count test, plus a node-environment unit suite covering
all five legs, Cancelled, and the no-transport absence — none of which the
seed fixture exercises end to end.
- The tracker screen's root also carries data-testid="ward-mode-transport" so
the existing cross-mode navigation test keeps working now that this route no
longer mounts WardModeWorkspace.
* docs(ward-flow): record Tasks 8-10, the fixture coherence fix, and rulings R54-R62
Verification of every commit from 171adb69a to b2e0a92aa, measured in this
session rather than read from any implementer report.
The substantive finding is a fixture defect, not a code one. Six movements sat
at stage 'moving' with no collectedAt - a state PATIENT_COLLECTED is the only
producer of and always sets, so the reducer could never have reached it, and a
patient cannot be in a vehicle that never picked them up. The consequence
landed on the transport officer's phone: six of eight jobs had four dead
controls each, because PATIENT_ARRIVED requires both. Fixed at 1349c213f with
a contract invariant derived from the reducer rather than only correcting the
six records. Counted from the live DOM afterwards: 8 of 8 jobs now have at
least one available action, up from 2.
R60 caught the phase's signature defect inside the guard written to prevent
it: the vacuity tripwire counted loop iterations rather than matches, so its
companion arrived-branch assertion ran on zero records and passed regardless.
Corrected to count matches and to assert the honest zero rather than inventing
a fixture record to satisfy a positive one.
R55 records that no Ward Flow browser spec has ever run in CI's Production UI
lane - scripts/playwright-pr-shards.mjs holds its own copy of the spec pattern
and it has no ward alternation at all. Pre-existing on main, surfaced not
fixed: correcting it needs hosted timing measurements this machine cannot take.
R61 is a new environment trap that cost twenty minutes: the dev server is
reaped when the shell that launched it exits, and the symptom is a Playwright
identity guard failing with a Node connection error while curl to the same URL
still succeeds - which reads exactly like an IPv6 mismatch and is not one.
* feat(ward-flow): add the emergency department screen with both clocks
Task 11: /ward-management/ed/[edId] shows one department's own patients,
the department clock (from openedAt) and the legal clock (from formedAt
where earlier), the four-hour departmental access target — labelled and
computed so it can never be mistaken for a legal deadline and never
touches a LegalForm/dueAt — a police-attendance flag, and each movement's
single outstanding item. Adds a raise-referral form (RAISE_REFERRAL) and
a record-examination form (RECORD_EXAMINATION), plus a mark-handover-ready
control (HANDOVER_READY) — the missing control that lets a movement ever
reach the transport officer's screen. Statewide capacity is shown
read-only. Registers the route in the nav rail, the adoption contract,
the codebase index, and the Playwright roles spec.
* fix(ward-flow): …
Summary
/ward-management: a synthetic-data ward/bed-management coordination tool (coordinator, queue, movements, transport, capacity, network, exceptions, governance, and a per-patient workspace screen), wired into the real app launcher/tools catalog.ward-model.ts,ward-derivations.ts,ward-eligibility.ts,ward-movements.ts,ward-pressure.ts,ward-priority.ts,ward-sites.ts,ward-clock.ts) plus unit and Playwright test coverage.category-identity.tsregistry instead of a separate local map, and registers the two new Playwright specs in the PR shard matcher.Verification
npm run verify:pr-localDuring development, use
npm run verify:cheapas the faster iteration gate before the final PR-local preflight.npm run verify:uiwhen UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changednpm run verify:releasebefore release or handoff confidence claimsFor retrieval, ranking, selection, chunking, source/citation rendering, or answer-contract changes,
verify:pr-localrunseval:rag:offlineautomatically. Run the offline command directly during iteration before spending a live eval.npm run eval:retrieval:quality(must stay 36/36) when retrieval, ranking, selection, chunking, or scoring behavior changed — CI cannot run it (needs live keys), so run it locally and paste the summary. A metadata/governance-weighting change once buried correct docs (recall 1.0→0.76) and only this eval caught it.npm run eval:rag -- --limit 15+npm run eval:quality -- --rag-onlywhen answer generation, the synthesis prompt, or answer post-processing changed (grounded-supported must not drop; citation-failure 0)npm run check:production-readinesswhen clinical workflow, privacy, environment, Supabase, source governance, or deployment behavior changednpm run check:deployment-readinesswhen deployment startup, hosting, or rollout behavior changedVerification not run: this description was completed to satisfy the required PR metadata and does not itself change any code. It does not certify local verification — see this PR's own CI run (Build, Unit coverage, Static PR checks, Production UI) at the current head for the automated evidence, and confirm locally before merge if that run is not fully green.
UI verification not run: this description-only update makes no UI change; see CI's Production UI and Advisory UI jobs at the current head for the code's own UI verification evidence.
Risk and rollout
mainto production, so merging makes/ward-managementlive atpsychiatry.tools. The design spec (docs/superpowers/specs/2026-08-14-ward-management-design.md) explicitly records this as a prototype — "not validated clinical decision support and is not production-ready" for real operational use. Confirm that's acceptable before merging; this is a product decision independent of this checklist.Clinical Governance Preflight
docs/ward-flow-context.md, "Synthetic only"). No Supabase or document-access code is touched in this diff.Clinical KB Database(sjrfecxgysukkwxsowpy)docs/ward-flow-context.md, "Not a medical device, and the page says so."Notes
docs/ward-flow-context.md,docs/ward-management-context.md, anddocs/ward-management-decisions.mdfor the full governance and design record behind the answers above.