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
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).