diff --git a/docs/branch-review-records/e87f5e78703c1434f51e380613c31b831ca9f402e40895d6b295c4ac28e01f39.record.md b/docs/branch-review-records/e87f5e78703c1434f51e380613c31b831ca9f402e40895d6b295c4ac28e01f39.record.md new file mode 100644 index 000000000..3c51ba808 --- /dev/null +++ b/docs/branch-review-records/e87f5e78703c1434f51e380613c31b831ca9f402e40895d6b295c4ac28e01f39.record.md @@ -0,0 +1 @@ +| 2026-09-03 | claude/issues-followups (PR #2544) | 9c485f59e73e001e35b6e2075a6d933e080aeaa7 | Run PR sweep: CI fix + threads + drift | No CI had run on this head (pending/no checks); branch was behind main with a clean merge-tree, merged origin/main (no conflicts, package.json script additions only, no lockfile change, no npm install needed); both review threads were already resolved from a prior sweep pass, none left open | npm run check:outstanding-issues (pass, includes check:outstanding-issues-snapshot), npm run check:ledger-write-discipline (pass), npx prettier --check . (pass, whole tree); no provider-backed checks run | diff --git a/docs/outstanding-issues-inbox/0327a00a-f12d-4907-9c52-45d8e7e887eb.json b/docs/outstanding-issues-inbox/0327a00a-f12d-4907-9c52-45d8e7e887eb.json new file mode 100644 index 000000000..86085c996 --- /dev/null +++ b/docs/outstanding-issues-inbox/0327a00a-f12d-4907-9c52-45d8e7e887eb.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "0327a00a-f12d-4907-9c52-45d8e7e887eb", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "The generated counts sentence in docs/scripts-index.md conflicts on every concurrent PR, the same failure #Y090R5 describes for the issues snapshot", + "detail": "docs/scripts-index.md opens with a machine-written sentence counting script files and npm scripts; scripts/update-docs-inventory.mjs rewrites it by regex and docs:check-inventory fails if it drifts. Because every change to the script surface rewrites that one line, any two concurrent PRs collide on it. Measured on PR #2531 five times in about three hours, at main c0ee4ddd, ff37de6d, 59b3490e, 88ffbdb9 and 4dcd8dde: each time the whole conflict was that single line, with both sides correct about their own half. Twice it came bundled with a real package.json conflict on verify:cheap:internal, where two PRs each inserted a different gate into the same chain -- resolvable only as a union, and it silently desynchronises the gate counts that check:gate-manifest reads from CLAUDE.md and .claude/skills/gates/SKILL.md, so those must be re-derived after every such merge. This is the same root cause as #Y090R5 (data/outstanding-issues-snapshot.json): a single-line generated artefact that every PR must regenerate. PR #2530 has now fixed the snapshot half by moving regeneration into the serialised reconcile step; the scripts-index half is untouched. Two traps for whoever resolves it: taking main's whole file silently deletes the branch's own new script entry (hit once on #2531), and after resolving you must re-run scripts/update-docs-inventory.mjs rather than picking a side. Worth either moving the counts out of the prose sentence into a generated block with a union merge strategy, or regenerating them in a serialised step as #2530 did for the snapshot.", + "source": "PRs #2531 and #2544, session 2026-09-02", + "issueUlid": "01M1GZH5MH2MTYDVYDW7HKXMXF" + } +} diff --git a/docs/outstanding-issues-inbox/09170645-4b03-414b-b3cc-3bf5a640c497.json b/docs/outstanding-issues-inbox/09170645-4b03-414b-b3cc-3bf5a640c497.json new file mode 100644 index 000000000..a6138134d --- /dev/null +++ b/docs/outstanding-issues-inbox/09170645-4b03-414b-b3cc-3bf5a640c497.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "id": "09170645-4b03-414b-b3cc-3bf5a640c497", + "createdOn": "2026-09-02", + "action": "cancel", + "payload": { + "requestId": "e3133c1b-dd9c-483d-b05e-42a1cf006ea6", + "reason": "The detail asserted that disabled text extraction is 'exactly the condition that sends the worker down the OCR fallback path'. That is false, verified against the code Codex cited on PR #2544: worker/python/extract_pdf_assets.py should_ocr_page() (line 186) decides purely on extracted text length and image coverage ratio, never on the /P permission bits, and the extractor has no needs_pass or authenticate handling at all, so a user-password PDF fails before any OCR decision is reached. A wrong causal mechanism in a durable record is worse than no record, because the next person plans ingestion work from it. Cancelled and replaced by a corrected request carrying the same permission-bits finding without the OCR claim." + } +} diff --git a/docs/outstanding-issues-inbox/2f6d84c1-9b03-4a17-8e52-c14d7f0b6ea9.json b/docs/outstanding-issues-inbox/2f6d84c1-9b03-4a17-8e52-c14d7f0b6ea9.json new file mode 100644 index 000000000..41faf22e6 --- /dev/null +++ b/docs/outstanding-issues-inbox/2f6d84c1-9b03-4a17-8e52-c14d7f0b6ea9.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "2f6d84c1-9b03-4a17-8e52-c14d7f0b6ea9", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "50 of the 51 committed WA MHA form PDFs cannot be opened without a password, so no text-extraction or OCR path can reach their content \u2014 measured, not inferred", + "detail": "Measured 2026-09-02 with PyMuPDF 1.28.0 (MuPDF 1.29.0), the same library worker/python/extract_pdf_assets.py uses, against the committed bytes in public/forms-pdf/. RESULT: fitz.open() reports needs_pass=1 and is_encrypted=True for 50 of 51 files; page_count is 0, doc.authenticate('') returns 0 (the empty user password is REJECTED), and loading page 0 raises ValueError('document closed or encrypted'). MuPDF additionally logs 'corrupt object stream' because the streams stay encrypted. form-12a.pdf is the sole exception: it opens, has no /Encrypt, and yields 3274 characters of first-page text. The encryption dictionary on a representative locked file (form-10a.pdf) reads /Filter/Standard /V 4 /R 4 /Length 128 /P -1084 with a non-empty /U string. WHY THIS IS RECORDED: a concurrent review comment on PR #2544 asserted the opposite \u2014 that 'PyMuPDF does not enforce /P permission bits' and '-1084 restricts permissions without requiring a user password to open', so 'these files open normally and yield their text layer'. That is false for these specific files, and the measurement above is the disproof. The distinction matters because /P alone would indeed not block opening; these files also carry a non-empty user password, which does. CONSEQUENCE: any ingestion of these assets fails at the open call, before should_ocr_page() is ever consulted, so the OCR fallback cannot rescue them; whether the JavaScript fallback in src/lib/extractors/document.ts behaves differently was NOT measured. It also confirms the passwordProtected badge is correct for those 50 and was wrong only for form-12a.pdf, which is exactly what PR #2531 fixed. NEXT STEP: decide whether these forms are ever intended to be indexed. If yes, they need an unlocked source from the WA Chief Psychiatrist rather than a code change, because no extractor setting can defeat a user password. If no, record that decision so a future ingestion attempt is not planned against them.", + "source": "Direct measurement of public/forms-pdf/*.pdf with PyMuPDF, 2026-09-02; disputes a review comment on PR #2544", + "issueUlid": "01M1J4R7VD8KFQ3XN6B2YC5WPT" + } +} diff --git a/docs/outstanding-issues-inbox/4b71f0d2-5c66-4a3e-9d0e-2a9f6c1e8b47.json b/docs/outstanding-issues-inbox/4b71f0d2-5c66-4a3e-9d0e-2a9f6c1e8b47.json new file mode 100644 index 000000000..2d505c3c4 --- /dev/null +++ b/docs/outstanding-issues-inbox/4b71f0d2-5c66-4a3e-9d0e-2a9f6c1e8b47.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "id": "4b71f0d2-5c66-4a3e-9d0e-2a9f6c1e8b47", + "createdOn": "2026-09-02", + "action": "cancel", + "payload": { + "requestId": "d1b3491f-5e2e-49e3-b6a5-fa47be1cb8c1", + "reason": "The detail called the mobile /documents/search LCP breach reproducible and said the one passing re-run was the outlier rather than the rule. A third graded measurement on the same PR refutes that: CI run 33684986161 (head 957a0387, 2026-09-02 21:36 UTC) measured 2323ms against the 2282ms baseline, +41ms, and reported 'Every graded route is within tolerance of the committed baseline.' The cell has now produced two breaches near 2750ms and at least two passes near 2320ms on the same pinned Chromium, so the correct reading is a bimodal measurement whose two modes straddle the tolerance, not a page that got half a second slower. Cancelled and replaced by a corrected request that states all four outcomes and asks for the variance to be characterised rather than for a regression hunt or a baseline refresh." + } +} diff --git a/docs/outstanding-issues-inbox/4fb343b6-00a9-45d6-9b9a-536d32f43a74.json b/docs/outstanding-issues-inbox/4fb343b6-00a9-45d6-9b9a-536d32f43a74.json new file mode 100644 index 000000000..d8aa56a6e --- /dev/null +++ b/docs/outstanding-issues-inbox/4fb343b6-00a9-45d6-9b9a-536d32f43a74.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "4fb343b6-00a9-45d6-9b9a-536d32f43a74", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "A stored source_status of \"outdated\" on a medication, service or differential record can never be cleared — nothing writes that column back", + "detail": "PR #2536 deliberately preserves a stored \"outdated\" through read-time re-derivation, because supersession is a recorded clinical judgement that age can neither establish nor refute. That is correct, but it makes the value permanent: verified 2026-09-02 that no code path writes source_status back on medication_records, clinical_registry_records or differential_records — the only writers are the insert-time recordToRow/presentationToRow/diagnosisToRow paths, which derive from source text and can never emit \"outdated\", and grep for '\"outdated\"' across src, scripts, worker and supabase finds no update or upsert against any of those three tables. So a record that is legitimately re-sourced stays red forever. Stuck-red is the safe direction, but it is stuck, and it means the badge stops tracking reality. A precedent already exists for documents and is the shape to copy: supabase/migrations/20260711164441_domain_1_source_review_lifecycle.sql defines record_source_review(), which writes an immutable source_review_events row and then sets documents.metadata.document_status to \"outdated\" for rejected/decommissioned/superseded and back to \"current\"/\"review_due\" otherwise (lines 41-122). NEXT STEP: an equivalent recorded-supersession flow for the three record tables that can both set and clear the column, evidence-bearing in the same way, so clearing is a recorded decision rather than an age calculation.", + "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", + "issueUlid": "01M1GA1CK1H3PXP04461F5SG03" + } +} diff --git a/docs/outstanding-issues-inbox/5ad9c07e-41b8-4c62-9f3e-6d0a2b5e73c1.json b/docs/outstanding-issues-inbox/5ad9c07e-41b8-4c62-9f3e-6d0a2b5e73c1.json new file mode 100644 index 000000000..b65c2e311 --- /dev/null +++ b/docs/outstanding-issues-inbox/5ad9c07e-41b8-4c62-9f3e-6d0a2b5e73c1.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "5ad9c07e-41b8-4c62-9f3e-6d0a2b5e73c1", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "Three unscoped getByTestId('caring-contacts-guidance') assertions can hit a strict-mode violation while the lazily-imported shell is still being placed, reddening unrelated PRs", + "detail": "OBSERVED ONCE, then cleared by a single re-run. CI run 33690849576 attempt 1, job 'Production UI (2)', head 22fd4da9e of PR #2536: tests/ui-caring-contacts-workspace.spec.ts:2670 failed with 'strict mode violation: getByTestId(\"caring-contacts-guidance\") resolved to 2 elements', the two being an unscoped match and getByRole('main').getByTestId(...), with identical class attributes. 212 other tests in the shard passed. Attempt 2 of the same job on the same commit passed, so the condition is intermittent rather than a standing break. classify-playwright-failures.mjs reported 'needs investigation' and the flake ledger is empty, so nothing had recorded it. NOT PR #2536's: that diff is twelve medication and documentation files and touches no Caring Contacts code; the same job passed on its previous head 957a0387. WHAT THE SOURCE SHOWS: data-testid='caring-contacts-guidance' is rendered in exactly one place, ProgrammeGuidance at src/components/caring-contacts/workspace/programme-guidance.tsx:63; src/app/caring-contacts/guidance/page.tsx renders it once, and CaringContactsShell interpolates {children} once (shell.tsx:392). So the second node is not a second render site in the source. HYPOTHESIS, NOT VERIFIED \u2014 I did not reproduce it: that page loads its shell through next/dynamic, and the test navigates with waitUntil:'load', which resolves before React finishes relocating out-of-order streamed content. During that window the streamed copy and the placed copy can both be in the DOM, which matches the two locators exactly. Anything that makes the runner slower widens the window, which is consistent with a first-attempt failure clearing on re-run. NEXT STEP: scope the locator to the landmark rather than the document \u2014 page.getByRole('main').getByTestId('caring-contacts-guidance') \u2014 at all three unscoped sites (lines 2294, 2669 and 2803 as of origin/main 705c2f64a), or await the heading before resolving the locator. Confirm the hypothesis first by reading the retained trace (artifact production-ui-diagnostics-33690849576-shard2) rather than changing the test on this reasoning alone. Do NOT quarantine it: one reproduction is below the repository's three-on-the-same-SHA bar, and the fix is a locator change rather than a suppression.", + "source": "CI run 33690849576 attempts 1 and 2 on PR #2536, 2026-09-02", + "issueUlid": "01M1J7X4KP2NRB8VCT6D3ZQY5F" + } +} diff --git a/docs/outstanding-issues-inbox/63cd34c6-29e0-4d49-a79a-c25a769b273e.json b/docs/outstanding-issues-inbox/63cd34c6-29e0-4d49-a79a-c25a769b273e.json new file mode 100644 index 000000000..a62696a66 --- /dev/null +++ b/docs/outstanding-issues-inbox/63cd34c6-29e0-4d49-a79a-c25a769b273e.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "63cd34c6-29e0-4d49-a79a-c25a769b273e", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Services and differentials governance still returns the frozen source_status column verbatim, and derives it with a substring test that matches its own negation", + "detail": "Two defects, one fix. (a) FROZEN COLUMN: src/lib/registry-records.ts:112 rowGovernance() returns registrySourceStatus(row.source_status) (line 119) and src/lib/differential-records.ts:105 rowGovernance() returns differentialSourceStatus(row.source_status) (line 112). That column is written once at insert time by recordToRow/presentationToRow/diagnosisToRow and never ages, so a row written while its sources were fresh keeps claiming \"current\" indefinitely — the exact defect PR #2536 fixed for medications by re-deriving on the read path (see the rowGovernance doc comment on the #2536 head, which spells out why the stored column must stop being the answer). Both feed clinician-facing governance: /api/registry/records, /api/differentials, and the services and differentials detail pages. (b) SUBSTRING: src/lib/registry-records.ts:42 uses status.includes(\"checked\") and src/lib/differential-records.ts:37 uses reviewStatus.includes(\"checked\") || includes(\"current\") — both substrings also match \"not checked\" and \"unchecked\", which would grade an explicitly unchecked source as \"current\". Latent today, not live: verified 2026-09-02 that data/services-snapshot.json (219 records) carries only \"Added/checked via internet review\" or \"Extracted from uploaded deep research only\", and neither snapshot contains any negative form. src/lib/medication-records.ts:30 already carries the veto regex /\\b(?:not\\s+checked|unchecked|unverified)\\b/i. NEXT STEP: mirror #2536's read-time derivation and that negative-form veto into both modules; keep #2536's rule that a stored \"outdated\" survives re-derivation.", + "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", + "issueUlid": "01M1GA10FXZV7H8Q1EF31NKWR8" + } +} diff --git a/docs/outstanding-issues-inbox/73631e01-fb51-44b3-9ef0-aacef6a2b98f.json b/docs/outstanding-issues-inbox/73631e01-fb51-44b3-9ef0-aacef6a2b98f.json new file mode 100644 index 000000000..f1b3b0fbc --- /dev/null +++ b/docs/outstanding-issues-inbox/73631e01-fb51-44b3-9ef0-aacef6a2b98f.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "73631e01-fb51-44b3-9ef0-aacef6a2b98f", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P3", + "type": "task", + "summary": "Medication considerations panel formats its two not-assessed sentences differently — the contraindication one uses a bare comma join, the advisory one a serial-and helper", + "detail": "In src/components/clinical-dashboard/medication-considerations.tsx as landed in PR #2538 (head ef7c55ab), the advisory not-assessed sentence renders its input list through formatInputList(), a helper added in that PR which produces \"eGFR\", \"eGFR and QTc\", \"eGFR, QTc and hepatic status\". The contraindication sentence one InlineNotice above still renders result.unassessed.join(\", \"). With a single missing input the two read identically; with two or more they read inconsistently, and the bare join is the case formatInputList's own doc comment argues against, because the sentence continues into a relative clause (\"which this profile does not include\") that a comma-only list runs straight into. NEXT STEP: apply formatInputList() to the contraindication sentence too. It is identity for a single input (it returns items.join(\"\") when length <= 1), so no existing assertion in tests/medication-interaction-surfaces.dom.test.tsx breaks; add a two-input case for the contraindication tier so the shared formatting is pinned rather than incidental.", + "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", + "issueUlid": "01M1GA2GWJDFMVMN4YACPREHS4" + } +} diff --git a/docs/outstanding-issues-inbox/7663384b-4556-4d35-ae93-7f8d8105e9a0.json b/docs/outstanding-issues-inbox/7663384b-4556-4d35-ae93-7f8d8105e9a0.json new file mode 100644 index 000000000..df8a44a92 --- /dev/null +++ b/docs/outstanding-issues-inbox/7663384b-4556-4d35-ae93-7f8d8105e9a0.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "7663384b-4556-4d35-ae93-7f8d8105e9a0", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "Forms PDF manifest records only passwordProtected, folding two separate facts together — the committed PDFs permit printing and form-filling but block modification, text extraction and assembly", + "detail": "Replaces cancelled request e3133c1b, which asserted a false OCR-routing mechanism (see that cancellation's reason). The permission-bits finding itself stands and is restated here without the incorrect claim. data/forms-pdf-manifest.json records one boolean per asset, passwordProtected, generated by scripts/build-forms-pdf-manifest.mjs (PR #2531, now on main). Verified 2026-09-02 against the committed bytes: 50 of the 51 files in public/forms-pdf/ carry /P -1084 in their encryption dictionary; form-12a.pdf is the one exception, with no /P entry found by a strings scan and worth confirming separately. -1084 is 0xFFFFFBC4, which per the PDF permissions bit table permits print (bit 3) and fill-in form fields (bit 9) while blocking modify contents (bit 4), copy/extract text (bit 5), modify annotations (bit 6) and assemble document (bit 11). That is a materially different clinician-facing fact from 'requires a password to open': these forms can be printed and filled but not edited or copied from, and the single badge does not say so. NO INGESTION CLAIM IS MADE HERE. The OCR path is governed by should_ocr_page() in worker/python/extract_pdf_assets.py, which reads only extracted text length and image coverage and never consults permission bits; the extractor has no password handling, so what actually happens to these files on ingestion is untested and would need measuring, not inferring. NEXT STEP: derive an editingRestricted fact (or the decoded permission bits) alongside passwordProtected in scripts/build-forms-pdf-manifest.mjs, regenerate with that script rather than by hand, surface it as its own line in src/components/forms/form-detail-page.tsx instead of folding two facts into one badge, and extend tests/forms.test.ts to pin both fields.", + "source": "PR #2531 bytes read + Codex review finding on PR #2544, 2026-09-02", + "issueUlid": "01M1HTH7SCYJGWDZXST4HJAZHM" + } +} diff --git a/docs/outstanding-issues-inbox/8c2a6e91-3f70-4d55-b1c4-7ae5d9036f28.json b/docs/outstanding-issues-inbox/8c2a6e91-3f70-4d55-b1c4-7ae5d9036f28.json new file mode 100644 index 000000000..fefe09461 --- /dev/null +++ b/docs/outstanding-issues-inbox/8c2a6e91-3f70-4d55-b1c4-7ae5d9036f28.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "8c2a6e91-3f70-4d55-b1c4-7ae5d9036f28", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "The Lighthouse mobile /documents/search cell is bimodal across runners and its two modes straddle the tolerance, so the gate fails intermittently on whichever PR draws a slow runner", + "detail": "Observed across four graded runs of PR #2536 on the pinned Chromium (chromium-1234, HeadlessChrome/151), baseline LCP 2282ms with a tolerance of +20% AND +100ms. BREACHED: head 30afc398 measured 2748ms (+466ms, +20.4%) in run 33628667758 attempt 1; head cda377fb measured 2753ms (+471ms, +20.6%) in run 33681754629. PASSED: the attempt-2 re-run of 30afc398 (number not read); head 957a0387 measured 2323ms (+41ms) in run 33684986161, which reported 'Every graded route is within tolerance of the committed baseline.' The gate samples each cell three times and calls a regression at 2 of 3, so it is already noise-tolerant within a run; the split here is BETWEEN runs, which that design does not cover. A 2323 vs 2750 spread on identical browser, identical route and near-identical code is about 18%, and the +20%/+100ms tolerance sits inside that gap, so the same commit can grade either way depending on the runner it lands on. NOT ESTABLISHED: which mode is the true one, whether the slow mode correlates with runner class, concurrent jobs on the host, or the isolated production build's cold start, and whether main alone reproduces it. main was never measured directly; the job is path-scoped and rarely runs on main pushes. WHY IT MATTERS: an intermittent red on a required check trains reviewers to re-run rather than read, and it lands on PRs whose diff cannot reach the failing page \u2014 #2536 changes only medication files, while src/app/(search-app)/documents/search/page.tsx imports one symbol (Metadata from next) and nothing outside medication-named files calls /api/medications. NEXT STEP: characterise the cell before changing anything. Run the dispatch-only 'Refresh Lighthouse baseline' job (workflow_dispatch on ci.yml with refresh_lighthouse_baseline=true) two or three times against main, which measures the same routes on the pinned Chromium and commits nothing, and compare the spread. If the slow mode reproduces on main, attribute it before touching lighthouse-budget.json; if it does not, the fix is in how the cell is measured, not in the baseline. Do not refresh the baseline reactively to clear a red PR.", + "source": "PR #2536 Lighthouse runs 33628667758, 33681754629 and 33684986161, 2026-09-02", + "issueUlid": "01M1J0K8QW4TXR6ZB9F2N5PD3H" + } +} diff --git a/docs/outstanding-issues-inbox/8df3eb40-1fa8-4daf-964e-699192930738.json b/docs/outstanding-issues-inbox/8df3eb40-1fa8-4daf-964e-699192930738.json new file mode 100644 index 000000000..c0abe3f8e --- /dev/null +++ b/docs/outstanding-issues-inbox/8df3eb40-1fa8-4daf-964e-699192930738.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "8df3eb40-1fa8-4daf-964e-699192930738", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "isProfileEmpty treats a recorded hepatic \"none\" as no information, so an affirmative \"no hepatic impairment\" profile still renders the empty state instead of a verdict", + "detail": "src/lib/medication-patient-alerts.ts:164-180 isProfileEmpty() short-circuits on hepatic with 'if (profile.hepatic && profile.hepatic !== \"none\") return false;' (line 176), so a profile whose only entry is hepatic: \"none\" — an affirmative clinical answer, not a blank — is still reported empty. Verified 2026-09-02 on origin/main and unchanged by PR #2538. The engine disagrees with it: evaluateRow() at line 269-276 treats \"none\" as an answer, not a gap, using 'if (!hepatic) missingGates.push(\"hepatic status\")' and then testing membership only when hepatic !== \"none\" — so an entered \"none\" clears the hepatic gate. The consequence is that both consumers of isEmpty (MedicationConsiderations and the interactions block in src/components/clinical-dashboard/medication-considerations.tsx) render \"Enter patient details above\" and produce no verdict at all for that profile. Both surfaces therefore degrade conservatively — nothing is claimed that should not be — so this is a cosmetic inconsistency with the engine rather than a safety defect. NEXT STEP: its own change with its own tests, not a rider on other work: making a one-field profile produce evaluated output is a behaviour change, and the DOM tests in tests/medication-interaction-surfaces.dom.test.tsx and tests/patient-profile-panel.dom.test.tsx pin the current empty-state text.", + "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", + "issueUlid": "01M1GA265PY2E4DX4J556D3TWW" + } +} diff --git a/docs/outstanding-issues-inbox/a7fa5c79-1638-4f1f-8ac6-5f0aba14bdd9.json b/docs/outstanding-issues-inbox/a7fa5c79-1638-4f1f-8ac6-5f0aba14bdd9.json new file mode 100644 index 000000000..569483ee4 --- /dev/null +++ b/docs/outstanding-issues-inbox/a7fa5c79-1638-4f1f-8ac6-5f0aba14bdd9.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "a7fa5c79-1638-4f1f-8ac6-5f0aba14bdd9", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "differential-records.ts asserts validation_status \"locally_reviewed\" from a literal, over a snapshot whose own governance says \"Pending review\"", + "detail": "src/lib/differential-records.ts:31 deriveGovernanceFromSnapshot() returns validation_status: \"locally_reviewed\" unconditionally (line 44), while data/differentials-snapshot.json governance.reviewStatus is \"Pending review\" (verified 2026-09-02) — the same call derives source_status: \"review_due\" from that string, so one field says pending and the other claims review happened. Both presentationToRow (line 55) and diagnosisToRow (line 78) write it into differential_records, and src/components/differentials/differential-detail-page.tsx:673-692 renders it to the clinician. src/lib/medication-records.ts:65-86 fixed exactly this and its comment states the reason: deriveTrust in src/lib/answer-render-policy.ts:146 accepts \"locally_reviewed\" as satisfying the authority gate for high-risk clinical claims (line 177), so the vocabulary must never be asserted from a literal. Verified caveat: deriveTrust reads documents.metadata.clinical_validation_status (src/lib/source-authority-registry.ts:410-424), not differential_records.validation_status, so the two share the vocabulary rather than one code path — the direct harm is an unearned governance badge on the differentials surface, and the shared vocabulary is why the medication sibling was fixed. NEXT STEP: derive it from recorded verification evidence as src/lib/registry-records.ts:47-48 does from verification.locallyVerified, or default to \"unverified\"; add a test pinning that a \"Pending review\" snapshot never produces \"locally_reviewed\".", + "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", + "issueUlid": "01M1GA0K9SWM4DNWT4AGYW2ZNY" + } +} diff --git a/docs/outstanding-issues-inbox/d1b3491f-5e2e-49e3-b6a5-fa47be1cb8c1.json b/docs/outstanding-issues-inbox/d1b3491f-5e2e-49e3-b6a5-fa47be1cb8c1.json new file mode 100644 index 000000000..ed386d657 --- /dev/null +++ b/docs/outstanding-issues-inbox/d1b3491f-5e2e-49e3-b6a5-fa47be1cb8c1.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "d1b3491f-5e2e-49e3-b6a5-fa47be1cb8c1", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "mobile /documents/search LCP is 20.6% over its 2026-08-27 Lighthouse baseline, reproducibly, and the gate blames whichever PR carries the newest main", + "detail": "Measured on PR #2536, which cannot be the cause. The Lighthouse gate samples each cell three times and declares a regression only at 2/3; it reached that on mobile /documents/search twice on different heads: 30afc398 gave LCP 2748ms vs 2282ms baseline (+466ms, +20.4%) and cda377fb gave 2753ms (+471ms, +20.6%), against a tolerance of +20% AND +100ms. Five milliseconds apart across two independent runs is not measurement noise, and a re-run of 30afc398 that passed is the outlier rather than the rule. RULED OUT AS THE CAUSE: PR #2536 changes only medication files (medications/[slug]/page.tsx, the two api/medications routes, use-medication-catalog.ts, medication-badges.ts, medication-records.ts). src/app/(search-app)/documents/search/page.tsx imports exactly one symbol, Metadata from next; nothing outside medication-named files calls /api/medications; and src/lib/document-summary-badges.ts names medication-badges only in a comment, reached from DocumentViewer rather than the search route. HYPOTHESIS, NOT PROVEN: lighthouse-budget.json was last refreshed at b22639b5 on 2026-08-27 and main has taken heavy change since (about 40 merges on 2026-09-02 alone). PR #2536 merged main in repeatedly that day so it carries all of that growth, while #2531 and #2538 passed the same check earlier against lighter main heads. That is the same disease #QSHHGK records for the bundle budget: no scheduled baseline refresh, so accumulated growth fails whichever unrelated PR lands last. Not verified: main itself was never measured against the baseline, and the job is path-scoped so it rarely runs on main pushes. WHY THIS NEEDS A DECISION RATHER THAN A FIX: either the page really is about half a second slower to paint on mobile than it was six days ago and the regression should be found, or it has legitimately grown and the baseline is overdue. The gate as configured cannot distinguish those, and its failure lands on an innocent PR. NEXT STEP: run the Lighthouse job against main at a known head to establish whether main itself breaches; if it does, attribute the growth before refreshing lighthouse-budget.json, and schedule the refresh rather than doing it reactively to clear a red PR.", + "source": "PR #2536 Lighthouse runs 33628667758 and 33681754629, 2026-09-02", + "issueUlid": "01M1HZ295CWDSM2WE78A64YKWV" + } +} diff --git a/docs/outstanding-issues-inbox/e2128050-18a7-4a39-b2a8-001063a57f01.json b/docs/outstanding-issues-inbox/e2128050-18a7-4a39-b2a8-001063a57f01.json new file mode 100644 index 000000000..4f7db6b98 --- /dev/null +++ b/docs/outstanding-issues-inbox/e2128050-18a7-4a39-b2a8-001063a57f01.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "e2128050-18a7-4a39-b2a8-001063a57f01", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P3", + "type": "rec", + "summary": "Knowingly-retained gap: patient-alert rows with action \"info\" contribute to neither unassessed tier, so three medications keep a green all-clear over an unread gate", + "detail": "In src/lib/medication-patient-alerts.ts as landed in PR #2538 (head ef7c55ab), evaluatePatientAlerts() short-circuits with 'if (patient.action === \"info\") continue;' before the tier split, so info rows never reach unassessed (contraindication tier) or unassessedAdvisory (dosing/monitoring tier). MedicationConsiderations reads hasUnassessedGates from those two arrays only, so the success-tone all-clear stays reachable for a medication whose only unread gate sits on an info row. Measured against data/medications-snapshot.json on 2026-09-02: 330 records, 63 info rows, of which 8 carry a gate readable from an empty profile, across paracetamol, atorvastatin, nystatin, doxycycline, levetiracetam, lorazepam, oxazepam and temazepam; for three of those — nystatin, levetiracetam, lorazepam — both other tiers are empty, so the green all-clear is what a clinician actually sees. The exclusion is defensible on the current data: lorazepam's hepatic row reads \"Existing row describes this as a safer option in hepatic impairment; highlight as information only, not as a contraindication\", levetiracetam's reads \"absence of hepatic enzyme induction or inhibition\", and nystatin's is \"source-reviewed safe-use context\" — none is a gate whose absence changes management. NEXT STEP: no change now; revisit if any info row ever carries a gate whose absence would change management, and re-run the count above against the snapshot when the medication catalogue is next re-exported.", + "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", + "issueUlid": "01M1GA1S5QGNNMAFVF62KYTXS3" + } +} diff --git a/docs/outstanding-issues-inbox/e3133c1b-dd9c-483d-b05e-42a1cf006ea6.json b/docs/outstanding-issues-inbox/e3133c1b-dd9c-483d-b05e-42a1cf006ea6.json new file mode 100644 index 000000000..c37cce428 --- /dev/null +++ b/docs/outstanding-issues-inbox/e3133c1b-dd9c-483d-b05e-42a1cf006ea6.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "e3133c1b-dd9c-483d-b05e-42a1cf006ea6", + "createdOn": "2026-09-02", + "action": "add", + "payload": { + "pri": "P3", + "type": "rec", + "summary": "Forms PDF manifest records only passwordProtected, folding two separate facts together — the committed PDFs permit printing and form-filling but block modification, text extraction and assembly", + "detail": "data/forms-pdf-manifest.json records one boolean per asset, passwordProtected, generated by scripts/build-forms-pdf-manifest.mjs (added in PR #2531; not yet on origin/main). Verified 2026-09-02 against the committed bytes: 50 of the 51 files in public/forms-pdf/ carry /P -1084 in their encryption dictionary — form-12a.pdf is the one exception, with no /P entry found by a strings scan and worth confirming separately. -1084 is 0xFFFFFBC4, which per the PDF permissions bit table means print (bit 3) and fill-in form fields (bit 9) are PERMITTED while modify contents (bit 4), copy/extract text (bit 5), modify annotations (bit 6) and assemble document (bit 11) are BLOCKED. That is a materially different clinician-facing fact from \"requires a password to open\": these forms can be printed and filled but not edited or copied from. It also matters to ingestion — text extraction being disabled is exactly the condition that sends the worker down the OCR fallback path, so anyone who later considers these PDFs for the index needs the fact recorded rather than inferred. NEXT STEP: derive an editingRestricted fact (or the decoded permission bits) alongside passwordProtected in scripts/build-forms-pdf-manifest.mjs, regenerate the manifest with that script rather than by hand, and surface it as its own line in src/components/forms/form-detail-page.tsx instead of folding two facts into one badge; extend tests/forms.test.ts to pin both fields.", + "source": "session 2026-09-02 — follow-up from PRs #2538/#2536/#2531", + "issueUlid": "01M1GA2XWP2ED1FA7VS63Q8QRV" + } +}