feat(#193 follow-up): filter the capabilities registry by Lifebook#256
Merged
jayzalowitz merged 3 commits intoMay 12, 2026
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a Lifebook-based filter to the Capabilities “Browse registry” section by fetching the user’s Lifebooks and intersecting registry results with the selected Lifebook’s suggestedCapabilities list. This completes the deferred “filter capabilities by domain/Lifebook” follow-up from #242 / #193.
Changes:
- Fetch Lifebooks on Capabilities page render and conditionally render a Lifebook
<select>filter. - Introduce a unified
readRegistryFilterState()helper and update registry re-render call sites to use a single state object. - Apply client-side Lifebook intersection filtering and enhance empty-state messaging to reflect the active Lifebook filter.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| CHANGELOG.md | Adds an unreleased changelog entry describing the Lifebook registry filter feature and UX behavior. |
| apps/web/public/js/pages/capabilities.js | Fetches Lifebooks, renders a Lifebook filter dropdown, centralizes filter state reading, and intersects registry results with Lifebook suggestions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _cachedDormant = capData.dormant ?? []; | ||
| _cachedRecipes = recipesData.recipes ?? []; | ||
| _cachedPendingOptIns = optInsData.optIns ?? []; | ||
| _cachedLifebooks = (lifebooksData?.lifebooks ?? []).filter((lb) => !lb.hiddenAt); |
Comment on lines
+257
to
+261
| document.getElementById('registry-category')?.addEventListener('change', () => { | ||
| renderRegistryResults(userId, readRegistryFilterState()); | ||
| }); | ||
| document.getElementById('registry-lifebook')?.addEventListener('change', () => { | ||
| renderRegistryResults(userId, readRegistryFilterState()); |
Comment on lines
+581
to
+582
| * `suggestedCapabilities` set. Pure function so it's trivial to unit-test | ||
| * separately if a vitest harness for `apps/web` lands later. |
Comment on lines
+579
to
+596
| /** | ||
| * Intersect a registry-entries list with the selected Lifebook's | ||
| * `suggestedCapabilities` set. Pure function so it's trivial to unit-test | ||
| * separately if a vitest harness for `apps/web` lands later. | ||
| * | ||
| * Returns the input unchanged when: | ||
| * - No Lifebook is selected (empty string passed) | ||
| * - The Lifebook isn't in our cached list (defensive — should never happen) | ||
| * - The Lifebook has no suggested capabilities (empty registryId[] means | ||
| * "the domain extractor didn't propose anything yet" — better to show | ||
| * all than show nothing) | ||
| */ | ||
| function applyLifebookFilter(entries, lifebookDomain) { | ||
| if (!lifebookDomain) return entries; | ||
| const lb = _cachedLifebooks.find((x) => x.domainName === lifebookDomain); | ||
| if (!lb) return entries; | ||
| const allowed = lb.suggestedCapabilities ?? []; | ||
| if (allowed.length === 0) return entries; |
Comment on lines
+251
to
262
| // Wire category + lifebook <select> change events. The singleton-delegator | ||
| // pattern in `handleCapabilitiesAction` handles clicks via `data-action`, | ||
| // but native `change` events on form controls don't bubble through the | ||
| // same path on every browser — wire them explicitly. Using | ||
| // `readRegistryFilterState()` keeps both selectors agreeing on the state | ||
| // they hand to renderRegistryResults. | ||
| document.getElementById('registry-category')?.addEventListener('change', () => { | ||
| renderRegistryResults(userId, readRegistryFilterState()); | ||
| }); | ||
| document.getElementById('registry-lifebook')?.addEventListener('change', () => { | ||
| renderRegistryResults(userId, readRegistryFilterState()); | ||
| }); |
Comment on lines
+13
to
+19
| **No DB migration required.** The filter is purely client-side: the | ||
| intersection set already lives on `lifebooks.suggested_capabilities` | ||
| (populated by the domain-extraction worker that landed in #242). The | ||
| capabilities page now fetches `/api/lifebooks/:userId` alongside its | ||
| existing data and runs the intersect in `applyLifebookFilter()` — a | ||
| pure helper that's trivial to lift to a vitest harness if/when one | ||
| lands in `apps/web`. |
jayzalowitz
added a commit
that referenced
this pull request
May 11, 2026
…t handlers, make filter truly pure Three Copilot findings on PR #256 addressed: 1. Lifebook visibility filter now reads `hidden: boolean` (the actual API field) instead of the nonexistent `hiddenAt`. The previous `!lb.hiddenAt` was a no-op AND would have fail-opened if the endpoint ever returned hidden rows. The server-side listVisible() still does the real filtering; the client-side guard is defense in depth. 2. Removed data-action from the category + lifebook <select> elements. Each had an explicit change listener too, so the global click delegator double-fired renderRegistryResults on every dropdown-open. Change listener is now the sole entry point. Dead switch cases deleted. 3. applyLifebookFilter() is actually pure now — lifebooks is the third argument with a default that pulls from cache only at the call site. Future vitest harness can drive it without monkeypatching module state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 11, 2026
jayzalowitz
added a commit
that referenced
this pull request
May 12, 2026
Copilot round-2 flagged that the CHANGELOG entry described readRegistryFilterState() as "pure" — but it reads the current dropdown / input values from the DOM, so it's stateful by definition. Rewrote the bullet to describe it as "isolated" (one place to change when filters are added) instead, which is the actual property the helper provides. The other two round-2 comments on this PR (applyLifebookFilter "pure" docstring + data-action duplicate listeners) were both already addressed in the round-1 fix-up (2c7eef2) — they were re-flagged because Copilot was reading stale state.
Owner
Author
|
Round-2 review reply: Already fixed in round 1 (2c7eef2):
Round-2 finding fixed in eac5b7b:
|
Closes the "capability filter by domain on Capabilities page" item PR carries a Lifebook dropdown next to the existing category filter; selecting a Lifebook intersects the registry results with that Lifebook's suggestedCapabilities: registryId[] set. No DB migration required — the intersection set already lives on lifebooks.suggested_capabilities (populated by the domain-extraction worker shipped in #242). The capabilities page fetches /api/lifebooks alongside its existing data and applies applyLifebookFilter() — a pure helper trivial to lift into a vitest harness if/when one lands for apps/web. UX details: - Dropdown shows visible (non-hidden) Lifebooks only. - When the user has zero Lifebooks, the dropdown is omitted entirely. - Empty-result state surfaces the active Lifebook name in the copy. - A Lifebook with empty suggestedCapabilities means "extractor proposed nothing yet" — shows everything rather than collapsing, distinguishing "haven't decided" from "decided nothing matches." Internal cleanup: three renderRegistryResults() call sites collapsed to (userId, readRegistryFilterState()) via a new readRegistryFilterState helper. Adding the Lifebook filter without this would have meant editing five separate call sites. Test plan: smoke-tested in Chrome across three scenarios (all-Lifebooks, Health-only, Money-only, switch back to all-Lifebooks). Filter toggles correctly; hidden Lifebook stays out of the dropdown. node --check clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t handlers, make filter truly pure Three Copilot findings on PR #256 addressed: 1. Lifebook visibility filter now reads `hidden: boolean` (the actual API field) instead of the nonexistent `hiddenAt`. The previous `!lb.hiddenAt` was a no-op AND would have fail-opened if the endpoint ever returned hidden rows. The server-side listVisible() still does the real filtering; the client-side guard is defense in depth. 2. Removed data-action from the category + lifebook <select> elements. Each had an explicit change listener too, so the global click delegator double-fired renderRegistryResults on every dropdown-open. Change listener is now the sole entry point. Dead switch cases deleted. 3. applyLifebookFilter() is actually pure now — lifebooks is the third argument with a default that pulls from cache only at the call site. Future vitest harness can drive it without monkeypatching module state. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot round-2 flagged that the CHANGELOG entry described readRegistryFilterState() as "pure" — but it reads the current dropdown / input values from the DOM, so it's stateful by definition. Rewrote the bullet to describe it as "isolated" (one place to change when filters are added) instead, which is the actual property the helper provides. The other two round-2 comments on this PR (applyLifebookFilter "pure" docstring + data-action duplicate listeners) were both already addressed in the round-1 fix-up (2c7eef2) — they were re-flagged because Copilot was reading stale state.
eac5b7b to
23d3256
Compare
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the "capability filter by domain on Capabilities page" item that PR #242 (#193 Child 1) explicitly deferred. The Capabilities page now carries a Lifebook dropdown alongside the existing category filter; selecting a Lifebook intersects the registry results with that Lifebook's
suggestedCapabilities: registryId[]set.What changed
One file:
apps/web/public/js/pages/capabilities.js/api/lifebooks/:userIdalongside the existing capabilities + recipes + opt-ins data on render. Best-effort — failure degrades to "no Lifebook filter offered" rather than blocking the page.renderLifebookFilterDropdown(lifebooks)renders the<select>only when at least one visible Lifebook exists; hidden Lifebooks (hiddenAtset) are filtered out, matching the existing "hidden surfaces stay queryable in memory; surface visibility is the user's call" contract Lifebooks already follow.applyLifebookFilter(entries, lifebookDomain)is the load-bearing piece. Pure function — intersects the registry results with the selected Lifebook'ssuggestedCapabilitiesset. Three short-circuit cases documented in the function docstring: no filter selected, Lifebook not in cached list, Lifebook has emptysuggestedCapabilities(extractor proposed nothing yet — show everything rather than collapse to zero).No results found for the "Health" Lifebook.) so the user can immediately tell why nothing's showing.Internal cleanup
The three
renderRegistryResults(userId, q, category)call sites collapsed to a singlerenderRegistryResults(userId, readRegistryFilterState())form via a newreadRegistryFilterState()helper. Adding the Lifebook filter without this would have meant editing five separate call sites; the next filter to land only has to touch two places now (the dropdown markup and the state reader).What this PR deliberately does NOT do
lifebooks.suggested_capabilities, populated by the domain-extraction worker that shipped in PR feat(#193 Child 1): Emergent Lifebooks — domain extractor + dashboard surface #242. Adding adomain_namecolumn onmcp_serverswould have been the heavier approach; this one stays purely on the read side.Test plan
localhost:3202/#/capabilitieswith mocked Lifebook + registry data:cap-fitbitonly (other Health-suggested entries aren't in the registry mock; the intersect handled it correctly).cap-mintonly.node --checkclean oncapabilities.js.Smoke-test results captured live:
{ "dropdownPresent": true, "dropdownOptions": ["", "Health", "Money"], // "Hidden" correctly absent "allEntriesCount": 4, "healthEntries": ["cap-fitbit"], "moneyEntries": ["cap-mint"], "allBackEntries": ["cap-fitbit", "cap-mint", "cap-github", "cap-spotify"] }Notes for reviewers
applyLifebookFilter()is intentionally pure (no DOM reads, no module-level state mutation) so it's portable to a vitest harness ifapps/webever gets one. The three short-circuit cases (no filter selected / Lifebook missing / empty suggested list) are documented inline.🤖 Generated with Claude Code