feat(filter-contract): converge documents onto the shared filter component - #1910
Conversation
Services gains its first real filtering: five typed dimensions (catchments, age_groups, setting_flags, acuity_flags, housing_flags) become facets with union counts, substance_flags ships as a lens (an exact partition — every service carries exactly one of general/aod), and a scope segment escapes a filtered-to-zero state without discarding the query. The six query-replacing "quick filters" are evicted from the filter sheet to AnswerSuggestionChips, closing the navigate-kind defect docs/filter-contract.md exists to remove. The shared filter renderer (ResultFilterSheet/ResultFilterFacetChips) grows a dense tier — find-a-filter plus collapse-by-default — for services' 5 groups / 24 options, resolving the open density decision tracked as /issues #309. The OR-within-group/AND-across-group selection algorithm is extracted from document-tags.ts into facet-selection.ts so services and documents share one counting rule rather than each inventing their own. Live-registry verification that catalog_payload.tags is fully populated on the production Supabase project is still outstanding — rowToServiceRecord degrades conservatively to empty facets if it isn't, and this is called out explicitly for review. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1kfPCeWWSrbprFz56UeDD
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1kfPCeWWSrbprFz56UeDD # Conflicts: # docs/outstanding-issues.md
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1kfPCeWWSrbprFz56UeDD
…onent Deletes the ~500-line bespoke DocumentFilterPanel/DocumentFilterTrigger and rebuilds documents' filter sheet on ResultFilterSheet/ResultFilterTrigger (src/components/clinical-dashboard/result-filter-control.tsx), reusing the dense tier (find-a-filter, collapse-by-default, disclosure headers) that PR C already generalized from documents' own >3-groups rule. This is the last mode in the docs/filter-contract.md rollout. Three small additive extensions to the shared component, all optional and inert for the six modes that adopted earlier: - ResultFilterSheet gains meterContent (the "N of M documents shown" bar, rendered first in the body). - ResultFilterSheet gains footerOverride (replaces the default footer entirely, for documents' "Show N documents" + "Browse all sources"). - resultFilterGroup() gains an optional note (the "one only" annotation on the source-type lens, now that it shares a sheet with facet groups for the first time). toggleTagFacet narrows from SmartDocumentTagFacet to its key. Two testids move from literal strings to the shared component's own derivation (document-filter-clear -> document-filter-panel-clear, document-filter-find -> document-filter-panel-find); document-filter-done and document-filter-browse-library stay unchanged as custom footerOverride JSX. The dead-end facet's sr-only reason text changed from documents' bespoke copy to the shared component's generic message; the guard mechanics (disabled, focusable, click-blocked) are unchanged. decoration-on-text contract checks move to result-filter-control.tsx along with the markup they guard. docs/filter-contract.md's Rollout section is updated to close out; a new /issues follow-up (#312) notes services has the same lens-beside-facets shape without the annotation, and /issues #170 records documents as done. Stacked on claude/artifact-build-ygfit8 (PR C, #1882, not yet merged) — this branch's diff includes PR C's commits until #1882 merges.
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in:52 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
Comment |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:4cefac631f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:745c0181e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
… band The Codex reviewer on PR #1925 is right and my closure was wrong. Verified on main 2d27039: result-filter-control.tsx computes const dense = facetGroups.length > 3 || totalFacetOptions > 20 so formulation — one facet group of nine derived domains, the exact case that opened #309 — evaluates dense=false and still renders a wrapping chip row. The 6-20 full-width tier with its right-aligned count column does not exist, and neither does the nine-option DOM assertion the row asked for. What PR F (#1910) delivered is the upper tier: find-a-filter and collapse for >3 groups or >20 options, which is what documents needed. Section 5 has two thresholds; I conflated them and would have archived the row whose specific band is unbuilt. Replaces the done request with an update recording the partial delivery, what remains, and an explicit stop rule against closing on the strength of the upper tier. #170 is unaffected — mode adoption did complete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011btGFwWKYFWDs5McQkqz9J
Summary
claude/artifact-build-ygfit8(PR C, feat(services): adopt the filter contract with real facets (PR C) #1882, not yet merged), becausedocuments' convergence reuses the dense tier (find-a-filter, collapse-by-default, disclosure
headers) PR C already added to
ResultFilterSheet/ResultFilterFacetChips. This PR's diffwill show PR C's commits until feat(services): adopt the filter contract with real facets (PR C) #1882 merges, then shrink automatically to just this PR's
changes.
DocumentFilterPanel/DocumentFilterTrigger(
src/components/clinical-dashboard/document-search-results.tsx) and rebuilds documents'filter sheet on the shared
ResultFilterSheet/ResultFilterTrigger(
src/components/clinical-dashboard/result-filter-control.tsx). This is the last mode in thedocs/filter-contract.mdrollout — every filter surface in the original audit now shares onecomponent.
bespoke pieces have a home. All default to inert; the six modes that adopted earlier (services,
factsheets, formulation, differentials, medication, applications, specifiers) render
byte-identical output:
ResultFilterSheetgainsmeterContent— theN of M documents shownprogress bar,rendered first in the body, above the find-a-filter field.
ResultFilterSheetgainsfooterOverride— replaces the entire default footer, fordocuments' "Show N documents" primary button + "Browse all sources" secondary action.
resultFilterGroup()gains an optionalnote— the "one only" annotation on the source-typelens group's heading, distinguishing it from the facet groups sharing the same sheet. This is
the first sheet where a lens and facets sit side by side; services (PR C) has the identical
shape (a
substancelens beside five facet groups) without the annotation, which I filed as afollow-up (
/issues #312) rather than in scope here.toggleTagFacetnarrows from(facet: SmartDocumentTagFacet) => voidto(key: string) => void— it only ever read
facet.key.`${testId}-x`derivation (testId="document-filter-panel"is preserved, so the manyexisting
document-filter-panelassertions need no change):document-filter-clear→document-filter-panel-cleardocument-filter-find→document-filter-panel-finddocument-filter-doneanddocument-filter-browse-librarystay unchanged — they're nowcustom JSX inside
footerOverride, under this file's own control, not derived by the sharedcomponent.
sr-onlyreason text changed from documents' bespoke copy ("No documentsmatch this with the current filters") to the shared component's generic message ("No matches
with your current filters"). The guard mechanics —
aria-disabled, focusable, click-blocked,dashed border, muted text tier — are unchanged; only the copy simplified to the one message
every mode now shares. Updated the one DOM assertion that pinned the old copy.
tests/decoration-on-text.contract.test.ts's four source-scan checks (dead-end styling, facethint text, find-field placeholder) moved from
document-search-results.tsxtoresult-filter-control.tsxalong with the markup they guard; the{facet.count}/{tab.count}pair collapsed into one
{option.hint}check since both now render through the same code path.This is markup relocating, not a change to the token-policy rule these checks enforce.
docs/filter-contract.md's Rollout section is updated to record documents' convergence andclose out the rollout.
/issues #170(the tracking row for this whole initiative) is updatedto record documents as done; therapy-compass was not part of this session's scope and remains
open on that row.
Verification
npm run verify:pr-local— summary:completed: check:runtime, check:installed-lock-parity, format:changed, sitemap:check, docs:check-index, docs:check-inventory, docs:check-scripts, docs:check-links, check:branch-review-ledger, check:outstanding-issues, lint, typecheck, test, build, check:rag:fixtures — failed: (none) — not reached: (none)npm run test:Test Files 566 passed (566)/Tests 6172 passed | 4 skipped (6176)(after regenerating
docs/design-system/adoption-manifest.jsonfor the file's newResultFilterSheet/ResultFilterTriggerusage, same as PR C).npm run check:bundle-budgetafter a cleanrm -rf .next && npm run build: production1306.1 KiB gzipvs baseline1279.1 KiB(within 10% tolerance); mockups294.5 KiBvsbaseline
267.5 KiB(within 25% tolerance).tests/document-filter-panel.dom.test.tsx(26 cases, the primary proof surface for the porteddense/collapse/needle/dead-end mechanics against a 2,014-document fixture) passes with only the
two testid renames and the one copy-text assertion above.
npm run verify:ui— ran targeted Chromium proof instead,same approach as PR C/D:
npm run ensure, then a Playwright script (pinned executable per/issues #255) against a real production build covering both triggers, source-type toggle, the"one only" annotation, the meter, footer commit/browse-library buttons, and 0px horizontal
overflow at 1440/800/390/320 — 17/19 checks passed. The two that didn't (find-a-filter dense
tier, a genuine dead-end facet) don't trip live because this environment's demo corpus (~10
synthetic documents) is far below the
>3 groups/>20 optionsthreshold and too small to forcea real dead end — that mechanics is what the 26-case DOM suite above proves instead, against a
fixture sized to trip both. Also ran the existing
tests/ui-smoke.spec.tsblock covering allthree
document-filter-*testid sections (document search mode lists matching documents and result actions @critical) against the converged code vianode scripts/run-playwright.mjs tests/ui-smoke.spec.ts --project=chromium --grep <name>withthe pinned executable — 1 passed, proving the ported tier behaves identically in a real
production Chromium journey.
Risk and rollout
or ranking changes.
classifyPullRequestFilesreportsclinicalRisk: false,operationalRisk: false,ragRanking: false,ui: true.DocumentFilterPanel/DocumentFilterTriggerare fully deleted soa revert restores them along with their original tests.
Notes
claude/artifact-build-ygfit8, feat(services): adopt the filter contract with real facets (PR C) #1882). Please review/merge PR Cfirst; this PR's diff will shrink to just its own changes once that lands.
docs/filter-contract.mdRollout section (item 4, "Documents last").Generated by Claude Code