Skip to content

Claude/suicide contact mockup b5aaa0 - #2279

Merged
BigSimmo merged 162 commits into
mainfrom
claude/suicide-contact-mockup-b5aaa0
Aug 22, 2026
Merged

Claude/suicide contact mockup b5aaa0#2279
BigSimmo merged 162 commits into
mainfrom
claude/suicide-contact-mockup-b5aaa0

Conversation

@BigSimmo

@BigSimmoBigSimmo commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Synchronise Caring Contacts with current main, retaining both its synthetic workspace contracts and the newer Ward Flow route, catalogue and Playwright registrations.

Verification

  • Focused offline Vitest: design-system adoption, PR-shard and project-isolation contracts, route reachability, and Caring Contacts session/API/domain-boundary tests (121 tests).
  • TypeScript source check completed without errors.
  • UI verification not run: the full Chromium journey suite is delegated to required CI; the focused static and contract coverage above exercises the merged registrations.

Risk and rollout

  • Risk: medium — this includes a synthetic caring-contact workspace and its server boundary; the merge preserves its production fail-closed guard and its isolation from the Clinical KB project.
  • Rollback: revert this pull request, including the main-sync commit, to remove the workspace and its isolated local-database contract together.
  • Provider or production effects: None. The module does not use the Clinical KB Supabase project and its separate local connection rejects that target.
  • RAG impact: none.

Clinical Governance Preflight

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

Notes

  • Caring Contacts is synthetic, noindex, and production-fails-closed until enterprise authentication is available. Its migrations remain isolated under caring-contacts/ and are not Clinical KB migrations.

