Files
ersatztv/docs/decisions/archive/spa.md
T
timothy 6cf99718ee
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 34s
PR Gates / Docs update reminder (pull_request) Successful in 38s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 42s
PR Gates / decisions lifecycle (pull_request) Successful in 1m1s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 17m3s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 17m14s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 22m32s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
feat(436): arbitrary-depth rule-builder group nesting
The rule builder's Group nesting was capped at one level (#176's Kodi
model). Generalize it to recursive nesting bounded by a single shared
constant, MAX_GROUP_DEPTH (types.ts, = 5, root group is depth 0):

- parse.ts: replace the allowNested boolean with a depth counter that
  recurses to the cap; deeper input stays out of subset (null -> raw-text
  fallback), so parse remains the exact inverse of compile. Sub-group
  detection now requires the leading '(' to be the one closed by the
  trailing ')' (quote/escape aware), so '(a)x(b)' can't be mistaken for
  one wrapped group.
- RuleBuilder.tsx: 'Add group' is offered while depth < MAX_GROUP_DEPTH
  instead of only at the root; nested group boxes get box-sizing:
  border-box so per-level padding can't overflow (no global reset).
- roundtrip.test.ts: the 500-tree generator nests to the cap and asserts
  the corpus actually reached it; explicit depth-3 cases added to
  compile/parse/validation tests and a depth-gate test to RuleBuilder.

compile.ts and validation.ts already recursed correctly and are unchanged.
No backend/OpenAPI change.

Docs: spa-conventions.md §12; decisions lifecycle — new active record
spa.rulebuilder-nesting, predecessor spa.smartcollection-rule-builder
relocated to docs/decisions/archive/spa.md as superseded.

fixes #436
2026-07-25 11:01:35 +02:00

3.8 KiB

Archive — SPA / rule-builder conventions

Superseded/retired records for ChicoryTV SPA client-side conventions. See docs/decisions/archive/README.md for the archive's general rules (rationale kept verbatim, never in the active read-path). Active successor for the rule builder: spa.rulebuilder-nesting in docs/decisions.md.


2026-07-18 — SmartCollection rule builder: compile-only closed subset, no stored AST, one-level nesting (#176)

key: spa.smartcollection-rule-builder · status: superseded · since: 2026-07-18 · supersedes: none · superseded-by: spa.rulebuilder-nesting@2026-07-25 Rule: (superseded) The SmartCollection visual rule builder compiles to/from a closed subset of the Lucene grammar over the existing stored query string — no new AST, one level of group nesting. Signals: SmartCollection, rule builder, Lucene compile/parse · paths: web/src/builder/rules/, compile.ts, parse.ts, roundtrip.test.ts · issues: #176, #69 Mechanics: superseded by spa.rulebuilder-nesting (ersatztv#436) — the compile-only closed-subset and field-catalog stances carry forward there; only the one-level nesting cap was reversed. See docs/decisions.md → that record; spa-conventions.md §12

The SmartCollection create/edit dialog gained a visual rule builder (web/src/builder/rules/) alongside the existing raw-Lucene textarea. The SmartCollection still stores a plain Lucene query string — no new stored rule AST, no schema change. The builder compiles its in-memory rule tree into a closed subset of the Lucene grammar (compile.ts) and parses exactly that subset back out (parse.ts, the exact inverse — returns null, not a best-effort guess, for anything outside the subset); escaping is total, so any builder-authored query round-trips losslessly, proven by a 500-tree property test (roundtrip.test.ts, including Lucene special characters). Opening an existing SmartCollection tries the parse first and falls back to raw-text mode on null (fuzzy queries, boosts, mixed AND/OR at one nesting level, or nesting deeper than one level).

Why compile-only over persisting an authoritative rule AST: an AST would still need a Lucene→rules parser to open every pre-existing free-text query — including every query the Auto-Tune feature (#69) generates — so the AST would buy almost nothing (it still can't represent arbitrary hand-written Lucene) while costing a dual-provider EF migration and a second source of truth to keep in sync with the Lucene grammar. Compile-only keeps the query string as the single source of truth and treats the builder as a structured editor over it, not a new storage model.

One-level-nesting "Kodi" model. types.ts defines a top Group (match: all|any) over Rules and/or one level of sub-Groups — enough to express type:movie AND (genre:Horror OR genre:Thriller), which covers the smart-playlist patterns Kodi-style rule builders are known for. Arbitrary/recursive nesting was scoped out as YAGNI; revisit only if a real query needs it.

Field vocabulary comes from a new catalog endpoint, not a hardcoded list. GET /api/v1/search/fields (read-only, MCP-introspectable; see api-conventions.md) returns the curated, typed, labeled field set derived from LuceneSearchIndex — name/label/type/group/values — and is the single source of truth the builder's field pickers (fieldCatalog.ts's useSearchFields hook) and operator/value-input choices are driven from, so the builder's vocabulary can't drift from what the index actually supports.

Deferred as separate follow-up issues (explicitly out of scope for #176): facet-value typeahead for value inputs, relative-date operators, nesting deeper than one level, and inline adoption of RuleBuilder by ChannelBuilder / Auto-Tune (it was built reusable for exactly that reuse — see spa-conventions.md §12).