diff --git a/.agents/skills/audit/SKILL.md b/.agents/skills/audit/SKILL.md index 94317da179..585434db94 100644 --- a/.agents/skills/audit/SKILL.md +++ b/.agents/skills/audit/SKILL.md @@ -5,7 +5,7 @@ description: Perform a deep evidence-backed PsychSift repository audit and produ # Audit -1. Define scope, read `docs/codex-review-protocol.md`, and inspect the review ledger. +1. Define scope, read `docs/codex-review-protocol.md`, and check prior reviews with `npm run ledger:lookup -- --scope ""` rather than reading the ledger table directly. 2. Inventory architecture, trust boundaries, changed areas, tests, and documented controls. 3. Trace realistic correctness, security, privacy, clinical, data-loss, and reliability failures. 4. Prove findings with exact files, lines, triggers, impact, and focused checks. diff --git a/.cursor/agents/design-review.md b/.cursor/agents/design-review.md index 496905e9f6..78e7d997a3 100644 --- a/.cursor/agents/design-review.md +++ b/.cursor/agents/design-review.md @@ -8,8 +8,8 @@ You are a senior design and UX reviewer for this clinical knowledge-base app (de ## Repository protocol 1. Follow `docs/codex-review-protocol.md` and `AGENTS.md` review throttling. -2. Check `docs/branch-review-ledger.md` before repeating an unchanged HEAD/scope review. -3. Pure review is read-only except appending a ledger row when the review completes. +2. Before every branch review, run `npm run ledger:lookup -- --scope ""`, and use its result to skip a repeat of an unchanged HEAD/scope review; never scan `docs/branch-review-ledger.md` by eye. +3. Pure review is read-only except recording the review with `npm run ledger:append` when it completes; never hand-edit the ledger table. 4. Read `docs/search-chrome-behaviour.md` and `docs/wiring-conventions.md` before judging search chrome or controls. 5. Do not call OpenAI, Supabase, GitHub/hosted CI, or provider-backed gates without explicit approval. @@ -72,4 +72,4 @@ Always report: - Routes and viewports exercised - Checks run / not run (and provider skips) - Residual visual or interaction risk -- Ledger row fields for `docs/branch-review-ledger.md` +- Fields for the immutable `npm run ledger:append` record (see `docs/branch-review-ledger.md`) diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md index 007fa34e7f..7eb0db7de3 100644 --- a/.design-sync/conventions.md +++ b/.design-sync/conventions.md @@ -38,8 +38,9 @@ arbitrary-value form — never hardcoded colours: Outline only — never add a companion `focus:ring-*` / `box-shadow`. The shared base rule is one focus owner by design; a second ring both stacks a halo and wipes the control's resting elevation. -Radius rules: `rounded-md` chips/pills, `rounded-lg` controls/cards/panels, -`rounded-xl` sheets/dialogs. Tap targets: `min-h-tap` / `h-tap w-tap` (44px) — +Radius rules: `rounded-sm` chips/pills, `rounded-md` controls/inputs/buttons, +`rounded-lg` cards/table cards/popovers, `rounded-xl` panels/answer card, `rounded-2xl` +sheets/dialogs. Tap targets: `min-h-tap` / `h-tap w-tap` (48px) — interactive controls only. A static chip is 28px text, not a touch target; putting `min-h-tap` on one is why dense tables scrolled so much. Dark mode is automatic via the `.dark` class — the variables flip; never write @@ -65,8 +66,8 @@ glyphs). Label and caption **text** uses `--text-muted`. ## Opt-in v2 token layer -`.ckb-v2` is an opt-in class that swaps in the v2 shell: white surfaces, a blue -`--command`, a crisper `--e1`…`--e4` ladder, a 7-step type scale (size steps with +`.ckb-v2` is an opt-in class that swaps in the v2 shell: white surfaces, a dark slate +`--command` (`#111827`), a crisper `--e1`…`--e4` ladder, a 7-step type scale (size steps with shared `--leading-prose`; hero companions `--text-hero--line-height` / `--text-hero-tr` only — no per-step leading or tracking), semantic spacing (`--gap-*`, `--pad-*`), density (`--tap-min`, `--chip-height`, `--row-*`), icon diff --git a/README.md b/README.md index 235d852ba1..21e7a0f9fd 100644 --- a/README.md +++ b/README.md @@ -302,8 +302,9 @@ production Chromium gate, the repo-owned Supabase `db-reset-verify` migration replay, and both Docker image builds run only when their file scopes apply; a failed applicable container build therefore fails `pr-required`. UI PRs also get one non-blocking advisory Chromium invocation. The full Playwright browser matrix -(`release-browser-matrix`) runs on `main`, `release/*`, manual dispatch, and a -weekly schedule. Live drift and live eval canary checks remain provider-gated; +(`release-browser-matrix`) runs on `release/*`, manual dispatch, and a weekly +schedule, and on `main` when the diff is in UI, performance or lockfile scope. It is +not part of `pr-required`, so it is a post-merge signal rather than a merge gate. Live drift and live eval canary checks remain provider-gated; Docker image builds also run independently on main/release pushes, schedule, and manual dispatch. They are not required for source-only PRs outside the container scope. @@ -321,7 +322,7 @@ npm run samples:check npm run lint npm run typecheck npm run test -npm run test:focused -- --files src/lib/example.ts +npm run test:focused -- --files tests/a.test.ts,tests/b.test.ts # replace with real paths npm run test:live # requires ALLOW_PROVIDER_TESTS=true npm run test:coverage npm run test:e2e diff --git a/docs/branch-review-archival-policy.md b/docs/branch-review-archival-policy.md index ff570b7b23..c28f79bd93 100644 --- a/docs/branch-review-archival-policy.md +++ b/docs/branch-review-archival-policy.md @@ -258,7 +258,7 @@ ref: **New with the generated index — the first partial enforcement.** `docs/branch-review-index.md` links every record it lists with a relative Markdown link, and `npm run docs:check-links` -(which runs in `verify:cheap` and in CI) resolves relative link targets and fails when one does +(which runs in `verify:full` and in CI) resolves relative link targets and fails when one does not exist. Deleting a record that the index lists therefore now breaks a required check. Be honest about the limit: this catches only deletions of records the index **already lists**, so a record appended and deleted before the index is next regenerated remains completely diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index eb351a10db..4d67279630 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -37,7 +37,7 @@ The six columns are fixed: **Date · Branch or ref · Reviewed HEAD · Scope · Record the **Reviewed HEAD** as the full 40-character SHA. `see PR head`, `pending final head`, and 8-character abbreviations are what made the throttle unreliable: no lookup can match them, so the review runs again. When the head genuinely does not exist yet, write `n/a - ` and append a real record once it does. -`npm run check:branch-review-ledger` (in `verify:cheap` and CI) enforces the `ledger` merge attribute, six cells per record, clean UTF-8 (no mojibake from a non-UTF-8 append), table-row records only, real dates, and no duplicates. Records dated 2026-07-29 or later must additionally carry a resolvable HEAD. The merge driver is installed by `npm install` / `hooks:install` (`merge.ledger.driver`). Archives under `docs/archive/branch-review-ledger-*.md` are checked for the same structural rules. +`npm run check:branch-review-ledger` (in `verify:full` and CI) enforces the `ledger` merge attribute, six cells per record, clean UTF-8 (no mojibake from a non-UTF-8 append), table-row records only, real dates, and no duplicates. Records dated 2026-07-29 or later must additionally carry a resolvable HEAD. The merge driver is installed by `npm install` / `hooks:install` (`merge.ledger.driver`). Archives under `docs/archive/branch-review-ledger-*.md` are checked for the same structural rules. ### Rotation diff --git a/docs/clinical-governance.md b/docs/clinical-governance.md index 41f9741e32..3ed760f1b2 100644 --- a/docs/clinical-governance.md +++ b/docs/clinical-governance.md @@ -64,7 +64,7 @@ and the ten locally authored differential records still carry no clinician sign- Use the `.github/pull_request_template.md` clinical governance section for any change that touches ingestion, answer generation, search/ranking, source rendering, document access, privacy, production environment behavior, or clinical output. -- Confirm the Supabase target remains `Clinical KB Database` (`sjrfecxgysukkwxsowpy`). +- Confirm the Supabase target remains `PsychSift Production` (`sjrfecxgysukkwxsowpy`). - Confirm service-role credentials and private document access remain server-only. - Confirm unknown or outdated source metadata is treated conservatively. - Confirm demo/synthetic content remains separated from real clinical sources. diff --git a/docs/db-maintenance.md b/docs/db-maintenance.md index a22cf68500..82f0d66d36 100644 --- a/docs/db-maintenance.md +++ b/docs/db-maintenance.md @@ -75,12 +75,12 @@ A schema restore is not operationally complete until all five environment-owned ### 1. `pg_cron` schedules & retention jobs -- **Procedure:** Re-create scheduled jobs for ingestion workers and telemetry retention (`purge-expired-rag-queries`, `purge-rag-retrieval-logs`, `purge-rag-query-misses`, `purge-rag-response-cache`). +- **Procedure:** Re-create the `invoke_indexing_v3_agent` schedule and the telemetry retention jobs (`purge-expired-rag-queries`, `purge-rag-retrieval-logs`, `purge-rag-query-misses`, `purge-rag-response-cache`). Do **not** re-create `invoke_ingestion_worker`: it was retired by `20260926041000` and the container worker owns ingestion. The step-by-step cron re-creation (copy the definition from the source project, keep it inactive on non-production until the endpoint check passes) is in `docs/disaster-recovery-runbook.md` step 1. - **Sanity check:** Verify with a read-only query: ```sql SELECT jobid, jobname, schedule, active FROM cron.job ORDER BY jobid; ``` - Confirm all four retention jobs and any active ingestion cron rows report `active = true`. + Confirm all four retention jobs report `active = true`, that `invoke_indexing_v3_agent` exists (active on production; inactive on a non-production target until the GUC check below passes), and that no `invoke_ingestion_worker` job exists. ### 2. Supabase Vault secrets @@ -93,20 +93,22 @@ A schema restore is not operationally complete until all five environment-owned ### 3. Custom database GUCs -- **Procedure:** Re-set required custom parameters (e.g. `app.indexing_agent_url`) using `ALTER DATABASE postgres SET ...`. -- **Sanity check:** Verify settings with a read-only query: +- **Procedure:** Re-set required custom parameters (e.g. `app.indexing_v3_agent_base_url`) by running `ALTER DATABASE postgres SET ...` as the project owner (`postgres`) in an operator session. The migration/`schema.sql` statement skips with a notice when its role lacks the privilege, so it cannot be relied on. If the value is unset, `invoke_indexing_v3_agent` falls back to the hardcoded production host; on a non-production restore, keep the `invoke_indexing_v3_agent` cron job inactive until the sanity check below shows the right value. +- **Sanity check:** Verify the stored database-level value with a read-only query (`ALTER DATABASE ... SET` does not change the current session, so `pg_settings` in the same session can be stale; use this or a new connection): ```sql - SELECT name, setting, source FROM pg_settings WHERE name LIKE 'app.%'; + SELECT unnest(setconfig) FROM pg_db_role_setting + WHERE setdatabase = (SELECT oid FROM pg_database WHERE datname = 'postgres') AND setrole = 0; ``` Confirm configured values match the target environment. ### 4. Supabase Edge Functions -- **Procedure:** Redeploy required edge functions (`indexing-v3-agent`, ingestion worker) with the Deno v2.x toolchain in an explicitly approved change window: +- **Procedure:** Redeploy both required edge functions (`indexing-v3-agent` and `site-content-sync`, which consumes the `site_content_sync_events` outbox; the ingestion-worker function is retired and must not be redeployed) with the Deno v2.x toolchain in an explicitly approved change window: ```bash supabase functions deploy indexing-v3-agent --project-ref + supabase functions deploy site-content-sync --project-ref ``` -- **Sanity check:** Verify function presence and status via `supabase functions list` and run health probes without mutating data. +- **Sanity check:** Verify both `indexing-v3-agent` and `site-content-sync` are present and active via `supabase functions list`. Run `supabase secrets list --project-ref ` (it prints names and digests only, never values) and confirm `OPENAI_API_KEY` is present; `site-content-sync` needs it to embed new or changed content and otherwise records `provider_failure`. If it is missing, restore it through the approved secret-management procedure without printing its value. `site-content-sync` has no read-only health endpoint: it accepts only `POST`, and a `POST` records an invocation and claims queued outbox events, so invoking it is an operational action that needs separate approval, not a health probe. ### 5. Dashboard-owned configuration diff --git a/docs/deployment-architecture.md b/docs/deployment-architecture.md index 0ac0f5ae9b..b6b6c0bf17 100644 --- a/docs/deployment-architecture.md +++ b/docs/deployment-architecture.md @@ -581,9 +581,9 @@ using Railway's `ALWAYS` restart policy so repeated bootstrap failures cannot exhaust a finite retry allowance and leave the queue undrained. The `indexing-v3-agent` Edge Function **stays** in its current role as the cron-triggered completion/repair gate — the two are complementary, not -alternatives. The worker service selects its Dockerfile via the -`RAILWAY_DOCKERFILE_PATH=Dockerfile.worker` variable (captured in -`railway.worker.json`). +alternatives. The worker service selects its Dockerfile through +`build.dockerfilePath: "Dockerfile.worker"` in `railway.worker.json` (Railway +config-as-code), not through a service variable. > **Operator run recipe:** the copy-pasteable build/run/verify steps, the > required env + secrets, and the pre-deploy migration gate live in diff --git a/docs/design-system/ADOPTION.md b/docs/design-system/ADOPTION.md index da374f3543..ca6ea6000d 100644 --- a/docs/design-system/ADOPTION.md +++ b/docs/design-system/ADOPTION.md @@ -5,8 +5,9 @@ of truth is `adoption-contract.json`; the generated manifest and marked tables i must match it exactly. - **Date:** 12 August 2026 -- **Current state:** 55 visual references are locally registered; all 51 production page routes - are owned across 14 surface families, with 59 route/component roots scanned; every declared +- **Current state:** 55 visual references are locally registered; all 224 production page routes + are owned across 14 surface families, with 232 route/component roots scanned (the generated + table below is authoritative if these drift); every declared root uses the v2 shell and has declared proof with no committed visual baseline. - **Phase 1 blockers resolved first, in their own commits:** `#207` ungrounded `AnswerState`, `#208` clipboard composition. See [SPEC.md](SPEC.md) §13 PR 6 clinical review, blockers 1–2. diff --git a/docs/design-system/GATES.md b/docs/design-system/GATES.md index 171bba6473..139031d054 100644 --- a/docs/design-system/GATES.md +++ b/docs/design-system/GATES.md @@ -165,7 +165,7 @@ because it contributed nothing. | # | Gate | Status | Evidence / path to blocking | | --- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | Contrast ≥4.5:1 for every text/background pair, both themes, live **and** v2; `--decoration-soft` asserted below 4.5 and never on a text node | **implemented-blocking** | v2 pairs + `--text-placeholder` + `--decoration-soft` tier + recipe AST (`decoration-on-text.contract.test.ts`); live full matrix remains follow-on. | -| 2 | Tap targets ≥48px interactive; static pills never carry `min-h-tap`; no production target reduced | **implemented-blocking for new use** | Enforced today: `--spacing-tap` is 48px and pinned from both sides (`ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias), `ui-style-contract` measures the rendered floor for declared `min-height` carriers in Chromium, and the legacy-class / literal ratchets still run. **Still open for the `h-10` case, and 9 Aug 2026 measured why.** An enumeration of _rendered interactive_ elements was written, shown to find genuine defects, and then **reverted rather than landed**: it is not deterministic on this route. Six runs against one production build returned 6, 5, 4, 3, 3 and 9 distinct sub-floor shapes, largely disjoint — one run saw the answer-suggestion chips and a sort band, another the settled results list. `waitForLoadState("networkidle")` plus deduplication to distinct shapes did not fix it, and two consecutive agreeing runs turned out to be coincidence. This spec runs in the required `Production UI` job, so an intermittent version of it would block every merge in the repo; that is a worse outcome than the gap it closes. Path to blocking: give the audit a deterministic surface — a static route or a fixed seeded state — before re-attempting it. **What the enumeration did establish, in every one of the six runs, is a live defect:** controls that carry `min-h-tap` compute `min-height: 0px` and render at 16–36px, six distinct shapes in total. The declared-carrier audit cannot report them by construction — it only measures elements already computing at or above the floor, so a floor overridden downward is skipped rather than flagged. Tracked as `#293`. **Correction, same date — "`test:e2e:style-contract` is not part of `verify:cheap`" was true and badly misleading, and it sent one session looking for a wiring bug that does not exist.** The npm script is only a convenience alias for running this one spec; the spec matches `productionSpecPattern` in `playwright.config.ts` and is listed explicitly in `scripts/playwright-pr-shards.mjs`, so it already runs in the `Production UI` job that `pr-required` demands on any UI-scoped PR. It must **not** be added to `verify:cheap:internal`: `check:gate-manifest` requires every gate in that chain to also run in `static-pr`, which has no browser and no server. Per-surface geometry stays in the held visual harness. **Both named gaps are now closed, 15 Aug 2026.** The rendered enumeration landed first: `885c613` (PR #1962) added the deterministic phone-viewport `min-h-tap` audit this row was waiting on — `/forms`'s fixed-array home rather than a live-search route, polled until three consecutive reads agree, explicitly sorted, and asserted identical across three full navigate-and-enumerate cycles. The `h-10` case is closed here by `interactiveTapFloorDeclarations` in `check:design-system-contract`: an interactive element (`a`, `button`, `input`, `select`, `summary`, `textarea`) declaring its own **unprefixed** `min-h-*` below the 48px token. Comparable arbitrary lengths and mutually exclusive/composed class branches are evaluated independently, so one safe branch cannot hide a sub-floor runtime path. Ratcheted at **5** across 4 files with per-path pins (updated 2 Sep 2026 — see below), so a new sub-floor control anywhere in `src/**` fails today. Mutation-verified: lowering one shortlist button to `min-h-9` produces both the total and matching per-path regression. **Deliberately scoped to `min-h-*`, not `h-*`/`size-*`:** a short `h-4` on an interactive element is routinely the visible box of a control whose hit area is owned by a tap-sized wrapper (`SelectionCheckbox` in `differentials-home.tsx`, whose label `ui-smoke` asserts still meets the floor), so flagging those would pad the baseline with non-defects — the §5 failure mode. **The `` blind spot is now closed** (2 Sep 2026): the walker also recognizes the `Link` JSX tag, which briefly raised the total from 2 to 18 (16 newly-visible real violations across 11 files) — a same-day adversarial re-audit found 12 of those 16 were mechanically easy (bare, unconditional, no rationale comment, matched against an already-correct sibling in the same file) and fixed them, leaving the 4 genuinely-hard sites (documented pseudo-element/row-pitch/test-coupled tradeoffs) at the current 5. It remains blind to other component-tag wrappers (`legacyTapClasses` has the same limit) and to a `className` forwarded through a component boundary (e.g. `DiagnosisTermChip`, which now carries the TOKENS.md-required density-reason comment at its two call sites instead), both tracked in `#265`. | +| 2 | Tap targets ≥48px interactive; static pills never carry `min-h-tap`; no production target reduced | **implemented-blocking for new use** | Enforced today: `--spacing-tap` is 48px and pinned from both sides (`ckb-v2-token-contract` asserts the `@theme` knob is ≥48 and that `--tap-min` is its alias), `ui-style-contract` measures the rendered floor for declared `min-height` carriers in Chromium, and the legacy-class / literal ratchets still run. **Still open for the `h-10` case, and 9 Aug 2026 measured why.** An enumeration of _rendered interactive_ elements was written, shown to find genuine defects, and then **reverted rather than landed**: it is not deterministic on this route. Six runs against one production build returned 6, 5, 4, 3, 3 and 9 distinct sub-floor shapes, largely disjoint — one run saw the answer-suggestion chips and a sort band, another the settled results list. `waitForLoadState("networkidle")` plus deduplication to distinct shapes did not fix it, and two consecutive agreeing runs turned out to be coincidence. This spec runs in the required `Production UI` job, so an intermittent version of it would block every merge in the repo; that is a worse outcome than the gap it closes. Path to blocking: give the audit a deterministic surface — a static route or a fixed seeded state — before re-attempting it. **What the enumeration did establish, in every one of the six runs, is a live defect:** controls that carry `min-h-tap` compute `min-height: 0px` and render at 16–36px, six distinct shapes in total. The declared-carrier audit cannot report them by construction — it only measures elements already computing at or above the floor, so a floor overridden downward is skipped rather than flagged. Tracked as `#293`. **Correction, same date — "`test:e2e:style-contract` is not part of `verify:cheap`" was true and badly misleading, and it sent one session looking for a wiring bug that does not exist.** The npm script is only a convenience alias for running this one spec; the spec matches `productionSpecPattern` in `playwright.config.ts` and is listed explicitly in `scripts/playwright-pr-shards.mjs`, so it already runs in the `Production UI` job that `pr-required` demands on any UI-scoped PR. It must **not** be added to `verify:cheap:internal`: `check:gate-manifest` requires every gate in that chain to also run in `static-pr`, which has no browser and no server. Per-surface geometry stays in the held visual harness. **Both named gaps are now closed, 15 Aug 2026.** The rendered enumeration landed first: `885c613` (PR #1962) added the deterministic phone-viewport `min-h-tap` audit this row was waiting on — `/forms`'s fixed-array home rather than a live-search route, polled until three consecutive reads agree, explicitly sorted, and asserted identical across three full navigate-and-enumerate cycles. The `h-10` case is closed here by `interactiveTapFloorDeclarations` in `check:design-system-contract`: an interactive element (`a`, `button`, `input`, `select`, `summary`, `textarea`) declaring its own **unprefixed** `min-h-*` below the 48px token. Comparable arbitrary lengths and mutually exclusive/composed class branches are evaluated independently, so one safe branch cannot hide a sub-floor runtime path. Ratcheted at **4** across 3 files with per-path pins (updated 2 Sep 2026 — see below), so a new sub-floor control anywhere in `src/**` fails today. Mutation-verified: lowering one shortlist button to `min-h-9` produces both the total and matching per-path regression. **Deliberately scoped to `min-h-*`, not `h-*`/`size-*`:** a short `h-4` on an interactive element is routinely the visible box of a control whose hit area is owned by a tap-sized wrapper (`SelectionCheckbox` in `differentials-home.tsx`, whose label `ui-smoke` asserts still meets the floor), so flagging those would pad the baseline with non-defects — the §5 failure mode. **The `` blind spot is now closed** (2 Sep 2026): the walker also recognizes the `Link` JSX tag, which briefly raised the total from 2 to 18 (16 newly-visible real violations across 11 files) — a same-day adversarial re-audit found 12 of those 16 were mechanically easy (bare, unconditional, no rationale comment, matched against an already-correct sibling in the same file) and fixed them, leaving the 4 genuinely-hard sites (documented pseudo-element/row-pitch/test-coupled tradeoffs) at the current 4. It remains blind to other component-tag wrappers (`legacyTapClasses` has the same limit) and to a `className` forwarded through a component boundary (e.g. `DiagnosisTermChip`, which now carries the TOKENS.md-required density-reason comment at its two call sites instead), both tracked in `#265`. | | 3 | Focus outline present, `--focus`, no companion ring | **implemented-blocking for new use** | Corrected 6 Aug 2026: the previous evidence ("`--focus` is referenced nowhere in the DS export", finding N3) was false — **[verified: grep]** 4 declarations (2 theme, 2 forced-colours) against **273** `var(--focus)` consumers, 260 of them in `.tsx`. Two-carrier idiom: controls get `outline: 2px solid var(--focus)`; fields use `fieldControl` / search-shell. Production TSX `focus:ring-4` is banned by `tests/search-shell-focus.contract.test.ts` (mutation: add `focus:ring-4` on a production field → red; revert). Rendered Gate 3 is in `tests/ui-style-contract.spec.ts` on `/forms/search?q=transport&run=1` at a phone viewport (three consecutive reads, then Tab to a labelled button/link and a field; composer pill box-shadow excluded). Residual: `.answer-footer-search-pill` halo required by `ui-smoke`. | | 4 | Non-colour encoding on every status indicator | **implemented-blocking** | `colourOnlyStatusIndicators` scans production class roots and shared swatch recipes. The remaining calculator spectrum and shared status-marker debt was retired on 22 Aug 2026: severity bands now add distinct neutral edge patterns, ready/review/muted markers differ by outline/fill/geometry, and visible labels remain at call sites. The metric and per-path baseline are pinned at **0**; `StatusMark`, persistent text, and decoration-only `--decoration-soft` retain their documented roles. | | 5 | Tables: semantic caption, associated headers, `aria-controls` on the expander | **implemented-blocking** | `AccessibleTableProps.caption` is required; DOM and alignment tests prove the semantic ``, associated headers, and expander relationship. | @@ -195,7 +195,7 @@ component (DECISIONS §Q2). **Manifest and publication governance are implemented-blocking for local source truth:** the design-sync contract checks export/source/props/preview parity, and the adoption generator checks -component-family parity plus all 47 production routes. Token-count sanity, deleted-token checks, +component-family parity plus all 224 production routes. Token-count sanity, deleted-token checks, theme-list parity, and remote design-project publication remain separate concerns. ## 3 · The prohibition table diff --git a/docs/design-system/SPEC.md b/docs/design-system/SPEC.md index 480ee27216..165739e522 100644 --- a/docs/design-system/SPEC.md +++ b/docs/design-system/SPEC.md @@ -263,8 +263,9 @@ Shadow-only ladder `--e0…--e4`; `--ring-hairline` for borderless floating surf > A card inside a panel never carries a heavier `--eN` than its parent. The ladder itself is gated against baked-in hairlines (**[verified:** contract test -`:171-179`**]**); the per-surface co-occurrence rule and the spread-term check are **planned** -(GATES §8). +`:171-179`**]**); the per-surface co-occurrence rule and the spread-term check both ship, ratcheted in +`check:design-system-contract` as `edgeOwnershipConflicts` and `onePixelShadowSpreads` (GATES §8, +row 8). `--shadow-inset` **stays the DS bevel**; `--shadow-well` is the recessed-well role. The former v2 `--shadow-inset` overrides became `--shadow-well` in `59e4c3dfc`, with the diff --git a/docs/disaster-recovery-runbook.md b/docs/disaster-recovery-runbook.md index 92fd2661e0..c0977bb6e4 100644 --- a/docs/disaster-recovery-runbook.md +++ b/docs/disaster-recovery-runbook.md @@ -60,8 +60,9 @@ trap - EXIT ``` On a hosted target, prefer the migration chain (`supabase db push` / -`supabase db reset --linked` semantics); measured chain replay: **48 s** for -102 migrations into the same container. Note the chain has known fidelity gaps +`supabase db reset --linked` semantics); measured chain replay: **48 s** into the same container +(measured 2026-07-07 against the migration chain as it stood on that date; the chain has grown +since, so re-measure before relying on this figure). Note the chain has known fidelity gaps vs schema.sql (buckets, two triggers, a handful of indexes — item 10 in [database-drift-detection.md](database-drift-detection.md#reconciliation-backlog)). @@ -140,20 +141,21 @@ Rollback is a no-update operation: before it starts there must be no candidate r A schema restore is not operationally complete until all five environment-owned controls have been re-created and verified (`#326`, `docs/operator-backlog.md`). Follow this operational checklist after any schema restore or disaster recovery drill: 1. **`pg_cron` schedules & retention jobs:** - - Re-create scheduled jobs for ingestion workers and telemetry retention (`purge-expired-rag-queries`, `purge-rag-retrieval-logs`, `purge-rag-query-misses`, `purge-rag-response-cache`). - - Sanity check: Run `SELECT jobid, jobname, schedule, active FROM cron.job ORDER BY jobid;` and confirm all 4 retention jobs + ingestion jobs are active (`active = true`). + - Re-create the `invoke_indexing_v3_agent` schedule (do this inside the approved change window for the restore and record the copied definition in the drill log; the schedule row is live-only, so copy its `jobname`, `schedule` and `command` from the source project with `SELECT jobname, schedule, command, active FROM cron.job WHERE jobname = 'invoke_indexing_v3_agent';` and re-create it with `SELECT cron.schedule(, , );`; on a non-production target immediately run `SELECT cron.alter_job(, active := false);` and activate it with `active := true` only after step 3 confirms its endpoint GUC) and the telemetry retention jobs (`purge-expired-rag-queries`, `purge-rag-retrieval-logs`, `purge-rag-query-misses`, `purge-rag-response-cache`). Do **not** re-create `invoke_ingestion_worker`: it was retired by `20260926041000` and the container worker owns ingestion. + - Sanity check: Run `SELECT jobid, jobname, schedule, active FROM cron.job ORDER BY jobid;` and confirm all 4 retention jobs are active (`active = true`), that `invoke_indexing_v3_agent` exists (active on production; still inactive on a non-production target until step 3 passes, then activate it and re-run this query), and that no `invoke_ingestion_worker` job exists. 2. **Supabase Vault secrets:** - Re-add required secrets (`cron_ingestion_jwt`, `indexing_v3_agent_secret`) via Supabase Vault. - Sanity check: Run `SELECT name, description, created_at FROM vault.decrypted_secrets ORDER BY name;` (names only; never log decrypted secret values). 3. **Custom database GUCs:** - - Re-set custom parameters (e.g. `app.indexing_agent_url`) via `ALTER DATABASE postgres SET ...`. - - Sanity check: Run `SELECT name, setting, source FROM pg_settings WHERE name LIKE 'app.%';`. + - Re-set custom parameters (e.g. `app.indexing_v3_agent_base_url`) by running `ALTER DATABASE postgres SET ...` as the project owner (`postgres`) in an operator session; the `schema.sql` statement skips with a notice when its role lacks the privilege, so do not rely on it. If the value is left unset, `invoke_indexing_v3_agent` falls back to the hardcoded production host (`supabase/schema.sql`, `invoke_indexing_v3_agent`) and would send the Vault secret there, so confirm the value with the check below before activating that job (step 1). + - Sanity check: Confirm the stored database-level value with `SELECT unnest(setconfig) FROM pg_db_role_setting WHERE setdatabase = (SELECT oid FROM pg_database WHERE datname = 'postgres') AND setrole = 0;` (or `pg_settings` from a new connection): `ALTER DATABASE ... SET` does not change the current session, so `pg_settings` in the same session can show a stale value. 4. **Supabase Edge Functions:** - Redeploy required functions (`indexing-v3-agent`, `site-content-sync`) with the Deno v2.x toolchain in an approved change window: ```bash supabase functions deploy indexing-v3-agent --project-ref + supabase functions deploy site-content-sync --project-ref ``` - - Sanity check: Verify function list via `supabase functions list` and run non-mutating health probes. + - Sanity check: Verify function list via `supabase functions list`, and confirm `OPENAI_API_KEY` appears in `supabase secrets list --project-ref ` (names and digests only); restore it through the approved secret-management procedure if missing. Do not `POST` to `site-content-sync` as a probe: it has no read-only endpoint and a `POST` claims queued outbox events, so invoking it needs separate approval. 5. **Dashboard-owned configuration:** - Re-enter auth provider configuration (magic link, Google/Apple/Microsoft SSO redirect URLs, rotating secrets), connection-pool caps (auth pool pinned at 10 connections), per-project API keys (`NEXT_PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`), and `E2E_USER_*` test credentials. - Sanity check: Run `npm run check:env-parity` and names-only checks to verify all consuming environments have valid references without logging secrets. diff --git a/docs/launch-operator-runbook.md b/docs/launch-operator-runbook.md index 11cf24896a..35192eb528 100644 --- a/docs/launch-operator-runbook.md +++ b/docs/launch-operator-runbook.md @@ -64,7 +64,7 @@ PIA-4 retention migrations. **Do not redeploy the worker until `20260708130000` apply the reviewed committed migration through the normal guarded workflow. Do not use this status reconciliation as authority to replay the verified July-8 or forward-codification groups. -After applying, query `cron.job` and expect exactly these active retention jobs: `purge-rag-query-misses` +After applying, query `cron.job` and expect at least these active retention jobs: `purge-rag-query-misses` at `45 3 * * *` and `purge-rag-response-cache` at `15 * * * *`. The obsolete `purge-expired-rag-response-cache` name must be absent. diff --git a/docs/observability-slos.md b/docs/observability-slos.md index 0bc0fb435e..1b3fd67477 100644 --- a/docs/observability-slos.md +++ b/docs/observability-slos.md @@ -409,7 +409,7 @@ thresholds map directly onto these fields. ## 5. Operational alert delivery and drill -`scripts/lib/operational-alerts.mjs` is the provider-neutral policy owner for the §2 answer SLOs. The hourly digest emits stable alert codes with owner, escalation owner and this runbook, and publishes `alerting`, highest `severity`, and a compact JSON summary to the workflow. Missing SLO telemetry and a zero-query denominator are `unknown`, never healthy. Each authorised deep probe returns only safe internal hybrid-RPC identifiers and counts. The workflow stores the last three identity-only hourly observations in the GitHub Actions cache, then pages `OPS_HYBRID_RPC_ERROR_RATE_PAGE` only when the same RPC has a nonzero count in all three contiguous UTC-hour buckets. A missing, evicted, malformed, incomplete, or non-contiguous history is never promoted to a page. Degraded answers page independently and immediately only above the documented 50% hourly threshold. +`scripts/lib/operational-alerts.mjs` is the provider-neutral policy owner for the §2 answer SLOs. The daily (22:00 UTC) digest emits stable alert codes with owner, escalation owner and this runbook, and publishes `alerting`, highest `severity`, and a compact JSON summary to the workflow. Missing SLO telemetry and a zero-query denominator are `unknown`, never healthy. Each authorised deep probe returns only safe internal hybrid-RPC identifiers and counts. The workflow stores the last three identity-only hourly observations in the GitHub Actions cache, then pages `OPS_HYBRID_RPC_ERROR_RATE_PAGE` only when the same RPC has a nonzero count in all three contiguous UTC-hour buckets. The digest runs once a day (`.github/workflows/ops-digest.yml`), so each scheduled run measures a single 60-minute window and consecutive windows are a day apart: this three-contiguous-hour condition needs one successful run in each of three consecutive UTC-hour buckets; scheduled and manual runs both count, so in practice manual dispatches supply at least two of them (a second run in the same bucket replaces that hour's observation rather than adding one); the daily schedule alone can never page it, because at least two of the three buckets must come from manual dispatches. A missing, evicted, malformed, incomplete, or non-contiguous history is never promoted to a page. Degraded answers page independently and immediately only above the documented 50% hourly threshold. The digest's **Service readiness** headline describes the service checks, not answer quality. An exactly 60-minute snapshot with zero answered queries displays **No answered queries observed—answer quality not assessed**, with answer-SLO rates shown as **N/A**. The underlying counters and machine-readable `OPS_ANSWER_SLO_UNKNOWN` alert remain unchanged: this is an unassessed window, not a measured degraded-answer incident or proof of healthy answers. A missing SLO block or invalid window must not be labelled as no answer activity. diff --git a/docs/operations-runbook.md b/docs/operations-runbook.md index c8c3c149ee..4cce3eaf76 100644 --- a/docs/operations-runbook.md +++ b/docs/operations-runbook.md @@ -21,7 +21,7 @@ Two critical production call sites filter on bare columns rather than the concat ```typescript query = query.or(`title.ilike.${pattern},file_name.ilike.${pattern}`); ``` -2. **RAG Candidate Assembly** (`src/lib/rag/rag-candidate-sources.ts:485` - `fetchDocumentTitleAliasRows`): +2. **RAG Candidate Assembly** (`src/lib/rag/rag-candidate-sources.ts:975` - `fetchDocumentTitleAliasRows`): ```typescript const filters = terms.flatMap((term) => [`title.ilike.%${term}%`, `file_name.ilike.%${term}%`]).join(","); query = query.or(filters).eq("status", "indexed").limit(12); @@ -31,10 +31,13 @@ Because PostgreSQL GIN expression indexes can only be matched by predicates whos ### The Pagination Query Pattern -Separately, `src/lib/search-scope.ts:271-277` executes paged queries: +Separately, `src/lib/search-scope.ts:311-320` executes paged queries (page size `documentScopeQueryPageSize` = 1000): ```typescript -query = query.eq("status", "indexed").order("id").limit(5000); +documentQuery = documentQuery + .eq("status", "indexed") + .order("id", { ascending: true }) + .range(offset, Math.min(offset + documentScopeQueryPageSize - 1, maxResolvedDocuments - 1)); ``` Without a composite index on `(status, id)`, PostgreSQL uses the single-column `documents_status_idx` and executes an in-memory or on-disk sort for `ORDER BY id`. @@ -45,7 +48,7 @@ Without a composite index on `(status, id)`, PostgreSQL uses the single-column ` > [!WARNING] > **Canary-Gated RAG Boundary**: -> `fetchDocumentTitleAliasRows` (`src/lib/rag/rag-candidate-sources.ts:485`) executes an unordered `.limit(12)`. +> `fetchDocumentTitleAliasRows` (`src/lib/rag/rag-candidate-sources.ts:975`) executes an unordered `.limit(12)`. > In PostgreSQL, an unordered `LIMIT` has no deterministic selection guarantee. Introducing an index alters query planner access paths, which can select a different set of 12 candidate documents to pass downstream into retrieval and synthesis. > Consequently, applying additive indexes to production is **canary-gated** and requires full RAG golden evaluation (`eval:retrieval:quality`) before promotion. @@ -81,7 +84,7 @@ SELECT id, title, file_name, metadata FROM public.documents WHERE status = 'indexed' ORDER BY id -LIMIT 5000; +LIMIT 1000; ``` **Expected Pre-Index Plan:** @@ -167,7 +170,7 @@ SELECT id, title, file_name, metadata FROM public.documents WHERE status = 'indexed' ORDER BY id -LIMIT 5000; +LIMIT 1000; ``` **Expected Post-Index Plan:** diff --git a/docs/outstanding-issues-inbox/144345ea-d294-494d-a768-5b911c028306.json b/docs/outstanding-issues-inbox/144345ea-d294-494d-a768-5b911c028306.json new file mode 100644 index 0000000000..5f2837802a --- /dev/null +++ b/docs/outstanding-issues-inbox/144345ea-d294-494d-a768-5b911c028306.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "144345ea-d294-494d-a768-5b911c028306", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Wrong file:line anchors in clinical safety documents (hazard analysis, RAG injection threat model)", + "detail": "docs/clinical-hazard-analysis.md has ~13 stale control anchors (e.g. answer-verification.ts:402 and :497 are really ~:1375 and ~:1512, off by up to 1,015 lines; sourceGovernanceRefusalAnswer moved files) and docs/rag-injection-threat-model.md :15/:135/:137 point at wrong lines (rag-source-block.ts:126 is :228; rag.ts:4753 is past a 4,139-line file). Both attribute evaluateEvidenceCoverageGate to rag.ts:3683/:5892 but it is defined in rag-coverage-gate.ts. docs:check-links cannot see this (paths resolve). Next action: re-point to current lines or symbol-anchored links; a ~40-line sweep asserting each symbol-plus-file:line pair resolves would catch the whole class.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67NJR5PNWGR1W5MV46FWW" + } +} diff --git a/docs/outstanding-issues-inbox/29314f51-76ca-451c-a478-ee5d47ef109b.json b/docs/outstanding-issues-inbox/29314f51-76ca-451c-a478-ee5d47ef109b.json new file mode 100644 index 0000000000..6d2acc6d98 --- /dev/null +++ b/docs/outstanding-issues-inbox/29314f51-76ca-451c-a478-ee5d47ef109b.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "29314f51-76ca-451c-a478-ee5d47ef109b", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "Roster week strip shows only the first shift kind on a day; Teaching next-session ignores all-day entries; WA public holidays end at 2028", + "detail": "roster-week-strip.tsx uses kinds[0] so a morning plus night shift looks like a morning alone (de-duplicate kinds and count distinct kinds for any overflow marker, test with two shifts of the same kind). teaching-view-model.ts:138 excludes all-day sessions from nextSession (restore a date guard for all-day entries: today or later). src/lib/on-call/wa-public-holidays.ts has data for 2026-2028 only, including the fairness count call site.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67NEHNHR0MM72GY0TM3HN" + } +} diff --git a/docs/outstanding-issues-inbox/2b3d5fda-8ce9-4996-8634-8b1b50d34476.json b/docs/outstanding-issues-inbox/2b3d5fda-8ce9-4996-8634-8b1b50d34476.json new file mode 100644 index 0000000000..bf4f40b7ee --- /dev/null +++ b/docs/outstanding-issues-inbox/2b3d5fda-8ce9-4996-8634-8b1b50d34476.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "2b3d5fda-8ce9-4996-8634-8b1b50d34476", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "listOwnerTeams issues up to 401 queries on one request (two count queries per service across up to 200 services)", + "detail": "src/lib/roster/owner/teams.ts listOwnerTeams (~:105-138), reached by GET /api/roster/owner/teams. Next action: replace the per-service Promise.all count pairs with one joined or grouped query, or a batched RPC; measure on staging only, never production.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67N097JJWKH6MRG27CDV3" + } +} diff --git a/docs/outstanding-issues-inbox/2b7a5ade-0197-41e7-baaa-6b089cedf636.json b/docs/outstanding-issues-inbox/2b7a5ade-0197-41e7-baaa-6b089cedf636.json new file mode 100644 index 0000000000..32a62fbec8 --- /dev/null +++ b/docs/outstanding-issues-inbox/2b7a5ade-0197-41e7-baaa-6b089cedf636.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "2b7a5ade-0197-41e7-baaa-6b089cedf636", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "Gate wiring: sast.yml is entirely continue-on-error and eslint-rules/ is not linted by lint:internal", + "detail": "A non-blocking repo-wide Semgrep job means no finding from the broad scan blocks a merge (the blocking Ingestion SAST job covers the upload surface only; SECURITY.md names the advisory job). package.json lint:internal does not include eslint-rules/. Next action: owner decision on whether sast.yml should block, and add eslint-rules to lint:internal.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67NGF237JN5WCKWM2JCFC" + } +} diff --git a/docs/outstanding-issues-inbox/3344a3a9-1a81-4a6a-85b3-3141ddc90c1c.json b/docs/outstanding-issues-inbox/3344a3a9-1a81-4a6a-85b3-3141ddc90c1c.json new file mode 100644 index 0000000000..97542bbdac --- /dev/null +++ b/docs/outstanding-issues-inbox/3344a3a9-1a81-4a6a-85b3-3141ddc90c1c.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "3344a3a9-1a81-4a6a-85b3-3141ddc90c1c", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P1", + "type": "issue", + "summary": "PDF extractor can turn prose beneath a table into a table row and rebuild the table markdown from it", + "detail": "worker/python/extract_pdf_assets.py recover_table_rows_from_edge_strip (~:557, caller ~:1367-1390) admits blocks by position alone (four bbox tests) and treats each line as a cell, then rebuilds the whole accessible table markdown, which reaches the vision model, document_table_facts (threshold_value/action) and page.text/search. Also appends recovered rows unsorted, drops single-baseline rows silently, has no test, and worker/python/** is outside the coverage inventory. Next action: accept a recovered block only when its line x-positions align with table.rows cell bboxes, sort by y0, drop the row on alignment failure, cover the markdown rebuild path too, and add a synthetic test (3-column table followed by a 3-line paragraph asserting no new row and unchanged accessible_table_markdown).", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67MV6ZWJCK4234NWY7VKF" + } +} diff --git a/docs/outstanding-issues-inbox/334f4e12-2bb6-4545-8318-ce9e86f0b933.json b/docs/outstanding-issues-inbox/334f4e12-2bb6-4545-8318-ce9e86f0b933.json new file mode 100644 index 0000000000..55e87f7933 --- /dev/null +++ b/docs/outstanding-issues-inbox/334f4e12-2bb6-4545-8318-ce9e86f0b933.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "334f4e12-2bb6-4545-8318-ce9e86f0b933", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "ui-specifiers Top match assertion cannot fail in the broken state, and the duplicate render behind it is uninvestigated", + "detail": "tests/ui-specifiers.spec.ts:149 uses visibleByText, which passes when a second copy of the Top match badge exists. The failing-run payload contains one copy and the React 19 reveal runtime moves children rather than copying, so the second copy is created by client JS after the reveal (assertion aborts at ~100 ms on the strict-mode violation). Next action: replace with expectSingleSettledOwner (reports count/visibleCount), then find and fix the app-level double render; the corrected assertion may stay red until that is fixed.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67N59HN9RD00VG8KRWJ3S" + } +} diff --git a/docs/outstanding-issues-inbox/6f0eb676-57af-4a30-9338-3f4f0f6dfbfc.json b/docs/outstanding-issues-inbox/6f0eb676-57af-4a30-9338-3f4f0f6dfbfc.json new file mode 100644 index 0000000000..ef950fe1bb --- /dev/null +++ b/docs/outstanding-issues-inbox/6f0eb676-57af-4a30-9338-3f4f0f6dfbfc.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "6f0eb676-57af-4a30-9338-3f4f0f6dfbfc", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "InboxRecord is an unchecked JSON.parse cast, so one malformed inbox file silently empties the whole ledger inbox", + "detail": "src/lib/developer-area/ledger-snapshot.ts:115 does JSON.parse(raw) as InboxRecord inside one try/catch that returns [] for the whole directory; no schema exists. Next action: add an inboxRecordSchema and safeParse per file so one bad record is skipped (and logged) rather than emptying the queue.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67N2F3156217SG5N7Z6B7" + } +} diff --git a/docs/outstanding-issues-inbox/a3510f93-98e4-423b-b336-8b7394a294a2.json b/docs/outstanding-issues-inbox/a3510f93-98e4-423b-b336-8b7394a294a2.json new file mode 100644 index 0000000000..6eeaceec64 --- /dev/null +++ b/docs/outstanding-issues-inbox/a3510f93-98e4-423b-b336-8b7394a294a2.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "a3510f93-98e4-423b-b336-8b7394a294a2", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P1", + "type": "issue", + "summary": "teaching_command('display.revoke') authorisation guard passes when display link created_by is NULL", + "detail": "supabase/migrations/20260926225609_teaching_mode.sql:1473 uses 'if not (v_link.created_by = p_actor_id or v_role = organiser) then raise'. created_by is nullable (on delete set null), so NULL or false is NULL and the raise never fires: a non-organiser can revoke a link whose creator was deleted. The same pattern was repaired in two other functions by a null-guards migration. Next action (needs an approved migration window, a Supabase migration reaches the live clinical database on merge): coalesce(v_link.created_by = p_actor_id, false) is not true and v_role <> 'organiser'. Fix before the teaching surface opens.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67MXDVDAXJX3GX37BA6DV" + } +} diff --git a/docs/outstanding-issues-inbox/ad367416-8819-4bc4-89b8-999c5a7d6ef3.json b/docs/outstanding-issues-inbox/ad367416-8819-4bc4-89b8-999c5a7d6ef3.json new file mode 100644 index 0000000000..3507f51930 --- /dev/null +++ b/docs/outstanding-issues-inbox/ad367416-8819-4bc4-89b8-999c5a7d6ef3.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "ad367416-8819-4bc4-89b8-999c5a7d6ef3", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Roster has no double-booking check on a doctor's own shifts at import or manual add", + "detail": "src/lib/roster/shifts (diffRoster reconciles by sourceUid then perthDate|title and never checks that two kept rows overlap; addManualShifts inserts without a check). Team-side eligibility already detects clashes (team/eligibility.ts). Next action: detectOwnConflicts(shifts) as a non-blocking warning at import review and server-side, strict interval overlap not adjacency.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67NCKMS6VXW9VZAYRPFGT" + } +} diff --git a/docs/outstanding-issues-inbox/bd1a2627-a534-4714-bdf9-f79c44c9f535.json b/docs/outstanding-issues-inbox/bd1a2627-a534-4714-bdf9-f79c44c9f535.json new file mode 100644 index 0000000000..789708d15a --- /dev/null +++ b/docs/outstanding-issues-inbox/bd1a2627-a534-4714-bdf9-f79c44c9f535.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "bd1a2627-a534-4714-bdf9-f79c44c9f535", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "On Call identifier guard covers only logistics and contacts, and it is not established whether it blocks or only warns", + "detail": "src/lib/on-call/free-text-guard.ts adminFreeTextProblem returns null for every section except logistics and contacts, and wraps identifierShapeWarning. Whether the other four sections have another validator, and whether a warning prevents persistence or transmission, is unestablished. Next action: one parameterised test with unique PII canaries in each of the six sections asserting rejected/redacted/persisted/logged/transmitted.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67NAMHEWR5X5PBZN9RSHZ" + } +} diff --git a/docs/outstanding-issues-inbox/cc5a466d-68c6-4c38-86e7-b3f520afbc74.json b/docs/outstanding-issues-inbox/cc5a466d-68c6-4c38-86e7-b3f520afbc74.json new file mode 100644 index 0000000000..a0000dbb58 --- /dev/null +++ b/docs/outstanding-issues-inbox/cc5a466d-68c6-4c38-86e7-b3f520afbc74.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "cc5a466d-68c6-4c38-86e7-b3f520afbc74", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P3", + "type": "issue", + "summary": "Three gate-script headers claim verify:cheap membership that is false (they are verify:full steps)", + "detail": "scripts/check-type-scale.mjs:15, scripts/check-codebase-index-coverage.mjs:16 and scripts/check-docs-links.mjs:20 assert a verify:cheap chain they are not in; scripts/check-docs-script-refs.mjs:40 also exempts test:e2e:advisory on a false 'renamed' comment (both test:e2e:advisory and test:e2e:regression exist). These are code files, so left for owner approval. Next action: correct the comments.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67NMXVWNEQ020K49BAEXW" + } +} diff --git a/docs/outstanding-issues-inbox/d2785add-72d1-47db-9e4b-32be1a8729e9.json b/docs/outstanding-issues-inbox/d2785add-72d1-47db-9e4b-32be1a8729e9.json new file mode 100644 index 0000000000..04d7ec8610 --- /dev/null +++ b/docs/outstanding-issues-inbox/d2785add-72d1-47db-9e4b-32be1a8729e9.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "d2785add-72d1-47db-9e4b-32be1a8729e9", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Doc/instruction drift left open after the doc-fix pass: encoding damage, cloud-profile worktree rule, RAG duty missing from skills, stale decision", + "detail": "(1) docs/privacy-impact-assessment.md and docs/openai-cross-border-basis.md carry double-encoded text and a BOM (a verified-lossless guarded per-run repair exists on an unpushed branch; never a naive whole-file re-encode); docs/outstanding-issues.md:15 has a corrupted arrow (canonical ledger, edit only via reconciliation). (2) .claude/cloud-profile/skills/prompt/references/coding-workflow.md:21 has the pre-hardening worktree rule and is copied into every container session (owner decision on owned versus vendored). (3) No .agents skill mentions the RAG ranking-protection duty, and the retrieval-quality and reindex runbooks have no canary-pair or approval requirement. (4) docs/decisions/2026-09-26-remove-ward-flow-and-caring-contacts.md says Caring Contacts was removed but src/lib/caring-contacts/clock.ts is live with importers; needs a dated superseding decision, do not delete the file. (5) Admin renewals page test dropped its checked-wording assertion while admin-renewed-sheet still forbids the word; record the decision. (6) min-h-[48px] remains at account-setup-dialog.tsx:246 against a 'none remain' claim in docs/redesign/07-token-adoption-audit.md.", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67NPZWMGPSGFPRBRSPTZ0" + } +} diff --git a/docs/outstanding-issues-inbox/f1bd7d17-da23-4984-bdeb-f2905a49748b.json b/docs/outstanding-issues-inbox/f1bd7d17-da23-4984-bdeb-f2905a49748b.json new file mode 100644 index 0000000000..81f7365b09 --- /dev/null +++ b/docs/outstanding-issues-inbox/f1bd7d17-da23-4984-bdeb-f2905a49748b.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "f1bd7d17-da23-4984-bdeb-f2905a49748b", + "createdOn": "2026-10-13", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Test-deletion guard blind spots: each-tables count as one case, Python tests are invisible, mass skip is undetected", + "detail": "scripts/check-diff-integrity.mjs counts each it.each(table) once (100 tables with 8+ rows, largest 48), TEST_FILE_PATTERN matches only TS/JS .spec/.test files (worker/python/test_*.py and tests/roster-publication-concurrency.mjs are outside it), and skip/only are counted as declared cases. Next action: owner decision on counting each-rows, adding .py, and flagging mass .skip; do not add skip to NON_CASE_MEMBERS (that would be wrong).", + "source": "PsychSift audit review (16 audit reports) checked against main 4382217, 2026-10-13", + "issueUlid": "01M4T67N833ZHFH9X3YYR7EKNZ" + } +} diff --git a/docs/reindex-runbook.md b/docs/reindex-runbook.md index 582d945bb5..5f773cd03b 100644 --- a/docs/reindex-runbook.md +++ b/docs/reindex-runbook.md @@ -55,6 +55,7 @@ If the response includes `safety.safeToRun: false`, do not retry repeatedly. Use - `supabase_unavailable`: pause reindexing and rerun `npm run supabase:recovery-status`. - `active_jobs`: wait for pending or processing jobs to finish before retrying. +- `active_agent_enrichment`: an agent-enrichment pass is running on one or more selected documents; wait for it to finish before reindexing. - `stale_processing_jobs`: run `npm run recover:ingestion -- --apply --limit 20`, then rerun `npm run reindex:health`. - `queued` documents with no open job (upload crash between document insert and job insert): dry-run `npm run recover:ingestion -- --include-stranded-queued`, then re-run with `--apply` after reviewing the candidate list. Optional `--owner-id` and `--stranded-min-age-minutes` keep the sweep bounded. The six-hour ingestion-autopilot probe detects aged owner-scoped rows and opens/updates the operator alert issue; it never applies this recovery unless the guarded manual apply dispatch is used. diff --git a/docs/testing.md b/docs/testing.md index b7275b4c8a..6d4a22739e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -226,7 +226,7 @@ Firefox and WebKit. The reason is runtime against the evidence in this repository. Each of these projects replays the **entire** production journey suite on an emulated iPhone 14, the same shape as the Firefox and WebKit suites. The last recorded complete matrix (run 4012) took **38 minutes** for three such -suites, the job carries a **70-minute** timeout, and a ~70-minute matrix run holding +suites, the job then carried a **70-minute** timeout (it has since been split into per-engine legs capped at 45 minutes; see `.github/workflows/ci.yml`), and a ~70-minute matrix run holding `CI-refs/heads/main` is the incident recorded in [ci-operations.md](ci-operations.md). Two more full suites is roughly another 25 minutes on a job already sized to its ceiling — and on the blocking PR gate, which today runs sharded Chromium only, it would be the difference between a diff --git a/src/components/first-nations/where-is-home.tsx b/src/components/first-nations/where-is-home.tsx index 312b7505b0..ef4325d30e 100644 --- a/src/components/first-nations/where-is-home.tsx +++ b/src/components/first-nations/where-is-home.tsx @@ -133,6 +133,14 @@ export function WhereIsHomePanel({ regions, map, interpreter, mapSource = null } () => setNote("Copying isn't available on this phone"), ); const regionLabels = useMemo(() => Object.fromEntries(regions.map((r) => [r.id, r.label])), [regions]); + const selectRegion = (id: string) => { + setNote(null); + if (id === regionId) return; + setRegionId(id); + // The letter lists one region's numbers; keeping it after a switch would let Copy + // paste region A's services while region B is on screen. + setLetter([]); + }; return (
- +
{regions.map((r) => (