# 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 `Rule`s and/or **one level** of sub-`Group`s — 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).