Caring Contacts: extend the prohibited-language scan to the sealed domain, demo seed and API routes - #2521
Conversation
…ibited-language helper #AGRAKQ asked for the message-side B2 narrowing to be copied into tests/helpers/caring-contacts-prohibited-language.ts, which it recorded as still matching /\bleads?\b/ and so still flagging ordinary job titles like "the incident lead". That row was overtaken before it could be acted on. Ruling [143] (2026-08-27) put the three-alternative narrowing into this helper, and ran the fix the opposite way round from what the row anticipates: the interface definition was the STRICTER of the two, so COMMERCIAL_LEAD_PATTERN in src/lib/caring-contacts/message-rules.ts was tightened to mirror it term for term, rather than the reverse. Both surfaces now refuse "leads" outright, refuse "lead" before a commercial companion word even when a job-title qualifier sits immediately before it, and otherwise exempt only incident/programme/clinical/service/team lead. There is nothing left to copy, and the regex is unchanged here by a single character. What was left wrong is the docstring. It still said word boundaries were all that kept "lead" off "already" while catching the sales sense of the word -- the pre-narrowing rationale, and the reason this row was filed against a file that had already been fixed. It now records the actual three-alternative rule in plain words, why refusal is the default rather than an allowlist of sales phrasing (that allowlist was itself the earlier defect), that the same rule is necessarily written a second time in message-rules.ts because nothing under src/lib/caring-contacts/** may import a test helper, and that the Ruling [143] parity block is what holds the two in step -- so anyone editing either definition knows which test to run. Closes #AGRAKQ as already-resolved via an inbox request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
…e wording #Z5P2BW recorded two joined defects in the interface vocabulary scan. The first is already closed: Ruling [143] narrowed the "lead" rule on 2026-08-27, so the correct plain-words label "the clinical programme lead" now passes alongside the raw identifier clinicalProgrammeLead, and the scan no longer rewards leaving developer wording on a clinical screen. This closes the second and larger one. src/lib/caring-contacts/** is the sealed domain, and it is where this feature's plain-words wording actually lives -- STOP_REASON_WORDING and APPROVAL_ROLE_WORDING in service-state.ts, CARING_CONTACT_ROLE_WORDING in permissions.ts, PATHWAY_APPROVAL_ROLE_WORDING in pathway-versions.ts, PLAN_ASSURANCE_WORDING in assurances.ts, and all of message-copy.ts. No prohibited-language check in the repository read any of it: this scan stopped at the workspace and app trees, and the overlay tests only ever covered the 24 frozen rows. So the recorded workaround for the "lead" rule -- move the wording down into src/lib -- moved it somewhere unwatched rather than somewhere exempt. Adds that tree as a third scan root, with two exemptions shaped so neither can become blanket permission. The first is the definition site, message-rules.ts, and it is deliberately NOT a whole-file exemption. That file states the vocabulary, so it must contain the banned words; but it also holds the five sentences a discharged patient reads in an SMS, and exempting the file whole would have put the highest-consequence wording in this feature permanently outside the scan, on the very commit meant to reach wording nothing was watching. That is not theoretical: this vocabulary is wider than PROVISIONAL_MESSAGE_RULES.prohibitedTerms, adding "clinical risk", "risk score", "wellbeing score" and two reply-monitoring claims that validateGovernedMessage does not check at all. So the exemption is computed from the policy -- an offence in that file is exempt only when its text is exactly one of the terms the file declares. That covers all eleven of its offences and needs no maintenance when a tenth term is added, while leaving every sentence in it scanned. The second is one literal, reach-reporting.ts's no-safe-disclosure discriminant, exempted by exact string in a named file with its reason recorded. No exemption of any kind may suppress a multi-line or long string, so a mis-tokenised range can never be pasted into the table and take a whole region of copy with it. Held against rot by four tests: the definition-site exemption is asserted as a SET rather than as merely non-empty, because non-emptiness there cannot fail and a control that cannot fail is not a control; a listed literal that stops matching fails; the literal table is pinned as not-an-ignore-list, since a different offending string in the same file still fails and the same string still fails in any other file; and the patient-visible sentences are pinned as inside the scan. Two supporting changes the new root made necessary. The raw-prose pass now blanks quoted literals instead of reading inside them: it is the same tokeniser as the literal pass, so every blanked region is one already read whole, and no pattern in this vocabulary can match across a quote, which is why this is coverage-neutral rather than a quiet weakening. What it stops is one offence being reported twice at two granularities -- which mattered because no-safe-disclosure surfaced there as a bare "safe", and exempting that would have hidden a genuine safety claim anywhere else in the file. And the files-scanned floor is now per root rather than a total, because the aggregate floor was already satisfied by the two original roots, so a newly added root resolving to nothing would have read as clean rather than as unread. Verified by mutation, not only by a green run. Planting "the patient is safe and the service is high risk" into service-state.ts fails the scan naming the file and the exact string. Planting a second offending literal into reach-reporting.ts fails despite that file already holding an exempted literal. Editing closingStatement to "Replies are monitored 9 am-6 pm" fails, which is the case a whole-file exemption would have hidden. And JSX text containing an apostrophe, the one input where blanking could plausibly have lost coverage, is still caught -- pinned as its own test. Closes #Z5P2BW via an inbox request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
…ed wording
#0HYHTH recorded that the prohibited-language check ran only against outgoing
messages and the 24 frozen overlay rows, so the ban on "high risk", "safe",
"engagement score" and "risk score" in interface strings was policy held by
people rather than by software. B3 covered the workspace and app trees, and the
preceding commit covered the sealed domain. This covers what was left.
Two more roots:
* src/lib/caring-contacts-server/** is deliberately outside the seal -- it
reads environment variables, which the sealed domain may not -- and holds
demo-seed.ts, whose fixture copy is what a demo-mode screen actually shows.
* src/app/api/caring-contacts/** carries the refusal wording the workspace
renders when a request is declined.
One literal exemption follows: demo-seed.ts's write label
"pathway-approve-clinical-programme-lead", which becomes the idempotencyKey on
the audit entry recording which approval was written. Nothing in the workspace
or app trees renders an idempotencyKey, so it reaches the audit record and
never a screen. Its hyphens are the whole reason it matches -- the job-title
exemption looks for a qualifier followed by a space, so "programme-lead" reads
as a bare "lead" where "programme lead" does not. It is exempted by exact
string in a named file and held by the same anti-rot tests as the other entry.
Two surfaces stay deliberately unscanned, and are now written down as decisions
rather than left as gaps. The mockup trees 404 in production and hold the one
prohibited phrase the owner ruled under B4 to leave alone; a test pins their
exclusion, so widening a root to "all of src/components/caring-contacts" cannot
quietly reverse that ruling. This feature's entries in the cross-cutting
catalogues it does not own -- tools-catalog.ts, developer-area/** -- carry every
feature's copy, so covering them needs its own scoping decision.
The comment recording that second omission also records something an earlier
draft of it got wrong: tools-catalog.ts is NOT clean of this vocabulary. Its
prescribing entry reads bestFor: "Safe and effective prescribing", which
\bsafe\b matches. It is a section title about prescribing practice rather than
a claim that a person is safe, so it is harmless where it is -- but a comment
that tells the next reader a file is clean, when it is not, wastes exactly the
person who trusted it, so it now names the match instead.
Verified by mutation: planting "your engagement score is high risk" into
demo-seed.ts and "replies are monitored" into an API route each fails the scan,
naming the file and the exact string.
Closes #0HYHTH via an inbox request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
CI triageCI failed on this PR. Automated classification of the 2 failed job(s):
Compared with main CI run #14802 (success). Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger. |
CI fix. `Static PR checks` failed on `docs:check-links`, which plans the inbox reconciliation and refuses to guess when two pending requests target the same row: another session queued mutations for #0HYHTH and #AGRAKQ earlier on 2026-09-02, and this branch's requests collide with both. The tool requires an explicit cancellation decision per pair. Both decisions are recorded in full in the cancel requests themselves; in short: #0HYHTH — cancels the other session's close (59f035ef) and keeps this branch's. Both close the row, so the row closes either way and the only question is which outcome is recorded. Theirs was written against d1bb2c1 and describes the scan as two roots; this branch takes it to five, so the residue theirs had to name as outstanding is closed within this PR. Keeping the narrower text would leave the ledger describing a smaller fix than the one that landed. #AGRAKQ — cancels THIS branch's close and keeps the other session's update, so the row stays open. My close was right about what the row names and wrong to close the row on it. Re-measured by executing both patterns: the two examples the row names, "the incident lead" and "the clinical programme lead", are permitted on both surfaces, so the singular collision it was filed for is genuinely fixed. But every plural job title is still refused on both -- "clinical leads", "programme leads", "team leads" -- because the bare \bleads\b alternative sits ahead of the narrowed branch and carries no lookbehind. That is ordinary English being refused, and closing the row would have buried a live disagreement between two sessions. One correction to the surviving update, recorded in the cancel reason so it is not acted on as written: reusing COMMERCIAL_LEAD_PATTERN is no longer the route, because since Ruling [143] that pattern is term for term identical to the interface helper and carries the same bare \bleads\b. Both surfaces are now equally strict, so the row's "record why interface copy should be stricter than message copy" framing no longer fits either. What is actually open is whether Ruling [143]'s reasoning that nobody's job title is plural survives contact with "the clinical leads met on Tuesday" -- an owner decision that would move a guard on patient-visible message copy, so not one to take while closing a row. No test or source file changes; the docstring correction this branch makes to the prohibited-language helper is accurate whatever is decided about plurals. Reproduced the failure locally before fixing and confirmed the fix: `npm run docs:check-links` failed with the same "multiple pending mutations" error, and now reports "docs link check passed: 4731 repo path references resolve." The rest of that CI job passes too -- check:outstanding-issues (snapshot in step, 75 open, 13 pending), docs:check-index, docs:check-inventory, docs:check-scripts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
…on blindness Two append-only paperwork items closing out this branch. Neither touches test or source code. First, the immutable review record this branch owed. `ledger:lookup` reported NOT REVIEWED at head c4ead39 for the scope "caring-contacts prohibited-language scan", so the record is written now rather than left for a later sweep to guess at. It carries the clinical-governance review's verdict (no blocker; no guard weaker than origin/main at any point in the diff), the three findings fixed in the diff rather than deferred, the mutation evidence, and the gates actually run with their decisive lines. Second, an outstanding-issues request for the residual that review raised and that is not this branch's to fix: scripts/pr-policy.mjs decides whether a PR must complete the Clinical Governance Preflight by matching changed paths against clinicalRiskPatterns, and that list has no pattern for tests/**. But several of this repository's clinical safety guards ARE tests -- the interface vocabulary scan, the shared prohibited-language definition, the 24 frozen overlay rows, and the Ruling [143] parity block. This PR was classified clinicalRisk:false, correctly by the letter of the rule. The asymmetry is that the same paths would carry a PR deleting a scan root, widening an exemption from one literal to a whole file, or relaxing the "lead" lookbehind, and the gate would wave each one through: it reads paths, not intent, so it cannot tell strengthening a guard from removing one. The near miss that makes it concrete rather than theoretical is in this branch's own history. Its first draft exempted message-rules.ts as a whole file, which would have put the five sentences a discharged patient reads by SMS permanently outside the scan. A subagent review caught it; nothing in the required checks would have. Verified with the gates that read these two files: docs:check-links (4731 repo path references resolve), check:outstanding-issues (snapshot in step, 75 open, 14 pending), check:branch-review-ledger (880 live table records + 1206 archived + 577 immutable, immutable review writes, no duplicates), check:ledger-stamp-retention (0 touched rows, 0 lost), and prettier on both files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
… patient messages Closes the half of #AGRAKQ that was genuinely open, on an owner decision taken 2026-09-02. The row had two halves. The singular one was already fixed before the row could be acted on: Ruling [143] put the job-title lookbehind into the interface helper on 2026-08-27, so "the incident lead" and "the clinical programme lead" -- the two examples the row names -- are permitted on both surfaces. The plural one was real and stayed open. The bare \bleads\b alternative sat ahead of the narrowed branch with no lookbehind, so "clinical leads", "programme leads", "team leads", "service leads" and "incident leads" were all refused. Ruling [143]'s reasoning that nobody's job title is plural is true of one person's title and false of a group of them, and "the clinical leads met on Tuesday" is ordinary English a clinician had no way to write. By 2026-09-02 neither of the two ways out the row offered still fitted -- apply the same narrowing, or record why interface copy should be stricter than message copy -- because the two definitions had become equally strict. So it was put to the owner as the decision it had become. DECISION: extend the job-title exemption to the plural ON THE SCREEN ONLY. A screen may now say "the clinical leads met on Tuesday"; a message to a discharged patient still may not. The asymmetry is deliberate and is the safe direction. Loosening patient-visible message wording is the highest-consequence change available in this feature and nothing needed it, and the Ruling [143] parity invariant is one-directional -- it forbids a message permitting what the screen refuses, not the reverse -- so this weakens nothing that invariant guards. Implemented as two new alternatives mirroring the singular pair exactly: a plural commercial-companion branch ahead of an exempting plural branch, so the exemption cannot license what follows the word it exempts. Commercial plurals are untouched -- "sales leads", "new leads", "leads capture" and "clinical leads capture" are all still refused, and a bare "Leads" still is too. Pinned in BOTH directions, not only the half that changed: the test asserts the screen permits each plural job title AND that a message still refuses it, so a later edit tidying the message side to match would go red rather than quietly loosening the patient's wording. The five plurals move out of the Ruling [143] divergence list into their own, with the reason recorded beside them; the two commercial phrases that list was also measuring stay where they were. Verified: all 69 caring-contacts test files, 1477 tests passing; typecheck, eslint and prettier clean; docs:check-links passes (4731 repo path references resolve) and check:outstanding-issues reports the snapshot in step (75 open, 16 pending). Closes #AGRAKQ via an inbox request, and cancels the other session's pending update on that row, which was right about the measurement and stale about the route: it proposed reusing COMMERCIAL_LEAD_PATTERN, which since Ruling [143] is term for term identical to this helper and carries the same bare \bleads\b, so reusing it would have changed nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
…y, and stop message-rules inviting the unsafe edit Acts on the clinical-governance review of 33e1ffd, the one commit on this branch that loosens a guard rather than widening one. The review found no blocker and confirmed the loosening is screen-only, the message side provably untouched (`git diff origin/main 33e1ffd -- src/` was empty), the parity invariant still non-vacuous, and the new tests falsifiable by the mutation that matters. Four of its findings are acted on here. No pattern changes. FIRST, and the reason this commit exists: message-rules.ts's own comment told a future editor to make the one edit the owner's decision forbids. It still said the two "lead" definitions mirror each other term for term, and still carried "Nobody's title is plural" as live reasoning. The whole safety argument for a screen-only change rests on nobody tidying the message side to match, and the message side was inviting exactly that. It now records the 2026-09-02 divergence, says plainly not to copy the interface plural branches down, and notes what would catch it if someone did. Comment-only: COMMERCIAL_LEAD_PATTERN and PROVISIONAL_MESSAGE_RULES are unchanged. SECOND, the plural exemption is wider than "plural job titles", and that boundary is now measured in the helper rather than assumed. The companion list guards words that come AFTER the word, which is where singular commercial English puts them; plural commercial English puts them BEFORE, and nothing guards that position. So "Capture clinical leads", "Unconverted service leads" and "clinical leads dashboard" are now permitted where they were refused. The enumerated list is in the comment. It is left open deliberately: the singular pair has the same shape of hole, requiring a determiner would refuse an ordinary "Clinical leads" roster heading -- the exact wording the decision existed to permit -- and listing commercial verbs would rebuild the allowlist message-rules.ts records as the original B2 defect. Filed as its own P3 issue so it is tracked rather than buried in a comment. THIRD, "clinical leads capture" and "sales leads" move into LEAD_PHRASES_BOTH_ALREADY_REFUSED. They were pinned inline against the screen only, so the union invariant never checked that a message refuses them too -- asymmetric with every other lead phrase in that block, and they are precisely what the plural companion branch exists for. FOURTH, two smaller consequences of the same shape are recorded: `leads` as a verb after a qualifier is now permitted ("the team leads the review"), benign but not a job title; and the lookbehind wants exactly one literal space, so a Prettier line break between "clinical" and "leads" in JSX text produces a confusing refusal of wording that is now explicitly permitted. Rewrap rather than adding an exemption. Also replaces the governance-gate issue queued in fa5d727 with a stronger version of itself. That row listed "relaxing the lead lookbehind" among the changes that would slip past pr-policy's clinicalRiskPatterns; the next commit on this branch did exactly that, was classified clinicalRisk:false, and required no preflight. The gap is now demonstrated rather than hypothesised, which is the difference between a row someone reads and a row someone acts on. Verified: all 69 caring-contacts test files, 1477 tests passing; typecheck clean (6011 input files); eslint clean on all three changed files; prettier clean; docs:check-links passes (4731 repo path references resolve); check:outstanding-issues reports the snapshot in step (75 open, 19 pending). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
The first review record covers head c4ead39 and the scope "caring-contacts prohibited-language scan". The branch has since gained the plural job-title change and its own review, so `ledger:lookup` reported NOT REVIEWED for that scope and head. This is that record, not a supersede: two genuinely different reviews of two different pieces of work, and collapsing them would lose which evidence belongs to which. It carries the second review's verdict (nothing blocks; loosening is screen-only; message side byte-identical to origin/main), the four findings acted on, the measured boundary of the plural exemption and why it was left open, and one honest note about the review's own limits -- it ran vitest on 3 caring-contacts files rather than the full 69, and said so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
…gressions Found while diagnosing the CI failure on this PR, and worth recording because the diagnosis cost more than the fix would. This session's container clones the repository shallow -- 106 commits -- and on that clone `npm run test` reports two failed files against 12063 passing tests. Neither is a defect. tests/clinical-hazard-controls.test.ts emits 37 entries of the form "reviewedCommit does not exist 883f100…" and "path is absent from reviewedCommit src/lib/answer-verification.ts"; tests/rag-plan-package-parity.test.ts fails on "manifest reconciledBase is unavailable locally: f3d1a3c…". Both simply cannot see the commits their manifests name. Proven environmental rather than assumed: `git rev-parse --is-shallow-repository` returned true and `git cat-file` could not resolve either commit; after `git fetch --deepen=3000` took the clone to 5466 commits, both resolved and both files passed -- 19 tests, no code change. The reason to log it rather than shrug: this is the broadest gate in the repository, and on this container type it is always two files red. An agent reading that either re-derives the diagnosis from scratch, as this session did, or learns to discount failures in the full suite -- and the second habit is how a real regression in one of these two files gets waved past. One of them is the clinical hazard controls contract, which is the worst possible file to train anyone to skip. The pattern to adopt already exists here: other surfaces detect a shallow clone and refuse explicitly rather than returning an indeterminate result, including an exported shallowCloneRefusal and check:dead-code-candidate's "run git fetch --deepen=2000 first". The request asks for the same, and says explicitly not to relax the assertions -- checking that a manifest's reviewedCommit still exists is the point of both tests. Also confirms what the earlier CI failure was. `PR required` on 4682686 failed with its own message: "CANCELLED with no failing job: static-pr. Nothing here describes the diff, so this is not a broken change… Usually a newer push superseded this run (cancel-in-progress)." That newer push was mine, three minutes later. `PR required` is success on the current head. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_06e3da3d-fa8b-49b3-93bd-db567b29b6c7) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 91c676611f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…declaration actually declares Acts on the Codex review of 91c6766, which is right. The exemption asked "is this literal one of the declared terms" when the question is "is this literal the declaration". Matching on file plus normalised text, a rendered label in message-rules.ts whose whole literal happened to equal a banned word -- a stop-reason or status wording of exactly "safe" -- could not be told apart from the entry inside prohibitedTerms, so the scan stayed green. In the one file that also holds the five sentences a discharged patient reads. That is the same failure the whole-file exemption had, returning through a narrower door. Closed with a budget rather than an AST. The array declares each term once, so one occurrence of each is explained and a second is not, whatever it is and wherever it sits. The budget is consumed as literals are seen, so the second "safe" in that file is reported while the declaration itself is not. Fails closed in both directions, which is the part worth keeping: if the declaration cannot be located, or the literals parsed out of it do not match PROVISIONAL_MESSAGE_RULES.prohibitedTerms exactly, it throws rather than exempting on text alone. That cross-check earned itself immediately -- the first version anchored on the bare field name and matched the TYPE declaration, readonly string[], whose ] closes an empty range. It produced an empty budget, the cross-check caught it, and two tests went red instead of the exemption quietly widening. The anchor is now the value, prohibitedTerms: Object.freeze([. The raw-prose pass is deliberately outside the budget, and that is sound rather than convenient: it blanks every quoted literal, so in this tree it can only surface identifiers and regex source -- here "conversion" and "lead" out of COMMERCIAL_LEAD_PATTERN. A rendered label is always a literal, so nothing that pass reports can be one. Proved the way the review asked, with an exact-term literal elsewhere in the file, and against the REAL source with one line added rather than a hand-built fixture, so the parse under test is the one that runs in anger: prepending const PLANTED_STOP_REASON_LABEL = "safe" reports exactly ["safe"], and a multi-word term behaves the same. Two more tests pin the fail-closed paths -- a source with no declaration, and a declaration with a term dropped. Verified: all 69 caring-contacts test files, 1479 tests passing; typecheck clean (6013 input files); eslint and prettier clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
…s-vocabulary-tmnc89
…s-vocabulary-tmnc89
…rows first Sync fix, found by babysitting rather than by anything in this diff. Merging main brought in the reconciliation from #2542, which applied another session's requests for two of the three rows this PR closes. That did not merely supersede my requests -- it hard-failed `docs:check-links`, which runs the inbox planner, so an unrelated CI check on this PR went red because of a race in the ledger. Three requests were left unapplicable and are cancelled or reissued: * #0HYHTH is now CLOSED on main by the other session's done. Mine threw "#0HYHTH is no longer open" and is cancelled. The row's end state is right either way; what is lost is the outcome TEXT, which describes the scan at B3 -- two roots -- and names the sealed domain as outstanding residue. This PR takes it to five roots and closes that residue, so the archive now understates the fix by about half. * #AGRAKQ is still open, but main applied the other session's update to it, so my done went stale on its baseRowFingerprint. Cancelled and reissued from the current ledger, unchanged in substance -- and it fits the row's new detail better than the old one, since that update is what established the plural half was still broken, which is exactly what this branch fixes. * The two cancels I had queued against the other session's requests are now no-ops: their targets were already applied. The planner says so as a warning rather than an error, so they are left in place; they record the intent and why it did not take. #Z5P2BW was unaffected and still closes normally. The archived #0HYHTH outcome cannot be corrected through the tooling -- `issues:update` rejects with "not in Open items", because its fingerprint branch requires the row be in Open items -- so the correction is carried by a new issue instead, which also records the race itself. That row asks for three things: a done against an already-closed row should no-op with a warning like the cancel case rather than throwing and reddening an unrelated PR's CI; an archived outcome should be amendable, since today the first close to reconcile is final by accident; and reconcile should consider preferring the later of two competing closes, which is usually the better-informed one. Verified after the merge: all 69 caring-contacts test files, 1479 tests passing; typecheck clean (6030 input files); prettier clean; and the four ledger gates green -- docs:check-links (4787 references), snapshot in step (70 open, 17 pending), ledger-write-discipline, branch-review-ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_1a5c34c5-afd9-4ff8-a735-d67b3174ef3d) |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_0cef6b9e-fc95-46c6-a08c-15e8dc0c6dde) |
…s-vocabulary-tmnc89
Behind-but-clean, confirmed with git merge-tree before merging. Auto-merge is armed on this PR and requires an up-to-date branch, so this sync is what lets it land. Gates: check:diff-integrity, check:repo-awareness-snapshot, check:outstanding-issues, check:outstanding-issues-snapshot, check:gate-manifest, check:pr-policy, docs:check-inventory and docs:check-links pass; typecheck clean; unit suite 949 files / 12,186 passed, 1 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193brbqFWHbpVnLfgDBkVNP
Summary
tests/caring-contacts-interface-vocabulary.test.tsto scan the sealed domainsrc/lib/caring-contacts/**, which no prohibited-language check in the repository read, even though it holds this feature's plain-words wording (STOP_REASON_WORDING,APPROVAL_ROLE_WORDING,CARING_CONTACT_ROLE_WORDING,PATHWAY_APPROVAL_ROLE_WORDING,PLAN_ASSURANCE_WORDING, and all ofmessage-copy.ts). The issue's first defect — the scan refusing "lead" with no notion of a job title — was already closed by Ruling [143]; this closes the second and larger one. The definition sitemessage-rules.tsis exempted by a rule computed from the policy, not whole-file, so the five sentences a discharged patient reads stay inside the scan.src/lib/caring-contacts-server/**(demo-seed fixture copy, which is what a demo-mode screen shows) andsrc/app/api/caring-contacts/**(refusal wording the workspace renders). The ban on "high risk", "safe", "engagement score" and "risk score" in interface strings is now held by software rather than by people. What stays deliberately unscanned is written down as a decision, with a test pinning the mockup exclusion.pr-policy's risk classification.#AGRAKQ: the one change here that loosens a guard
The row had two halves. The singular half was already fixed before the row could be acted on — Ruling [143] (2026-08-27) put the job-title lookbehind into the interface helper, and ran the fix the opposite way round from what the row anticipates: the interface definition was the stricter of the two, so
COMMERCIAL_LEAD_PATTERNwas tightened to mirror it rather than the reverse. Both examples the row names by name are permitted on both surfaces.The plural half was real and stayed open. The bare
\bleads\balternative sat ahead of the narrowed branch with no lookbehind, so every plural job title was refused. Ruling [143]'s reasoning that nobody's job title is plural is true of one person's title and false of a group of them, and "the clinical leads met on Tuesday" is ordinary English a clinician had no way to write. By 2026-09-02 neither of the two ways out the row offered still fitted — apply the same narrowing, or record why interface copy should be stricter than message copy — because the two definitions had become equally strict. So it was put to the owner as the decision it had become.Decision (owner, 2026-09-02): extend the job-title exemption to the plural on the screen only. A screen may now say "the clinical leads met on Tuesday"; a message to a discharged patient still may not.
COMMERCIAL_LEAD_PATTERNandPROVISIONAL_MESSAGE_RULESare untouched.the incident leadthe clinical programme leadclinical leadsThe clinical leads met on Tuesday.sales leadsnew leadsleads captureclinical leads captureThe asymmetry is deliberate and is the safe direction. Loosening patient-visible message wording is the highest-consequence change available in this feature and nothing needed it, and the Ruling [143] parity invariant is one-directional — it forbids a message permitting what the screen refuses, not the reverse — so this weakens nothing that invariant guards. Implemented as two alternatives mirroring the singular pair exactly: a plural commercial-companion branch ahead of an exempting plural branch, so the exemption cannot license what follows the word it exempts.
Pinned in both directions, not only the half that changed: the test asserts the screen permits each plural job title and that a message still refuses it, so a later edit tidying the message side to match would go red rather than quietly loosening the patient's wording.
A correction that cannot be made where it was written
Commit
62311c5's message says "Closes #AGRAKQ as already-resolved via an inbox request." That was withdrawn inc4ead39— another session had queued a contradicting update, and re-measuring showed it was right about the plural. The row is now closed by33e1ffdon the owner decision instead. The original commit message cannot be corrected without rewriting pushed history, so the correction is recorded here.Verification
npm run verify:pr-localRun instead, as the smallest gates covering this diff, with the arbiter consulted for
typecheck(verdictRUN— "ui scope never defers"):npx vitest runover all 69 caring-contacts test files —Test Files 69 passed (69) / Tests 1477 passed (1477)npm run typecheck— clean;[gate-receipts] recorded a pass for "typecheck:internal" (6008 input files)npx eslinton both changed test files — cleannpx prettier --checkon every changed file —All matched files use Prettier code style!npm run check:outstanding-issues—[snapshot] in step with data/outstanding-issues-snapshot.json (75 open, 16 pending)npm run docs:check-links— reproduced a CI failure locally first, thendocs link check passed: 4731 repo path references resolvenpm run check:branch-review-ledger—880 live table records + 1206 archived + 577 immutable, immutable review writes, no duplicatescheck:ledger-write-discipline,docs:check-index,docs:check-inventory,docs:check-scripts,check:ledger-stamp-retention— all passedVerification not run:
verify:pr-local,verify:cheap,verify:ui,verify:release. No UI, routing, styling or browser behaviour changed, and no production module is touched, so no browser journey has a changed failure path. Nothing provider-backed was run.Proof by mutation, not only by a green run. A scan that cannot fail is worse than no scan, so each new root and each exemption boundary was checked by planting a real offence and confirming the failure, then reverting:
"the patient is safe and the service is high risk"src/lib/caring-contacts/service-state.tssrc/lib/caring-contacts/reach-reporting.tsclosingStatement: "… Replies are monitored 9 am-6 pm."src/lib/caring-contacts/message-rules.ts"your engagement score is high risk"src/lib/caring-contacts-server/demo-seed.ts"replies are monitored"src/app/api/caring-contactsroute<p>Don't check your inbox for the campaign.</p>git diff --stat src/was empty after each revert.Risk and rollout
33e1ffd) deliberately loosens the interface rule for plural job titles, on an owner decision, on the screen surface only; patient message wording is byte-identical tomain.On the one change that could have been an accidental weakening. The raw-prose pass now blanks quoted string and template literals instead of reading inside them. That is coverage-neutral for three reasons that hold together: it is the same tokeniser as the literal pass, so every blanked region is one already read whole; no alternative in the vocabulary can match across a quote, since each is built only from word characters, literal spaces and
\s+, while every boundary character is a quote; and blanking substitutes a space, whose word boundaries behave identically for a match lying wholly outside a literal. The reviewer confirmed this empirically by replicating both scanners across all 109 files in the five roots — the only differences are removed duplicate reports — and by running 13 adversarial fixtures without finding an input where the old scan catches and the new one misses.On the exemptions. There are two, and neither can become blanket permission. Anti-rot is by test, not by intention: the definition-site exemption is asserted as a set rather than as merely non-empty — non-emptiness there cannot fail, and a control that cannot fail is not a control; a listed literal that stops matching fails; the literal table is pinned as not-an-ignore-list; and no exemption may suppress a multi-line or long string, so a mis-tokenised range cannot be pasted into the table and take a whole region of copy with it.
Clinical Governance Preflight
Not machine-required —
classifyPullRequestFilesreturnsclinicalRisk: falsefor this diff, whose paths aretests/**anddocs/**. Completed anyway, because the subject is a clinical safety guard. (That mismatch is itself now logged as an issue — see Notes.)Clinical KB Database(sjrfecxgysukkwxsowpy) — unchanged; no Supabase surface toucheddemo-seed.tsis now inside the scanNotes
Reviewed by the
clinical-governance-reviewersubagent before the first push; no blocker. Three findings are fixed in the diff rather than deferred: the definition-site exemption was whole-file and would have left the five patient-visible message sentences permanently unscanned; its anti-rot test was unfalsifiable by construction; and the scope comment claimedtools-catalog.tswas clean of this vocabulary when it containsbestFor: "Safe and effective prescribing". The immutable review record is atdocs/branch-review-records/2672ead5….record.md. The plural-job-title commit is under separate review, since it is the only one that loosens rather than widens a guard.A residual the review raised, now logged as its own outstanding-issues request rather than fixed here:
pr-policy'sclinicalRiskPatternshas no pattern fortests/**, yet several of this repository's clinical safety guards are tests. The gate reads paths, not intent, so it cannot tell strengthening a guard from removing one — and the same paths that carry this PR would carry one deleting a scan root or widening an exemption. This PR's own first draft is the near miss that makes it concrete: it exemptedmessage-rules.tswhole, and a subagent review caught it where no required check would have.Each ledger row moves through an immutable inbox request; run
npm run issues:reconcileafter this lands. That batch includes three cancellations resolving mutations that collided with requests queued by another session on the same day.🤖 Generated with Claude Code
https://claude.ai/code/session_01CQg8otWecCGsBJcLvkR7pm
Note
Medium Risk
Changes clinical copy safety guards and deliberately loosens interface vocabulary for plural job titles; patient message validation is unchanged but the test gate is the primary enforcement surface.
Overview
Extends the Caring Contacts interface vocabulary scan from two trees to five: sealed
src/lib/caring-contacts/**,src/lib/caring-contacts-server/**, andsrc/app/api/caring-contacts/**, so wording maps, demo seed copy, and API refusal strings are checked—not just workspace/app UI.Refactors the scanner: raw JSX prose no longer double-counts quoted literals;
message-rules.tsis exempt only for declaredprohibitedTermsliterals (with a one-per-term budget), not the whole file, so patient SMS sentences stay in scope; named machine literals (no-safe-disclosure, demo idempotency keys) get exact-string exemptions with anti-rot tests.Loosens screen copy only (#AGRAKQ):
CARING_CONTACTS_PROHIBITED_LANGUAGEnow permits plural job titles (e.g. “clinical leads”) on the clinician surface whileCOMMERCIAL_LEAD_PATTERNinmessage-rules.tsis unchanged—comments document the intentional divergence and warn against “tidying” message rules to match. Tests pin screen permit + message still refuse for those phrases.Docs/ledger: branch review records, inbox done/cancel/add requests for closed issues and follow-ups (governance preflight gap, ledger race, shallow-clone test noise, plural-exemption residual).
Reviewed by Cursor Bugbot for commit dbbee2c. Configure here.