Incomplete between / empty-text rules are invalid: blocked at Save (groupHasErrors) and never emit malformed Lucene (compile returns '', filtered) — including nested all-invalid subgroups (no more "()").
Single-child groups normalize match→all (normalizeGroup) so they round-trip; the any/all toggle is hidden for <2 children.
Non-blocking warning when the top group is entirely negative.
inLast/notInLast + a unit picker, compile/parse-mapped onto the pre-existingreleased_inthelast/added_inthelast (+notinthelast) CustomMultiFieldQueryParser macros. No backend change. Strict /^\d+$/ operand validation (rejects 1e2/7.0 that would break the round-trip).
New GET /api/v1/search/fields/{name}/values?q=&limit= returning distinct whole values from the database (per-field EF queries over shared lookup entities / metadata columns), allow-listed to facet fields — title/show_title/album_artist and non-text/unknown → 404; content_rating split on /; state/video_dynamic_range from fixed enums; case-insensitive prefix, limit clamped [1,50].
Frontend autocomplete combobox for text-field value inputs with a free-text fallback.
(Initially prototyped over the Lucene term dictionary; reworked to DB-distinct because analyzed TextFields only yield lowercased word fragments.)
Verification
Full web suite 980/980; C# search handler 9/9 (EF-backed); dotnet build clean; format/BOM clean.
Independent reviews per task + a Fable/opus whole-branch pass + a dedicated opus review of the DB-sourcing rework (all MERGEABLE); must-fixes applied.
Live-E2E (booted from source): supported fields 200; state/video_dynamic_range return real whole values; case-insensitive prefix works live (state?q=n→Normal); title/show_title/album_artist/minutes/plot/unknown → 404.
Bundle of three RuleBuilder issues, built on one branch (shared `RuleBuilder.tsx`).
fixes #438
fixes #435
fixes #434
## #438 — validation & polish (frontend)
- Incomplete `between` / empty-text rules are invalid: blocked at Save (`groupHasErrors`) and never emit malformed Lucene (`compile` returns `''`, filtered) — including nested all-invalid subgroups (no more `"()"`).
- Single-child groups normalize `match`→`all` (`normalizeGroup`) so they round-trip; the any/all toggle is hidden for <2 children.
- Non-blocking warning when the top group is entirely negative.
## #435 — relative-date operators (frontend only)
- `inLast`/`notInLast` + a `unit` picker, compile/parse-mapped onto the **pre-existing** `released_inthelast`/`added_inthelast` (+`notinthelast`) `CustomMultiFieldQueryParser` macros. No backend change. Strict `/^\d+$/` operand validation (rejects `1e2`/`7.0` that would break the round-trip).
## #434 — facet-value typeahead
- New `GET /api/v1/search/fields/{name}/values?q=&limit=` returning **distinct whole values from the database** (per-field EF queries over shared lookup entities / metadata columns), allow-listed to facet fields — `title`/`show_title`/`album_artist` and non-text/unknown → 404; `content_rating` split on `/`; `state`/`video_dynamic_range` from fixed enums; case-insensitive prefix, `limit` clamped [1,50].
- Frontend autocomplete combobox for text-field value inputs with a free-text fallback.
- (Initially prototyped over the Lucene term dictionary; reworked to DB-distinct because analyzed `TextField`s only yield lowercased word fragments.)
## Verification
- Full web suite **980/980**; C# search handler **9/9** (EF-backed); `dotnet build` clean; format/BOM clean.
- Independent reviews per task + a Fable/opus whole-branch pass + a dedicated opus review of the DB-sourcing rework (all MERGEABLE); must-fixes applied.
- **Live-E2E** (booted from source): supported fields 200; `state`/`video_dynamic_range` return real whole values; case-insensitive prefix works live (`state?q=n`→`Normal`); `title`/`show_title`/`album_artist`/`minutes`/`plot`/unknown → 404.
## Docs
`docs/decisions.md` (`api.search-field-values`, `rulebuilder.relative-date-macros`) + catalog, `docs/spa-conventions.md` §12, `docs/api-conventions.md`, regenerated OpenAPI (`v1.json`/`v1.d.ts`/`endpoint-index.md`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds GET /api/v1/search/fields/{name}/values?q=&limit= — the backend slice of the
visual rule builder's facet-value typeahead (#434). Enumerates distinct Lucene term
values for a text field via MultiFields.GetTerms + TermsEnum, filtered by a
case-insensitive prefix, limit clamped to [1,50]. 404s when the field is absent from
SearchFieldCatalog or is not type "text". ElasticSearchIndex (the optional external
backend) throws NotSupportedException for this method — its text fields are analyzed,
not keyword-mapped, so a terms aggregation isn't safe to guess at without verifying
against a live cluster.
Regenerated OpenAPI trio (v1.json, v1.d.ts, endpoint-index.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Lucene term dictionary stores lowercased word tokens for analyzed text
fields ("Science Fiction" -> science/fiction), so the typeahead was
suggesting fragments instead of whole values. GetSearchFieldValuesHandler
now injects IDbContextFactory<TvContext> and resolves an explicit
per-field-name distinct-values query (genre/studio/director/writer/actor/
artist/tag/network/collection/video_codec/album), with state and
video_dynamic_range computed in memory and content_rating split on '/' to
match what search actually matches on. title/show_title/album_artist have
no distinct source and now correctly 404 (free-text fallback), same as
before. Reverts the GetFieldValues additions to ISearchIndex/
LuceneSearchIndex/ElasticSearchIndex back to their pre-#434 state (BOM
stripped per #311, otherwise byte-identical). Endpoint shape, DTO,
controller, and OpenAPI are unchanged (no diff from
./scripts/update-openapi.sh).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Edits the api.search-field-values decision record's rationale prose in
place (same key, same date, not a reversal) to describe the DB-sourced
per-field distinct-values design and the narrowed allow-list, replacing
the superseded Lucene-term-dictionary description. Updates the endpoint's
api-conventions.md entry the same way. Regenerated docs/decisions/README.md
via build_decisions_catalog.py; decisions_validate.py passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ISearchIndex.cs / LuceneSearchIndex.cs still carried a BOM after the prior
commit — the strip ran after `git add`, so the staged (BOM'd) content was
what got committed. No content change beyond the BOM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GetFieldValues additions were reverted in the DB-sourcing rework; these two
files had only an incidental BOM strip left, which pulled unrelated pre-existing
whitespace debt into the scoped format gate. Restore byte-identical to origin/main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gates: per-task reviews (all clean) + opus whole-branch pass + dedicated opus review of the DB-sourcing rework — all MERGEABLE, must-fixes applied (strict relative-date N validation; ES graceful-empty [later removed in DB rework]; accurate endpoint description). Local gate green (web 980/980, C# search 9/9, build/format/BOM clean). Live-E2E confirmed the DB-sourced endpoint + narrowed allow-list.
Review-verdict: MERGEABLE @ 7e6861a9
Gates: per-task reviews (all clean) + opus whole-branch pass + dedicated opus review of the DB-sourcing rework — all MERGEABLE, must-fixes applied (strict relative-date N validation; ES graceful-empty [later removed in DB rework]; accurate endpoint description). Local gate green (web 980/980, C# search 9/9, build/format/BOM clean). Live-E2E confirmed the DB-sourced endpoint + narrowed allow-list.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Bundle of three RuleBuilder issues, built on one branch (shared
RuleBuilder.tsx).fixes #438
fixes #435
fixes #434
#438 — validation & polish (frontend)
between/ empty-text rules are invalid: blocked at Save (groupHasErrors) and never emit malformed Lucene (compilereturns'', filtered) — including nested all-invalid subgroups (no more"()").match→all(normalizeGroup) so they round-trip; the any/all toggle is hidden for <2 children.#435 — relative-date operators (frontend only)
inLast/notInLast+ aunitpicker, compile/parse-mapped onto the pre-existingreleased_inthelast/added_inthelast(+notinthelast)CustomMultiFieldQueryParsermacros. No backend change. Strict/^\d+$/operand validation (rejects1e2/7.0that would break the round-trip).#434 — facet-value typeahead
GET /api/v1/search/fields/{name}/values?q=&limit=returning distinct whole values from the database (per-field EF queries over shared lookup entities / metadata columns), allow-listed to facet fields —title/show_title/album_artistand non-text/unknown → 404;content_ratingsplit on/;state/video_dynamic_rangefrom fixed enums; case-insensitive prefix,limitclamped [1,50].TextFields only yield lowercased word fragments.)Verification
dotnet buildclean; format/BOM clean.state/video_dynamic_rangereturn real whole values; case-insensitive prefix works live (state?q=n→Normal);title/show_title/album_artist/minutes/plot/unknown → 404.Docs
docs/decisions.md(api.search-field-values,rulebuilder.relative-date-macros) + catalog,docs/spa-conventions.md§12,docs/api-conventions.md, regenerated OpenAPI (v1.json/v1.d.ts/endpoint-index.md).🤖 Generated with Claude Code
Adds GET /api/v1/search/fields/{name}/values?q=&limit= — the backend slice of the visual rule builder's facet-value typeahead (#434). Enumerates distinct Lucene term values for a text field via MultiFields.GetTerms + TermsEnum, filtered by a case-insensitive prefix, limit clamped to [1,50]. 404s when the field is absent from SearchFieldCatalog or is not type "text". ElasticSearchIndex (the optional external backend) throws NotSupportedException for this method — its text fields are analyzed, not keyword-mapped, so a terms aggregation isn't safe to guess at without verifying against a live cluster. Regenerated OpenAPI trio (v1.json, v1.d.ts, endpoint-index.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The Lucene term dictionary stores lowercased word tokens for analyzed text fields ("Science Fiction" -> science/fiction), so the typeahead was suggesting fragments instead of whole values. GetSearchFieldValuesHandler now injects IDbContextFactory<TvContext> and resolves an explicit per-field-name distinct-values query (genre/studio/director/writer/actor/ artist/tag/network/collection/video_codec/album), with state and video_dynamic_range computed in memory and content_rating split on '/' to match what search actually matches on. title/show_title/album_artist have no distinct source and now correctly 404 (free-text fallback), same as before. Reverts the GetFieldValues additions to ISearchIndex/ LuceneSearchIndex/ElasticSearchIndex back to their pre-#434 state (BOM stripped per #311, otherwise byte-identical). Endpoint shape, DTO, controller, and OpenAPI are unchanged (no diff from ./scripts/update-openapi.sh). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Review-verdict: MERGEABLE @
7e6861a9Gates: per-task reviews (all clean) + opus whole-branch pass + dedicated opus review of the DB-sourcing rework — all MERGEABLE, must-fixes applied (strict relative-date N validation; ES graceful-empty [later removed in DB rework]; accurate endpoint description). Local gate green (web 980/980, C# search 9/9, build/format/BOM clean). Live-E2E confirmed the DB-sourced endpoint + narrowed allow-list.