docs(434,435,438): decisions records, spa-conventions §12, api-conventions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3599,3 +3599,52 @@ This is #72 scope item (a), deferred in `api.channel-health-signal` because "no
|
||||
**Immutable provenance, not a mutable "still managed" flag.** `Origin` records how the row was *born* and a later user edit never changes it, so "auto-generated then user-edited" stays `AutoTuned`. This deliberately avoids reviving the fragile "detect when it's been edited away" heuristic the issue rejected. A future "has diverged from its auto-tune template" signal, if wanted, is a *separate* concern owned by the #383/#384 auto-tune arc (which knows the template), not this column — mirroring the `api.channel-health-signal` reasoning that kept health a raw fact rather than freezing a policy enum.
|
||||
|
||||
**`Unknown = 0` is the honest legacy default.** A new non-null int column defaults existing rows to `0`; making that `Unknown` (rather than `UserCreated`) means pre-migration rows say "we never recorded this" instead of asserting a provenance we cannot know. The SPA badges only `AutoTuned`, so `Unknown` and `UserCreated` both render unbadged. Enum (not `bool IsAutoTuned`) so a future origin (e.g. `Imported`) is additive without a wire-contract break. Stamped in `CreateChannelFromLineupHandler.BuildChannel`, which is the single channel-construction primitive `CreateAutoTunedChannelsHandler` delegates to, so both the lineup endpoint and bulk auto-tune are covered by one stamp site. Empty-schedule and broken-source fault detection remain deferred to #415.
|
||||
## 2026-07-23 — Facet-value typeahead is a new endpoint, allow-listed to text fields, no caching (#434)
|
||||
|
||||
`key: api.search-field-values` · `status: active` · `since: 2026-07-23` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** `GET /api/v1/search/fields/{name}/values?q=&limit=` enumerates distinct Lucene index terms for one catalog field, allow-listed to `type: "text"` fields (404 for an unknown or non-text field), case-insensitive prefix-filtered on `q`, and `limit` clamped to `[1, 50]` (default 50).
|
||||
**Signals:** facet-value typeahead, rule builder value combobox, distinct field values, GetSearchFieldValues, text field allow-list, ElasticSearch empty-list degrade · paths: `ErsatzTV/Controllers/Api/SearchController.cs`, `ErsatzTV.Application/Search/Queries/GetSearchFieldValues.cs`, `ErsatzTV.Application/Search/Queries/GetSearchFieldValuesHandler.cs`, `ErsatzTV.Core/Interfaces/Search/ISearchIndex.cs`, `ErsatzTV.Infrastructure/Search/LuceneSearchIndex.cs`, `ErsatzTV.Infrastructure/Search/ElasticSearchIndex.cs`, `web/src/api/search.ts` · issues: #434, #176
|
||||
**Mechanics:** `SearchController.GetSearchFieldValues`; `GetSearchFieldValuesHandler`; `ISearchIndex.GetFieldValues`; api-conventions.md; spa-conventions.md §12
|
||||
|
||||
Enum fields (e.g. `type`, `content_rating` group) already ship their allowed values inline on
|
||||
`SearchFieldResponseModel` from the existing `GET /api/v1/search/fields` catalog (`spa.smartcollection-rule-builder`,
|
||||
#176), so they need no endpoint — a client already has the full value set. **Text** fields (title, studio,
|
||||
genre-as-free-text, etc.) don't: their values are whatever strings the library actually contains, so the
|
||||
rule builder's value input for a text field needs a live lookup against the index rather than a fixed list.
|
||||
The handler allow-lists on `field.Type != "text"` (matching the same `SearchFieldCatalog.Fields` the
|
||||
`/fields` endpoint serves) and returns `Option.None` → 404 for anything else, rather than silently returning
|
||||
an empty list for a field that will never have values — a 404 tells a caller "wrong field kind," an empty
|
||||
200 would look like "no matches yet."
|
||||
|
||||
**Why a thin query, not a cache.** `GetFieldValues` is a direct pass-through to `ISearchIndex.GetFieldValues`
|
||||
— no result cache, no debounce on the server side (the SPA combobox debounces the keystroke). The Lucene
|
||||
backend (`LuceneSearchIndex`) walks the field's term dictionary directly, which is cheap for a bounded
|
||||
`limit`; adding a cache before there's a measured cost would be premature. The **ElasticSearch** backend
|
||||
(currently unused in prod, kept for parity) degrades to an empty list rather than a 500 — its term-aggregation
|
||||
support is incomplete, and a typeahead returning nothing is a graceful degrade, not a broken feature.
|
||||
|
||||
## 2026-07-23 — Relative-date rule builder operators are a frontend-only mapping onto existing Lucene macros (#435)
|
||||
|
||||
`key: rulebuilder.relative-date-macros` · `status: active` · `since: 2026-07-23` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** The visual rule builder's `inLast`/`notInLast` date operators compile to/parse from the pre-existing `CustomMultiFieldQueryParser` macros `released_inthelast`/`released_notinthelast` and `added_inthelast`/`added_notinthelast`, value form `"<n> day|week|month|year"`; there is no backend change.
|
||||
**Signals:** relative date operator, inLast, notInLast, inthelast macro, released_inthelast, added_inthelast, date unit picker, rule builder relative dates · paths: `web/src/builder/rules/dateMacro.ts`, `web/src/builder/rules/compile.ts`, `web/src/builder/rules/parse.ts`, `web/src/builder/rules/types.ts`, `web/src/builder/rules/validation.ts` · issues: #435, #176, #438
|
||||
**Mechanics:** `dateMacro.ts` (`compileRelative`/`parseRelative`, `OP_TO_SUFFIX`/`SUFFIX_TO_OP`, `RELATIVE_SYNTHETIC`); spa-conventions.md §12
|
||||
|
||||
`released_inthelast`/`added_inthelast` (+ their `notinthelast` negations) already existed in
|
||||
`CustomMultiFieldQueryParser` as free-text macro fields before the rule builder could reach them — they
|
||||
were only usable by typing raw Lucene. #435 exposes them as first-class builder operators without touching
|
||||
the parser: `release_date`/`added_date` gain `inLast`/`notInLast` alongside the existing `before`/`after`/
|
||||
`between`, backed by a numeric-value input plus a `day|week|month|year` unit picker (`types.ts`'s `unit?:
|
||||
DateUnit`). `dateMacro.ts` is the single seam — a small `field↔macro-prefix` table (`release_date↔released`,
|
||||
`added_date↔added`) plus the `inLast/notInLast ↔ inthelast/notinthelast` suffix maps — that `compile.ts`
|
||||
delegates to for these two fields and `parse.ts` recognizes via `RELATIVE_SYNTHETIC` before falling into the
|
||||
generic field:value grammar. Validation (`ruleError` in `validation.ts`) requires the value to parse as a
|
||||
positive integer; a non-numeric or non-positive value is a builder-side error, never sent to the server.
|
||||
|
||||
**Frontend-only because the macros are the query-string wire format, not a new query kind.** The compiled
|
||||
query for `release_date inLast "7 day"` is literally `released_inthelast:"7 day"` — the same string a user
|
||||
could type by hand — so nothing downstream (search index, SmartCollection storage, Auto-Tune) needs to know
|
||||
the rule builder exists. This keeps `rulebuilder.relative-date-macros` symmetric with
|
||||
`spa.smartcollection-rule-builder`'s "compile-only, no new stored AST" stance (#176): a relative-date rule is
|
||||
just another point in the same closed grammar subset, proven by the same compile→parse round-trip discipline
|
||||
(`dateMacro.test.ts`, and the property test in `roundtrip.test.ts`, #438) rather than a special case.
|
||||
|
||||
Reference in New Issue
Block a user