BigSimmoand others added 30 commits August 19, 2026 03:32
Records the nine decision-lock revisions agreed 19 August 2026 (auto-reply to
inbound messages, closing message at month 12, coordinator-set first contact
date, third-party pause, cultural-identity reach reporting, configurable
retention with real deletion, message rules as data, the enforced repository
seam, and one bounded clinical-record document).
Specifies the domain rules layer, a dedicated caring-contact Supabase project
kept hard-separated from the Clinical KB project, the seven screens required by
existing decisions but never designed, four recommended screens, the
design non-regression contract, the elevation brief, and the open governance
register.
Synthetic data only. No SMS provider, no real patient data, no migration
against the Clinical KB project, no production deployment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…governance artefacts
Renames the service from Callback (41 occurrences across the rollout plan, the
coordination spec, the production build spec and the prototype shell header).
Callback named a promise the service never keeps: it never calls back, never
receives a reply and never responds. A patient enrolled in "Callback" could
reasonably expect a telephone call, which is the same expectation-mismatch
hazard as the silent-reply problem corrected on 19 August. Recorded as decision
revision 2.10. No test asserted the old name; the 38 focused caring-contact
tests pass unchanged.
Adds five artefacts the programme lacked:
- hazard-log.md: 30 clinical, patient-facing, scheduling, privacy and
operational hazards with severity, controls, status and named owner. Six are
unmitigated and block a pilot.
- evidence-brief.md: the honest evidence position for a sponsor, including the
equivocal meta-analyses and null replications. Citations are explicitly
marked unverified and must be checked before use.
- referral-feasibility.md: the questions to ask about the WA hospital referral
feed, who to ask, and the manual-entry fallback. Largest programme risk.
- message-review-pack.md: how to run the lived-experience review, and the
reply-boundary wording correction it must settle first.
- demo-script.md: the five-minute path through the built system.
Repairs the binding reference to design-handoff.md, a file that never existed,
repointing both mentions at interaction-matrix.md, which holds the 24-row
modality matrix. The documentation link gate passed over this for days; queued
as its own ledger request.
Queues six outstanding-work requests covering the blocking hazards, the now
inaccurate reply wording, referral feasibility, build status, the Australian
hosting gap and the link-gate defect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e, and plan the domain build
Reply wording. Decision revision 2.1 made the number receiving-capable, which
made "Replies are not received, stored, analysed or monitored" untrue: replies
are received, then discarded unread. Stating something false about a safety
boundary is the failure this programme can least afford, so the notice now
claims only what remains true — "No one reads replies to this number" — and a
new AUTOMATED_REPLY_RESPONSE constant supplies the message a person gets at the
moment they reach out. Both are marked provisional pending the lived-experience
and dual-approval gate. GSM-7 evidence recomputed independently: the patient
message drops from 272 to 252 septets, still two segments; the automated reply
is 218 septets, two segments. Both are now pinned by test, as is the absence of
any patient mobile number in the automated reply.
Evidence brief. Every citation verified against journal records rather than
recalled. The material find is Stevens et al. (Br J Psychiatry 2024;224(3):
106-113), an Australian randomised trial of automated SMS brief contact after
hospital-treated self-harm reporting a significant 22% reduction in repeat
event rates at 12 and 24 months, on a nine-contact schedule nearly identical to
this pathway's. Comtois et al. (JAMA Psychiatry 2019) is corrected: both primary
outcomes were null and only secondary outcomes reached significance, so the
common summary of it as simply positive overstates it. Milner et al. (2015)
retained as the strongest sceptical citation, with the note that it predates
both SMS trials.
Adds outreach-drafts.md with two ready-to-send approaches — the lived-experience
message review and the hospital referral feed feasibility conversation — since
those are the two actions that need a person rather than a keyboard.
Adds the part-one implementation plan: eleven test-first tasks covering the
sealed domain layer, the twelve-month simulation and the team-scoped Postgres
schema, with three design decisions recorded (Week 1 collision suppression,
cancellation always permitted, safety stop ungated by role).
Focused caring-contact suites: 38 passed. Typecheck: exit 0. Link gate: 1910
references resolve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…p non-building material
Replaces the twelve-phase framing with three phases, every one of which can be
built now on invented patients without a sponsor, a contract, or anyone's
permission: (1) the rules and the database, (2) the working screens, (3) the
demonstrable system. Most of the previous twelve boundaries were not real —
nothing outside the code changed between them, and several described work that
cannot be done from a keyboard at all.
States the out-of-scope set plainly: no message sent to any number real or
test, no SMS provider, no hosting change, no hospital system connection, no
enterprise sign-on, no real patient, and no migration against the Clinical KB
Supabase project. The build runs against local Postgres.
Removes six governance and sponsor documents from the working tree — hazard
log, evidence brief, referral feasibility, outreach drafts, message review pack
and demonstration script. They are not building material. All six remain in git
history at 32d408c and restore with a single checkout when a sponsoring
service exists.
Trims the specification's open register to the four decisions that actually
affect the build, and drops the companion-document index whose every entry
pointed at a removed file.
Applies the Task 1 finding: the plan advertised toAwstParts(date, clock?) while
the implemented function takes one argument. Corrected before Task 2 so no
later task builds against a signature that does not exist.
Link gate: 1907 references resolve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… cancellation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…amed refusals
Adds canPerformCaringContactAction (team-scope check before role grant,
frozen ROLE_ACTIONS map, named refusal reasons) and
canApproveOwnAuthoredVersion (self-approval-denied). A table-driven test
over the frozen ALL_ACTIONS constant asserts every CaringContactAction is
explicitly granted or explicitly ungranted, so a future action cannot
silently default to allowed.
…y audited writes
Adds the storage contract every caring-contact change passes through, plus the
in-memory reference implementation and one shared contract suite that Task 11's
Postgres store will run unchanged.
Guarantees, each proved by a test that fails when the mechanism is removed:
* a replayed idempotency key returns the original result and writes nothing;
* a write appends exactly one audit event in the same uninterrupted commit as
the change, so a write that throws part-way leaves neither;
* a write against a stale version is refused by name, including when two
simultaneous writes race;
* a patient may hold only one non-terminal plan, checked across every team;
* reads are team- and capability-scoped and return empty rather than a
refusal that would reveal a record exists.
Absorbed contacts are stored in the terminal suppressed state, so dispatch
cannot key off sendAt. A recorded death cancels every non-terminal contact
outright, with no comparison of any send instant to now.
The episode shape moves from retention.ts to a new episode.ts and is re-exported
unchanged, so the store can project retention's own Episode type without
tripping the Task 8 sibling guard. There is still exactly one episode shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 9 could not record that a caring contact went out: permissions.ts had no concept
of a non-human actor, so there was no attributable author for a provider status and
Episode.counts.contactsSent/contactsDelivered could only ever report 0.
Adds a SystemActor with one system role (contactDispatcher), granted exactly four
contact-status actions and nothing else. The human and system grant tables are pinned
disjoint by test: a dispatcher can never activate, pause, withdraw, reassign, approve
or read anything, and no human role can write a delivery receipt by hand. Human roles
smuggled onto a system actor are ignored.
The store gains startContactDispatch, recordContactSent, recordContactProviderStatus
and recordContactMissed, each an ordinary idempotent, version-checked, atomically
audited write against objectType "contact". Only the write that BEGINS a dispatch is
gated on an active plan: once a contact is `processing` the send is committed, and the
contact lifecycle has no exit from `processing` other than sending, so gating the later
writes would strand a contact rather than protect anyone. A recorded death is not
carried by that gate at all — it cancels every unsent contact outright.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t as a safety stop
Task 9 routed death and death-correction through triggerServiceSafetyStop because no
action in CaringContactAction covered hospital events at all. The capability model
therefore said a recorded death was a service safety stop, which is not what happened.
Adds recordHospitalStatusEvent and routes every hospital status event through it. It is
a clinical write, granted to coordinator and teamLead; the auditor stays confined to
reading (rule 3), and is refused a readmission or a mobile-number change.
A death and its correction additionally accept triggerServiceSafetyStop, the one
capability every role holds. That preserves the property the old routing was reaching
for: recording a death must never be blocked by a permission check, because a refusal
would leave a plan sending to someone who has died. Proved by a contract test in which
an auditor is refused a readmission and still succeeds in recording a death.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…zero duplicate sends
The integration proof for Tasks 1-9: one twelve-month episode driven through the real
store, permissions, lifecycles and calendar, with ten scenarios plus the suppression
collision case.
simulation.ts adds no rules. What may be sent comes from the store's listSendableContacts
(contact state, never sendAt); whether a send is still on time comes from schedule.ts's
approved window, now exported as APPROVED_SEND_WINDOW / isWithinApprovedSendWindow rather
than restated here; whether a send may happen at all is the store's refusal, which the
driver records instead of pre-empting. The one policy no module owns — how many times a
transient provider failure may be retried, and how far apart — is a required input, not a
constant invented in the driver.
Proved: ten dispatches ascending with no duplicates; three attempts and never a fourth; a
retry that would leave the window (or roll into the next day's) abandoned and marked
missed rather than sent late; a pause permanently skipping months 2 and 3 with the
calendar untouched; a withdrawal cancelling everything later; a readmission that neither
resumes nor rebases nor admits a competing plan; no dispatch at or after a recorded death
under three separate retry states; a pause landing mid-dispatch yielding exactly one
outcome; identical dispatch days under a +/-5 minute clock skew; one audit event per write
with no mobile number, message body or patient name; and the collision case sending nine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… audit and RLS
The caring-contact schema, its row-level security, and the Postgres store, proven
against a real Postgres 17 in a disposable local container.
Schema (caring-contacts/supabase/migrations/, never the repository's live
supabase/migrations/):
- teams, actors, referrals, plans, contacts, pathway_versions, audit_events,
service_state, retention_state, plus contact_dispatches, idempotency_records,
and cultural_identity_reports.
- Cultural identity lives ONLY in the reporting projection; the patient row has
no such column.
- plans_one_non_terminal_per_patient: a unique PARTIAL index over patient_id
alone, so one team cannot start a second plan for a person another team is
already contacting.
- contact_dispatches_unique_attempt: unique (contact_id, attempt).
- No CREATE INDEX CONCURRENTLY; every migration replays as a no-op.
Row-level security is enabled and forced on every table, deny by default: each
policy compares the row's team to a transaction-local setting that resolves to
NULL when unset, so an unscoped session matches nothing. A cross-team select
returns zero rows rather than an error that would confirm the row exists, and
the anonymous role is granted SELECT deliberately so its denial proves policy
rather than a missing grant.
A DEFERRABLE constraint trigger fires at commit and fails any change to a
patient-bearing table that carries no audit event in the same transaction, so a
direct update cannot bypass the audit path.
The store reproduces the in-memory store's version-check ordering and its
active-plan dispatch gate, and the Task 9 contract suite now runs against both
implementations from one definition rather than being duplicated.
Also carried from Task 10: markMissed now accepts `processing` as well as
`scheduled`, so a contact abandoned after a provider timeout is no longer
stranded; and the retry policy (2 retries, 3 attempts, 45 minutes apart) has a
governed home in service-rules.ts instead of living in test fixtures.
tests/test-runner-safety.test.ts's live-test discovery guard was rewritten to
assert the loaded config's node project rather than one literal line of source,
and confirmed still able to fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sses at zero warnings
PAUSE_EVENT_TYPES was an `as const` array used only in a type position, which
this repository's zero-warning lint correctly flagged as an unused runtime
value. It is now a union type, which is what the code actually needed.
Found by re-reading the phase-gate output rather than trusting its exit code:
the gate had been piped through `tail`, so the shell reported 0 while lint had
failed on one warning and prettier had flagged files. Exit code alone is not
proof when a pipe is in the way.
Gate now, each with a real exit code: lint 0, prettier 0 across src, tests,
scripts, caring-contacts, docs and config, tsc 0, and the two affected suites
59/59.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…session scratch
The build ran through a ledger under .superpowers/, which is git-ignored. Every
decision taken on the owner's behalf lived only there and in one long
conversation, and both are losable. This records them where the repository
keeps them.
Contains: the three-phase scope and what Phase 1 actually built; the thirteen
decisions with their reasoning and what each costs if wrong; the deliberate
sabotage results, including the two tests found unable to fail and rewritten;
six open items for Phase 2; three open decisions for the owner; and the exact
command that restores the six governance documents from history.
Phase gate, each verified with a real exit code: 7,531 tests across 682 files,
tsc silent, lint zero warnings, Prettier clean, and 55 database tests against
Postgres 17 in a disposable container.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
19 test-first tasks covering the rules layer the seven undesigned screens need,
the production route group and four-state shell, the API boundary that audits
every view, and all 24 overlays against the frozen modality matrix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…domain so production can use it
The three patient-visible strings (PATIENT_VISIBLE_NO_REPLY_NOTICE,
EXACT_PATIENT_VISIBLE_MESSAGE, AUTOMATED_REPLY_RESPONSE) and the fictional
contact numbers previously lived only in a mockup component
(src/components/caring-contacts/mockups/personalisation-screen.tsx and
types.ts). Production code can never import from a mockup path
(eslint no-restricted-imports), so this moved them into the sealed domain
at src/lib/caring-contacts/:
- New src/lib/caring-contacts/synthetic-contacts.ts: FICTIONAL_CONTACTS_BY_ROLE,
DESIGNATED_FICTIONAL_MOBILE_NUMBERS and their types.
- New src/lib/caring-contacts/message-copy.ts: the three provisional
patient-visible strings (byte-identical, PROVISIONAL comments carried
across verbatim) plus their GSM-7 evidence, derived from the single
calculateGsm7 in message-policy.ts.
- The mockup's duplicated calculateGsm7/Gsm7Evidence was deleted; both
mockup files now import from the domain and re-export the same names so
every existing pinned test (tests/caring-contact-mockups.dom.test.tsx,
tests/caring-contact-product-redesign.dom.test.tsx) keeps passing
unchanged.
- Fixed two spots where the brief's plain "export { X } from '...'"
re-export would not have created a usable local binding: personalisation-
screen.tsx uses EXACT_MESSAGE_GSM7/EXACT_PATIENT_VISIBLE_MESSAGE/
PATIENT_VISIBLE_NO_REPLY_NOTICE internally (MessagePreview,
CompactMessagePreview, PersonalisationScreen), and types.ts uses
SyntheticPatientMobile as a field type on SyntheticPatient. Both files
now import-then-locally-export instead, which typecheck caught
(TS2304: Cannot find name 'SyntheticPatientMobile').
Mutation proof: temporarily removed one space from
EXACT_PATIENT_VISIBLE_MESSAGE (252 septets became 251). This correctly
turned red: tests/caring-contacts-message-copy.test.ts "keeps the pinned
GSM-7 evidence..." and tests/caring-contact-mockups.dom.test.tsx "derives
exact GSM-7 septets...". A same-length digit swap ("6 pm" -> "7 pm", the
brief's suggested example) does NOT change septets since both characters
are GSM-7 basic-set digits costing 1 septet each, so it does not exercise
any assertion — recorded here rather than used as the proof. Reverted the
space-removal mutation; final diff is byte-identical to the original
strings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e workspace performs
Adds the two dual-approval roles (clinicalProgrammeLead, livedExperienceRepresentative)
and the ten action names the Phase 2 screens need, none of which existed yet. Because
canPerformCaringContactAction denies by default, an unnamed action could not be granted,
so this is the gate every later Group 1 task passes through.
- publishPathwayVersion is granted only to clinicalProgrammeLead, never teamLead:
publication is the clinical act; the team lead approves and retires but does not publish.
- triggerServiceSafetyStop stays granted to every human role, including both new ones.
- The auditor gains only read/no-op actions (viewPatientRecord, manageNotificationPreferences,
enterTrainingMode) and stays confined from every plan-mutating action.
- UNGRANTED_ACTIONS stays a frozen empty array; every new action is granted to at least
one role.
Extended tests/caring-contacts-permissions.test.ts with the new roles/actions describe
block and widened its ROLES constant to include both new roles, which the pre-existing
completeness test needs since publishPathwayVersion is granted only to a role outside the
old three-role list. Verified test-first: the appended test failed for the right reason
(undefined ROLE_ACTIONS lookups for the not-yet-existing roles) before the registry
changes, and passed (90/90) after. Mutation check: granting publishPathwayVersion to
teamLead flipped exactly one assertion red (89/90), naming teamLead's decision; reverted.
Full unit suite (685 files) and typecheck both pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…approval
Spec 4.2: a confirmed wrong-recipient message, duplicate send, unauthorised
content, privacy/security incident, or loss of audit integrity halts ALL sending
across every patient and team. The first recorded stop is permanent -- a second
stop is refused rather than allowed to overwrite the original reason, actor,
time or note.
The restart cannot be one person's decision. All three of incidentLead,
privacySecurityOwner and clinicalProgrammeLead must be recorded, and the module
refuses a repeat ROLE (restart-approval-role-already-recorded) as well as a
repeat ACTOR in a different role (restart-approval-actor-already-recorded), so
three approvals mean three people. The service returns to running only on the
approval that completes the third distinct role; two approvals leave it stopped
with both approvals recorded. There is no force flag or override path.
describeServiceStop is banner text rendered on screens showing no patient, so it
deliberately excludes the free-text incident note, which a responder can write a
name or a number into.
Pure transitions: injected Clock, no storage, no permission check (the caller has
already asked canPerformCaringContactAction), no imports outside the sealed
domain.
The AWST +08:00 timestamp formatter moves from a private helper in audit.ts to
clock.ts as awstIsoTimestamp, and audit.ts now calls it. Behaviour is unchanged
(caring-contacts-audit.test.ts stays green); this keeps one timestamp format in
the domain rather than a second one drifting alongside the audit trail.
Mutation testing -- each applied alone, the named test observed red, then reverted:
1. restart on `approvals.length >= 2` instead of all three required roles
-> caught by "requires all three approval roles before it restarts"
(AssertionError: expected false to be true)
2. same-actor guard deleted from applyServiceRestartApproval
-> caught by "refuses a single person supplying more than one approval"
(expected { ok: true } to deeply equal { ok: false })
3. serviceStopBlocksDispatch hardcoded to false
-> caught by "stops the whole service and blocks dispatch"
(AssertionError: expected false to be true)
Verified: node scripts/run-vitest.mjs run tests/caring-contacts-service-state.test.ts
-> Tests 8 passed (8); full caring-contacts suite (15 files) -> Tests 316 passed (316);
npm run typecheck clean; prettier clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…, and name the stop service-wide
Review round 1.
describeServiceStop now takes ServiceStopBannerFacts -- { stopped: false } |
{ stopped: true; reason; restartApprovals } -- instead of the whole ServiceState.
The incident note is free text a responder types mid-incident and is the one
field guaranteed to hold patient data; the banner renders on every screen,
including ones showing no patient. Previously only a doc comment held it out
while it sat in scope on every line. Now the compiler does.
The discriminated shape was kept rather than a bare { reason, restartApprovals }
| null so that the existing assertion `describeServiceStop(runningService(team))
=== null` keeps testing the running-service contract instead of degrading to
null-maps-to-null. ServiceState stays structurally assignable, so no caller
changes.
New test: a stop whose note names a patient and a +61 mobile, asserting neither
substring reaches the banner while the reason and the "0 of 3" count still do.
ServiceState.teamId is renamed reportedByTeamId on both variants and on
runningService's parameter. The spec halts sending "across every patient and
team", but a field called teamId makes a per-team storage table the natural
implementation, which would leave every other team sending through the incident.
The type now says in a doc comment that the field is provenance only and that
storage must persist a single service-wide record, not one row per team.
Removed the unreachable "All approvals are in." branch: a stopped state can
never hold three approvals, because the third restarts the service.
Mutation testing, both halves run and reverted:
A. interpolate ${state.note} with the parameter left narrow
-> does not compile: TS2339 Property 'note' does not exist on type
'{ stopped: true; reason: ServiceStopReason; restartApprovals: ... }'
B. widen the parameter back to ServiceState, then interpolate ${state.note}
-> compiles, and is caught by "never leaks the incident note into the
banner, even when the note names a patient" (expected ... not to
contain 'Rowan'; the received banner carried the full note)
Restore verified byte-identical by diff before committing.
Verified: focused + audit + clock + isolation -> Tests 32 passed (32);
full caring-contacts suite -> Tests 317 passed (317); typecheck no diagnostics;
eslint clean; prettier "All matched files use Prettier code style!".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd urgent retirement
Adds applyPathwayVersionTransition (draft -> inReview -> approved -> retired,
plus publish) and retirementPausesFutureContacts, per Phase 2A task 4.
Self-approval is delegated to the existing canApproveOwnAuthoredVersion in
permissions.ts rather than re-implemented; its self-approval-denied reason is
surfaced unchanged. Two independent approvals are required (role and actor
each checked against prior approvals) before a version reaches approved, and
snapshot is never spread into or replaced by any transition.
Mutation proof (each reverted after confirming the expected failure):
- Changed the approved-state condition from "both required roles recorded"
to "approvals.length >= 1" -> 4 of 6 tests in
tests/caring-contacts-pathway-versions.test.ts went red (the two-role
gating test, the one-person-both-approvals test, the urgent-retirement
test, and the snapshot-immutability test all depend on reaching approved
only via the real two-role path).
- Removed the canApproveOwnAuthoredVersion delegation call -> exactly
"refuses the author approving their own version, with the shared reason"
went red, confirming that check is load-bearing and not decorative.
Test: node scripts/run-vitest.mjs run tests/caring-contacts-pathway-versions.test.ts
-> Test Files 1 passed (1) / Tests 6 passed (6)
Domain isolation: node scripts/run-vitest.mjs run tests/caring-contacts-domain-isolation.test.ts
-> Test Files 1 passed (1) / Tests 3 passed (3)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…amps
Fix round 1 for task 4: review found retire's legality gate had zero test
coverage (nothing called retire outside "approved") and that publish/retire
success paths set publishedAt/retiredAt but never asserted the value.
- Add "refuses retirement from every state except approved", exercising
draft, inReview, and retired, each asserting the exact
{ ok: false, reason: "pathway-not-retirable" } object.
- Assert published.publishedAt and routine/urgent.retiredAt are non-null
AWST timestamps ending "+08:00".
Mutation proof (both reverted, pathway-versions.ts diff empty afterward):
- Dropped the retire legality guard -> exactly the new retirement test went
red (1 failed, 6 passed), draft state returned ok:true instead of the
refusal.
- Left publishedAt as null on publish -> exactly the snapshot-immutability
test (which now asserts publishedAt) went red on
"expected null not to be null" (1 failed, 6 passed).
Test: node scripts/run-vitest.mjs run tests/caring-contacts-pathway-versions.test.ts
-> Test Files 1 passed (1) / Tests 7 passed (7)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

