fix(search): key ext facet filters on the NIP-AMB <ns>:<facet> grammar (#54) #58

Merged
laoc merged 1 commit from fix/issue-54-ext-filter-shape into dev 2026-07-30 10:31:14 +00:00
Owner

Closes #54.

The issue understated this — it is not latent

The issue's severity section says filters.extFields is "never populated outside tests". That was true when it was filed and is not true on dev @ 09842a48: LearningContentFilters.svelte holds an extFields map and emits it, ExtFacetField.svelte renders the chips, and LearningView.svelte:250 passes it to the loader. The ext facet filters in the learning view are live and match nothing. Toggling a chip narrows the result set to zero rather than filtering it.

The correct key, derived rather than guessed

formValuesToAmbJson.js:63-66 stores a form-driven ext value at amb.ext[form.dTag][field.id], which serializes to the tag key ext:<dTag>:<fieldId>; ambJsonToFormValues.js:100 inverts exactly that. So the filter key is <dTag>:<fieldId> — the kind and the author pubkey are not part of the grammar.

Three places, not two

The issue named the two helpers in searchQueryBuilder.js. It missed the one that matters most: LearningContentFilters.svelte:extKeyFor() builds the stale key and feeds it to those helpers, so fixing only the helpers would have changed nothing observable. Both test files also asserted the stale shape as correct.

Decisions worth reviewing

Malformed keys now emit no filter. extPathToTagKey() returns null unless it gets exactly two non-empty colon-free segments. A malformed key producing nothing is much better than one producing a filter that cannot match — the second reads to a user as "no results" rather than "this is broken".

Ext facets are gone from the NIP-50 search string. The dot path was removed rather than repaired, per the issue's own recommendation: with reverse-DNS namespaces, ext.org.edufeed.ekw.konfi.themen.id gives no way to tell where the namespace ends and the facet begins. It is unfixable, not just wrong. The exact-match #ext: tag filter is what the relay indexes (nostrlib edufeed @ 99dfee4).

Verification

At e99dfecb: full suite 5153 passed / 5153, 468 files, lint clean, pnpm check 0 errors.

Negative control: restoring the old extKeyFor() fails both component tests, including the new end-to-end one that runs the UI's emitted key through buildSearchFilterObject. Asserting the two halves separately is exactly what let them drift apart in the first place.

The one Errors: 1 line in the suite output is the pre-existing GlobalFAB.test.js teardown race — #55, unrelated, every test passes.

Not browser-verified yet — a unit test cannot show that a chip now actually filters instead of zeroing the list. Handed to TestOER.

Closes #54. ## The issue understated this — it is not latent The issue's severity section says `filters.extFields` is "never populated outside tests". That was true when it was filed and is not true on `dev` @ `09842a48`: `LearningContentFilters.svelte` holds an `extFields` map and emits it, `ExtFacetField.svelte` renders the chips, and `LearningView.svelte:250` passes it to the loader. **The ext facet filters in the learning view are live and match nothing.** Toggling a chip narrows the result set to zero rather than filtering it. ## The correct key, derived rather than guessed `formValuesToAmbJson.js:63-66` stores a form-driven ext value at `amb.ext[form.dTag][field.id]`, which serializes to the tag key `ext:<dTag>:<fieldId>`; `ambJsonToFormValues.js:100` inverts exactly that. So the filter key is **`<dTag>:<fieldId>`** — the kind and the author pubkey are not part of the grammar. ## Three places, not two The issue named the two helpers in `searchQueryBuilder.js`. It missed the one that matters most: **`LearningContentFilters.svelte:extKeyFor()` builds the stale key and feeds it to those helpers**, so fixing only the helpers would have changed nothing observable. Both test files also asserted the stale shape *as correct*. ## Decisions worth reviewing **Malformed keys now emit no filter.** `extPathToTagKey()` returns null unless it gets exactly two non-empty colon-free segments. A malformed key producing *nothing* is much better than one producing a filter that cannot match — the second reads to a user as "no results" rather than "this is broken". **Ext facets are gone from the NIP-50 `search` string.** The dot path was removed rather than repaired, per the issue's own recommendation: with reverse-DNS namespaces, `ext.org.edufeed.ekw.konfi.themen.id` gives no way to tell where the namespace ends and the facet begins. It is unfixable, not just wrong. The exact-match `#ext:` tag filter is what the relay indexes (nostrlib `edufeed` @ `99dfee4`). ## Verification At `e99dfecb`: full suite **5153 passed / 5153**, 468 files, lint clean, `pnpm check` **0 errors**. **Negative control:** restoring the old `extKeyFor()` fails both component tests, including the new end-to-end one that runs the UI's emitted key through `buildSearchFilterObject`. Asserting the two halves separately is exactly what let them drift apart in the first place. The one `Errors: 1` line in the suite output is the pre-existing `GlobalFAB.test.js` teardown race — #55, unrelated, every test passes. **Not browser-verified yet** — a unit test cannot show that a chip now actually filters instead of zeroing the list. Handed to TestOER.
The ext facet filters in the learning view could not match anything. The
filter key carried the kind, the author pubkey and the d-tag —
`30168:<pub>:<d>:<fieldId>` — but `ns` and `facet` MUST NOT contain `:`
under the NIP-AMB grammar, so the key had no valid reading. Toggling a
facet chip narrowed the result set to zero instead of filtering it.

The correct key comes from the write path rather than from guessing:
`formValuesToAmbJson.js` stores a form-driven ext value at
`amb.ext[form.dTag][field.id]`, which serializes to the tag key
`ext:<dTag>:<fieldId>`. `ambJsonToFormValues.js` inverts exactly that.
So the filter key is `<dTag>:<fieldId>`.

Three places built or asserted the stale shape; the issue named only the
first two:

- `searchQueryBuilder.js` — the two helpers
- `LearningContentFilters.svelte:extKeyFor()` — the live caller, which
  fed the helpers the stale key, so fixing only the helpers would have
  changed nothing
- both test files, which asserted the stale shape *as correct*

`extPathToTagKey()` now returns null for anything that is not exactly two
non-empty colon-free segments, and callers skip it. A malformed key
emitting no filter is much better than one emitting a filter that cannot
match: the second reads to the user as "no results" rather than "broken".

Ext facets are no longer put in the NIP-50 `search` string at all. The
dot path was removed rather than repaired — with reverse-DNS namespaces
`ext.org.edufeed.ekw.konfi.themen.id` gives no way to tell where the
namespace ends and the facet begins. The exact-match `#ext:` tag filter
is what the relay indexes.

Added a test that runs the UI's emitted key through
buildSearchFilterObject, since asserting the two halves separately is
what let them drift apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
laoc merged commit 3353662b87 into dev 2026-07-30 10:31:14 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app!58
No description provided.