From 2bac3fb1efce2f75f7d9aeb6ba942ebef5dc5b5f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:32:35 +0800 Subject: [PATCH 01/51] docs(caring-contacts): correct the retired-branch records, close the browser gate, capture the deferred findings Phase 2A was squash-merged to main as e4cbe8d3a (#2279) on 2026-08-23, but the handoff, the ledger and the continuation prompt all still named the feature branch as the source of truth and told the next session to build a worktree from it. Corrected in place rather than deleted, because the reasoning about durability and about measuring a moving tree still holds -- and holds harder on main, which far more sessions touch. - Browser gate re-run on main: 32 passed, exit 0, no ECONNRESET. The test that failed on 2026-08-23 (the 1440px condensed-bar pin) ran and passed, so the residual failure was load, not a defect. Also records that :822 was the test's declaration line, never the failing statement -- the dropped connection was in the setup POST at line 672, before any pin assertion ran. - Seven deferred findings captured as immutable issues-inbox requests, so they no longer survive only in the build record. - copy-decisions-recommended.md is new: the copy recommendations existed only in a previous session's conversation and did not survive it. Also corrects the count -- the records said seven items need the owner, the copy review actually raises thirteen. Co-Authored-By: Claude Opus 5 --- docs/caring-contacts/PROGRESS-LEDGER.md | 41 +++- .../copy-decisions-recommended.md | 214 ++++++++++++++++++ docs/caring-contacts/phase-2a-build-record.md | 79 +++++++ .../phase-2a-continuation-prompt.md | 122 ++++++---- .../18b32d38-540f-4d7c-af35-3f4f067e4998.json | 14 ++ .../854ca9ba-ba3b-442b-be6d-6f5873ed60ed.json | 14 ++ .../9da200a8-0ae0-451b-bef8-d1636b9e3cff.json | 14 ++ .../a4b0610d-ef92-4d1b-bcc8-3da0eb646d5a.json | 14 ++ .../ba655ad0-9934-4885-a959-5efd375a9bf1.json | 14 ++ .../cd97d402-7af1-473e-a2eb-dbac597cd03d.json | 14 ++ .../fa126adf-ade8-4027-a029-d59b4bef8967.json | 14 ++ 11 files changed, 497 insertions(+), 57 deletions(-) create mode 100644 docs/caring-contacts/copy-decisions-recommended.md create mode 100644 docs/outstanding-issues-inbox/18b32d38-540f-4d7c-af35-3f4f067e4998.json create mode 100644 docs/outstanding-issues-inbox/854ca9ba-ba3b-442b-be6d-6f5873ed60ed.json create mode 100644 docs/outstanding-issues-inbox/9da200a8-0ae0-451b-bef8-d1636b9e3cff.json create mode 100644 docs/outstanding-issues-inbox/a4b0610d-ef92-4d1b-bcc8-3da0eb646d5a.json create mode 100644 docs/outstanding-issues-inbox/ba655ad0-9934-4885-a959-5efd375a9bf1.json create mode 100644 docs/outstanding-issues-inbox/cd97d402-7af1-473e-a2eb-dbac597cd03d.json create mode 100644 docs/outstanding-issues-inbox/fa126adf-ade8-4027-a029-d59b4bef8967.json diff --git a/docs/caring-contacts/PROGRESS-LEDGER.md b/docs/caring-contacts/PROGRESS-LEDGER.md index 7c0d736d49..d73cca33b1 100644 --- a/docs/caring-contacts/PROGRESS-LEDGER.md +++ b/docs/caring-contacts/PROGRESS-LEDGER.md @@ -4,11 +4,26 @@ a second copy of the detail: each row points at the file that holds the reasoning. Where this file and a detailed record disagree, **the detailed record wins** — this one is a summary and can go stale. -Last updated at head `05584f9b5`, 2026-08-23. Branch `claude/suicide-contact-mockup-b5aaa0`, pushed. - -> **The branch is SHARED.** On 2026-08-22 a commit (`c3ef20c3f`) authored elsewhere — not from this -> machine's clone — landed on it while a session was mid-task. `git fetch` before every push, and treat -> any full-suite result taken on a moving tree as a hypothesis. See Ruling 66. +Last updated 2026-08-24. + +> **PHASE 2A HAS LANDED ON `main`, AND THE FEATURE BRANCH IS RETIRED.** Verified 2026-08-24: the whole +> of Phase 2A was squash-merged as `e4cbe8d3a` — "Claude/suicide contact mockup b5aaa0 (#2279)", +> 2026-08-23 — and every `docs/caring-contacts/**`, `src/lib/caring-contacts/**`, +> `src/components/caring-contacts/**`, `caring-contacts/supabase/migrations/**` and +> `tests/ui-caring-contacts-workspace.spec.ts` path on `main` now matches the old branch tip +> `cf03f99a4`, except where later `main` work is NEWER (the design-system token consolidation touched +> three component files). **`main` is the source of truth. Do the closing work on a fresh worktree off +> `origin/main`, not on `claude/suicide-contact-mockup-b5aaa0`.** No local remote-tracking ref for that +> branch remains, which is consistent with the PR branch having been deleted after the merge — not +> verified against GitHub, which needs approval. + +> **Sections below that instruct you to work on, push, or fetch the feature branch are superseded by +> the paragraph above.** They are kept because their *reasoning* about durability and about shared-tree +> measurement is still correct and still paid for; only the branch name is stale. In particular the +> historical note that the branch was SHARED — commit `c3ef20c3f` landed on it on 2026-08-22 from a +> clone not on this machine, mid-task — is why any full-suite result taken on a moving tree is a +> hypothesis and not a result. That lesson generalises to `main`, which many sessions touch. See +> Ruling 66. --- @@ -39,6 +54,8 @@ experience and clinical sign-off are required before any real use. | 1 | Phase 1 + early 2A | Built the sealed domain and database. 13 owner-behalf decisions. Phase 1 gate passed. | `D:\Repos\caring-contacts-handoff-2026-08-20\` | | 2 | Phase 2A controller | Plan written; Tasks 1-10 and 11a built and reviewed. Rulings 1-31. Died on an account limit mid-fix-round-2. | same bundle | | 3 | Phase 2A recovery | Verified the abandoned commit, re-reviewed it, Rulings 32-34, survived a worktree deletion, rebuilt and re-proved | `D:\Repos\caring-contacts-handoff-2026-08-21\` | +| 4 | Phase 2A completion | Task 11b, Tasks 12-19, the final whole-branch review, Rulings 35-66, the copy review, the condensed bar | build record, Session 4 onward | +| 5 | Phase 2A closing | Found the phase already merged to `main`; browser gate green; mutation proofs; issues sweep; 2B planning begun | build record, Session 5 | ## 4. Task status — Phase 2A (19 tasks, 5 groups) @@ -68,8 +85,14 @@ experience and clinical sign-off are required before any real use. | 19 | Browser proof at six widths + plan closing steps | Complete, 1 fix round, review clean | | — | Final whole-branch review | **Done** — three parallel reviewers, distinct lenses | | — | Post-review fixes (Rulings 60-65) | Complete — incl. a CRITICAL patient-data finding | -| — | Condensed pinned safety bar (owner-requested) | Built, 1 fix round; **2 mutation proofs still unrun** | -| — | Copy review document for the owner | **Delivered** — `copy-review.md`, 7 items need his call | +| — | Condensed pinned safety bar (owner-requested) | Built, 1 fix round; browser gate green 2026-08-24 | +| — | Copy review document for the owner | **Delivered** — `copy-review.md`; recommendations now | +| | | tracked in `copy-decisions-recommended.md`. 13 open, | +| | | 9 clinical/policy + 4 engineering (the "7" was an | +| | | undercount and is corrected) | +| — | Deferred-findings `/issues` sweep | **Done 2026-08-24** — 7 request files queued in | +| | | `docs/outstanding-issues-inbox/`, awaiting reconcile | +| — | Phase 2B plan | **In progress** — no plan existed; being written | ## 5. Verification evidence, as recorded @@ -84,7 +107,9 @@ experience and clinical sign-off are required before any real use. | Current known-red (expected) | **NONE in this work.** Both 2026-08-23 failures were artefacts, proven so: the | | | caring-contacts one passed 22/22 on re-run of the same commit (a concurrent agent | | | held a source file mid-edit), and the other was a 120 s timeout under machine load. | -| Browser gate | **Was fully red — 32/32 — for a reason outside this work**, then unblocked. See §5b. | +| Browser gate | **GREEN 2026-08-24 on `main`: `32 passed (55.5s)`, exit 0**, no ECONNRESET in a 341-line | +| | log. The 2026-08-23 residual failure at 1440px was LOAD, not a defect — see §5b. | +| Condensed-bar mutation proofs | Run 2026-08-24 against `main`. See §5c. | ### 5b. The production lock, and why the browser gate went red diff --git a/docs/caring-contacts/copy-decisions-recommended.md b/docs/caring-contacts/copy-decisions-recommended.md new file mode 100644 index 0000000000..ac8635500b --- /dev/null +++ b/docs/caring-contacts/copy-decisions-recommended.md @@ -0,0 +1,214 @@ +# Caring Contacts — the copy decisions, with a recommendation for each + +**Written 2026-08-24. Nothing here has been implemented.** Patient-visible wording stays frozen until +the owner answers, per the standing constraint in `phase-2a-handoff.md`. + +## Why this file exists + +`copy-review.md` sets out every concern but deliberately offers no recommendations — it is a mark-up +document, and guessing at intent was explicitly ruled out when it was written. Recommendations were +given to the owner on 2026-08-23 **in conversation only, and were never written to a tracked file**, +so they did not survive the session that made them. This file closes that gap. It is the +recommendation half; `copy-review.md` stays the evidence half and wins on any question of what the +current wording actually says. + +**Two counts, and the disagreement is worth stating rather than smoothing over.** `PROGRESS-LEDGER.md` +and the continuation prompt both say **seven** items need the owner's call. `copy-review.md` Part 7 +lists **ten** concerns and Part 5 lists **three** more — thirteen in total. Reading all thirteen, +**nine are genuinely clinical or policy decisions only the owner can take** and four are engineering +work needing no clinical input. The "seven" does not reconcile against the document and appears to be +an undercount carried forward from an earlier draft, so this file uses the thirteen and marks which +is which. + +**"Cost if wrong" means the cost of having followed my recommendation and been mistaken** — that is +the thing worth weighing, not the recommendation itself. + +--- + +## A. The nine that need the owner's clinical or policy decision + +### A1 — A patient is given a crisis number labelled "Fictional Support Line" + +Current wording, in every message: `Fictional Support Line: +61 491 570 158`. + +**Recommend:** keep the fictional label, and add a machine check that refuses to send any message +whose crisis-line text still contains the word "Fictional". Not a comment and not a note in a +runbook — a test that goes red, in the same place the two-segment length limit is already enforced. + +**Why:** the label is correct today and is the honest thing for a prototype to say. The risk is not +the wording, it is that nothing forces it to be replaced. The programme already proves this pattern +works: the required-fragment checker rejects a message missing `In an emergency call 000`, so the +same mechanism can reject one that still says `Fictional`. + +**Cost if wrong:** near zero. A check that fires on the day a real number arrives is a two-line edit +to remove. + +### A2 — "Your message has not been seen by anyone and has not been kept" may not be true + +**Recommend:** narrow it to what this system can actually promise, and say who is not reading rather +than what is not stored — something in the shape of *"No one at Example Aftercare Team reads this +number."* Do not restore any claim about storage until a telephony provider is chosen and its +retention terms have been read. + +**Why:** this is the highest-risk sentence in the programme. It is a firm factual claim about data +handling, made to a person in distress, about a system that has no telephony provider yet — so +nobody can currently know whether it is true. The same reasoning already forced one narrowing here +on 2026-08-19, when "Replies are not received, stored, analysed or monitored" became untrue the +moment the number was made able to receive. This is the identical mistake one step further down. + +**Cost if wrong:** a patient is told slightly less than the eventual truth. That is the safe +direction of error. The other direction is a false promise about confidentiality to a suicidal +person, which no later correction reaches. + +### A3 — "No one reads replies to this number" says what does not happen, not what does + +**Recommend:** say both, in that order — nobody reads it, and something does come back +automatically — so the patient knows the auto-reply is not a person. + +**Why:** a patient who reads "no one reads replies" and then receives a message may reasonably +conclude somebody did read it after all. That is worse than the original ambiguity, because it +teaches them the stated boundary is unreliable at exactly the moment the boundary matters. + +**Cost if wrong:** characters. Message A is already at its two-segment ceiling of 252 — about nine +characters from rejection — so this may not fit in Message A and may have to live only in Message B. +Decide it as a pair with A9. + +### A4 — The required closing message does not exist + +The checker requires a final message to contain `This is the final message in this programme`. No +final message has been written, so a plan reaching its end today sends nothing. + +**Recommend:** treat the wording as a Phase 2B deliverable and write it with a lived-experience +representative, not before. In the meantime make the gap loud rather than silent: a plan whose last +contact has no message body should refuse and raise, not pass quietly. + +**Why:** the end of a caring-contacts series is a clinically loaded moment — it is the point the +patient loses the contact — and drafting that text is not a job for a coding session. But a silent +no-op is the worst available behaviour, because it looks like success. + +**Cost if wrong:** the refusal fires during a demo and needs explaining. Cheap. A silently missing +final message means a patient's series stops with no closure and nothing recorded that it happened. + +### A5 — A patient is never told when sending stops + +During a service-wide stop, a pause, or a contact-changed block, clinicians are told in detail and +the patient is told nothing. + +**Recommend:** keep it that way for a service-wide safety stop, and record it as a deliberate +decision rather than leave it an omission. Revisit only for a **withdrawal**, where the patient +asked. + +**Why:** a service-wide stop is triggered by a serious incident affecting somebody else. A message +saying "your messages have stopped" to a person who did not ask, at a moment nobody can explain to +them, invites the reading that they did something wrong or that something has happened to their +clinician. Silence is the more conservative option and it is reversible — nothing prevents adding a +notice later. + +**Cost if wrong:** a patient notices the messages stopped and does not know why. That is real, and it +is exactly why this needs the owner's decision rather than a controller ruling. + +### A6 — "Contacts that fall inside the pause are skipped for good" + +**Recommend:** confirm the behaviour is intended, and change the clinician-facing wording to state +the consequence as a number rather than a fact — for example *"3 contacts fall inside this pause and +will not be sent later."* + +**Why:** in caring contacts the schedule is the intervention. Silently and permanently removing +contacts from it is a clinical act, and "skipped for good" is easy to read past when you are pausing +for an ordinary administrative reason. + +**Cost if wrong:** none — showing the count is strictly more information. Whether pausing *should* +drop contacts is the owner's question, and this recommendation does not settle it. + +### A7 — Withdrawal is immediate, irreversible, and needs nobody else's agreement + +Restarting the service after a stop needs three approvals from three people. Withdrawing a patient +needs none and cannot be undone. + +**Recommend:** keep the asymmetry and record why. Add a confirmation step that names what is lost. +Do not add an approver. + +**Why:** the asymmetry is defensible and I think correct, because the two actions are not comparable. +A restart resumes sending to everybody after an incident, so the risk is in acting too readily. A +withdrawal is a patient exercising a choice about contact they receive, and putting a second +clinician in front of that turns a patient's decision into a request. Irreversibility is the part +worth softening, and a confirmation naming the consequence does that without an approval gate. + +**Cost if wrong:** a withdrawal made in error cannot be reversed and the patient must be re-enrolled +from the start. The confirmation step is what makes that acceptable, so it should not be dropped +from this recommendation. + +### A8 — "All three attempts in the original window are finished and there is no later retry" + +A patient whose contact fails receives nothing that day and nothing later. + +**Recommend:** confirm as clinical policy, and surface it where a clinician will actually meet it — +on the patient's plan, not only inside a button panel. + +**Why:** no-later-retry is a reasonable design. A caring contact arriving days late is a different +intervention, and stacking retries turns a non-demanding contact into a demanding one. But it is a +clinical policy currently stated only in a place a clinician sees once something has already gone +wrong. If a patient is missing contacts, that belongs on their plan. + +**Cost if wrong:** a clinician assumes a failed contact will be retried and does not follow up +manually. That is a real gap in a suicide-prevention programme, which is why the visibility half +matters more than the policy half. + +### A9 — "000" is the only emergency direction given + +No Lifeline, no 13YARN, no after-hours mental health line. + +**Recommend:** decide this as a pair with A3, because they compete for the same nine spare +characters. If only one thing can be added: **add Lifeline `13 11 14`, and drop the +`Fictional Support Line` line once a real crisis number is chosen** — the fictional line is already +occupying the space a real one would need. + +**Why:** 000 alone directs a person in distress to an emergency-services response. That is the right +answer for an emergency in progress and the wrong answer for someone distressed and not in immediate +danger, and Lifeline is the standard Australian answer to that second state. 13YARN matters for +Aboriginal and Torres Strait Islander patients, and the schema already carries cultural identity, so +it could be conditional rather than universal — but that is a Phase 2B capability, not a wording +change. + +**Cost if wrong:** the length ceiling is hard at about nine characters, so anything added means +something removed, and removing the wrong thing is worse than adding nothing. This is the one item +where I would not act on my own recommendation without the owner's explicit choice of what goes. + +--- + +## B. The four that need no clinical input + +None of these changes a patient-visible string. All can proceed as soon as the owner says go. + +### B1 — Two panels describe content they do not show + +"Preview the message the patient would see" shows no message; "Plan activation recorded" records +nothing. **Recommend:** change the words now to match what exists, and let Phase 2B change them back +when the content lands. A true statement about a smaller product beats a false one about a larger +one. + +### B2 — "lead" appears in visible wording in its ordinary English sense + +"the incident lead", "the clinical programme lead". **Recommend:** narrow the prohibited-vocabulary +ban to the commercial sense rather than exempting the sentence — an exemption would have to be +re-argued every time the sentence changes. These are people's job titles. + +### B3 — The prohibited-vocabulary ban is not enforced on screen wording + +It runs on outgoing messages and the 24 frozen overlay rows only. **Recommend:** extend it to a +static scan over interface strings. Captured in the issues inbox on 2026-08-24 as a P2 issue. + +### B4 — One banned word is rendered, in the frozen design-scratch prototype + +"Delivered is a transport receipt only and never means the message was read or the patient is safe." +**Recommend:** leave it. Those screens 404 in production and Phase 2B replaces them, and the sentence +uses "safe" in order to deny it, which is the defensible use. Decide it when the wording is carried +across, and let B3's scan flag it at that point. + +--- + +## What happens next + +1. The owner marks this file up, or answers item by item. +2. Only then does any patient-visible string change, and each change carries its decision reference. +3. A1's machine check, B1, B2 and B3 can start as soon as he says go; none touches patient wording. diff --git a/docs/caring-contacts/phase-2a-build-record.md b/docs/caring-contacts/phase-2a-build-record.md index cb2bdc24a2..fcf14f5ccc 100644 --- a/docs/caring-contacts/phase-2a-build-record.md +++ b/docs/caring-contacts/phase-2a-build-record.md @@ -2666,3 +2666,82 @@ failure as a regression, run the other way. **Also still unrun, and still recorded as unrun:** the condensed bar's 1440px pin mutation and its dark-mode colour mutation. Both were blocked on the browser gate; the gate is now available, so the next session can run them. + + +# Session 5 — 2026-08-24 — closing work, and the branch turned out to be merged + +Working copy `D:\Repos\Database\.claude\worktrees\browser-test-gate-handoff-d5c1db`, on `main` at +`6299857df`. Dependencies came from `node scripts/setup-codex-worktree.mjs`, which found every +`package-lock.json` on this machine byte-identical and reused `D:\Repos\Database`'s `node_modules` in +seconds rather than the 15-58 minutes `npm ci` costs here. Worth knowing before anyone budgets an hour +for a fresh worktree again. + +Ruling: [67] This session appends to THIS tracked file rather than opening a `.superpowers/sdd/` +ledger, even though it is running the subagent-driven-development method which asks for one. +— Why: the programme already ruled that the SDD workspace is a GENERATED MIRROR and never a source, +after the original git-ignored workspace was destroyed on 2026-08-21 and took the only copy of a +session ledger with it. A second ledger in git-ignored scratch would recreate exactly that loss, and +two ledgers disagreeing is worse than one that is occasionally terse. — Cost if wrong: the SDD +scripts' `progress.md` conventions are not used, so a future controller resuming by those conventions +finds no ledger where the skill says to look. That is why this ruling is written here, where that +controller is told to read first. + +## The finding that reframes everything below: Phase 2A ALREADY MERGED + +The handoff, the ledger and the continuation prompt all named branch `claude/suicide-contact-mockup-b5aaa0` +as the source of truth and told the next session to build a worktree from it. **That is stale.** Verified +before any work was done: + +- `e4cbe8d3a` on `main` is "Claude/suicide contact mockup b5aaa0 (#2279)", dated 2026-08-23 — a squash + merge of the whole phase. `main` has advanced 18 commits since. +- Every caring-contacts path on `main` matches the old branch tip `cf03f99a4`. `git diff origin/main + claude/suicide-contact-mockup-b5aaa0` over `src/lib/caring-contacts`, `caring-contacts/` and + `tests/ui-caring-contacts-workspace.spec.ts` is EMPTY; `docs/caring-contacts/` differs by two lines in + one archive file; `src/components/caring-contacts/` differs only where **`main` is newer** — the + design-system consolidation replaced a shadow literal with a token, swapped a local `SectionHeading` + for the shared one, and added a `focusTimerRef` teardown the branch never had. +- No local remote-tracking ref for that branch survives, which is what a deleted PR branch looks like + after a prune. Not verified against GitHub — that needs approval and buys nothing here. + +So the branch is retired and `main` is the source of truth. The whole "push after every task / +`SKIP_STATIC_GUARD=1`" apparatus in the older records was correct for its moment and is now noise. The +records have been corrected in place rather than deleted, because the *reasoning* about durability and +about measuring a moving tree still holds — and holds harder on `main`, which far more sessions touch. + +**The general lesson, and it is the same shape as the idempotency-table one:** a handoff document +describes where work *was*, and no part of it updates when the work moves. Four documents agreed with +each other and all four were wrong together, because they were written in one session and copied from +one another. **Agreement between records that share an ancestor is not corroboration.** Check the claim +against git, not against the other records. + +## Closing item 1 — the browser gate is GREEN, and the residual failure was load + +Re-run against `main` content, via the repository runner, whole log kept to a file and not tailed: + +``` +Running 32 tests using 1 worker + ok 30 [chromium] > ui-caring-contacts-workspace.spec.ts:822:9 > caring-contacts service stop, stated on + every screen > pins the condensed bar under the header once the banner has gone at 1440px (898ms) + 32 passed (55.5s) +EXIT=0 +``` + +341-line log, no `ECONNRESET` anywhere in it. **The exact test that failed on 2026-08-23 ran and passed.** +The disposition recorded then — "unresolved, re-run it on a quiet machine" — therefore resolves to +**load, not a defect**, and the condensed bar's fix round is closed on that count. Note also that the +count is **32**, not the 33 the continuation prompt predicted; the prompt has been corrected. + +Two corrections to how that failure was read, both worth more than the incident: + +- **The machine was NOT quiet for this run** — 73 `node` processes and 23 Claude processes were live. A + pass under load is *stronger* evidence than a pass on a quiet machine, not weaker, because load is the + very hypothesis being tested. Waiting for quiet would have bought less and cost hours. +- **`:822` was never the failing line.** Playwright reports a failure at the test's DECLARATION line, and + 822 is the `test(...)` line. The `apiRequestContext.post: read ECONNRESET` came from + `arrangeServiceStop`'s setup POST at line **672**, before a single pin assertion executed. The previous + session's honest worry — "it is precisely the assertion the fix round strengthened, so the one test most + likely to be genuinely wrong is the one that failed" — could not have been true as stated: a dropped + HTTP connection during arrange cannot be caused by wrong geometry in an assertion that never ran. That + did not make the re-run unnecessary, and running it was still right — the difference between narrowing + a hypothesis and confirming one. But **read which line the runner is actually naming before inferring + what a failure means.** diff --git a/docs/caring-contacts/phase-2a-continuation-prompt.md b/docs/caring-contacts/phase-2a-continuation-prompt.md index 6263695249..2450aaa530 100644 --- a/docs/caring-contacts/phase-2a-continuation-prompt.md +++ b/docs/caring-contacts/phase-2a-continuation-prompt.md @@ -14,20 +14,31 @@ writing anything. ═══ WHERE THE WORK IS ═══ Repository: D:\Repos\Database (remote: github.com/BigSimmo/Database) -Branch: claude/suicide-contact-mockup-b5aaa0 — PUSHED. origin holds it. That is the source of truth. - -THE BRANCH IS SHARED. It is not yours alone. On 2026-08-22 commit c3ef20c3f landed on it from a clone -that is NOT on this machine (this repo's branch reflog never held it), while a session was mid-task. -So: `git fetch` BEFORE every push, not after a rejection; never force-push it; and treat any full-suite -result taken while another agent is editing as a hypothesis, not a result. A phantom failure and a -phantom pass are equally possible — see "the phantom failures" in the build record. - -FIRST ACTION — make yourself a working copy. Do NOT assume one already exists: +Branch: main. PHASE 2A HAS ALREADY MERGED. Verified 2026-08-24: the whole phase went in as + e4cbe8d3a, "Claude/suicide contact mockup b5aaa0 (#2279)", on 2026-08-23. Every + caring-contacts path on main matches the old branch tip cf03f99a4 except where later + main work is newer. The old feature branch claude/suicide-contact-mockup-b5aaa0 is + RETIRED — do not work on it, do not push it, do not resurrect it. + +MEASUREMENT DISCIPLINE CARRIES OVER, AND MATTERS MORE ON main. The retired branch was shared: on +2026-08-22 commit c3ef20c3f landed on it from a clone not on this machine, mid-task, and invented both +a phantom failure and a phantom green. main is touched by many more sessions than that branch ever was. +So: record the exact commit any full-suite or browser result was taken against, re-run a single file +alone against a named commit before believing a failure, and treat green under concurrency as worth no +more than red under concurrency. See "the phantom failures" in the build record. + +FIRST ACTION — make yourself a working copy off main. Do NOT assume one already exists: cd D:\Repos\Database git fetch origin - git worktree add D:\Worktrees\Database\ claude/suicide-contact-mockup-b5aaa0 -Then confirm you are where you think you are: `git rev-parse --abbrev-ref HEAD` must print -claude/suicide-contact-mockup-b5aaa0. + git worktree add D:\Worktrees\Database\ -b claude/ origin/main +Then confirm you are where you think you are: `git rev-parse --abbrev-ref HEAD` must print your new +branch, and `git log --oneline -1` must show a commit at or after e4cbe8d3a. + +DEPENDENCIES ARE FREE IF A COMPLETE INSTALL EXISTS. `npm ci` is 15-58 minutes here, but every +package-lock.json on this machine was byte-identical on 2026-08-24, so +`node scripts/setup-codex-worktree.mjs` reused D:\Repos\Database's node_modules in seconds and +reported "PASS: worktree dependencies match package-lock.json." Run that FIRST, before reaching for +npm ci. Check with `--dry-run` if you want to see what it would do. WORKING DIRECTORIES ON THIS MACHINE DO NOT SURVIVE. On 2026-08-21 four were destroyed by another process — under .claude\worktrees\ AND under D:\Worktrees\, one of them holding this exact work, and @@ -76,22 +87,33 @@ idempotency table. Rulings now run to 66. OPEN, in the order I would take them: - 1. THE BROWSER GATE WAS RUNNING WHEN THE SESSION ENDED, and its result is unknown. Re-run it: - npm run test:e2e -- tests/ui-caring-contacts-workspace.spec.ts --project=chromium - Redirect the whole log to a file; do NOT pipe it through `tail`, which destroys the per-failure - detail. NEVER invoke `npx playwright test` directly — the repo refuses it with an `Error:` line - and EXIT CODE 0, so it looks like a pass and ran nothing. Expect 33 tests. - 2. TWO MUTATION PROOFS ARE UNRUN and are recorded as unrun, not as passed — the condensed bar's - pin assertion at 1440px, and its dark-mode colour assertion. Both were blocked on the browser - gate. Run them once (1) is green, or the bar's fix round is not closed. - 3. THE SEVEN COPY ITEMS await the owner. Recommendations are already written and given to him. + 1. DONE 2026-08-24 — THE BROWSER GATE IS GREEN ON main. `32 passed (55.5s)`, exit 0, no + ECONNRESET anywhere in a 341-line log. The one test that failed on 2026-08-23 — + `:822 pins the condensed bar under the header once the banner has gone at 1440px` — ran and + passed in 898ms. So the residual failure was load, not a defect, and the condensed bar's fix + round IS closed on that count. Two things made the earlier reading harder than it needed to be + and are worth keeping: Playwright reports a failure at the test's DECLARATION line, so + `:822` named the `test(...)` line and not the failing statement — the ECONNRESET was actually + in `arrangeServiceStop`'s setup POST at line 672, before any pin assertion ran, which is why a + transport error could never have been the pin being wrong. And `npm run test:e2e` is still the + only correct invocation: `npx playwright test` refuses with an `Error:` line and EXIT CODE 0, + so it reads as a pass having run nothing. Redirect the whole log to a file; never pipe a gate + through `tail`. Expect 32 tests, not 33. + 2. TWO MUTATION PROOFS — the condensed bar's pin assertion at 1440px, and its dark-mode colour + assertion. See the build record's 2026-08-24 section for their result; if that section is + absent they are still unrun and must be reported as unrun, never as passed. + 3. THE COPY DECISIONS await the owner. `docs/caring-contacts/copy-decisions-recommended.md` + (new, 2026-08-24) carries a recommendation, a reason and a cost-if-wrong for each. Note the + count: this prompt and the ledger both said SEVEN, but `copy-review.md` actually raises + THIRTEEN — nine clinical or policy, four pure engineering. The seven was an undercount. + Do not change patient-visible wording until he has answered. 4. PHASE 2B has no plan yet. The owner's stated order: patients and their plans -> schedule and what's due -> message templates -> team, workload and coverage. - 5. An `/issues capture` sweep has NOT been run. Deferred items currently survive only in the build - record: the accidental same-team serialisation; postgres-repository.ts at ~2,080 lines; four - bare foreign keys onto plans/contacts; the prohibited-language gate covering only the 24 overlay - rows; the frozen matrix's ambiguous "Recovery action only" wording; Ruling 60's 640-767px band; - `connection-unavailable` and `permission-unavailable` having no runtime caller. + 5. DONE 2026-08-24 — the `/issues capture` sweep ran. All seven deferred items are now immutable + request files under `docs/outstanding-issues-inbox/`, so they no longer survive only in the + build record. They reach `docs/outstanding-issues.md` when someone runs + `npm run issues:reconcile` from a serialized fresh-base branch; until then they are queued, + not filed. THE AUTHORITIES, if the brief sends you there or a conflict arises - docs/superpowers/plans/2026-08-19-caring-contact-phase-2a-foundations.md @@ -105,19 +127,16 @@ THE AUTHORITIES, if the brief sends you there or a conflict arises changes against most training data. Reading beats reasoning; more thinking does not repair a wrong prior. -THE CODE TASK 11b TOUCHES — three files, all large; let a subagent read them - - tests/helpers/caring-contacts-repository-contract.ts receives the moved tests — do this FIRST - - tests/caring-contacts-repository.test.ts loses the moved tests - - src/lib/caring-contacts/db/postgres-repository.ts the ~22 missing methods - Reference for intended behaviour (read as specification, do NOT copy its structure): - - src/lib/caring-contacts/in-memory-repository.ts +THE STORAGE LAYER — Task 11b finished this; the map is here because Phase 2B extends it - src/lib/caring-contacts/repository.ts the interface: 38 methods - Also relevant: - - tests/caring-contacts-postgres-repository.test.ts carries temporary scaffolding 11b removes + - src/lib/caring-contacts/in-memory-repository.ts implements all 38 + - src/lib/caring-contacts/db/postgres-repository.ts implements all 38; ~2,080 lines + - tests/helpers/caring-contacts-repository-contract.ts the SHARED contract BOTH stores run. + New behaviour goes HERE, not in one store's + own file, or the two stores drift. - tests/helpers/caring-contacts-postgres.ts harness; CARING_CONTACTS_DATA_TABLES is a hand-maintained truncation list every new table must join - - tests/caring-contacts-retention.test.ts the one known-red test; Ruling 26, Step 0b THE SEALED DOMAIN — src/lib/caring-contacts/ (27 modules). The store PERSISTS decisions; it must never re-derive a rule a module already owns, even if the answer matches today: @@ -162,19 +181,20 @@ THE SCRATCH WORKSPACE — generated, never a source record instead. The original was git-ignored, was destroyed, and took the only copy of the session ledger with it. That is why it is now regenerable. -═══ STATE — nothing is outstanding ═══ -Tasks 1-10 and Task 11a are COMPLETE and reviewed. Task 11a went through three fix rounds; Rulings -27-34 are implemented and verified. The caring-contact database suite is at 96 passed, and its three -newest tests are proven falsifiable by deliberate mutation, not merely green. - -Two failures are EXPECTED and must NOT be "fixed" by weakening anything: - - `npm run typecheck` is RED on src/lib/caring-contacts/db/postgres-repository.ts. The interface - declares 38 methods, the in-memory store implements 38, Postgres implements 16 — a gap of 22. - Task 11b closes it, and restoring typecheck is the task's headline deliverable. Do NOT narrow the - interface and do NOT stub methods: a stub that satisfies the compiler while failing at runtime - turns a visible failure into a hidden one. - - `npm run test` has exactly one failure, tests/caring-contacts-retention.test.ts. Ruling 26 - specifies the fix and it is Step 0b of your brief. +═══ STATE — all 19 tasks built; NOTHING is knowingly red ═══ +Every Phase 2A task is complete and reviewed, both checkpoints passed, the final whole-branch review +ran, and its findings were fixed through Ruling 65. Rulings run to 66. + +THE TWO EXPECTED FAILURES THIS SECTION USED TO NAME ARE BOTH GONE, and neither was fixed by +weakening anything — that matters, because a green tree reached by softening an assertion is worse +than the red one it replaced: + - `npm run typecheck` was RED on db/postgres-repository.ts, a 22-method gap against the + 38-method interface. Task 11b implemented all 22. Green. + - `npm run test` had exactly one failure, tests/caring-contacts-retention.test.ts. Ruling 26's + fix landed as Step 0b of Task 11b. Green, and the retention suite went 23 -> 24 tests. +So a red typecheck or a red retention test is now a REGRESSION, not the documented baseline. If you +meet one, do not reach for `SKIP_STATIC_GUARD=1` — that override existed only while the red above +was expected. ═══ WHAT THIS IS ═══ A suicide-prevention caring-contacts workspace: patients discharged from hospital receive a fixed @@ -249,7 +269,11 @@ Sonnet 5 at medium-high for ordinary implementer work. Opus 5 at high for: migra security, Tasks 17-18 (the 24-overlay modality contract), anything displaying delivery or clinical state, and the final whole-branch review. -═══ STOP AFTER Checkpoint 2. Do not start Task 12. ═══ +═══ WHERE TO STOP ═══ +Phase 2A is finished, so there is no task boundary left to stop at. Stop instead when the closing +items above are done, and DO NOT begin building Phase 2B screens: 2B needs its own written plan and +the owner's copy decisions first, and starting it early is how the wording gets settled by an +implementer instead of by him. ═══ TELL ME AT THE END ═══ What was built, what you decided on my behalf and what each costs if wrong, and anything you could not diff --git a/docs/outstanding-issues-inbox/18b32d38-540f-4d7c-af35-3f4f067e4998.json b/docs/outstanding-issues-inbox/18b32d38-540f-4d7c-af35-3f4f067e4998.json new file mode 100644 index 0000000000..f2f03d8c0e --- /dev/null +++ b/docs/outstanding-issues-inbox/18b32d38-540f-4d7c-af35-3f4f067e4998.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "18b32d38-540f-4d7c-af35-3f4f067e4998", + "createdOn": "2026-08-24", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Caring Contacts: nothing checks screen wording against the prohibited-vocabulary list", + "detail": "The prohibited-language check runs against outgoing messages and the 24 frozen overlay rows only. No gate scans interface strings elsewhere in the workspace, so the ban on words such as high risk, safe, engagement score and risk score is currently policy held by people rather than by software. Copy review Part 5 item 2 records the same gap.", + "source": "docs/caring-contacts/copy-review.md Part 5; Phase 2A deferred list", + "issueUlid": "01M0SA6T7M0HYHTHE5MHJJ66G1" + } +} diff --git a/docs/outstanding-issues-inbox/854ca9ba-ba3b-442b-be6d-6f5873ed60ed.json b/docs/outstanding-issues-inbox/854ca9ba-ba3b-442b-be6d-6f5873ed60ed.json new file mode 100644 index 0000000000..3e6c47aadf --- /dev/null +++ b/docs/outstanding-issues-inbox/854ca9ba-ba3b-442b-be6d-6f5873ed60ed.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "854ca9ba-ba3b-442b-be6d-6f5873ed60ed", + "createdOn": "2026-08-24", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "Caring Contacts: connection-unavailable and permission-unavailable have no runtime caller", + "detail": "Both are keys in the total blockReason lookup in src/components/caring-contacts/workspace/overlays/overlay-host.tsx (lines 159-160) with a definition at definitions.ts:361, but nothing in the workspace yet produces either reason, so neither wording has ever been rendered from a real code path. Phase 2B should either wire them or record that they are reserved.", + "source": "docs/caring-contacts/phase-2a-build-record.md deferred list; verified in source 2026-08-24", + "issueUlid": "01M0SA6TXVF5EYAJNH4TMJT374" + } +} diff --git a/docs/outstanding-issues-inbox/9da200a8-0ae0-451b-bef8-d1636b9e3cff.json b/docs/outstanding-issues-inbox/9da200a8-0ae0-451b-bef8-d1636b9e3cff.json new file mode 100644 index 0000000000..996eaac799 --- /dev/null +++ b/docs/outstanding-issues-inbox/9da200a8-0ae0-451b-bef8-d1636b9e3cff.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "9da200a8-0ae0-451b-bef8-d1636b9e3cff", + "createdOn": "2026-08-24", + "action": "add", + "payload": { + "pri": "P3", + "type": "rec", + "summary": "Caring Contacts: postgres-repository.ts is ~2,080 lines and holds five self-contained clusters", + "detail": "src/lib/caring-contacts/db/postgres-repository.ts would split naturally into sibling modules under db/. Pure structure, no behaviour change; the file is the code half of row-level security so any split must keep every method's set_config/set local role preamble intact and be proved by the existing shared contract suite.", + "source": "docs/caring-contacts/phase-2a-build-record.md deferred list item 2", + "issueUlid": "01M0SA6SZ8EG4Q7WZK6W3X2Z0C" + } +} diff --git a/docs/outstanding-issues-inbox/a4b0610d-ef92-4d1b-bcc8-3da0eb646d5a.json b/docs/outstanding-issues-inbox/a4b0610d-ef92-4d1b-bcc8-3da0eb646d5a.json new file mode 100644 index 0000000000..dcbdff4b55 --- /dev/null +++ b/docs/outstanding-issues-inbox/a4b0610d-ef92-4d1b-bcc8-3da0eb646d5a.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "a4b0610d-ef92-4d1b-bcc8-3da0eb646d5a", + "createdOn": "2026-08-24", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Caring Contacts: four bare foreign keys onto plans/contacts predate the composite same-team rule", + "detail": "caring-contacts/supabase/migrations/0001_caring_contacts_foundation.sql lines 146, 172, 218 and 227 declare plan_id/contact_id references without the team_id composite that Rulings 25 and 27 later made mandatory, so a row written by one team can point at another team's plan or contact. Verified in the migration text 2026-08-24. Decide whether to add the composite keys by migration or record the exception; a bare key already caught one real cross-team defect.", + "source": "docs/caring-contacts/phase-2a-build-record.md (Rulings 25/27); Phase 2A deferred list", + "issueUlid": "01M0SA6SHX4VKAA1PFAVQY1B9A" + } +} diff --git a/docs/outstanding-issues-inbox/ba655ad0-9934-4885-a959-5efd375a9bf1.json b/docs/outstanding-issues-inbox/ba655ad0-9934-4885-a959-5efd375a9bf1.json new file mode 100644 index 0000000000..a240c7dc62 --- /dev/null +++ b/docs/outstanding-issues-inbox/ba655ad0-9934-4885-a959-5efd375a9bf1.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "ba655ad0-9934-4885-a959-5efd375a9bf1", + "createdOn": "2026-08-24", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "Caring Contacts: Ruling 60's 640-767px overlay-modality band is pinned on one side only", + "detail": "The frozen contract is authoritative below 640 and at 768 and above; the shared Sheet's own breakpoint is 640, and the overlay code hard-codes that 640 as a literal read from nothing. The regression pin catches a move in one breakpoint, not the other, so the band could widen and the test stay green. Reconciling the two breakpoints is a design decision.", + "source": "docs/caring-contacts/phase-2a-build-record.md Ruling 60", + "issueUlid": "01M0SA6TNM1CYR4V44KPSNXXGA" + } +} diff --git a/docs/outstanding-issues-inbox/cd97d402-7af1-473e-a2eb-dbac597cd03d.json b/docs/outstanding-issues-inbox/cd97d402-7af1-473e-a2eb-dbac597cd03d.json new file mode 100644 index 0000000000..90d775586e --- /dev/null +++ b/docs/outstanding-issues-inbox/cd97d402-7af1-473e-a2eb-dbac597cd03d.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "cd97d402-7af1-473e-a2eb-dbac597cd03d", + "createdOn": "2026-08-24", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "Caring Contacts: the frozen interaction matrix's 'Recovery action only' wording is ambiguous", + "detail": "Two rows (session-expiry and version-conflict) carry the prose 'Recovery action only', which reads either as recovery-action only or as recovery, action-only. Ruling 58 kept both rows faithful to the frozen record rather than reword them. Resolve the wording when the matrix is next unfrozen, not before.", + "source": "docs/caring-contacts/phase-2a-build-record.md Ruling 58; docs/caring-contacts/interaction-matrix.md", + "issueUlid": "01M0SA6TG3WP76E62MYHW82KPF" + } +} diff --git a/docs/outstanding-issues-inbox/fa126adf-ade8-4027-a029-d59b4bef8967.json b/docs/outstanding-issues-inbox/fa126adf-ade8-4027-a029-d59b4bef8967.json new file mode 100644 index 0000000000..534ce96466 --- /dev/null +++ b/docs/outstanding-issues-inbox/fa126adf-ade8-4027-a029-d59b4bef8967.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "fa126adf-ade8-4027-a029-d59b4bef8967", + "createdOn": "2026-08-24", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Caring Contacts: the same-team write serialisation is accidental and still unpinned", + "detail": "ensureTeam's insert ... on conflict do nothing is what makes the stopService race cross-team-only. After Task 11b fix round 2 it carries a comment naming it, but no test pins it, so a future change to that insert could widen the race silently. Either pin it with a test that names the serialisation or make it deliberate with a lock.", + "source": "docs/caring-contacts/phase-2a-build-record.md deferred list item 4", + "issueUlid": "01M0SA6SQ6XEGPCD8KR25KSSTR" + } +} From ac9dd3b5743349a3c3ae0e3942f9abff0c82bb31 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:35:22 +0800 Subject: [PATCH 02/51] docs(caring-contacts): correct the self-contradicting handoff, and record the mutation proofs The entry-point handoff said on line 7 that the branch had never been pushed and in section 4 that it was pushed to origin. Both were true when written and neither was updated when the other changed; both are now superseded by the merge. Records mutation proof A for the condensed bar: top-full -> top-0 turns 32/0 into 13 failed / 19 passed, and the 1440px failure is the pin assertion itself at line 877 (barBox.top 64 -> 0) with the two preceding assertions passing first, so the assertion is reached and discriminating rather than merely present. Also records the trap that nearly produced a false proof: the first mutation-B anchor matched two elements, a uniqueness assertion refused the edit, and the script ran the full gate anyway on an unmutated tree -- reporting 32 passed, exit 0. Read without the abort line that is a real, green, strongest-looking gate run supporting exactly the wrong conclusion. A mutation proof therefore has two results, not one: prove the mutation is in the tree before believing the gate. Co-Authored-By: Claude Opus 5 --- docs/caring-contacts/phase-2a-build-record.md | 74 +++++++++++++++++++ docs/caring-contacts/phase-2a-handoff.md | 27 +++++-- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/docs/caring-contacts/phase-2a-build-record.md b/docs/caring-contacts/phase-2a-build-record.md index fcf14f5ccc..6b420398ca 100644 --- a/docs/caring-contacts/phase-2a-build-record.md +++ b/docs/caring-contacts/phase-2a-build-record.md @@ -2745,3 +2745,77 @@ Two corrections to how that failure was read, both worth more than the incident: did not make the re-run unnecessary, and running it was still right — the difference between narrowing a hypothesis and confirming one. But **read which line the runner is actually naming before inferring what a failure means.** + + +## Closing item 2 — the condensed bar's mutation proofs + +### Mutation A — the 1440px pin assertion IS reachable and DOES discriminate + +`top-full` -> `top-0` on the condensed bar's className, one line, nothing else touched. The bar then +sits at the header's top edge instead of hanging off its bottom, which is precisely the "buried behind +the header" defect the pin exists to prevent. + +Result: **13 failed, 19 passed** (against 32/0 unmutated). The failure at 1440px, verbatim: + +``` +12) ui-caring-contacts-workspace.spec.ts:822:9 > pins the condensed bar under the header ... at 1440px + Error: the condensed bar is behind the header at 1440px + expect(received).toBeGreaterThanOrEqual(expected) + Expected: >= 64 + Received: 0 + > 877 | expect(geometry.barBox!.top, `the condensed bar is behind the header at ${width}px`) +``` + +Three things make this a proof rather than a red light: + +- **It is the pin assertion itself that failed** — line 877 — not an earlier one standing in for it. +- **The two assertions before it passed first**: the round-1 guard at 869 (`the banner is still on + screen ... nothing about the handover is being measured`) and `the condensed bar did not appear` at + 874. So the pin is REACHED, which is exactly what round 1's degenerate version was not. +- **The mutation moved a value the assertion reads**: `barBox.top` went 64 -> 0. A mutation that leaves + every asserted value unchanged proves nothing however red the suite goes, and three proposed proofs + on this branch already failed that test. + +The other twelve failures are honest collateral, not scope creep: the six `keeps the stop stated exactly +once at every scroll position` cases and the forced-colours case all read the bar's on-screen position, +and an unpinned bar changes it. Worth stating explicitly so a later reader does not treat thirteen +failures from a one-line change as evidence the mutation was too broad. + +### THE TRAP, and it is the most valuable thing in this section + +The first attempt at mutation B was written to swap the danger tokens for theme-invariant literals. Its +anchor, `bg-[color:var(--danger-bg)]`, appears **twice** in the file — once on the full banner and once +on the condensed bar. A uniqueness assertion caught it and refused to edit: + +``` +AssertionError: mutation B anchor not unique for bg-[color:var(--danger-bg)]: 2 +``` + +But the surrounding script was not written to stop there, and it **ran the whole browser gate anyway, +on a completely unmutated tree**. That run reported: + +``` +32 passed (52.7s) +EXIT=0 +``` + +**Read that the way it would have been read without the abort line: a mutation applied, the suite still +green, therefore the dark-mode assertion cannot fail and the test is worthless.** The conclusion would +have been exactly backwards, it would have been recorded against a test that is in fact fine, and the +"evidence" would have been a genuine 32-passed gate run — the strongest-looking kind. + +This is the same family as the two traps already recorded here — `npx playwright test` exiting 0 having +run nothing, and the EPERM lock failure that produces no summary line — but it is worse than either, +because there is no missing output to notice. The gate really ran, really passed, and really measured +the unmutated code. + +**The rule, and it is now a standing one for this programme: a mutation proof has TWO results, not one. +Prove the mutation is in the tree before believing anything the gate says about it.** Concretely: assert +the anchor is unique, assert the replacement is present after writing, print `git diff` of the mutated +file into the same log as the gate output, and refuse to launch the gate at all if any of that fails. +The corrected script does all four, which is why its log opens with the applied-mutation line and the +diff before a single test runs. + +A quieter lesson sits underneath it. The uniqueness assertion was cheap insurance added almost as an +afterthought, and it is the only reason this was caught. **Guard the mutation, not just the assertion** +— the thing being manipulated is as capable of silent failure as the thing being tested. diff --git a/docs/caring-contacts/phase-2a-handoff.md b/docs/caring-contacts/phase-2a-handoff.md index 6da37a0039..1ab51fafa1 100644 --- a/docs/caring-contacts/phase-2a-handoff.md +++ b/docs/caring-contacts/phase-2a-handoff.md @@ -4,8 +4,15 @@ new session, a new machine, or a new account. Everything below is either in this repository or named with an exact path on the workstation. -Written at head `6322017ce` on branch `claude/suicide-contact-mockup-b5aaa0`. Nothing has been pushed; there -is no pull request; the branch exists only locally. +> **SUPERSEDED IN ONE RESPECT, 2026-08-24: Phase 2A HAS MERGED and the branch named below is RETIRED.** +> It went in as `e4cbe8d3a` — "Claude/suicide contact mockup b5aaa0 (#2279)" — on 2026-08-23, and `main` +> is now the source of truth. Everything else in this document still stands. Work from a fresh worktree +> off `origin/main`; do not resurrect `claude/suicide-contact-mockup-b5aaa0`. +> +> Note also that this file used to contradict itself: the line here said the branch had never been pushed +> while §4 said it was pushed to origin. Both were written truthfully at different moments and neither was +> updated when the other changed. **A record that disagrees with itself is telling you its update +> discipline failed, not which half to believe** — check git. --- @@ -97,17 +104,23 @@ contains `*`), so it remains disposable by design. ## 4. Exactly where the work stopped -**Branch:** `claude/suicide-contact-mockup-b5aaa0` — **PUSHED to origin.** GitHub holds it; that is the -source of truth, not any directory on this workstation. +**Branch:** MERGED AND RETIRED as of 2026-08-23. `claude/suicide-contact-mockup-b5aaa0` was squash-merged +into `main` as `e4cbe8d3a` (#2279); every caring-contacts path on `main` matches the old branch tip, and no +local remote-tracking ref for the branch survives. **`main` is the source of truth**, not that branch and +not any directory on this workstation. -**Working copy:** make your own. Do not assume one exists: +**Working copy:** make your own, off `main`. Do not assume one exists: ``` cd D:\Repos\Database git fetch origin -git worktree add D:\Worktrees\Database\ claude/suicide-contact-mockup-b5aaa0 +git worktree add D:\Worktrees\Database\ -b claude/ origin/main +node scripts/setup-codex-worktree.mjs ``` +That last line matters: `npm ci` costs 15-58 minutes here, but on 2026-08-24 every `package-lock.json` on +this machine was byte-identical, so the setup script reused an existing `node_modules` in seconds. + **Working directories on this machine do not survive.** On 2026-08-21 four were destroyed by another process — under `.claude\worktrees\` **and** under `D:\Worktrees\`, one of them holding this exact work, and one through an explicit `git worktree lock`. **Relocating is not protection.** The `.git` pointer file @@ -116,7 +129,7 @@ No warning, and the cause is not identified. Commit often, **push after every ta needed to resume in a **tracked** file — git-ignored scratch dies with the directory. This branch survived a destruction today only because it had been pushed. -**Head at last push:** `32bfbdae5`. Nothing merged, no pull request. +**Head at last push:** `32bfbdae5` — historical. The phase has since merged; see the banner at the top. ### Done and reviewed clean From d0b2880ad068d9ec482ca7f80b7a75b93c323a59 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:40:13 +0800 Subject: [PATCH 03/51] docs(caring-contacts): both mutation proofs run, and the Phase 2B plan Mutation B (dark-mode colour): 1 failed / 31 passed. The single failure is the scheme-comparison at line 931 and names the injected literal, so it is attributable by value and not merely by timing; the display guard before it passed, so the assertion is reached. A blast radius matching the mutation's intent is itself evidence the assertion measures what it claims. With mutation A already proven, both closing proofs the final review recorded as UNRUN are discharged and the condensed bar's fix round is closed. Adds the Phase 2B implementation plan. It follows the owner's stated order and is grounded in a measured reading of what Phase 2A actually left: one real route, thirteen stub destinations, zero of twenty-four overlays wired to a trigger, no empty-state component, and no read API for patients, schedule or team. Two gaps the plan surfaces rather than hides: message templates have a full governance lifecycle but only ONE hard-coded message, so a template library cannot show per-version content that does not exist; and 'workload and coverage' has an approved design only at roster-table depth, which is the single most likely place the plan under-delivers against what the owner means. Co-Authored-By: Claude Opus 5 --- docs/caring-contacts/PROGRESS-LEDGER.md | 17 + docs/caring-contacts/phase-2a-build-record.md | 47 +++ .../phase-2a-continuation-prompt.md | 10 +- ...6-08-24-caring-contact-phase-2b-screens.md | 387 ++++++++++++++++++ 4 files changed, 458 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-24-caring-contact-phase-2b-screens.md diff --git a/docs/caring-contacts/PROGRESS-LEDGER.md b/docs/caring-contacts/PROGRESS-LEDGER.md index d73cca33b1..48b27859b9 100644 --- a/docs/caring-contacts/PROGRESS-LEDGER.md +++ b/docs/caring-contacts/PROGRESS-LEDGER.md @@ -140,6 +140,23 @@ a re-sent undelivered contact; a cross-team row leak; a duplicate active plan; a committed cross-team write; and a silently rewritable safety incident. **Four tests have been found unable to fail and rewritten.** +### 5c. The condensed bar's two mutation proofs — both run 2026-08-24, both discriminate + +| Mutation | Result | What it proves | +| ------------------------------------------------------------ | --------------------------- | -------------------------------------------------- | +| Pin: `top-full` -> `top-0` | 13 failed / 19 passed | The 1440px pin assertion is REACHED and fails at | +| | | line 877 on `barBox.top` 64 -> 0, with the two | +| | | preceding assertions passing first. | +| Dark colours: danger tokens -> fixed light-theme literals | **1 failed** / 31 passed | Only the dark-scheme comparison reddens, at line | +| | | 931, naming the injected literal. | + +**And the trap that nearly produced a false proof.** The first attempt at the colour mutation used an +anchor matching TWO elements. A uniqueness assertion refused the edit — but the script ran the whole +browser gate anyway, on an unmutated tree, and reported `32 passed`, exit 0. Read without the abort +line, that is a real, green, strongest-looking gate run supporting exactly the opposite conclusion. +**A mutation proof has two results, not one: prove the mutation is in the tree before believing the +gate.** Full account in the build record. + ## 6. Decisions taken on the owner's behalf **Phase 1 — 13 numbered decisions**, each with why and cost-if-wrong: `phase-1-handoff.md` §"Decisions". diff --git a/docs/caring-contacts/phase-2a-build-record.md b/docs/caring-contacts/phase-2a-build-record.md index 6b420398ca..4fee428f32 100644 --- a/docs/caring-contacts/phase-2a-build-record.md +++ b/docs/caring-contacts/phase-2a-build-record.md @@ -2819,3 +2819,50 @@ diff before a single test runs. A quieter lesson sits underneath it. The uniqueness assertion was cheap insurance added almost as an afterthought, and it is the only reason this was caught. **Guard the mutation, not just the assertion** — the thing being manipulated is as capable of silent failure as the thing being tested. + +### Mutation B — the dark-mode colour assertion IS falsifiable, and reddens ONLY itself + +The corrected mutation swaps the condensed bar's `--danger-bg` / `--danger-text` for fixed literals +carrying the light theme's own values, so the bar renders identically in both schemes. This is exactly +the mutation the test's own comment names: "swapping `--danger-text` for a token whose value is +identical in both themes reddens this and would have sailed through the old assertion." + +No theme-invariant token exists to swap in — every colour token in `globals.css` that is defined before +the dark block is also redefined inside it. That is a good property of the design system and it is why +the mutation uses literals; recorded so nobody later reads the literals as sloppiness. + +Anchoring had to be precise, because `bg-[color:var(--danger-bg)]` matches the full banner too. The +unique anchors are the longer runs `bg-[color:var(--danger-bg)] px-4 py-2 text-sm font-semibold` and +`text-sm font-semibold text-[color:var(--danger-text)] data-[full-banner-out-of-view=true]:flex`, each +appearing exactly once, and the script asserts that count before writing. + +Result: **1 failed, 31 passed.** The single failure, verbatim: + +``` +31) ui-caring-contacts-workspace.spec.ts:913:7 > re-resolves the condensed bar's own colours in dark + Error: the condensed bar's ink did not change in dark + expect(received).not.toBe(expected) + Expected: not "rgb(163, 25, 15)" + > 931 | expect(dark.colour, "the condensed bar's ink did not change in dark").not.toBe(light.colour) +``` + +Everything a proof needs is in those four lines: + +- **Exactly one test reddened**, and it is the intended one. Mutation A's thirteen failures were honest + collateral from moving the bar; this one changes only colour, and only the colour assertion notices. + A mutation whose blast radius matches its intent is itself evidence the assertion is measuring what + it claims. +- **The failing assertion is the scheme-comparison at line 931**, not the `display !== "none"` guard at + 924 — which passed first, so the assertion is reached. +- **`Expected: not "rgb(163, 25, 15)"` is the literal the mutation injected.** The failure is + attributable to the mutation by value, not merely by timing. + +The `surface` assertion on the next line never ran, because `expect` throws on the first failure. That +is not a gap: the test is proven able to fail, which is the claim. Proving the second assertion +independently would need its own mutation, and nothing depends on it that the first does not already +establish. + +### Both closing proofs are now run, and the earlier "unrun" record is discharged + +The final review recorded these two as **unrun, not passed**, and blocked on the browser gate. The gate +is green, both are run, and both discriminate. The condensed pinned safety bar's fix round is closed. diff --git a/docs/caring-contacts/phase-2a-continuation-prompt.md b/docs/caring-contacts/phase-2a-continuation-prompt.md index 2450aaa530..e54f4a1f6e 100644 --- a/docs/caring-contacts/phase-2a-continuation-prompt.md +++ b/docs/caring-contacts/phase-2a-continuation-prompt.md @@ -99,9 +99,13 @@ OPEN, in the order I would take them: only correct invocation: `npx playwright test` refuses with an `Error:` line and EXIT CODE 0, so it reads as a pass having run nothing. Redirect the whole log to a file; never pipe a gate through `tail`. Expect 32 tests, not 33. - 2. TWO MUTATION PROOFS — the condensed bar's pin assertion at 1440px, and its dark-mode colour - assertion. See the build record's 2026-08-24 section for their result; if that section is - absent they are still unrun and must be reported as unrun, never as passed. + 2. DONE 2026-08-24 — BOTH MUTATION PROOFS RUN, and both discriminate. Pin (`top-full` -> `top-0`): + 13 failed / 19 passed, failing at line 877 on `barBox.top` 64 -> 0 with the two preceding + assertions passing first, so the 1440px pin is REACHED. Dark colours (danger tokens -> fixed + literals): exactly 1 failed / 31 passed, at line 931, naming the injected literal — no + collateral at all. The condensed bar's fix round is CLOSED. + Read the build record's account of the near-miss before writing your own mutation proof: the + first colour mutation silently failed to apply and its gate reported `32 passed`, exit 0. 3. THE COPY DECISIONS await the owner. `docs/caring-contacts/copy-decisions-recommended.md` (new, 2026-08-24) carries a recommendation, a reason and a cost-if-wrong for each. Note the count: this prompt and the ledger both said SEVEN, but `copy-review.md` actually raises diff --git a/docs/superpowers/plans/2026-08-24-caring-contact-phase-2b-screens.md b/docs/superpowers/plans/2026-08-24-caring-contact-phase-2b-screens.md new file mode 100644 index 0000000000..b89cccade1 --- /dev/null +++ b/docs/superpowers/plans/2026-08-24-caring-contact-phase-2b-screens.md @@ -0,0 +1,387 @@ +# Caring Contacts Phase 2B — The Working Screens: Implementation Plan + +**Status: DRAFT, not yet approved for execution.** Written 2026-08-24. + +**Spec:** `docs/superpowers/specs/2026-08-19-caring-contact-production-build-design.md` — binding. +Every conflict in this plan resolves **against the spec**, not against the mockups and not against +this document. + +**Phase 2A is complete and merged** to `main` as `e4cbe8d3a` (#2279). This plan consumes it. + +--- + +## 0. What Phase 2B is, and what Phase 2A actually left + +Spec §0 defines Phase 2 as "the working screens". Phase 2A built the foundations underneath them. +Establishing precisely what exists is the whole basis of this plan, so it was measured rather than +assumed (2026-08-24, by direct reading of the tree): + +| What exists | What that means here | +| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | +| **One real route**: `/caring-contacts`, a Server Component with a placeholder body | Thirteen further destinations are declared in `caring-contacts-routes.ts` and | +| | rendered as `UnavailableDestination` stubs. They are not links. | +| **The shell** — nav rail, phone dock, header, four width states, service-state banner | The extension backbone. A screen becomes real by gaining an `href` in | +| | `PRIMARY_DESTINATIONS` / `MORE_DESTINATIONS` in `shell.tsx`. | +| **24 overlays, fully defined and rendered by one data-driven renderer** | **Zero of the 24 are wired to a real trigger.** Every one is reachable only by | +| | typing `?overlay=`. Wiring them to controls is core Phase 2B work. | +| **11 API routes** on shared `readHandler` / `writeHandler` factories | Audit-on-every-read and idempotency-key-on-every-write come free. But there is no | +| | backing route for patients-list, schedule, workload, coverage, team, or | +| | reconciliation — those must be added. | +| **A sealed domain of ~27 modules and a 38-method repository**, two stores, one shared | The rules are done. A screen must never re-derive a rule a module already owns. | +| contract | | +| **Route-level loading and error states only** | Generic and not data-driven. **No empty-state component exists anywhere.** | + +So Phase 2B is not "add some pages". It is: turn one placeholder route into thirteen working screens, +give twenty-four already-built overlays their triggers, and add the read surfaces four of those screens +need. + +### The owner's order, which this plan follows + +1. **Patients and their plans** +2. **Schedule and what is due** +3. **Message templates** +4. **Team, workload and coverage** + +### Deliberately NOT in Phase 2B + +Phase 3 owns the demo clock, the synthetic caseload, training mode, and the bounded clinical-record +summary (spec §0). Guidance and Reports are approved screens but sit outside the owner's four groups; +they are scheduled last, after the four, and may be deferred to Phase 3 without blocking anything. + +--- + +## Global Constraints + +Every task's requirements implicitly include this section. + +### Absolutely out of scope + +- **No message is sent to any number, real or test.** No SMS provider, no adapter beyond a + deterministic fake. +- **No migration against the Clinical KB Supabase project `sjrfecxgysukkwxsowpy`.** Caring-contact + migrations live **only** in `caring-contacts/supabase/migrations/`, never in `supabase/migrations/`. +- No hosting change, no hospital system connection, no enterprise sign-on, no real patient data. +- Sign-in stays a **demo role switcher**, never credentials. +- **Do not push, do not open a pull request, and do not run `verify:release` or any provider-backed + gate** (`eval:*`, `check:supabase-project`, `test:live`) without asking the owner first. + +### Patient-visible copy is FROZEN until the owner answers + +`docs/caring-contacts/copy-decisions-recommended.md` carries thirteen open decisions — nine clinical +or policy, four engineering. **No task in this plan may change a patient-visible string.** Screens +render patient-visible copy by reading the sealed domain's `message-copy` module; a screen that +hardcodes a patient-visible string is a defect even when the string is correct, because it puts the +owner's eventual answer in two places. + +Four of the thirteen (B1–B4 in that file) need no clinical input and may proceed once he says go. + +### Design non-regression (spec §6) — frozen, may not change without a recorded decision + +1. The screen and overlay inventory of spec §4. +2. The 24-row modality and dismissal decisions in `docs/caring-contacts/interaction-matrix.md`. +3. The width-to-state mapping `compact` (320–430) / `rail` (768) / `split` (1024) / `wide` (1440). +4. The closed transport vocabulary and every prohibited clinical term. +5. Token usage: no hardcoded colour, no new colour semantics, no decorative clinical colour. +6. The continuity thread's meaning — elapsed schedule spacing only, never patient, delivery or + clinical state. + +**No existing assertion may be deleted or loosened to accommodate a change.** Any test that goes red +is a defect in the change, not in the test. + +### The four design corrections — implement the SPEC, not the mockup + +`docs/caring-contacts/phase-2a-visual-differences.md` records four places where the approved mockups +are out of date against later decisions. Each is an **edit to an existing design, not new design**, and +each resolves **to the spec**: + +| # | The mockup shows | The spec requires | Lands in | +| --- | ------------------------------------------------------------ | ------------------------------------------------------------- | ---------------- | +| 1 | The old first-contact-date control | §2.3 — the coordinator sets the first contact date | Group 1, Task 6 | +| 2 | "replies are not received, stored, analysed or monitored" | §2.1 — the automated non-monitored response copy | Group 3, Task 11 | +| 3 | No distinct closing message type at month 12 | §2.2 — the pathway ends with a closing message | Groups 1 and 2 | +| 4 | Ten sendable contacts | Phase 1 decision 1 — **nine** sendable contacts | Groups 1 and 2 | + +Corrections 3 and 4 travel together and touch both the activation review and every schedule display. + +### Repository contracts that fail the build + +- **Button wiring.** Every ` + ); +} diff --git a/src/components/caring-contacts/workspace/overlays/workspace-overlays.tsx b/src/components/caring-contacts/workspace/overlays/workspace-overlays.tsx index 349571863e..de3867e254 100644 --- a/src/components/caring-contacts/workspace/overlays/workspace-overlays.tsx +++ b/src/components/caring-contacts/workspace/overlays/workspace-overlays.tsx @@ -2,6 +2,17 @@ import { useCallback, useSyncExternalStore } from "react"; +import type { WorkspaceOverlayDefinition } from "./definitions"; +import { + clearStagedWorkspaceOverlayCommit, + commitForOpenOverlay, + commitUnavailableReasonFor, + noStagedWorkspaceOverlayCommit, + readStagedWorkspaceOverlayCommit, + stageWorkspaceOverlayCommit, + subscribeToStagedWorkspaceOverlayCommit, + type WorkspaceOverlayCommit, +} from "./overlay-commits"; import { OverlayHost } from "./overlay-host"; /** @@ -131,25 +142,81 @@ export function closeWorkspaceOverlay() { window.dispatchEvent(new Event(OVERLAY_URL_CHANGED_EVENT)); } +/** + * Opens an overlay AND states what confirming it does, in that order. + * + * This is the only way a control in the workspace opens an overlay: `commit` is + * required, so a control cannot raise a decision surface it has not wired + * (Ruling 87). `openWorkspaceOverlay` above stays available unchanged for the + * URL-only case its own tests cover, and is deliberately NOT the trigger's route. + * + * Staging first is the ordering `overlay-commits.ts` documents: both writes are + * synchronous, so the host's first render carrying the new id already carries its + * commit and never passes through a frame where the overlay is open with nothing + * staged. + */ +export function openWorkspaceOverlayWithCommit(id: string, commit: WorkspaceOverlayCommit) { + stageWorkspaceOverlayCommit(id, commit); + openWorkspaceOverlay(id); +} + export function WorkspaceOverlays() { const openOverlayId = useSyncExternalStore(subscribeToOverlayParam, readOverlayParam, noOverlayParam); + const slot = useSyncExternalStore( + subscribeToStagedWorkspaceOverlayCommit, + readStagedWorkspaceOverlayCommit, + noStagedWorkspaceOverlayCommit, + ); + + /** + * What the control that opened THIS overlay said confirming it does — or null, + * when nothing was staged for it and the decision therefore cannot be recorded. + */ + const commit = commitForOpenOverlay(slot, openOverlayId); + const commitUnavailableReason = commitUnavailableReasonFor(commit); const close = useCallback(() => { + // A staged commit belongs to one open/close cycle. Clearing it here is what + // stops a forward traversal re-entering a dismissed overlay with a live + // confirm control the person never re-authorised. + clearStagedWorkspaceOverlayCommit(); closeWorkspaceOverlay(); }, []); /** - * Confirming an overlay closes it, and records nothing yet. + * Confirming an overlay records the decision the opening control stated, then + * closes it. * - * Stated plainly rather than left to look finished: the screens that raise - * these overlays, and the stores their decisions are written to, are later - * tasks. Nothing in the workspace opens an overlay yet either — `?overlay=` - * is reachable only by typing it — so no control in the interface currently - * advertises an action this does not perform. + * The throw is not defensive padding. `commitUnavailableReason` is handed to the + * host, which refuses the action whenever it is non-null, so reaching here with + * no recordable commit means the refusal was not applied — and the failure that + * would otherwise follow is silent: a confirm control that appears to work and + * writes nothing, which is precisely the defect Ruling 87 exists to prevent. + * Loud is the conservative direction; nothing has been recorded at this point. */ - const commit = useCallback(() => { - closeWorkspaceOverlay(); - }, []); - - return ; + const recordDecision = useCallback( + (definition: WorkspaceOverlayDefinition) => { + if (commit === null || commit.kind !== "record") { + throw new Error( + `The overlay "${definition.id}" was confirmed with no recordable commit staged for it. ` + + `A control must open an overlay through openWorkspaceOverlayWithCommit, and the host must ` + + `refuse the action whenever commitUnavailableReason is set (Ruling 87).`, + ); + } + commit.record(definition.id); + clearStagedWorkspaceOverlayCommit(); + closeWorkspaceOverlay(); + }, + [commit], + ); + + return ( + + ); } diff --git a/tests/caring-contacts-explained-automation.dom.test.tsx b/tests/caring-contacts-explained-automation.dom.test.tsx index cbecf61de6..6827a5c068 100644 --- a/tests/caring-contacts-explained-automation.dom.test.tsx +++ b/tests/caring-contacts-explained-automation.dom.test.tsx @@ -248,6 +248,14 @@ const ALLOWED_CLIENT_COMPONENTS = [ // props cannot cross a Server → Client boundary. It takes no props at all, which is // what keeps the service-state record on the server side of this seam. "overlays/workspace-overlays.tsx", + // Task 3's control: the button a screen renders to raise one of the 24 overlays. A click + // handler is by definition a client capability, so this cannot be a Server Component. + // Added on the same three conditions as the entries above: its props are an overlay id, a + // class name, children, and a `WorkspaceOverlayCommit` — an intent union of a callback and + // a plain-words reason string, never a state object and nothing derived from the record; + // the companion test below proves its source and everything it reaches never name that + // module or type; and it is here deliberately rather than to clear a red test. + "overlays/overlay-trigger.tsx", // Decides WHEN the condensed stop bar is shown, and never what it says. A scroll position // and two element rectangles are browser facts, so this one cannot be answered on the // server — and the header is not the height of its token (87.5px at 320/390, 65px above, diff --git a/tests/caring-contacts-overlay-host.dom.test.tsx b/tests/caring-contacts-overlay-host.dom.test.tsx index 2600792003..954a367d0f 100644 --- a/tests/caring-contacts-overlay-host.dom.test.tsx +++ b/tests/caring-contacts-overlay-host.dom.test.tsx @@ -62,6 +62,7 @@ function OverlayHarness({ overlayId }: { overlayId: string }) { onClose={() => setOpenOverlayId(null)} onCommit={noop} blockReason={null} + commitUnavailableReason={null} /> ); @@ -76,7 +77,7 @@ describe("the overlay host", () => { ] as const) { setViewportWidth(width); const { unmount } = render( - , + , ); const body = screen.getByTestId("workspace-overlay-content"); expect(body, `${definition.id} at ${width}px`).toHaveAttribute("data-overlay-id", definition.id); @@ -110,6 +111,7 @@ describe("the overlay host", () => { onClose={noop} onCommit={noop} blockReason="permission-unavailable" + commitUnavailableReason={null} />, ); expect( @@ -119,7 +121,7 @@ describe("the overlay host", () => { refused.unmount(); const live = render( - , + , ); if (definition.requiresFreshAuthentication) { await userEvent.click(screen.getByTestId("workspace-overlay-action")); @@ -243,7 +245,7 @@ describe("the overlay host", () => { (definition) => definition.phoneModality === "bottom-sheet", ); expect(bottomSheetRow, "the frozen table no longer has a bottom-sheet row").toBeDefined(); - render(); + render(); expect(screen.getByTestId("workspace-overlay-content")).toHaveAttribute("data-overlay-modality", "bottom-sheet"); }); @@ -263,7 +265,7 @@ describe("the overlay host", () => { it("keeps the session gate open through Escape", async () => { setViewportWidth(1440); const onClose = vi.fn(); - render(); + render(); await userEvent.keyboard("{Escape}"); // The `onClose` assertion is the discriminating one, and it is the only one here. // A second line asserting the content is still in the document was REMOVED (Minor 5, @@ -282,7 +284,7 @@ describe("the overlay host", () => { // the opening focus lands on `document.body` -- on the ONE overlay a person cannot dismiss and // must act on, which a screen reader announces as nothing having happened at all. setViewportWidth(1440); - render(); + render(); const action = screen.getByTestId("workspace-overlay-action"); await waitFor(() => expect(action).toHaveFocus()); @@ -291,7 +293,7 @@ describe("the overlay host", () => { it("never traps focus in the offline status banner", () => { setViewportWidth(1440); - render(); + render(); expect(screen.getByRole("status")).toBeInTheDocument(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); @@ -299,7 +301,7 @@ describe("the overlay host", () => { it("commits a withdrawal only on the second activation", async () => { setViewportWidth(1440); const onCommit = vi.fn(); - render(); + render(); await userEvent.click(screen.getByRole("button", { name: /withdraw/i })); expect(onCommit).not.toHaveBeenCalled(); expect(screen.getByText(/fresh authentication checkpoint/i)).toBeInTheDocument(); @@ -311,7 +313,7 @@ describe("the overlay host", () => { setViewportWidth(1440); const onCommit = vi.fn(); render( - , + , ); const action = screen.getByRole("button", { name: /pause/i }); expect(action).toHaveAttribute("aria-disabled", "true"); @@ -332,6 +334,7 @@ describe("the overlay host", () => { onClose={noop} onCommit={readOnlyCommit} blockReason="permission-unavailable" + commitUnavailableReason={null} />, ); expect(screen.getByRole("button", { name: /close/i })).not.toHaveAttribute("aria-disabled"); diff --git a/tests/caring-contacts-overlay-trigger.dom.test.tsx b/tests/caring-contacts-overlay-trigger.dom.test.tsx new file mode 100644 index 0000000000..62ddfe96f6 --- /dev/null +++ b/tests/caring-contacts-overlay-trigger.dom.test.tsx @@ -0,0 +1,256 @@ +import { act, cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { WORKSPACE_OVERLAY_DEFINITIONS } from "@/components/caring-contacts/workspace/overlays/definitions"; +import { + clearStagedWorkspaceOverlayCommit, + commitForOpenOverlay, + commitUnavailableReasonFor, + NO_STAGED_COMMIT_REASON, + readStagedWorkspaceOverlayCommit, + stageWorkspaceOverlayCommit, +} from "@/components/caring-contacts/workspace/overlays/overlay-commits"; +import { WorkspaceOverlayTrigger } from "@/components/caring-contacts/workspace/overlays/overlay-trigger"; +import { + openWorkspaceOverlay, + WorkspaceOverlays, +} from "@/components/caring-contacts/workspace/overlays/workspace-overlays"; + +import { CARING_CONTACTS_PROHIBITED_LANGUAGE } from "./helpers/caring-contacts-prohibited-language"; + +/** + * Task 3: the control that raises an overlay, and the commit contract that had to + * ship with it. + * + * Ruling 87 is the whole reason this file is not simply "a button opens a panel". + * The 24 overlays are decision surfaces, every one of them renders a confirm + * control, and until this trigger existed none of them was reachable from any + * control — which is the only reason a confirm that recorded nothing was + * tolerable. So the assertions below come in two halves: the trigger opens what it + * names, and NOTHING it opens offers a confirm control the system will not honour. + */ + +/** jsdom reports a fixed 1024px viewport; the host needs a width to choose a modality at all. */ +function setViewportWidth(width: number) { + Object.defineProperty(window, "innerWidth", { configurable: true, value: width }); + window.dispatchEvent(new Event("resize")); +} + +const WORKSPACE_PATH = "/caring-contacts"; +/** A distinguishable prior entry, so what Back lands on is unambiguous. */ +const PRIOR_PATH = "/caring-contacts/somewhere-before"; + +function seedHistory() { + window.history.pushState(null, "", `${PRIOR_PATH}?marker=before`); + window.history.pushState(null, "", WORKSPACE_PATH); +} + +/** + * The staged commit lives in a module-scoped slot, so it outlives a render the way + * the browser tab does. Emptying it between tests is what stops one test's staged + * intent silently satisfying the next test's assertion. + */ +beforeEach(() => { + clearStagedWorkspaceOverlayCommit(); + setViewportWidth(1440); + seedHistory(); +}); + +afterEach(() => { + cleanup(); + clearStagedWorkspaceOverlayCommit(); +}); + +function contentFor(overlayId: string) { + return document.querySelector(`[data-testid="workspace-overlay-content"][data-overlay-id="${overlayId}"]`); +} + +describe("the overlay trigger", () => { + it("opens the overlay it names, and Back closes it", async () => { + render( + <> + {} }}> + Pause this plan + + + , + ); + + await userEvent.click(screen.getByRole("button", { name: "Pause this plan" })); + expect(contentFor("pause")).not.toBeNull(); + expect(window.location.search).toContain("overlay=pause"); + + act(() => window.history.back()); + await waitFor(() => expect(contentFor("pause")).toBeNull()); + expect(window.location.search).not.toContain("overlay="); + }); + + it("fails loudly for an id the frozen table does not carry, rather than opening nothing", () => { + // A control that opens an empty overlay is the silent version of exactly the + // defect the commit contract exists to prevent, so it throws at render. + expect(() => + render( + {} }}> + Pause this plan + , + ), + ).toThrow(/No overlay is defined for the id "pause-plan"/); + }); + + it("cannot be constructed without a commit", () => { + // A type-level guarantee checked by `tsc --noEmit`, not at runtime: `commit` + // is required, so a screen that opens an overlay it has not wired fails to + // compile. `@ts-expect-error` fails the typecheck if the error ever stops + // being raised — which is what makes this a proof rather than a comment. + const withoutCommit = ( + // @ts-expect-error `commit` is required — an overlay cannot be opened unwired. + Pause this plan + ); + expect(withoutCommit).toBeTruthy(); + }); + + it("carries the workspace's 48px tap floor", () => { + render( + + Pause this plan + , + ); + // `min-h-tap` is `--spacing-tap` (3rem). Never `min-h-11`: 44px reintroduces a + // known `ui-smoke` sub-pixel flake. + expect(screen.getByRole("button", { name: "Pause this plan" }).className).toContain("min-h-tap"); + }); +}); + +describe("the commit contract", () => { + it("records the screen's decision through the host mounted by the shell", async () => { + const record = vi.fn(); + render( + <> + + Pause this plan + + + , + ); + + await userEvent.click(screen.getByRole("button", { name: "Pause this plan" })); + const action = screen.getByTestId("workspace-overlay-action"); + expect(action).not.toHaveAttribute("aria-disabled"); + + await userEvent.click(action); + // The overlay the trigger named, not whatever happened to be open. + expect(record).toHaveBeenCalledTimes(1); + expect(record).toHaveBeenCalledWith("pause"); + + await waitFor(() => expect(contentFor("pause")).toBeNull()); + // The intent is spent, so a forward traversal cannot re-enter it live. + expect(readStagedWorkspaceOverlayCommit()).toBeNull(); + }); + + it("still runs the fresh-authentication checkpoint before recording", async () => { + const record = vi.fn(); + render( + <> + + Withdraw this patient + + + , + ); + + await userEvent.click(screen.getByRole("button", { name: "Withdraw this patient" })); + await userEvent.click(screen.getByTestId("workspace-overlay-action")); + expect(record, "the first activation raised the checkpoint; it must record nothing").not.toHaveBeenCalled(); + expect(screen.getByText(/fresh authentication checkpoint/i)).toBeInTheDocument(); + + await userEvent.click(screen.getByTestId("workspace-overlay-action")); + expect(record).toHaveBeenCalledTimes(1); + }); + + it("refuses the decision in the aria-disabled shape when the caller states it is unavailable", async () => { + const reason = "Pausing a plan is not built yet, so nothing can be changed from here."; + render( + <> + + Pause this plan + + + , + ); + + await userEvent.click(screen.getByRole("button", { name: "Pause this plan" })); + const action = screen.getByTestId("workspace-overlay-action"); + + expect(action).toHaveAttribute("aria-disabled", "true"); + // Never both: native `disabled` removes the tab stop, so the stated reason + // could never be reached by keyboard, and lint fails on the pair. + expect(action).not.toHaveAttribute("disabled"); + + // Reachable, and NOT in a `title`: a title is reached by hover and may never + // be announced at all. + expect(action).not.toHaveAttribute("title"); + const describedBy = action.getAttribute("aria-describedby"); + expect(describedBy, "the refused control points at no reason").not.toBeNull(); + expect(document.getElementById(describedBy!)?.textContent).toBe(reason); + + await userEvent.click(action); + expect(contentFor("pause"), "an inert action must not close the overlay either").not.toBeNull(); + }); + + it("refuses a read-only overlay's action too when its decision is unwired", async () => { + const readOnly = WORKSPACE_OVERLAY_DEFINITIONS.find((definition) => !definition.mutatesState); + expect(readOnly, "the frozen table carries at least one read-only row").toBeDefined(); + const reason = "This preview is not wired to a screen yet."; + render( + <> + + Open the preview + + + , + ); + + await userEvent.click(screen.getByRole("button", { name: "Open the preview" })); + // A permission refusal deliberately leaves a read-only overlay usable; an + // UNWIRED decision must not, because its action is just as dead as a mutating + // row's would be. + expect(screen.getByTestId("workspace-overlay-action")).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByText(reason)).toBeInTheDocument(); + }); + + it("refuses an overlay reached by address rather than from a control", async () => { + render(); + act(() => openWorkspaceOverlay("pause")); + await screen.findByTestId("workspace-overlay-content"); + + const action = screen.getByTestId("workspace-overlay-action"); + expect(action).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByText(NO_STAGED_COMMIT_REASON)).toBeInTheDocument(); + }); + + it("never offers one overlay's staged commit to another", () => { + const commit = { kind: "record", record: () => {} } as const; + stageWorkspaceOverlayCommit("pause", commit); + const slot = readStagedWorkspaceOverlayCommit(); + + expect(commitForOpenOverlay(slot, "pause")).toBe(commit); + // The identity check is the safeguard, not a formality: a slot left over from + // one overlay must never satisfy a different one. + expect(commitForOpenOverlay(slot, "withdrawal")).toBeNull(); + expect(commitForOpenOverlay(slot, null)).toBeNull(); + expect(commitForOpenOverlay(null, "pause")).toBeNull(); + }); + + it("answers every state of the slot, and refuses in two of the three", () => { + // Total by construction, so the rule can be read here rather than inferred + // from a rendered button. + expect(commitUnavailableReasonFor(null)).toBe(NO_STAGED_COMMIT_REASON); + expect(commitUnavailableReasonFor({ kind: "unavailable", reason: "Not built yet." })).toBe("Not built yet."); + expect(commitUnavailableReasonFor({ kind: "record", record: () => {} })).toBeNull(); + }); + + it("states the unstaged refusal in permitted vocabulary", () => { + expect(NO_STAGED_COMMIT_REASON).not.toMatch(CARING_CONTACTS_PROHIBITED_LANGUAGE); + }); +}); From f518734ed22572e8af9f68bcca7362cf644d8897 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:25:22 +0800 Subject: [PATCH 35/51] test(caring-contacts): Task 3 -- trigger and commit-contract proofs Covers: the trigger opens what it names and Back closes it; an id no frozen row carries throws at render; `commit` is required (`@ts-expect-error`, enforced by tsc); the record path reaches the shell-mounted host through the fresh-authentication checkpoint; the unavailable path renders the aria-disabled shape with its reason reachable via aria-describedby and not in a title; a read-only row is refused too; an overlay reached by address is refused; and one overlay's staged commit is never offered to another. Co-Authored-By: Claude Opus 5 --- .../caring-contacts-overlay-host.dom.test.tsx | 74 +++++++++++++++++-- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/tests/caring-contacts-overlay-host.dom.test.tsx b/tests/caring-contacts-overlay-host.dom.test.tsx index 954a367d0f..a18794efc4 100644 --- a/tests/caring-contacts-overlay-host.dom.test.tsx +++ b/tests/caring-contacts-overlay-host.dom.test.tsx @@ -77,7 +77,13 @@ describe("the overlay host", () => { ] as const) { setViewportWidth(width); const { unmount } = render( - , + , ); const body = screen.getByTestId("workspace-overlay-content"); expect(body, `${definition.id} at ${width}px`).toHaveAttribute("data-overlay-id", definition.id); @@ -121,7 +127,13 @@ describe("the overlay host", () => { refused.unmount(); const live = render( - , + , ); if (definition.requiresFreshAuthentication) { await userEvent.click(screen.getByTestId("workspace-overlay-action")); @@ -245,7 +257,15 @@ describe("the overlay host", () => { (definition) => definition.phoneModality === "bottom-sheet", ); expect(bottomSheetRow, "the frozen table no longer has a bottom-sheet row").toBeDefined(); - render(); + render( + , + ); expect(screen.getByTestId("workspace-overlay-content")).toHaveAttribute("data-overlay-modality", "bottom-sheet"); }); @@ -265,7 +285,15 @@ describe("the overlay host", () => { it("keeps the session gate open through Escape", async () => { setViewportWidth(1440); const onClose = vi.fn(); - render(); + render( + , + ); await userEvent.keyboard("{Escape}"); // The `onClose` assertion is the discriminating one, and it is the only one here. // A second line asserting the content is still in the document was REMOVED (Minor 5, @@ -284,7 +312,15 @@ describe("the overlay host", () => { // the opening focus lands on `document.body` -- on the ONE overlay a person cannot dismiss and // must act on, which a screen reader announces as nothing having happened at all. setViewportWidth(1440); - render(); + render( + , + ); const action = screen.getByTestId("workspace-overlay-action"); await waitFor(() => expect(action).toHaveFocus()); @@ -293,7 +329,15 @@ describe("the overlay host", () => { it("never traps focus in the offline status banner", () => { setViewportWidth(1440); - render(); + render( + , + ); expect(screen.getByRole("status")).toBeInTheDocument(); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); @@ -301,7 +345,15 @@ describe("the overlay host", () => { it("commits a withdrawal only on the second activation", async () => { setViewportWidth(1440); const onCommit = vi.fn(); - render(); + render( + , + ); await userEvent.click(screen.getByRole("button", { name: /withdraw/i })); expect(onCommit).not.toHaveBeenCalled(); expect(screen.getByText(/fresh authentication checkpoint/i)).toBeInTheDocument(); @@ -313,7 +365,13 @@ describe("the overlay host", () => { setViewportWidth(1440); const onCommit = vi.fn(); render( - , + , ); const action = screen.getByRole("button", { name: /pause/i }); expect(action).toHaveAttribute("aria-disabled", "true"); From 9a36d292e80e55b1c23fb117f3cee01230a4c18d Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:42:56 +0800 Subject: [PATCH 36/51] docs(caring-contacts): Task 3 report -- the handoff decision, its rejects, and the gates Records the architectural choice (single-slot commit handoff staged at the moment of opening), the three rejected alternatives with the reason each fails, the deep-link refusal as the change with the widest blast radius, six mutation proofs, and the gates -- including the one that did not run. Co-Authored-By: Claude Opus 5 --- .../phase-2b-sdd-archive/task-3-report.md | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 docs/caring-contacts/phase-2b-sdd-archive/task-3-report.md diff --git a/docs/caring-contacts/phase-2b-sdd-archive/task-3-report.md b/docs/caring-contacts/phase-2b-sdd-archive/task-3-report.md new file mode 100644 index 0000000000..b38f643faa --- /dev/null +++ b/docs/caring-contacts/phase-2b-sdd-archive/task-3-report.md @@ -0,0 +1,292 @@ +# Task 3 report — the overlay trigger, and the commit contract that shipped with it + +**Branch** `claude/browser-test-gate-handoff-d5c1db`, base `f65dd39d3`. +**Status: DONE.** Gates below. + +--- + +## 1. The architectural decision, and what was rejected + +This is the part of the task that carried a real choice, so it is first. + +### The problem + +`WorkspaceOverlays` is mounted **once, by the shell**, as a **sibling** of `children`. A screen's +confirm behaviour therefore has no shared parent with the host and no prop path to it. + +A second constraint narrows the space much further than the brief's framing implies, and it is the +one that decided the shape: **the screens are Server Components.** A Server Component cannot pass a +closure to a Client Component at all — props crossing that boundary must be serialisable (Next 16, +"Server and Client Components"). So "the screen's commit handler" can never be an arbitrary +callback the screen wrote. It can only be a serialisable value, or a Server Action reference. + +That is why `WorkspaceOverlayCommit` is a **discriminated union of intents**, not a function type: + +```ts +export type WorkspaceOverlayCommit = + | { readonly kind: "record"; readonly record: (overlayId: string) => void } + | { readonly kind: "unavailable"; readonly reason: string }; +``` + +`record` is a function _position_, satisfied from a Server Component by a Server Action and from a +Client Component by an ordinary function. `unavailable` is plain data. Both are required members of +a required prop — there is no default, no optional prop, and no no-op member. + +### The choice: a single-slot handoff, written at the moment of opening + +**An overlay opening carries its commit with it.** `openWorkspaceOverlayWithCommit(id, commit)` +stages the commit in a module-scoped slot and then pushes `?overlay=`. `WorkspaceOverlays` +subscribes to that slot with `useSyncExternalStore` — the same mechanism it already uses for the +URL — and uses the staged commit **only while the staged id and the URL's id are the same overlay**. + +Both writes are synchronous, so the host's first render carrying the new id already carries its +commit; there is no frame where the overlay is open with nothing staged. + +### What was rejected, and why + +Each of these is also written into `overlay-commits.ts` itself, not only here. + +**A React context provider — rejected as structurally impossible, not merely costly.** Two +independent reasons, either sufficient: the screen is a Server Component, so it cannot render a +provider or hold context state at all; and `WorkspaceOverlays` is a _sibling_ of `children` in the +shell, so a provider rendered inside a screen would not contain the host even if a screen could +render one. Making context work would mean a new client boundary wrapping the whole workspace — +against Ruling 13, which holds this route's client payload to a rounding error — **and the trigger +would still have to write the commit into it on activation**, which is this module's job with more +payload attached. + +**A per-screen host — rejected.** It duplicates the renderer the shell deliberately mounts once. +The shell's own comment gives the reason: the interaction matrix is a workspace-wide contract, and +a screen that forgot to mount the host would lose the session gate and the offline notice with it. +Two hosts would also mean two subscribers to one `?overlay=` parameter both rendering the same +overlay, and `tests/ui-caring-contacts-workspace.spec.ts` asserts single occupancy of the overlay +content node (`toHaveCount(1)`) throughout. + +**A registry keyed by overlay id, written on mount — rejected, and this is the one worth reading.** +It is the obvious way to close the "a second registration silently overwrites the first" hole the +brief named, and I built the argument for it before finding what kills it: **a list screen may +render ten `Pause` triggers, one per row.** All ten would claim the same key, and every resolution +is worse than not having the problem — throw on the second registration and a legitimate screen +cannot be built; last-write-wins and the silent-overwrite failure is back unchanged; compare +handlers by identity and an inline arrow re-registers on every render. Staging at the **moment of +activation** has no conflict to resolve at all, because exactly one control was activated. + +### The cost this design carries, stated rather than hidden + +Module-scoped mutable state is invisible coupling: nothing in a screen's source shows that +activating a trigger writes into a module. Two things pay that back, and both are load-bearing: + +1. **The identity check** (`commitForOpenOverlay`). A commit staged for one overlay is never + offered to a different one, so a stale slot cannot be mistaken for a wired control. +2. **`commitUnavailableReasonFor` is total.** Every state of the slot maps to an answer and two of + the three are a refusal in plain words. + +### The decision with the largest blast radius — flagged deliberately + +Point 2 has a consequence beyond what the brief asked for, and a reviewer should look at it +directly rather than take it as incidental. + +**An overlay open with nothing staged for it now refuses its own action, with a stated reason.** +That covers a typed or pasted `?overlay=`, a forward traversal into an overlay whose commit was +spent, and any slot/URL mismatch. Because no screen renders a trigger yet, **this is currently the +behaviour of all 24 overlays in the shipped workspace** — every deep-linked overlay's confirm +control is now `aria-disabled="true"` with `NO_STAGED_COMMIT_REASON` rendered as reachable text. + +I judged this required rather than optional. The clause that made a record-nothing `commit` +acceptable was _"nothing in the workspace opens an overlay yet … so no control in the interface +currently advertises an action this does not perform."_ A deep-linked overlay's confirm button is +such a control, and it does not stop being one because the overlay was reached by URL rather than +by a click. Leaving it live would have meant writing a fresh justification for the exact defect +Ruling 87 exists to prevent. + +The conservative direction was also checked: refusing degrades to _stating what cannot be done_, +never to silently doing nothing. + +**If the owner disagrees, the reversal is two lines** — make `WorkspaceOverlays` pass +`commitUnavailableReason={commit === null ? null : commitUnavailableReasonFor(commit)}` — and the +covering test ("refuses an overlay reached by address rather than from a control") names it. + +--- + +## 2. What was built + +| File | What | +| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/components/caring-contacts/workspace/overlays/overlay-commits.ts` | **New.** `WorkspaceOverlayCommit`, the single-slot handoff store, `commitForOpenOverlay`, the total `commitUnavailableReasonFor`, and the rejected-alternatives record. | +| `src/components/caring-contacts/workspace/overlays/overlay-trigger.tsx` | **New.** `WorkspaceOverlayTrigger` — the client control, with `commit` required. | +| `src/components/caring-contacts/workspace/overlays/workspace-overlays.tsx` | `openWorkspaceOverlayWithCommit`; the record-nothing `commit` replaced by `recordDecision`; the slot subscription; `commitUnavailableReason` passed to the host. | +| `src/components/caring-contacts/workspace/overlays/overlay-host.tsx` | New **required** prop `commitUnavailableReason: string \| null`, folded into the existing refusal path. | +| `tests/caring-contacts-overlay-trigger.dom.test.tsx` | **New.** 12 tests. | +| `tests/caring-contacts-overlay-host.dom.test.tsx` | 11 existing `OverlayHost` call sites given `commitUnavailableReason={null}`. **No assertion changed, weakened or removed.** | +| `tests/caring-contacts-explained-automation.dom.test.tsx` | `overlays/overlay-trigger.tsx` added to `ALLOWED_CLIENT_COMPONENTS` with its three-condition justification. | + +`definitions.ts` and `docs/caring-contacts/interaction-matrix.md` are untouched, as required. + +### The type-level requirement, and its exact signature + +```ts +export type WorkspaceOverlayTriggerProps = { + overlayId: string; + commit: WorkspaceOverlayCommit; // required — no default, no no-op member + children: ReactNode; + className?: string; +}; +``` + +This repo already has a pattern for proving a required prop (`caring-contacts-empty-state`, +`caring-contacts-explained-automation`), so it is proved rather than asserted: a `@ts-expect-error` +render in the test file, which **fails `tsc --noEmit`** the moment the error stops being raised. +Mutation M1 below shows it doing exactly that. + +### Two behaviours worth naming + +**An unknown overlay id throws at render, in every environment.** `overlayDefinition` returns null +for an id no row carries, so without the throw the trigger becomes a button that opens an empty +overlay — the silent form of the defect the whole contract forbids. The throw lands on +`src/app/caring-contacts/error.tsx`, which says plainly that nothing was sent and nothing was +changed. This follows `blockReasonWording`'s existing every-environment policy rather than +inventing a second one. At render, not on click, so a mistyped id cannot ship waiting for a +clinician to find it. + +**The trigger itself is always live, even when its commit is `unavailable`.** The `aria-disabled` +shape belongs on the _overlay's confirm control_, which is what the commit handler backs. Opening a +decision surface that then states in plain words what cannot be recorded yet tells the clinician +more than a dead button on the screen behind it. A screen with no overlay to raise at all should +still render `UnavailableDestination`. + +### Constraints held + +- **Incident `note` boundary:** `overlay-commits.ts` and `overlay-trigger.tsx` name neither the + service-state module nor its type, and the allowlist's companion guard walks the whole module + graph reachable from the trigger and proves it. The trigger's props are an id, a class name, + children, and an intent union — no state object. +- **Tap target:** `min-h-tap` (`--spacing-tap`, 3rem = 48px), the design system's one knob. Never + `min-h-11`. Pinned by its own test. +- **Never both `disabled` and `aria-disabled`:** the refused action carries `aria-disabled="true"` + only; asserted directly. +- **Design tokens only**, no hex; the reason is reachable via `aria-describedby`, never a `title`. +- **No import from `mockups/**`.** + +--- + +## 3. Mutation proof + +Every mutation was applied with a script that **asserts the original text exists before replacing +it**, and the mutated line was grepped out of the file and printed before the gate ran. Each was +reverted with `git checkout --` before the next. + +| # | Mutation (file, exact change) | Result | +| --- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| M1 | `overlay-trigger.tsx`: `commit: WorkspaceOverlayCommit` → `commit?: WorkspaceOverlayCommit` | `tsc` red: `tests/caring-contacts-overlay-trigger.dom.test.tsx(107,7): error TS2578: Unused '@ts-expect-error' directive.` plus `TS2345` at the call site. Baseline `tsc` on the same tree: exit 0, no output. | +| M2 | `overlay-trigger.tsx`: `if (overlayDefinition(overlayId) === null)` → `if (false as boolean)` | `Tests 1 failed \| 11 passed (12)` — "fails loudly for an id the frozen table does not carry". | +| M3 | `workspace-overlays.tsx`: `stageWorkspaceOverlayCommit(id, commit);` → `void commit;` | `Tests 4 failed \| 8 passed (12)` — the record path, the checkpoint path, and both unavailable paths. | +| M4 | `overlay-commits.ts`: `if (commit === null) return NO_STAGED_COMMIT_REASON;` → `return null;` | `Tests 2 failed \| 10 passed (12)` — "refuses an overlay reached by address" and the totality test. | +| M5 | `overlay-host.tsx`: refusal ternary's else branch `commitUnavailableReason` → `null` | `Tests 3 failed \| 23 passed (26)` — the unavailable shape, the read-only row, and the by-address refusal. | +| M6 | `overlay-commits.ts`: `return slot.overlayId === openOverlayId ? slot.commit : null;` → `return slot.commit;` | `Tests 1 failed \| 11 passed (12)` — "never offers one overlay's staged commit to another". | + +**A lock incident during M4, reported because it is exactly the trap the brief names.** The first +M4 run produced **no summary line at all** — the run had not happened. The output was +`Error: Database focused-test capacity is full (current owner PID 73752, worktree +D:\Worktrees\Database\dev-hub-phase-1)`. My first attempt filtered that away with `grep -E "FAIL|Tests "` +and would have shown a clean-looking empty result. It was retried on a bounded loop and acquired +the lease on the second attempt; the row above is the real run. + +--- + +## 4. Gates + +All four gates ran for real — each one carries its own summary line, and none is quoted from an +exit code. + +**Full unit suite** — `npm run test`, after a ~5 minute wait for the exclusive heavy-run lease: + +``` + Test Files 1 failed | 813 passed | 3 skipped (817) + Tests 2 failed | 9808 passed | 74 skipped (9884) + Duration 373.61s +``` + +The two failures are **exactly** the known environmental pair the brief names — both in +`tests/gate-receipts.test.ts > gate receipts — file modes (Codex review, PR #2216)`, both failing at +`chmodSync` because this Windows drive cannot represent Unix file modes: + +``` +AssertionError: expected '266c7353fd49d41fab93e1b15cba66ec148f4…' not to be '266c7353fd49d41fab93e1b15cba66ec148f4…' +AssertionError: expected 2 to be 3 +``` + +**No others.** Nothing in the caring-contacts surface, nothing in the design-system adoption +manifest, nothing in route reachability, and nothing in the interface-vocabulary scan. + +**Typecheck** — `npm run typecheck`, a fresh run through the heavy-run wrapper, no errors emitted: + +``` +> node ./node_modules/typescript/bin/tsc -p tsconfig.typecheck.json --noEmit … +[gate-receipts] recorded a pass for "typecheck:internal" (5222 input files). +``` + +**Lint** — `npm run lint`, `--max-warnings 0`, no errors emitted: + +``` +> eslint src tests scripts worker supabase playwright … --max-warnings 0 … +[gate-receipts] recorded a pass for "lint:internal" (5222 input files). +``` + +**Format** — `prettier --write` on all seven changed files before the second commit. + +### The gate that did NOT run + +**`npm run verify:ui` / `tests/ui-caring-contacts-workspace.spec.ts` was not run.** It needs a dev +server and the exclusive Playwright lease, and it is outside the gates the brief listed. Two things +in it were read closely instead: + +- _"returns focus to the control an overlay was opened from"_ — **unaffected**. It stands its opener + in with the shell's `New plan` control and pushes the parameter itself via `page.evaluate`. That + control is untouched, the host's `openedFromRef` capture is untouched, and the Sheet's focus + return is untouched. Escape still closes a dismissible overlay whether or not its action is + refused. +- _the 24-overlay matrix test_ — **the one place my change is visible to it**. Every deep-linked + overlay now renders one extra short refusal paragraph. The assertions it could touch are + `toBeInViewport({ ratio: 1 })` on the action and `expectFullyOnScreen`'s bottom-edge bound, both + against a 900px-tall viewport. Overlay content is a heading, a summary line and an action row, so + one added sentence has a wide margin — but I have **not** measured it, and say so plainly rather + than implying I did. + +--- + +## 5. Concerns + +1. **The deep-link refusal is the decision to review first.** It is written up in §1 under "the + decision with the largest blast radius". I believe it is required by Ruling 87 rather than + optional, but it changes what all 24 overlays do today, and it is the one judgement here I would + want an owner to agree with rather than inherit. The reversal is two lines and the covering test + names it. + +2. **The Playwright spec was not run** (above). Low risk, unmeasured, and I would rather it were + measured before this is treated as proven in a browser. + +3. **A failed asynchronous record has no policy yet, deliberately.** `record` is typed + `(overlayId: string) => void`, which accepts a Server Action (TypeScript's `void`-return rule), + and the host does not await it. What a failed write should do to the interface — hold the overlay + open, say what was not written, offer a retry — needs a real store and a real screen to answer, + and neither exists. Inventing that now would be behaviour nobody reviewed against a live surface. + **The task that introduces the first store must decide it**, and the comment on `record` says so. + This is a stated gap, not an oversight. + +4. **The `record` variant has no production consumer yet.** Every Phase 2B screen will pass + `{ kind: "unavailable", reason }` until the stores exist. `record` is covered by DOM tests with a + spy, including through the fresh-authentication checkpoint, but it has never run against a real + Server Action across a real Server → Client boundary. The assignability claim is a TypeScript + fact I am confident in; the end-to-end behaviour is untested and should not be described as + proven. + +5. **Module-scoped state is still module-scoped state.** The identity check and the total refusal + function bound its failure modes, but a screen's source does not show that activating a trigger + writes into a module. If a reviewer prefers the coupling to be visible in the tree, the honest + alternative is to move the host into a client boundary wrapping the whole workspace and use + context — which costs Ruling 13's payload budget. I judged that trade the wrong way round; it is + a legitimate place to disagree. + +6. **Nothing renders a trigger yet.** Task 3 ships the control and the contract; no screen uses + either. The first screen to adopt it is where the ergonomics get tested, and it may find the + `commit` prop wants to travel differently (per row, per action) than a single prop allows. From da696e2c567d248a9a8a3ac3d9d40ae856daee0a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:51:34 +0800 Subject: [PATCH 37/51] plan(caring-contacts): the owner's three answers, and formatting Push authorised, so Ruling 78 is superseded -- it forbade pushing precisely because he had not been asked. Team screen confirmed at roster-table depth, so Ruling 74 is now his decision rather than my inference, which is what flagging it was for. Guidance and Reports are IN this phase, REVERSING Ruling 75. Worth recording about the reversal rather than just the reversal: Ruling 75's reasoning was sound on its own terms and still wrong, because it optimised against a constraint -- protect the four groups he asked for -- that he never expressed as a constraint. Ruling rather than stalling is right, and this is its cost: a ruling made in the owner's absence is a guess with reasoning attached. Where a ruling is cheap to un-make and the owner is reachable, ask. Also formats 16 files that prettier flagged, which the push guard checks against the pushed commit rather than the working tree. Co-Authored-By: Claude Opus 5 --- docs/caring-contacts/PROGRESS-LEDGER.md | 116 +++++++++--------- .../copy-decisions-recommended.md | 45 ++++--- docs/caring-contacts/phase-2a-build-record.md | 13 +- docs/caring-contacts/phase-2b-build-record.md | 78 ++++++++---- .../phase-2b-sdd-archive/task-1-brief.md | 8 +- .../phase-2b-sdd-archive/task-1-report.md | 20 +-- .../phase-2b-sdd-archive/task-5-brief.md | 6 +- .../phase-2b-sdd-archive/task-c-brief.md | 4 +- .../phase-2b-sdd-archive/task-c-report.md | 16 ++- ...6-08-24-caring-contact-phase-2b-screens.md | 79 ++++++------ src/lib/caring-contacts/message-policy.ts | 4 +- .../caring-contacts-empty-state.dom.test.tsx | 6 +- ...ring-contacts-interface-vocabulary.test.ts | 6 +- tests/caring-contacts-message-policy.test.ts | 41 +++++-- 14 files changed, 246 insertions(+), 196 deletions(-) diff --git a/docs/caring-contacts/PROGRESS-LEDGER.md b/docs/caring-contacts/PROGRESS-LEDGER.md index 48b27859b9..03834b7b34 100644 --- a/docs/caring-contacts/PROGRESS-LEDGER.md +++ b/docs/caring-contacts/PROGRESS-LEDGER.md @@ -18,7 +18,7 @@ Last updated 2026-08-24. > verified against GitHub, which needs approval. > **Sections below that instruct you to work on, push, or fetch the feature branch are superseded by -> the paragraph above.** They are kept because their *reasoning* about durability and about shared-tree +> the paragraph above.** They are kept because their _reasoning_ about durability and about shared-tree > measurement is still correct and still paid for; only the branch name is stale. In particular the > historical note that the branch was SHARED — commit `c3ef20c3f` landed on it on 2026-08-22 from a > clone not on this machine, mid-task — is why any full-suite result taken on a moving tree is a @@ -54,62 +54,62 @@ experience and clinical sign-off are required before any real use. | 1 | Phase 1 + early 2A | Built the sealed domain and database. 13 owner-behalf decisions. Phase 1 gate passed. | `D:\Repos\caring-contacts-handoff-2026-08-20\` | | 2 | Phase 2A controller | Plan written; Tasks 1-10 and 11a built and reviewed. Rulings 1-31. Died on an account limit mid-fix-round-2. | same bundle | | 3 | Phase 2A recovery | Verified the abandoned commit, re-reviewed it, Rulings 32-34, survived a worktree deletion, rebuilt and re-proved | `D:\Repos\caring-contacts-handoff-2026-08-21\` | -| 4 | Phase 2A completion | Task 11b, Tasks 12-19, the final whole-branch review, Rulings 35-66, the copy review, the condensed bar | build record, Session 4 onward | -| 5 | Phase 2A closing | Found the phase already merged to `main`; browser gate green; mutation proofs; issues sweep; 2B planning begun | build record, Session 5 | +| 4 | Phase 2A completion | Task 11b, Tasks 12-19, the final whole-branch review, Rulings 35-66, the copy review, the condensed bar | build record, Session 4 onward | +| 5 | Phase 2A closing | Found the phase already merged to `main`; browser gate green; mutation proofs; issues sweep; 2B planning begun | build record, Session 5 | ## 4. Task status — Phase 2A (19 tasks, 5 groups) -| Task | What it is | State | -| ---- | ------------------------------------------------ | ------------------------------------------------------- | -| 1 | Patient-visible copy into the sealed domain | Complete, reviewed clean | -| 2 | Roles and actions | Complete, reviewed clean | -| 3 | Service safety stop | Complete, 1 fix round | -| 4 | Pathway versions and dual approval | Complete, 1 fix round | -| 5 | Referrals | Complete (batched 5-7) | -| 6 | Plan ownership, reassignment, coverage | Complete (batched 5-7) | -| 7 | Moving a contact / changing its date | Complete (batched 5-7) | -| 8 | Auditing a view, not only a write | Complete, 1 fix round (a CRITICAL finding) | -| 9 | Notification preferences and training | Complete, reviewed clean | -| — | **Checkpoint 1** | **PASSED** — 7,604 tests, typecheck and lint green | -| 10 | Storage contract + in-memory store (~21 methods) | Complete, 1 fix round (7 findings), 101 tests | -| 11a | Migration 0003 + row-level security | Complete, **3 fix rounds**, 96 database tests | -| 11b | Shared-contract move + 22 Postgres methods | Complete, 2 fix rounds, review clean — typecheck GREEN | -| — | **Checkpoint 2** | **PASSED** — see the build record | -| 12 | Database config that can never hit Clinical KB | Complete, batched with 13, 1 fix round, review clean | -| 13 | Demo role switcher | Complete, batched with 12, 1 fix round, review clean | -| 14 | Route handlers that audit every view | Complete, 2 fix rounds, review clean | -| 15 | Route group, four width states, inbound link | Complete, 1 fix round, review clean | -| 16 | Service-state banner | Complete, 1 fix round, review clean | -| 17 | The frozen 24-row overlay definition table | Complete, 1 fix round, review clean — 0 Important | -| 18 | One renderer, twenty-four overlays | Complete, 2 fix rounds, review clean | -| 19 | Browser proof at six widths + plan closing steps | Complete, 1 fix round, review clean | -| — | Final whole-branch review | **Done** — three parallel reviewers, distinct lenses | -| — | Post-review fixes (Rulings 60-65) | Complete — incl. a CRITICAL patient-data finding | -| — | Condensed pinned safety bar (owner-requested) | Built, 1 fix round; browser gate green 2026-08-24 | -| — | Copy review document for the owner | **Delivered** — `copy-review.md`; recommendations now | -| | | tracked in `copy-decisions-recommended.md`. 13 open, | -| | | 9 clinical/policy + 4 engineering (the "7" was an | -| | | undercount and is corrected) | -| — | Deferred-findings `/issues` sweep | **Done 2026-08-24** — 7 request files queued in | -| | | `docs/outstanding-issues-inbox/`, awaiting reconcile | -| — | Phase 2B plan | **In progress** — no plan existed; being written | +| Task | What it is | State | +| ---- | ------------------------------------------------ | ------------------------------------------------------ | +| 1 | Patient-visible copy into the sealed domain | Complete, reviewed clean | +| 2 | Roles and actions | Complete, reviewed clean | +| 3 | Service safety stop | Complete, 1 fix round | +| 4 | Pathway versions and dual approval | Complete, 1 fix round | +| 5 | Referrals | Complete (batched 5-7) | +| 6 | Plan ownership, reassignment, coverage | Complete (batched 5-7) | +| 7 | Moving a contact / changing its date | Complete (batched 5-7) | +| 8 | Auditing a view, not only a write | Complete, 1 fix round (a CRITICAL finding) | +| 9 | Notification preferences and training | Complete, reviewed clean | +| — | **Checkpoint 1** | **PASSED** — 7,604 tests, typecheck and lint green | +| 10 | Storage contract + in-memory store (~21 methods) | Complete, 1 fix round (7 findings), 101 tests | +| 11a | Migration 0003 + row-level security | Complete, **3 fix rounds**, 96 database tests | +| 11b | Shared-contract move + 22 Postgres methods | Complete, 2 fix rounds, review clean — typecheck GREEN | +| — | **Checkpoint 2** | **PASSED** — see the build record | +| 12 | Database config that can never hit Clinical KB | Complete, batched with 13, 1 fix round, review clean | +| 13 | Demo role switcher | Complete, batched with 12, 1 fix round, review clean | +| 14 | Route handlers that audit every view | Complete, 2 fix rounds, review clean | +| 15 | Route group, four width states, inbound link | Complete, 1 fix round, review clean | +| 16 | Service-state banner | Complete, 1 fix round, review clean | +| 17 | The frozen 24-row overlay definition table | Complete, 1 fix round, review clean — 0 Important | +| 18 | One renderer, twenty-four overlays | Complete, 2 fix rounds, review clean | +| 19 | Browser proof at six widths + plan closing steps | Complete, 1 fix round, review clean | +| — | Final whole-branch review | **Done** — three parallel reviewers, distinct lenses | +| — | Post-review fixes (Rulings 60-65) | Complete — incl. a CRITICAL patient-data finding | +| — | Condensed pinned safety bar (owner-requested) | Built, 1 fix round; browser gate green 2026-08-24 | +| — | Copy review document for the owner | **Delivered** — `copy-review.md`; recommendations now | +| | | tracked in `copy-decisions-recommended.md`. 13 open, | +| | | 9 clinical/policy + 4 engineering (the "7" was an | +| | | undercount and is corrected) | +| — | Deferred-findings `/issues` sweep | **Done 2026-08-24** — 7 request files queued in | +| | | `docs/outstanding-issues-inbox/`, awaiting reconcile | +| — | Phase 2B plan | **In progress** — no plan existed; being written | ## 5. Verification evidence, as recorded -| Gate | Result | -| ---------------------------- | --------------------------------------------------------------------------------------- | -| Phase 1 gate | 7,531 tests / 682 files; tsc silent; lint 0 warnings; 55 database tests | -| Phase 2A Checkpoint 1 | 7,604 tests passed; typecheck and lint green | -| Task 10 | 101 tests (up from 84) | -| Task 11a, through 3 rounds | 55 → 71 → 87 → 93 → **96 passed** | -| Task 11b, through 2 rounds | 96 → 159 → 162 → **163 passed** database; full suite **7671 passed, 0 failed** | -| Full suite, 2026-08-23 | `Test Files 2 failed \| 701 passed \| 2 skipped (705)`; `Tests 3 failed \| 7841 passed` | -| Current known-red (expected) | **NONE in this work.** Both 2026-08-23 failures were artefacts, proven so: the | -| | caring-contacts one passed 22/22 on re-run of the same commit (a concurrent agent | -| | held a source file mid-edit), and the other was a 120 s timeout under machine load. | -| Browser gate | **GREEN 2026-08-24 on `main`: `32 passed (55.5s)`, exit 0**, no ECONNRESET in a 341-line | -| | log. The 2026-08-23 residual failure at 1440px was LOAD, not a defect — see §5b. | -| Condensed-bar mutation proofs | Run 2026-08-24 against `main`. See §5c. | +| Gate | Result | +| ----------------------------- | ---------------------------------------------------------------------------------------- | +| Phase 1 gate | 7,531 tests / 682 files; tsc silent; lint 0 warnings; 55 database tests | +| Phase 2A Checkpoint 1 | 7,604 tests passed; typecheck and lint green | +| Task 10 | 101 tests (up from 84) | +| Task 11a, through 3 rounds | 55 → 71 → 87 → 93 → **96 passed** | +| Task 11b, through 2 rounds | 96 → 159 → 162 → **163 passed** database; full suite **7671 passed, 0 failed** | +| Full suite, 2026-08-23 | `Test Files 2 failed \| 701 passed \| 2 skipped (705)`; `Tests 3 failed \| 7841 passed` | +| Current known-red (expected) | **NONE in this work.** Both 2026-08-23 failures were artefacts, proven so: the | +| | caring-contacts one passed 22/22 on re-run of the same commit (a concurrent agent | +| | held a source file mid-edit), and the other was a 120 s timeout under machine load. | +| Browser gate | **GREEN 2026-08-24 on `main`: `32 passed (55.5s)`, exit 0**, no ECONNRESET in a 341-line | +| | log. The 2026-08-23 residual failure at 1440px was LOAD, not a defect — see §5b. | +| Condensed-bar mutation proofs | Run 2026-08-24 against `main`. See §5c. | ### 5b. The production lock, and why the browser gate went red @@ -142,13 +142,13 @@ to fail and rewritten.** ### 5c. The condensed bar's two mutation proofs — both run 2026-08-24, both discriminate -| Mutation | Result | What it proves | -| ------------------------------------------------------------ | --------------------------- | -------------------------------------------------- | -| Pin: `top-full` -> `top-0` | 13 failed / 19 passed | The 1440px pin assertion is REACHED and fails at | -| | | line 877 on `barBox.top` 64 -> 0, with the two | -| | | preceding assertions passing first. | -| Dark colours: danger tokens -> fixed light-theme literals | **1 failed** / 31 passed | Only the dark-scheme comparison reddens, at line | -| | | 931, naming the injected literal. | +| Mutation | Result | What it proves | +| --------------------------------------------------------- | ------------------------ | ------------------------------------------------ | +| Pin: `top-full` -> `top-0` | 13 failed / 19 passed | The 1440px pin assertion is REACHED and fails at | +| | | line 877 on `barBox.top` 64 -> 0, with the two | +| | | preceding assertions passing first. | +| Dark colours: danger tokens -> fixed light-theme literals | **1 failed** / 31 passed | Only the dark-scheme comparison reddens, at line | +| | | 931, naming the injected literal. | **And the trap that nearly produced a false proof.** The first attempt at the colour mutation used an anchor matching TWO elements. A uniqueness assertion refused the edit — but the script ran the whole diff --git a/docs/caring-contacts/copy-decisions-recommended.md b/docs/caring-contacts/copy-decisions-recommended.md index a21057ab2b..1ae41be98b 100644 --- a/docs/caring-contacts/copy-decisions-recommended.md +++ b/docs/caring-contacts/copy-decisions-recommended.md @@ -7,8 +7,8 @@ > recommendation is not the same as the recommendation being executable today: > > - **A9 (adding Lifeline `13 11 14`) CANNOT be executed yet, by its own terms.** The recommendation -> was to add Lifeline *and drop the `Fictional Support Line` line once a real crisis number is -> chosen* — because the message is roughly nine characters from its two-segment maximum, so nothing +> was to add Lifeline _and drop the `Fictional Support Line` line once a real crisis number is +> chosen_ — because the message is roughly nine characters from its two-segment maximum, so nothing > can be added until something comes out. No real crisis number exists. **A9 is APPROVED IN > PRINCIPLE and BLOCKED ON a real crisis number.** Do not implement it by removing some other > sentence chosen by an implementer; the owner was explicitly asked to name what goes, and the @@ -22,7 +22,6 @@ > FROZEN — but each change must cite the item number it implements, and the sealed domain's > `message-copy` module remains the single place they live. - **Written 2026-08-24, approved the same day, and NOT YET IMPLEMENTED.** The freeze that used to sit here is lifted — see the banner above. Approval and implementation are different things, and the status table at the foot of this file is the one that says which items are built. @@ -70,8 +69,8 @@ to remove. ### A2 — "Your message has not been seen by anyone and has not been kept" may not be true **Recommend:** narrow it to what this system can actually promise, and say who is not reading rather -than what is not stored — something in the shape of *"No one at Example Aftercare Team reads this -number."* Do not restore any claim about storage until a telephony provider is chosen and its +than what is not stored — something in the shape of _"No one at Example Aftercare Team reads this +number."_ Do not restore any claim about storage until a telephony provider is chosen and its retention terms have been read. **Why:** this is the highest-risk sentence in the programme. It is a firm factual claim about data @@ -134,14 +133,14 @@ is exactly why this needs the owner's decision rather than a controller ruling. ### A6 — "Contacts that fall inside the pause are skipped for good" **Recommend:** confirm the behaviour is intended, and change the clinician-facing wording to state -the consequence as a number rather than a fact — for example *"3 contacts fall inside this pause and -will not be sent later."* +the consequence as a number rather than a fact — for example _"3 contacts fall inside this pause and +will not be sent later."_ **Why:** in caring contacts the schedule is the intervention. Silently and permanently removing contacts from it is a clinical act, and "skipped for good" is easy to read past when you are pausing for an ordinary administrative reason. -**Cost if wrong:** none — showing the count is strictly more information. Whether pausing *should* +**Cost if wrong:** none — showing the count is strictly more information. Whether pausing _should_ drop contacts is the owner's question, and this recommendation does not settle it. ### A7 — Withdrawal is immediate, irreversible, and needs nobody else's agreement @@ -243,18 +242,18 @@ across, and let B3's scan flag it at that point. Approval is not implementation. Nothing below has been built yet. -| Item | Approved outcome | Where it gets built | -| --------- | ------------------------------------------------------------------ | ----------------------------------------- | -| A1 | Machine check refusing any message containing "Fictional" | Small change now, beside the length check | -| A2 | Narrow the storage promise to who is not reading | Small change now, `message-copy` | -| A3 | Say nobody reads it AND something automatic comes back | Small change now, `message-copy` | -| A4 | Refuse loudly when a final message is missing; wording deferred | Refusal now; wording a later phase | -| A5 | Patient not told during a service-wide stop — DELIBERATE | Decision only; nothing to build | -| A6 | Confirm intended; show the count of contacts a pause discards | Phase 2B, Group 1 | -| A7 | Keep the asymmetry; add a confirmation naming what is lost | Phase 2B, Group 1 | -| A8 | Confirm no-later-retry; surface it on the plan, not only in a panel | Phase 2B, Groups 1-2 | -| A9 | Add Lifeline — **BLOCKED**, needs a real crisis number first | Re-ask when one exists | -| B1 | Make the two panels describe what they actually show | Phase 2B, Group 3 | -| B2 | Narrow the "lead" ban to the commercial sense | Small change now | -| B3 | Extend the prohibited-word scan to interface strings | Small change now (issues inbox P2) | -| B4 | Leave the design-scratch sentence alone | No action | +| Item | Approved outcome | Where it gets built | +| ---- | ------------------------------------------------------------------- | ----------------------------------------- | +| A1 | Machine check refusing any message containing "Fictional" | Small change now, beside the length check | +| A2 | Narrow the storage promise to who is not reading | Small change now, `message-copy` | +| A3 | Say nobody reads it AND something automatic comes back | Small change now, `message-copy` | +| A4 | Refuse loudly when a final message is missing; wording deferred | Refusal now; wording a later phase | +| A5 | Patient not told during a service-wide stop — DELIBERATE | Decision only; nothing to build | +| A6 | Confirm intended; show the count of contacts a pause discards | Phase 2B, Group 1 | +| A7 | Keep the asymmetry; add a confirmation naming what is lost | Phase 2B, Group 1 | +| A8 | Confirm no-later-retry; surface it on the plan, not only in a panel | Phase 2B, Groups 1-2 | +| A9 | Add Lifeline — **BLOCKED**, needs a real crisis number first | Re-ask when one exists | +| B1 | Make the two panels describe what they actually show | Phase 2B, Group 3 | +| B2 | Narrow the "lead" ban to the commercial sense | Small change now | +| B3 | Extend the prohibited-word scan to interface strings | Small change now (issues inbox P2) | +| B4 | Leave the design-scratch sentence alone | No action | diff --git a/docs/caring-contacts/phase-2a-build-record.md b/docs/caring-contacts/phase-2a-build-record.md index 4fee428f32..4e81d29a9b 100644 --- a/docs/caring-contacts/phase-2a-build-record.md +++ b/docs/caring-contacts/phase-2a-build-record.md @@ -2667,7 +2667,6 @@ failure as a regression, run the other way. dark-mode colour mutation. Both were blocked on the browser gate; the gate is now available, so the next session can run them. - # Session 5 — 2026-08-24 — closing work, and the branch turned out to be merged Working copy `D:\Repos\Database\.claude\worktrees\browser-test-gate-handoff-d5c1db`, on `main` at @@ -2695,7 +2694,7 @@ before any work was done: - `e4cbe8d3a` on `main` is "Claude/suicide contact mockup b5aaa0 (#2279)", dated 2026-08-23 — a squash merge of the whole phase. `main` has advanced 18 commits since. - Every caring-contacts path on `main` matches the old branch tip `cf03f99a4`. `git diff origin/main - claude/suicide-contact-mockup-b5aaa0` over `src/lib/caring-contacts`, `caring-contacts/` and +claude/suicide-contact-mockup-b5aaa0` over `src/lib/caring-contacts`, `caring-contacts/` and `tests/ui-caring-contacts-workspace.spec.ts` is EMPTY; `docs/caring-contacts/` differs by two lines in one archive file; `src/components/caring-contacts/` differs only where **`main` is newer** — the design-system consolidation replaced a shadow literal with a token, swapped a local `SectionHeading` @@ -2705,11 +2704,11 @@ before any work was done: So the branch is retired and `main` is the source of truth. The whole "push after every task / `SKIP_STATIC_GUARD=1`" apparatus in the older records was correct for its moment and is now noise. The -records have been corrected in place rather than deleted, because the *reasoning* about durability and +records have been corrected in place rather than deleted, because the _reasoning_ about durability and about measuring a moving tree still holds — and holds harder on `main`, which far more sessions touch. **The general lesson, and it is the same shape as the idempotency-table one:** a handoff document -describes where work *was*, and no part of it updates when the work moves. Four documents agreed with +describes where work _was_, and no part of it updates when the work moves. Four documents agreed with each other and all four were wrong together, because they were written in one session and copied from one another. **Agreement between records that share an ancestor is not corroboration.** Check the claim against git, not against the other records. @@ -2734,7 +2733,7 @@ count is **32**, not the 33 the continuation prompt predicted; the prompt has be Two corrections to how that failure was read, both worth more than the incident: - **The machine was NOT quiet for this run** — 73 `node` processes and 23 Claude processes were live. A - pass under load is *stronger* evidence than a pass on a quiet machine, not weaker, because load is the + pass under load is _stronger_ evidence than a pass on a quiet machine, not weaker, because load is the very hypothesis being tested. Waiting for quiet would have bought less and cost hours. - **`:822` was never the failing line.** Playwright reports a failure at the test's DECLARATION line, and 822 is the `test(...)` line. The `apiRequestContext.post: read ECONNRESET` came from @@ -2746,7 +2745,6 @@ Two corrections to how that failure was read, both worth more than the incident: a hypothesis and confirming one. But **read which line the runner is actually naming before inferring what a failure means.** - ## Closing item 2 — the condensed bar's mutation proofs ### Mutation A — the 1440px pin assertion IS reachable and DOES discriminate @@ -2770,8 +2768,7 @@ Three things make this a proof rather than a red light: - **It is the pin assertion itself that failed** — line 877 — not an earlier one standing in for it. - **The two assertions before it passed first**: the round-1 guard at 869 (`the banner is still on - screen ... nothing about the handover is being measured`) and `the condensed bar did not appear` at - 874. So the pin is REACHED, which is exactly what round 1's degenerate version was not. +screen ... nothing about the handover is being measured`) and `the condensed bar did not appear` at 874. So the pin is REACHED, which is exactly what round 1's degenerate version was not. - **The mutation moved a value the assertion reads**: `barBox.top` went 64 -> 0. A mutation that leaves every asserted value unchanged proves nothing however red the suite goes, and three proposed proofs on this branch already failed that test. diff --git a/docs/caring-contacts/phase-2b-build-record.md b/docs/caring-contacts/phase-2b-build-record.md index a8eb7c8162..2a1663dc6a 100644 --- a/docs/caring-contacts/phase-2b-build-record.md +++ b/docs/caring-contacts/phase-2b-build-record.md @@ -19,27 +19,27 @@ Run before dispatching Task 1, per the method. The output is a table, not a verd ### Task pairs sharing a file or an interface -| Tasks | Shared surface | What one produces / the other consumes | Finding | -| ---------------------------- | ----------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------- | -| 4 → 5, 13, 15, 18 | `shell.tsx` destination lists | Task 4 adds the first `href`; each screen task adds its own | **Sequential edits to one file.** No contradiction. Implementers are never dispatched in parallel, so this is a merge risk only if that rule is broken. | -| 1 → 5, 13, 15, 18 | the empty-state component | Task 1 produces it; four list screens consume it | Clean. Task 1 must land before any consumer. | -| 2 → 5, 12, 17 | the list-read API pattern | Task 2 produces the helper + contract test; three routes consume it | Clean, and this is the whole reason Group 0 exists. | -| 3 → 11, 14, 16, 18, 20 | the overlay trigger | Task 3 produces it; five tasks wire overlays with it | Clean. Ruling 69 keeps wiring with the owning screen. | -| C → 16 | `message-copy.ts` | Task C rewrites the reply wording (items A2/A3); Task 16 renders it | **Ordering constraint.** C must land first, or Task 16 renders wording that is about to change. Recorded, not a conflict. | -| 7–9 ↔ 13 | the nine-contacts / closing-message | Corrections #3 and #4 touch the activation review AND every schedule | **Genuine cross-task requirement.** Whichever lands first sets the shape; the second must not re-derive it. Both must read the same source of truth in `schedule.ts`. | -| 5 ↔ 6 | `getEpisode` vs `listPlans` | Task 5 must NOT call `getEpisode`; Task 6 is the one screen that may | Clean, and stated in the plan. Worth re-stating in both briefs. | +| Tasks | Shared surface | What one produces / the other consumes | Finding | +| ---------------------- | ----------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 4 → 5, 13, 15, 18 | `shell.tsx` destination lists | Task 4 adds the first `href`; each screen task adds its own | **Sequential edits to one file.** No contradiction. Implementers are never dispatched in parallel, so this is a merge risk only if that rule is broken. | +| 1 → 5, 13, 15, 18 | the empty-state component | Task 1 produces it; four list screens consume it | Clean. Task 1 must land before any consumer. | +| 2 → 5, 12, 17 | the list-read API pattern | Task 2 produces the helper + contract test; three routes consume it | Clean, and this is the whole reason Group 0 exists. | +| 3 → 11, 14, 16, 18, 20 | the overlay trigger | Task 3 produces it; five tasks wire overlays with it | Clean. Ruling 69 keeps wiring with the owning screen. | +| C → 16 | `message-copy.ts` | Task C rewrites the reply wording (items A2/A3); Task 16 renders it | **Ordering constraint.** C must land first, or Task 16 renders wording that is about to change. Recorded, not a conflict. | +| 7–9 ↔ 13 | the nine-contacts / closing-message | Corrections #3 and #4 touch the activation review AND every schedule | **Genuine cross-task requirement.** Whichever lands first sets the shape; the second must not re-derive it. Both must read the same source of truth in `schedule.ts`. | +| 5 ↔ 6 | `getEpisode` vs `listPlans` | Task 5 must NOT call `getEpisode`; Task 6 is the one screen that may | Clean, and stated in the plan. Worth re-stating in both briefs. | ### Per-task self-consistency -| Task(s) | Its own text agrees with itself? | -| ------- | -------------------------------------------------------------------------------------------- | -| C | Yes — six named edits in two named modules. | -| 1–4 | Yes. | -| 5–11 | Yes, with the `getEpisode` restriction stated. | -| 12–14 | Yes. | +| Task(s) | Its own text agrees with itself? | +| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| C | Yes — six named edits in two named modules. | +| 1–4 | Yes. | +| 5–11 | Yes, with the `getEpisode` restriction stated. | +| 12–14 | Yes. | | 15–16 | **NO — defect found, see Ruling 73.** The design-corrections table routes correction #2 to "Group 3, Task 11", but Task 11 is Group 1's overlay wiring; Group 3 is Tasks 15–16. | -| 17–18 | Yes, with Ruling 72's scope limit stated. | -| 19–21 | Yes. | +| 17–18 | Yes, with Ruling 72's scope limit stated. | +| 19–21 | Yes. | ### Anything the plan mandates that the review rubric treats as a defect @@ -52,8 +52,7 @@ block. **Ruling [73] — the design-corrections table's "Group 3, Task 11" is a typo for Task 16; corrected in the plan.** — Why: Task 11 is Group 1's overlay wiring and cannot carry a Group 3 copy correction. The -correction is the reply-handling wording, which belongs to the message-preview surface built in Task -16. — Cost if wrong: had it stood, Task 11's implementer would have received a requirement it had no +correction is the reply-handling wording, which belongs to the message-preview surface built in Task 16. — Cost if wrong: had it stood, Task 11's implementer would have received a requirement it had no surface for, and either implemented it in the wrong place or reported BLOCKED — a wasted dispatch either way. This is exactly what the pre-flight scan is for and it is the first thing the scan found. @@ -74,8 +73,7 @@ nothing and protects the four groups he actually asked for. — Cost if wrong: i early — the equity reach section is the one part with external interest — it arrives later than he hoped. Reversible at any point by moving one task. -**Ruling [76] — the approved copy changes are executed as ONE batched task (Task C) ahead of Group -0.** — Why: the method says to batch small same-shape work into one dispatch rather than one subagent +**Ruling [76] — the approved copy changes are executed as ONE batched task (Task C) ahead of Group 0.** — Why: the method says to batch small same-shape work into one dispatch rather than one subagent per item. All six approved edits are small, independent, and land in two adjacent modules (`message-copy.ts`, `message-policy.ts`). They also unblock nothing else, so they are cheap to do first and get the owner's approved wording into the tree before any screen renders it. — Cost if @@ -374,9 +372,9 @@ UNCOMMITTED. On a machine that has destroyed four working directories mid-sessio expensive shape of an ordinary delay. Resumed with an explicit instruction ordering: **commit first, then retry the gate, bounded.** It committed, retried twice, acquired the lease and finished for real. -The instruction is now standing for every remaining brief: *commit before waiting on any gate, and a +The instruction is now standing for every remaining brief: _commit before waiting on any gate, and a lock-acquisition failure is neither a pass nor a failure — if the output carries no summary line the -run did not happen, whatever the exit code says.* Machine health was checked rather than assumed: +run did not happen, whatever the exit code says._ Machine health was checked rather than assumed: `node --version` returned in 0.083s, so this was ordinary lease contention and not the process-spawn starvation that has previously made everything slow. @@ -388,7 +386,7 @@ This is worth recording precisely, because it cuts both ways. It **fired on a ba user-facing prose** — which is exactly deferred item 2 from Task C's re-review ("the raw-prose pass scans identifiers, imports and JSX attribute names, not only prose … a future `const conversation = …` would fail the test with a confusing message"). That deferred concern materialised within one task, -so it is real and will recur. Whether it was a *false* positive is a judgement I have put to Task 1's +so it is real and will recur. Whether it was a _false_ positive is a judgement I have put to Task 1's reviewer rather than settled myself: an icon named `Inbox` is not text a patient or clinician reads, but it is also the kind of name that leaks into a `data-testid` or an `aria-label` without anyone noticing. If the reviewer judges it over-reach, narrow the scan; if not, leave it and accept the @@ -466,7 +464,7 @@ light up a destination is exactly "adding an `href` here". So Task 4's mechanica **The problem is its other half.** The plan has Task 4 create `patients/page.tsx` rendering the empty state, as Group 0's proving step — before Task 5 gives it real data. A page that renders `ListEmptyState` unconditionally **says "No patients yet" whether or not patients exist**. That is a -false statement on a clinical caseload screen, and it is the *precise* defect Task 1's component was +false statement on a clinical caseload screen, and it is the _precise_ defect Task 1's component was built to prevent: an empty-looking list that is not empty. Shipping it, even for one task, would mean the first screen of this phase overclaims in exactly the way the phase's first component exists to stop. @@ -517,3 +515,33 @@ rendered once by the shell rather than per screen, so a screen's commit handler somehow, and every obvious answer — module-level mutable state, a context provider, lifting the host per screen — has a different cost against Ruling 13's client-payload limit. That is a design decision, not transcription. + +## Owner answered the three open questions, 2026-08-24 — one ruling REVERSED + +Asked directly rather than left to inference, because two of them were shaping work already underway. + +**1. Push authorised.** The branch goes to `origin` after each task from now on. No pull request, no +merge, nothing live. This closes the durability gap that has cost this programme three working +directories: commits on a worktree branch already survive the worktree, but not the machine. +**Ruling 78 is superseded** — it forbade pushing precisely because he had not been asked yet, and now +he has. + +**2. Team screen: the simple staff list. Ruling 74 CONFIRMED by the owner, not merely by me.** Each +staff member with their active plans, unclaimed work and anything overdue for escalation — the +roster-table depth the approved design actually covers. Rosters, leave and caseload fullness are NOT +in scope. This was the item I flagged as most likely to under-deliver against what he pictured; it is +now his decision rather than my inference, which is the outcome flagging it was for. + +**3. Guidance and Reports are IN this phase. Ruling 75 is REVERSED.** I ruled them deferred to the +end and cuttable to Phase 3, reasoning that they sit outside his stated four groups. He wants them +built. That is his call and it is a reasonable one: both already have approved designs, so deferring +them was optimising for a scope boundary he did not ask for. **Task 19 is therefore a committed part +of Phase 2B, not a contingency**, and Reports carries the §2.5 equity reach section with small-cell +suppression. + +**Worth recording about the reversal rather than just the reversal.** Ruling 75's reasoning was sound +on its own terms and still produced the wrong answer, because it optimised against a constraint — +"protect the four groups he asked for" — that he had never expressed as a constraint. The method's +instruction to rule rather than stall is right, and this is its cost: a ruling made in the owner's +absence is a guess with reasoning attached. **Where a ruling is cheap to un-make and the owner is +reachable, ask.** All three of these took one question and reversed one of my decisions. diff --git a/docs/caring-contacts/phase-2b-sdd-archive/task-1-brief.md b/docs/caring-contacts/phase-2b-sdd-archive/task-1-brief.md index 0363b15a0d..f487d34f25 100644 --- a/docs/caring-contacts/phase-2b-sdd-archive/task-1-brief.md +++ b/docs/caring-contacts/phase-2b-sdd-archive/task-1-brief.md @@ -18,10 +18,10 @@ It renders a heading, a plain-words explanation, and **at most one action**. **It must distinguish two different emptinesses, because they need different words and a clinician who cannot tell them apart cannot act:** -| Kind | Means | Must tell the reader | -| ------------ | -------------------------------------------------------- | -------------------------------------------------------- | +| Kind | Means | Must tell the reader | +| ------------ | --------------------------------------------------------- | ------------------------------------------------------------- | | `"no-data"` | Nothing exists yet — no patients, no templates, no team | That the list is genuinely empty, and how a first one arrives | -| `"filtered"` | Things exist, but the current filter or search hides them | **Why** they are hidden, and **what would change it** | +| `"filtered"` | Things exist, but the current filter or search hides them | **Why** they are hidden, and **what would change it** | A `"filtered"` empty list that says only "Nothing to show" is the defect this task exists to prevent: it is indistinguishable from "there are no patients", and it invites a clinician to conclude a @@ -50,7 +50,7 @@ established pattern here. Read it before writing anything. In particular: render `AutomatedState` internally. They have different triggers — `AutomatedState` is for the system acting on its own (paused, suppressed, escalated), while an empty list is usually the user's own filter or simply nothing existing yet. Its `CircleAlert` icon and its state-name `aria-label` are both -wrong for "no patients yet". Reuse the *wording shape*, not the component. +wrong for "no patients yet". Reuse the _wording shape_, not the component. ## The action diff --git a/docs/caring-contacts/phase-2b-sdd-archive/task-1-report.md b/docs/caring-contacts/phase-2b-sdd-archive/task-1-report.md index 7ffe57f53d..b3404b1a0c 100644 --- a/docs/caring-contacts/phase-2b-sdd-archive/task-1-report.md +++ b/docs/caring-contacts/phase-2b-sdd-archive/task-1-report.md @@ -152,7 +152,7 @@ compile — a type cast on the one line that reads `props.explanation`, changed `(props as ListEmptyStateNoDataProps).explanation`. Nothing else changed; the `"filtered"` branch's own JSX (the `Why:`/`What changes it:` block) was left completely untouched, just made unreachable. This is the specified mutation, not a placeholder: it forces `"filtered"` instances -through the exact same rendering *code path* `"no-data"` uses, rather than substituting a fake +through the exact same rendering _code path_ `"no-data"` uses, rather than substituting a fake string. The effect on each kind, reasoned through before running it: - A genuine `"no-data"` instance still has a real `explanation` property, so the cast is a @@ -168,7 +168,7 @@ Verification, in order: exactly what the two `"filtered"`-content tests read, so this is the correct value to perturb. 2. **Proved the mutation was in the tree**, not silently unapplied: `grep -n "MUTATION" - src/components/caring-contacts/workspace/list-empty-state.tsx` returned the edited comment +src/components/caring-contacts/workspace/list-empty-state.tsx` returned the edited comment and line before running the test. 3. **Ran the suite**: **`Tests 2 failed | 9 passed (11)`.** Both failures were in the `"ListEmptyState — filtered"` describe block: "renders both the reason and the remedy in the @@ -177,8 +177,8 @@ Verification, in order: **Every `"no-data"` test stayed green**, confirming the mutation left `"no-data"`'s real output completely intact, which is the property the original mutation failed to have. 4. **Reverted** (`git`-tracked file restored from a pre-mutation backup), confirmed `grep -n - "MUTATION"` returned nothing, and reran: **`Test Files 1 passed (1)`, `Tests 11 passed - (11)`.** +"MUTATION"` returned nothing, and reran: **`Test Files 1 passed (1)`, `Tests 11 passed +(11)`.** ### Important 2 — renamed `EmptyState` → `ListEmptyState` @@ -232,7 +232,7 @@ entered a named group and finds the reason and the remedy without hunting elsewh page. `ListEmptyState`'s `"filtered"` branch has the identical three-piece shape (heading, "Why:", "What changes it:") and had no grouping at all before this fix. -Ruling 81 forbade *rendering* `AutomatedState`, not reusing its accessible structure — the +Ruling 81 forbade _rendering_ `AutomatedState`, not reusing its accessible structure — the review brief should have said so, and this fix applies the same structure without importing or rendering `AutomatedState` itself. @@ -270,9 +270,9 @@ code. 3. `npm run typecheck`: contended for the repo's heavy-run lease twice (another session's Playwright run, then a separate session `favourites-mockups-20260824`) before it ran. Decisive line once it acquired the lease: `[gate-receipts] recorded a pass for - "typecheck:internal" (5215 input files).`, exit 0. +"typecheck:internal" (5215 input files).`, exit 0. 4. `npm run lint`: acquired the lease on the first attempt. Decisive line: `[gate-receipts] - recorded a pass for "lint:internal" (5216 input files).`, exit 0. +recorded a pass for "lint:internal" (5216 input files).`, exit 0. ### Fix round 1 (commit `191761fc6`) @@ -286,14 +286,14 @@ failure needed reporting. 2. `npm run test` (full unit suite, run in the background under `run_in_background` because it exceeded the tool's foreground timeout — the completion notification carried the same summary line read below): `Test Files 1 failed | 812 passed | 3 skipped (816)` / `Tests 2 - failed | 9796 passed | 74 skipped (9872)`. Same one failing file, same two named +failed | 9796 passed | 74 skipped (9872)`. Same one failing file, same two named `gate-receipts.test.ts` file-mode failures as the original run, no others. (Total test count rose by 2 versus the original build's `9870`, matching the 2 new accessible-group tests added this round.) 3. `npm run typecheck`: acquired the lease on the first attempt this round. Decisive line: `[gate-receipts] recorded a pass for "typecheck:internal" (5217 input files).`, exit 0. 4. `npm run lint`: acquired the lease on the first attempt. Decisive line: `[gate-receipts] - recorded a pass for "lint:internal" (5217 input files).`, exit 0. +recorded a pass for "lint:internal" (5217 input files).`, exit 0. No gate reported a lock-acquisition failure in this round. The earlier `DATABASE_HEAVY_RUN_ADMISSION_BUSY` responses (both rounds, while other sessions on this machine @@ -310,7 +310,7 @@ treated as a pass or a fail; none of the numbers above come from a run that hit test. - No import from `src/components/caring-contacts/mockups/**`. - `automated-state.tsx`, `shell.tsx`, and all routes are untouched across both commits (`git - diff` confirms only `list-empty-state.tsx`, its test file, and the regenerated adoption +diff` confirms only `list-empty-state.tsx`, its test file, and the regenerated adoption manifest). - No dependency added — only lucide-react (already a dependency) and `react`'s `ReactNode` type. - Not wired into any screen. diff --git a/docs/caring-contacts/phase-2b-sdd-archive/task-5-brief.md b/docs/caring-contacts/phase-2b-sdd-archive/task-5-brief.md index ba16694862..98e791d4e6 100644 --- a/docs/caring-contacts/phase-2b-sdd-archive/task-5-brief.md +++ b/docs/caring-contacts/phase-2b-sdd-archive/task-5-brief.md @@ -14,7 +14,7 @@ number**. This is the **first real screen of Phase 2B** and the owner's first pr may see, plus the navigation and documentation that make it a real destination. **Task 4 was merged into this one (Ruling 89).** The plan originally lit up the navigation link with an -empty placeholder page first. That page would have said *"No patients yet"* whether or not patients +empty placeholder page first. That page would have said _"No patients yet"_ whether or not patients existed — a false statement on a clinical caseload screen, and precisely the defect the component you are about to use was built to prevent. So the link and the real screen land together, and the screen is never reachable in a state where it can lie. @@ -93,8 +93,8 @@ Build hrefs from `src/lib/caring-contacts-routes.ts`, never from string literals - **Internal navigation** uses `` / `router.push` / server `redirect()` — never a raw ``. - Every `