CI triage

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

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

Compared with main CI run #13299 (success).

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

@BigSimmoClaude

Copy link
Copy Markdown
OwnerAuthor

Run PR sweep update

Remote head is still cf03f99a4be3184a24bcda841fcf2e0acaf2fafc — the work below exists only in a local commit that could not be pushed (see Blocker).

Merge conflict: resolved (not staleness)

git merge-tree --write-tree origin/main cf03f99a4be3184a24bcda841fcf2e0acaf2fafc reported 11 real content conflicts, all in generated/tooling files, none in supabase/migrations/**, RLS/SECURITY DEFINER SQL, src/lib/rag/**, or clinical/source-governance content:

  • scripts/generate-site-map.ts, docs/site-map.md — combined this branch's /caring-contacts route description with main's new /dictionary/* routes; regenerated via npm run sitemap:update.
  • playwright.config.ts, scripts/playwright-pr-shards.mjs — combined this branch's caring-contacts-workspace spec pattern/shard entry with main's new ward-(management|coordinator) patterns/shard entries.
  • src/lib/tools-catalog.ts — combined the caring-contacts and ward-managementToolCatalogId union members.
  • tests/design-system-adoption.test.ts — updated the route-count census to 76 (59 baseline + main's 6 search-consolidation + 10 Ward Flow routes + this branch's 1 caring-contacts route); verified against the regenerated manifest (51/51 passing).
  • docs/codebase-index.md, docs/scripts-index.md — merged the product-pages table; regenerated counts via npm run docs:update.
  • docs/design-system/{ADOPTION.md,adoption-contract.json,adoption-manifest.json} — restored this branch's caring-contacts-workspace surface declaration into main's contract, then regenerated via npm run design-system:adoption:update.

Also fixed a legacy-shadow-alias ratchet regression the merge exposed (overlay-host.tsx used shadow-[var(--shadow-elevated)], tripping check:design-system-contract's ratchet 89→90); switched to the established shadow-[var(--e4)] resolved-token pattern already used in tools-search-results-page.tsx.

Two review threads fixed (locally; not yet visible on GitHub — see Blocker)

  • P1 "Isolate production-lock tests from runner demo flags" (tests/caring-contacts-session.test.ts): three production-denial assertions read real process.env instead of an explicit runtime, so this repo's own Cloud environment (scripts/setup-codex-cloud.sh, which exports both NEXT_PUBLIC_DEMO_MODE=true and PLAYWRIGHT_OFFLINE_MODE=true) could silently take the isolated-Playwright-server exception in a production-denial test. Passed an explicit empty runtime where the callee accepts one, stubbed both flags shut where it does not.
  • P2 "Audit the page's service-state read" (src/app/caring-contacts/page.tsx): extracted the audit-record-then-decide logic already inside readHandler into a new exported auditedRead helper in caring-contacts-server/handler.ts (readHandler now calls it too — byte-identical behaviour, proven by the existing 53 handler/session tests still passing) and routed the page's read through it, throwing to error.tsx on any non-"ok" result.

I replied in-thread (not resolving) on the third open thread — P1 "Share demo state across dev route boundaries" — pointing at the existing internal writeup in docs/caring-contacts/phase-2a-sdd-archive/condensed-service-bar-report.md that already documents this exact reproduction as a known, accepted, dev-mode-only (Turbopack module-registry) limitation that does not affect production. Left open for a human architecture decision rather than guessing at a fix.

Local verification (offline only, no provider-backed checks): typecheck clean, lint clean on all touched files, whole-tree prettier --check . clean, npm run check:design-system-contract passed, npm run docs:check-index/docs:check-scripts/docs:check-inventory/sitemap:check passed, npm run check:outstanding-issues passed, and the full offline tests/caring-contacts*.test.ts suite: 638/638 passing.

Blocker: push rejected by the local check:ledger-write-discipline pre-push guard

Same false positive a previous session on this branch already diagnosed and reported (see the 13:08 UTC comment above): the guard's fast-forward base-detection (guardBaseForRange in scripts/guard-push.mjs) uses this branch's own previous remote tip (cf03f99a, from before this merge) as the base, and against that ancient tip the large amount of legitimate, already-reconciled main ledger-inbox history that rides along in a 277-commits-behind merge looks like an unreconciled bulk edit. Checking against the PR's actual base confirms this is not a real problem:

$ node scripts/check-ledger-write-discipline.mjs --base origin/main --head HEAD
Ledger write discipline passed for origin/main..HEAD.

The guard's own message names its sanctioned override (SKIP_LEDGER_WRITE_GUARD=1 git push), but per this session's operating rules that override requires explicit user authorization and was not given, so the commit was not pushed. It is fully prepared and verified in the local worktree.

Required human action: either authorize SKIP_LEDGER_WRITE_GUARD=1 git push for this specific push, or push the prepared commit directly. Once pushed, required CI (static-pr, pr-required, build, etc.) will run for the first time against the merged tree — it has never run on this PR yet because GitHub could not build the merge ref while mergeable_state was dirty.

Separately, and independent of the above: the PR body's ## Clinical Governance Preflight checklist (all 7 items) and the Risk and rollout section (Risk:, Rollback:, Provider or production effects:, RAG impact:) are still blank — required because this PR introduces production (non-mockup) routes for clinical crisis-contact-adjacent content. Editing the PR body is out of scope for this sweep; a human needs to fill those in for PR policy to pass.

Merge left to the user.

BLOCKED


Run PR sweep — Claude Code


Generated by Claude Code

@BigSimmo
BigSimmo enabled auto-merge (squash) August 22, 2026 20:21
BigSimmoand others added 16 commits August 23, 2026 04:25
…b production-lock env in tests
Two review findings on PR #2279, fixed on top of the branch's current tip:
- The `/caring-contacts` page rendered `getServiceState` (which carries the
patient-data-bearing incident note) directly against the repository, so
rendering produced no `recordAccess` audit event and did not fail closed
when the access trail was unavailable -- unlike the equivalent API read,
which goes through `readHandler`. Extracted the audit/fail-closed wrapping
`readHandler` already did into a reusable `auditedRead` helper in
`caring-contacts-server/handler.ts`, and routed both `readHandler` and the
page's server-render `getServiceState` call through it. `readHandler`'s own
HTTP behaviour is unchanged (same 503/500/404/200 shape from the same
outcomes); its existing tests pass unmodified.
- Three production-lock/production-denial test cases relied on the ambient
process environment for `PLAYWRIGHT_OFFLINE_MODE`/`NEXT_PUBLIC_DEMO_MODE`
rather than controlling them explicitly, so they silently pass on Cloud
runners where `scripts/setup-codex-cloud.sh` exports both flags true (the
gate's own approved exception). Reproduced this locally by running the
affected suites with both flags forced true: 2 cases failed in
tests/caring-contacts-session.test.ts (as flagged in review) and, on
investigation, a third occurrence of the same pattern in
tests/caring-contacts-api-handler.test.ts. Fixed all three by passing an
explicit runtime object or stubbing both flags off, matching the existing
"the production lock" describe block's own pattern in
caring-contacts-session.test.ts. All three now pass with the flags forced
true or absent.
Added tests/caring-contacts-page-access-audit.test.ts to pin the new page
behaviour: an administrative access event is recorded on a normal render, and
the render throws (never renders) when either the read or the audit trail
itself fails.
Verification: npx vitest run on the three touched/added test files (52/52
passing, including with PLAYWRIGHT_OFFLINE_MODE/NEXT_PUBLIC_DEMO_MODE forced
true to reproduce the Cloud-environment condition), npx tsc -p
tsconfig.typecheck.json --noEmit (clean), npx eslint (clean), npx prettier
--check (clean).
RAG impact: no retrieval behaviour change -- this touches only the
Caring Contacts workspace's own audit/session code and tests, none of it
under src/lib/rag/** or any other protected retrieval/ranking surface.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbzjnWTeJ7GHBXBDL88Jgj
Pin the memoised Caring Contacts store on globalThis so a service stop
posted through the API is visible to the workspace page under next dev,
where pages and route handlers get separate module registries.
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Resolve generated outstanding-issues snapshot by regenerating it, and
keep both the scoped docs-link allowlist and main's applied-inbox
fallback in the docs link checker.
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
@BigSimmo
BigSimmo merged commit e4cbe8d into mainAug 22, 2026
26 checks passed
@BigSimmo
BigSimmo deleted the claude/suicide-contact-mockup-b5aaa0 branch August 22, 2026 21:09
BigSimmo added a commit that referenced this pull request Aug 24, 2026
…pty state, overlay commit contract (#2350)
* docs(caring-contacts): correct the retired-branch records, close the browser gate, capture the deferred findings
Phase 2A was squash-merged to main as e4cbe8d (#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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* docs(caring-contacts): record the owner's approval of all thirteen copy decisions
The owner answered 'go ahead with your recommendations' to all thirteen items on
2026-08-24. Recorded as the decision of record, with two qualifications that
approval alone does not settle:
A9 (add Lifeline 13 11 14) is approved in principle but BLOCKED. The
recommendation was conditional -- add Lifeline and drop the Fictional Support
Line once a real crisis number is chosen -- because the message sits about nine
characters from its two-segment maximum, so nothing can be added until something
comes out. No real number exists, and the owner was explicitly asked to name what
goes. An implementer must not pick the removal itself.
A4 (the closing message) is approved as a deferral, not as text: the refusal path
is buildable now, the wording waits for a lived-experience representative.
Patient-visible copy is no longer frozen, but every change must cite its item
number and still live only in the sealed domain's message-copy module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(caring-contacts): remove the freeze line the approval banner contradicts
The approval banner lifted the copy freeze, but the line directly beneath it still
said wording stays frozen until the owner answers. That is the same
self-contradiction this session just criticised in phase-2a-handoff.md, created
the same way -- a true sentence left in place when the thing it described changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* plan(caring-contacts): open the Phase 2B ledger, run the pre-flight scan, add Task C
The pre-flight scan found one real defect in the plan before any dispatch: the
design-corrections table routed correction #2 to 'Group 3, Task 11', but Task 11
is Group 1's overlay wiring and Group 3 is Tasks 15-16. An implementer would have
received a requirement it had no surface for. Fixed as Ruling 73.
Adds Task C -- the owner's six approved copy changes, batched into one dispatch
per the method's rule about small same-shape work. A9 (add Lifeline) is
deliberately excluded: it is approved in principle but conditional on a real
crisis number existing, and dispatching it would force an implementer to choose
which patient-facing sentence to delete.
Rulings 73-78 recorded, each with what it costs if wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* plan(caring-contacts): Task C brief, and the two rulings its conflicts forced
Ruling 79: the owner's approved 'refuse any message containing Fictional' cannot
be implemented as a prohibited term, because both approved patient messages
contain 'Fictional Support Line' -- every existing message would be invalid and
the check would have to be disabled to ship. Implemented instead as a validator
issue plus an explicit synthetic-acknowledgement flag on each call site, so a
real send path has to opt in deliberately rather than fail silently.
Ruling 80: A3's 'something automatic comes back' fits only in the reply message.
Message A is 252 septets against a two-segment ceiling. Measured with the repo's
own calculateGsm7 rather than estimated -- the proposed reply is 210 septets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* plan(caring-contacts): Task 1 brief -- the shared empty-state component
Carries Ruling 81: EmptyState is its own component and does not render
AutomatedState internally. The two have different triggers -- AutomatedState is
for the system acting on its own, an empty list is usually the user's own filter
or simply nothing existing yet -- and AutomatedState's alert icon and state-name
aria-label are both wrong for 'no patients yet'. The filtered variant reuses its
why/what-changes-it wording shape so a clinician learns one pattern.
The brief models the two emptinesses as a discriminated union rather than
optional strings, because an optional reason is a reason that will be omitted,
and a filtered-empty list that says only 'nothing to show' is indistinguishable
from an empty caseload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(caring-contacts): drop the unverifiable storage claim from the automated reply (A2 + A3)
Owner-approved 2026-08-24. The reply's first sentence claimed replies "had not
been seen by anyone and had not been kept" -- a firm storage claim about a
system with no telephony provider yet, so nobody could currently know if it
was true (A2). It also left a patient who had just been told "no one reads
this" unable to tell a reply was automatic rather than human (A3). Replaces
the sentence with wording that states only what the system can actually know
and names the reply as automatic. Verified 210 septets / 2 segments / GSM-7
valid (was 218). EXACT_PATIENT_VISIBLE_MESSAGE is untouched -- it has no
segment headroom left, so the "reply is automatic" fact lives only here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(caring-contacts): refuse an unacknowledged fictional contact detail (A1 / Ruling 79)
Both approved patient-visible messages name the reserved fictional crisis
number on purpose, so a bare prohibition on the word "Fictional" would make
every existing message invalid and the check would have to be disabled to
ship -- worse than no check. Instead validateGovernedMessage now always
reports fictional-contact-detail-present when a message contains the marker
(derived from message-rules.ts's crisisSupportContact, not hard-coded a
second time), unless the caller passes the new, explicit
syntheticFictionalContactsAcknowledged: true. Existing tests that build
compliant first/closing messages from the crisis contact are updated to pass
the acknowledgement, since they were never testing this rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(caring-contacts): refuse loudly when a closing contact has no authored body (A4)
No closing message has ever been written -- that wording is a clinical
decision deferred to a lived-experience representative, not an
implementation gap. resolveClosingContactMessageBody is the refusal only:
it never returns an empty string, never falls back to another message's
text, and never silently drops the contact when no authored body exists.
No closing-message wording is drafted here.
No existing seam resolved a contact's message body anywhere in this domain
(checked schedule.ts, simulation.ts, repository.ts, model.ts): PlannedContact
carries a messageType but no body content, and nothing supplies one yet. This
function is the mechanism a future sender will call once that seam exists; it
is deliberately not wired into schedule.ts/simulation.ts here, since doing so
would require inventing where an authored closing body comes from -- exactly
the decision this task defers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(caring-contacts): narrow "lead" to its commercial sense only (B2)
lowerText.includes("lead") also matched the ordinary English "the incident
lead" and "the clinical programme lead" -- job titles that appear in the
service-stop wording -- so a message using the word correctly would be
rejected. Word-boundary matching alone would not have fixed this (both job
titles contain "lead" as a whole word too), so "lead" is narrowed to a
commercial-specific form instead: a marketing-word modifier ("sales lead",
"a new lead") or companion ("lead generation", "lead conversion").
The narrowing lives as a per-term pattern override in message-rules.ts
(prohibitedTermPatternOverrides), keeping message-policy.ts's mechanism
generic. Every other prohibited term keeps its exact substring behaviour --
one covering test per term proves it, since narrowing one term is precisely
the change most likely to quietly widen what the rest of the list allows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(caring-contacts): scan interface string literals for prohibited vocabulary (B3)
Until now the prohibited-word ban ran only against outgoing messages
(message-policy.ts) and the 24 frozen overlay definition rows
(caring-contacts-overlay-definitions.test.ts) -- nothing checked the words
on a screen, so it was policy held by people rather than software. This
scans every string/template literal under src/components/caring-contacts/
workspace/** and src/app/caring-contacts/** against the existing wider
CARING_CONTACTS_PROHIBITED_LANGUAGE vocabulary.
src/components/caring-contacts/mockups/** is out of scope by construction
(not one of the two scan roots) -- it is frozen design scratch that 404s in
production and knowingly contains one prohibited phrase the owner ruled
(B4) to leave alone.
Extraction uses a small character-by-character scan rather than a regex
over the raw source: a naive quote-matching regex treats JSDoc inline-code
backticks (`` `useSearchParams` ``) as template-literal delimiters, and an
odd count across a comment pairs unrelated spans into one giant fake
literal spanning most of the file. className attribute values are excluded
before extraction -- narrowing which literals reach the scan, not adding a
file to an ignore list -- because they carry CSS custom-property names
(var(--safe-area-bottom)) that contain "safe" as a substring unrelated to
interface prose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(caring-contacts): bound the B3 fixture cleanup's rmSync retries
npm run test's repo-wide test-runner-safety.test.ts requires every recursive
rmSync in a test fixture to pass maxRetries/retryDelay, guarding against
Windows file-lock flakiness on cleanup. The B3 fixture cleanup added in the
previous commit was missing them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(caring-contacts): Task C report -- six copy/policy changes, mutation-proven
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task C evidence, and the zero-caller finding
Verified the two full-suite failures myself rather than accepting the report:
both are gate-receipts file-mode tests failing in chmodSync, on a Windows drive
that cannot represent file modes. Environmental, and a third known local failure.
Records the consequential finding: validateGovernedMessage has zero production
callers. The brief assumed callers to update and there are none. The checks are
real but guard a send path that does not exist yet, so 'the validator refuses
this' must not be read as 'the system refuses this'. Captured as a P2 issue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task C review verdict and fix round 1
Spec passed. Three Important findings, four Minor. Two lessons recorded that
generalise beyond this task:
An allowlist cannot close an open-ended set. B2 narrowed the 'lead' prohibition
by enumerating commercial phrasings, so everything unenumerated is now permitted
-- 'lead magnet', 'qualify this lead' and others all pass and all previously
failed. Enumerate the safe set, never the dangerous one.
A guard on a chokepoint fires; a guard beside one does not. A1 and A4 both look
'unwired' and have opposite futures: A1 sits inside validateGovernedMessage which
any sender must pass, while A4 is a standalone function nothing obliges anyone to
call. Ruling 83 therefore refuses to record A4 as closed.
Ruling 82 promotes the A1 marker finding from Minor: it matches the label
'Fictional Support Line', not the reserved number, and the number is the artefact
that would actually reach a patient.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(caring-contacts): fix round 1 -- seven review findings (B2, A1, B3, comment)
Fix round 1 against Task C, six of seven fixable findings (the seventh, A4's
standalone resolveClosingContactMessageBody, was recorded and reported to
the owner instead of changed -- it rides no chokepoint yet, and inventing
one is out of this task's scope).
Important 1+2 (B2): the first "lead" override was an ALLOWLIST of nine
commercial modifiers/companions, and commercial vocabulary is open-ended, so
anything not enumerated passed silently -- verified newly permitted "lead
nurturing", "lead magnet", "lead source", "leads database", "lead gen",
"qualify this lead", "convert the lead", "this lead is hot", "your lead".
Inverted: COMMERCIAL_LEAD_PATTERN now refuses "lead"/"leads" as a whole word
BY DEFAULT via a negative lookbehind, exempting only the closed set of job
titles this domain's own wording uses -- incident lead, programme lead,
clinical lead, team lead, service lead. The "scoring?" typo (matched "lead
scoring" but not "lead score") disappears with the allowlist it lived in;
confirmed via a dedicated "lead score" test.
Promoted Important (A1): the marker was `crisisSupportContact.split(":")
[0]`, i.e. the LABEL "Fictional Support Line" only -- a message carrying the
bare reserved NUMBER with no label raised nothing, which is the shape that
would actually reach a sender. fictionalContactMarkerPattern is now
`/Fictional/i` plus every reserved number in synthetic-contacts.ts
(escaped), so relabelling, reordering, or dropping the label entirely are
all still caught. One pre-existing rule-6 test (contains-patient-mobile)
used +61 491 570 006 -- itself one of the four reserved numbers -- as its
example patient mobile; its expectation is updated (not loosened) to the
now-correct two-issue result.
Minor 5: pins prohibitedTermPatternOverrides to exactly {"lead"} so a future
override for another term cannot land unnoticed by this regression suite.
Important 3 (B3): the interface-vocabulary scan only saw quoted/template
strings, missing the plain-JSX-text form this tree actually writes copy in
(shell.tsx, loading.tsx). Added a second raw-prose pass (comments and
className values stripped, everything else scanned as-is) and a fixture
test proving it catches a word planted as bare JSX text between tags, not
just inside quotes.
Minor 7 (B3): the real-tree scan now asserts filesScanned > 0, closing the
vacuous-pass hole a root with no matching files would otherwise leave open.
Minor 6: reworded the AUTOMATED_REPLY_RESPONSE comment claiming content "is
discarded... nothing is stored" as a design INTENT/contract rather than
settled fact, so it no longer contradicts the very next paragraph explaining
that no telephony provider exists yet to make that claim true or false.
Every fixable finding mutation-proven: B2 mutated back to the old allowlist
(the CRM test goes red) and to a bare `\bleads?\b` with no exemption (the
job-title test goes red); A1 mutated to drop the number half of the pattern
(the bare-number and four-numbers tests go red); B3's raw-prose pass and
floor assertion each mutated out and confirmed red. All mutations confirmed
present in the tree via direct file inspection before trusting the red
result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(caring-contacts): Task C fix-round-1 report
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task C fix round 1, with the B2 inversion verified independently
Executed the new negative-lookbehind pattern against 18 cases rather than
accepting the report. All ten previously-leaking commercial phrasings are now
refused -- including 'lead score', the case the scoring? typo let through, so
that finding is genuinely moot rather than relocated. All eight job-title and
ordinary-English cases still pass.
Records the rule the inversion confirms: when a check must separate a safe set
from a dangerous one, enumerate whichever set is CLOSED. Five job titles are
closed; commercial vocabulary is not. That test now applies to every allowlist,
ignore list and exemption the rest of this plan adds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task C COMPLETE -- all seven findings addressed
Re-review re-derived every claim by execution rather than reading assertions. Two
mechanism-level checks worth keeping: the A1/patient-mobile double report is
benign because the patient-mobile check is independent of the acknowledgement
flag, so acknowledging silences the noisy code and keeps the safety-critical one;
and the global-regex handling is correct, with the non-global copy used for
.test() and the global copy only with matchAll.
B2's mutation proof is two-directional as requested -- reverting to the allowlist
reddens the refusal test, widening to a bare pattern reddens the exemption test.
Those bracket the behaviour rather than being two views of one assertion.
Six minors deferred to the final whole-branch review, including a new one: the
job-title exemption needs whitespace adjacency but this domain writes 'team-lead'.
Inert today -- outside the scan roots, absent from both messages, and the
validator has no production callers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* plan(caring-contacts): reading the API layer cut one task and corrected two
Went to write Task 2's brief, read the code it was meant to extract a pattern
from, and found the pattern already there. Two more premises fell the same way.
Ruling 84 cuts Task 2: readHandler already is the list-read pattern, used by
eight routes, four of them sharing the collection objectId convention. One
requirement survives into the first list route -- a contract test pinning that an
empty list is 200 with an empty array, never a 404, since auditedRead maps a null
release to denied and an empty array is neither.
Ruling 85: Task 5 builds no API. GET /api/caring-contacts/plans already lists
team plans. The patientDirectory object type is not an unwired gap -- the
referrals route already uses it, for patients who may not yet have a plan.
Ruling 86: design correction 1 is already in the domain. schedule.ts takes and
validates firstContactDate and the plans schema accepts it; only the screen
control is missing.
All three came from recon reports that were factually correct and whose
implications I carried too far. Before a brief says build this, open the file.
The cost of skipping that is not a wasted task, it is a second implementation of
something that already works, sitting beside the first, both maintained.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(caring-contacts): add the shared EmptyState component (Phase 2B Task 1)
Adds src/components/caring-contacts/workspace/empty-state.tsx, exporting
EmptyState -- the one shared empty-list surface the four Phase 2B list
screens (patients, schedule, templates, team) will use, so each does not
invent its own.
Modelled as a discriminated union on `kind`: "no-data" (nothing exists yet)
and "filtered" (a filter/search is hiding existing records) cannot be
confused with each other, because "filtered" requires its `because` and
`changedBy` at the type level -- there is no shared optional field a caller
could omit. The "filtered" branch reuses AutomatedState's "Why: .../What
changes it: ..." wording shape without rendering AutomatedState itself
(Ruling 81): the two have different triggers, and AutomatedState's
CircleAlert icon and state-name aria-label are wrong for "no patients yet".
A Server Component with no hooks (Ruling 13): the optional `action` slot
takes an already-built ReactNode (a <Link>, a form-submit button, or an
UnavailableDestination) rather than raw onClick/href props, the same way
ServiceStateBanner hosts UnavailableDestination as a child without becoming
a Client Component itself.
The icon started as lucide-react's Inbox and had to change to FolderOpen:
tests/caring-contacts-interface-vocabulary.test.ts's raw-prose scan caught
the bare identifier "Inbox" as the prohibited reply-monitoring/marketing
term "inbox" (CARING_CONTACTS_PROHIBITED_LANGUAGE), even though it was never
in a string literal -- a real catch by that guard, not a false positive.
tests/caring-contacts-empty-state.dom.test.tsx: 9 tests covering both kinds'
required copy, the absence of the other kind's wording, the optional action
(rendered vs. omitted, and genuinely actionable), a type-level compile
check that "filtered" cannot omit because/changedBy, a 320px-container
render, and the forced-colors override class. Mutation-tested: forcing the
"filtered" branch to always render the "no-data" JSX (dropping the reason
and remedy) reddened exactly the two tests that read that content; reverted
and confirmed green again.
docs/design-system/adoption-manifest.json regenerated via
`npm run design-system:adoption:update` -- the only change is the new test
file being recorded against ui-primitives.tsx's testFiles array.
Not wired into any screen (later tasks' job). Does not modify
automated-state.tsx, shell.tsx, or any route.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* plan(caring-contacts): Ruling 87 -- Task 3 cannot ship a trigger without the commit contract
Verified in code before writing the brief. openWorkspaceOverlay already exists,
is exported and is DOM-tested, so Task 3 was never going to build an opening
mechanism. Reading it exposed the thing that matters instead.
WorkspaceOverlays' commit callback closes the overlay and records nothing, with
an honest comment noting this is safe because nothing in the workspace opens an
overlay yet, so no control advertises an action it does not perform. Task 3 is
precisely what would break that clause: the moment a screen can open an overlay,
its confirm button becomes a control that advertises an action the system does
not perform, which is what the button-wiring gate forbids.
So the trigger and the commit contract ship together, and the trigger requires a
commit handler rather than defaulting to a no-op. A screen must be unable to open
an overlay it has not wired, and the compiler is what finds the omissions rather
than a later sweep.
The general shape, worth keeping: a mechanism that is safe only because nothing
reaches it is not safe, it is unreached. Before making something reachable, check
what its arrival makes true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* plan(caring-contacts): Task 3 brief -- trigger plus commit contract
Carries Ruling 87 into the brief, and tells the implementer plainly that the
overlay opening mechanism already exists so it does not rebuild one. The task is
the small client control plus the type-level requirement that a screen cannot
open an overlay it has not wired.
Leaves one genuinely open design question to the implementer with instructions to
choose deliberately and record what it rejected: WorkspaceOverlays is rendered
once by the shell rather than per screen, so a screen's commit handler has to
reach it somehow, and the obvious answers each carry costs. If the honest answer
is that it needs a decision above its level, it is told to say so rather than
pick silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(caring-contacts): Task 1 report -- shared EmptyState component
Records the build of src/components/caring-contacts/workspace/empty-state.tsx,
the TDD red/green proof, the mutation proof, the interface-vocabulary catch
(Inbox -> FolderOpen), and the full verification chain (vitest, full test
suite, typecheck, lint -- including the two bounded lock-contention retries
before typecheck acquired the repo's heavy-run lease).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task 1 built, and the lock incident that corrected the briefs
The implementer paused mid-task on the heavy-run lease with its work uncommitted,
on a machine that has destroyed four working directories mid-session. Resumed
with an explicit ordering -- commit first, then retry the gate, bounded -- which
is now standing for every remaining brief. Machine health was measured rather
than assumed: node --version in 0.083s, so ordinary lease contention.
Records that Task C's interface-vocabulary scan caught a defect in Task 1 one
task after being built: a lucide Inbox icon, rejected because inbox is banned as
reply-monitoring language. It fired on a bare identifier rather than prose, which
is exactly the deferred concern Task C's re-review raised -- so that concern is
real and will recur. Whether it was a false positive is put to the reviewer
rather than settled here, with the note not to narrow the scan merely because it
was inconvenient once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task 1 review -- Ruling 88 renames the component, and my brief caused the collision
The reviewer re-ran both mutations rather than reading the report, and found the
described mutation does not produce the reported numbers: mutating the filtered
branch gives 1 failed, not 2, because with the guard hardcoded true that branch
is dead code. The evidence is real, the account of which branch was mutated is
not. Second time on this programme that re-deriving a self-reported mutation
changed the answer.
Ruling 88 renames to ListEmptyState. ui-primitives.tsx already exports an
EmptyState and the adoption-manifest generator matches tests to components by a
bare name regex with no import awareness, so the caring-contacts test now falsely
counts as coverage for a shared primitive it never imports. False evidence about
test coverage in a governance artifact. My brief mandated the colliding name.
Also settles two open questions: action: ReactNode is correct because an onClick
prop cannot cross a Server-to-Client boundary; and the vocabulary scan will not
be narrowed after firing on an identifier, closing Task C deferred item 2 as
decided rather than deferred.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(caring-contacts): Task 1 fix round 1 -- rename collision, accessible group, mutation-proof correction
Three review findings addressed:
1. Renamed EmptyState -> ListEmptyState (file, exports, test imports/usages).
src/components/ui-primitives.tsx already exports a registered design-system
primitive called EmptyState, used across 43 files.
scripts/generate-design-system-adoption.mjs credits test coverage by
matching \bName\b against raw test-file text with no import-path
awareness, so the bare name in this component's test file was crediting
tests/caring-contacts-empty-state.dom.test.tsx as coverage for the
unrelated ui-primitives EmptyState in the generated adoption manifest --
false evidence in a governance artifact. ListEmptyState does not match
\bEmptyState\b, so it cannot recreate the collision. Manifest
regenerated; the false testFiles line is confirmed gone from the
ui-primitives EmptyState entry (see report for the diff).
2. Wrapped the whole component in role="group" aria-label={props.heading},
reusing automated-state.tsx's accessible grouping (Ruling 81 forbade
rendering AutomatedState, not reusing its structure): a screen reader
that reaches the state enters a named group and finds "Why:"/"What
changes it:" without hunting elsewhere. Applied to both kinds, not only
"filtered", for one consistent grouping pattern. Uses the same
aria-label (not aria-labelledby) technique automated-state.tsx already
proves hook-free. Two new tests assert the group and its name for each
kind.
3. Re-ran the brief's named mutation correctly. The prior report described
mutating the "filtered" branch but had actually hardcoded the ternary's
guard to `true` AND replaced the no-data branch's real content with a
placeholder string -- coarser than specified, and it broke both kinds
equally rather than leaving no-data's real output intact. This round's
mutation touches only the guard (hardcoded true) plus a necessary
type-safe cast, reusing the actual no-data code path rather than a
placeholder: for a genuine no-data instance the cast is a no-op
(identical output); for a genuine filtered instance, reading a field
that does not exist on it renders nothing. Result: exactly 2 of 11
tests reddened, both in the "filtered" describe block; all "no-data"
tests stayed green throughout. Reverted and confirmed 11 passed again.
Report rewritten to describe this accurately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* plan(caring-contacts): Ruling 89 merges Task 4 into Task 5
Task 4 would have created the patients page rendering the empty state before
Task 5 gave it data -- a caseload screen saying 'No patients yet' whether or not
patients exist. That is precisely the defect Task 1's component was built to
prevent, and the orphan-route gate would have forced an inbound link at the same
moment, making the false state reachable rather than merely present.
Its real deliverables travel with the screen that has real data: the href, the
sitemap update, the codebase-index entry and the reachability assertion.
Same shape as Ruling 87: before making something reachable, ask what its arrival
makes true. There it was confirm buttons that do nothing; here a caseload screen
that says empty when it is not. Both invisible while unreachable.
Also records what already exists: caring-contacts-routes.ts declares all fifteen
destinations plus typed helpers for every dynamic route, and shell.tsx's own
comment says lighting one up is exactly adding an href.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(caring-contacts): correct Task 1 report -- mutation proof, plus fix round 1
Rewrites the "Mutation proof" section: the original described mutating the
"filtered" branch but the edit actually applied hardcoded the ternary guard
AND replaced the no-data branch's real content with a placeholder string --
coarser than the brief specified, breaking both kinds equally instead of
leaving no-data's real output intact. The section now describes the
corrected mutation (guard hardcoded true, plus a type-safe cast reusing
no-data's real render path rather than a placeholder) and its actual result:
2 failed | 9 passed (11), both failures in the filtered describe block only.
Adds the fix-round-1 record: the EmptyState -> ListEmptyState rename (with
the adoption-manifest diff proving the false test-coverage attribution to
ui-primitives.tsx's EmptyState is gone), and the role=group accessible
grouping added to both kinds. Updates the verification-chain section with
this round's fresh typecheck/lint/full-test-suite results.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* plan(caring-contacts): Task 5 brief -- the Patients directory, absorbing Task 4
The first real screen of Phase 2B. Carries Rulings 85 and 89: no data source to
build because GET /api/caring-contacts/plans and listPlans already exist, and the
navigation link ships with the real screen rather than ahead of it so the page is
never reachable in a state where it can say 'No patients yet' untruthfully.
Points the implementer at the Today page as the established server-read pattern
rather than describing it, and names the exact access identities to reuse so the
access trail does not grow a second vocabulary for the same read.
Carries the one requirement that survived cutting Task 2: a test pinning that an
empty caseload renders the empty state on a success path and never a 404, since
auditedRead maps a null release to denied and an empty array is neither.
Forbids getEpisode, which is the only read releasing patient name, mobile,
identifiers and cultural identity, and tells the implementer to report rather
than decide if the approved design appears to need it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task 1 COMPLETE -- all three findings addressed
The re-reviewer traced the corrected mutation through all eleven tests from the
code rather than accepting the count, and explained why exactly two fail rather
than three: the action slot sits outside the mutated ternary. That detail is what
separates a re-derivation from a re-reading.
Rename verified end to end -- no bare EmptyState word survives in the test file,
the move is a real rename, and the manifest diff is exactly the one deleted line.
The surviving mentions in the component's comments are safe because the generator
builds its testFiles list from a tests-only walk, checked in the generator source.
Task 3 goes to opus rather than the default implementer tier: it carries a real
architectural decision about how a screen's commit handler reaches an overlay
host the shell renders once, and every obvious answer costs something different
against the client-payload limit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(caring-contacts): Task 3 -- overlay trigger and the commit contract it must ship with
Ruling 87: making an overlay reachable makes its confirm control a control that
advertises an action the system does not perform, so the trigger and the commit
contract land together.
- overlay-commits.ts: WorkspaceOverlayCommit (record | unavailable) and a
single-slot handoff the opening control writes, with the rejected alternatives
(context provider, per-screen host, mount-time registry) recorded in the file.
- overlay-trigger.tsx: the required-commit client control; an unknown overlay id
throws at render rather than opening nothing.
- OverlayHost gains a required commitUnavailableReason, refusing the action
whatever the row's mutatesState says.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* 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 <noreply@anthropic.com>
* ledger(caring-contacts): Task 3 review -- Ruling 90 overrules the blanket refusal
Eight of the 24 overlay rows have mutatesState: false and their controls are
exits, not confirmations -- 'Sign in again', 'Try connecting again', 'Back to the
plan'. None records anything, so Ruling 87 never reached them, and refusing them
renders a sentence that is false about the control it points at.
On session-expiry and offline-banner it is actively harmful: both are
recovery-only, so Escape and backdrop are deliberately inert, and their only
control is now aria-disabled. That is the one overlay a person must not be able
to walk away from, and it offers them nothing. Live today, since the shell
renders the host and any deep link reaches it.
The lesson generalises: a rule derived from a real defect was applied uniformly
to a set whose members differ in exactly the property the rule depends on. The
rule was right; its domain was assumed rather than checked. The frozen matrix
already carried the flag that answers it row by row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): browser gate green at Task 3 head -- 32 passed
Closes Task 3's concern 2 by measurement rather than inference. The implementer
declined to claim it passed and the reviewer judged the risk real but its size
understated; the margin held. 32 passed, exit 0, no failures.
States plainly what the result does not cover: it was taken before fix round 1,
and Ruling 90 changes which rows render the paragraph at all while the confirm-
sequence fix changes what renders at commit time. This green must be re-taken
after the fixes. A browser result names the commit it ran against or it means
nothing -- the rule this branch learned when a concurrent session invented both a
phantom failure and a phantom pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(caring-contacts): Task 3 fix round 1 -- Ruling 90, the confirm flash, entry-bound commits, async signature
Important 1 (Ruling 90): the no-staged-commit refusal now carries scope
recording-rows-only and is withheld from the 8 mutatesState:false rows, whose
controls are exits rather than confirmations -- and two of which are
recovery-only, so refusing their single control left a person with nothing to do.
A caller-stated unavailable refusal still reaches every row.
Important 2: the slot is no longer cleared inside the confirm handler, which
emptied it while the URL still named the overlay and flashed the refusal in the
frame just after a withdrawal was confirmed.
Important 3: the staged commit is bound to a one-shot token carried in the pushed
history entry, not to the overlay id, and is reconciled in one effect. That closes
Back (which never calls onClose), a commit outliving its screen, and one list
row's commit answering another row's overlay.
Important 4: record widened to (overlayId) => void | Promise<void>; a rejection is
re-raised during render so it reaches the route error boundary. The policy for what
a failed write should do to the interface still defers.
M-3: the trigger ships a default token-based surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(caring-contacts): Task 3 fix round 1 -- record the four fixes and correct two claims
Corrects the section that argued for the blanket refusal (now OVERRULED, Ruling 90)
and the M-4 length estimate: NO_STAGED_COMMIT_REASON is 126 characters, roughly
4-5 lines at 390px, not 'one extra short paragraph'. Adds the fix-round mutation
table and reports one chained run that short-circuited and never happened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(caring-contacts): Task 3 fix round 1 gates -- 9823 passed, typecheck and lint clean
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(caring-contacts): Task 3 -- the browser green predates fix round 1 and is owed a re-take
The coordinator's 32-passed result in 9cc7fa5 ran against the pre-fix head. Ruling 90
changed which rows render the refusal and the confirm-sequence fix changed what renders
at commit time, so that evidence does not name this head.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task 3 fix round 1, and a fifth check that could not fail
Records two corrections the implementer made to my framing. My suggested remedy
for the flashing refusal -- close before clearing -- would not have worked, since
both are synchronous while the URL change is not. A controller's suggested remedy
is a hypothesis like any other, and this one was falsified by someone reading the
code more carefully than I did.
The valuable part is a gate it caught not running: grep -c chained with && before
the test, where the mutation had stripped the very classes being counted, so grep
exited non-zero on a legitimate no-match and the test never ran. No summary line,
and it reads as 'confirm the mutation landed, then test it'.
Fifth member of a family this repo keeps meeting. The tell that unites them: ask
what the check prints when it fails, and confirm you have seen that output once.
Four of the five produce no output at all in the failing case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): browser gate re-taken post-fix -- 32 passed at 1306c0b
The earlier green was correctly declared stale: Ruling 90 changed which rows
render the refusal paragraph and the confirm-sequence fix changed what renders at
commit time, so neither the input nor the output of the assertion that mattered
was the same thing twice.
Both greens read 32 passed, which is why the rule matters rather than why it does
not. Identical numbers across two different trees are two separate measurements
that happen to agree, and only one describes the code that now exists. Had the fix
broken the viewport assertion, the stale green would have said 32 passed about a
tree nobody was shipping.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ledger(caring-contacts): Task 3 COMPLETE -- Group 0 finished
All five findings addressed. The re-reviewer checked Ruling 90 against
definitions.ts itself: exactly 8 non-mutating rows, the withholding reads the
frozen flag, and definitions.ts is untouched across the task range, so the flag
was consulted rather than edited.
It corrected one of my claims. I described the fix as a one-shot token; it is two
mechanisms -- the token and a reconciliation effect -- and neither alone closes
the set. A mechanism described as one thing that is actually two is a description
under which a later maintainer can delete half and still believe the comment.
The generated manifest line is a different problem from Ruling 88 despite the
same weak generator: there the attribution was false, here it is true. Same
mechanism, opposite verdicts, which is why 'we saw this before' is not itself an
answer.
Group 0 done: Task 2 cut, Task 4 merged forward, leaving ListEmptyState and the
overlay trigger with its commit contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@BigSimmo@claude@cursoragent