From aa4127fcefee4e89c107c3ca4ef15afd991e24c4 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sat, 18 Jul 2026 00:20:34 +0200 Subject: [PATCH] docs(176): fix plan test-project path + parse.ts self-review nits; add SDD ledger --- ...2026-07-17-smartcollection-rule-builder.md | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/superpowers/plans/2026-07-17-smartcollection-rule-builder.md b/docs/superpowers/plans/2026-07-17-smartcollection-rule-builder.md index 2ca0b9641..7a6e52cf3 100644 --- a/docs/superpowers/plans/2026-07-17-smartcollection-rule-builder.md +++ b/docs/superpowers/plans/2026-07-17-smartcollection-rule-builder.md @@ -30,7 +30,7 @@ - `ErsatzTV.Application/Search/Queries/GetSearchFieldCatalogHandler.cs` — returns the curated static catalog. - `ErsatzTV.Application/Search/SearchFieldCatalog.cs` — the curated static list (product-owned, cross-references `LuceneSearchIndex`). - `ErsatzTV/Controllers/Api/SearchController.cs` — **modify**: add the `GET /api/v1/search/fields` action. -- `ErsatzTV.Application.Tests/Search/GetSearchFieldCatalogHandlerTests.cs` — NUnit test (new; place under the existing `ErsatzTV.Application.Tests` project — confirm the exact test project name with `ls *.Tests`). +- `ErsatzTV.Tests/Application/Search/GetSearchFieldCatalogHandlerTests.cs` — NUnit test (new; Application handler tests live under `ErsatzTV.Tests/Application//`, namespace `ErsatzTV.Tests.Application.`, run via `dotnet test ErsatzTV.Tests`). **SPA (new — `web/src/builder/rules/`):** - `types.ts` — `FieldType`, `Operator`, `Rule`, `Group`, `Match`, `isGroup`. @@ -56,15 +56,14 @@ - Create: `ErsatzTV.Application/Search/SearchFieldCatalog.cs` - Create: `ErsatzTV.Application/Search/Queries/GetSearchFieldCatalog.cs` - Create: `ErsatzTV.Application/Search/Queries/GetSearchFieldCatalogHandler.cs` -- Test: `ErsatzTV.Application.Tests/Search/GetSearchFieldCatalogHandlerTests.cs` +- Test: `ErsatzTV.Tests/Application/Search/GetSearchFieldCatalogHandlerTests.cs` **Interfaces:** - Produces: `record SearchFieldResponseModel(string Name, string Label, string Type, string Group, string[] Values)`; `record GetSearchFieldCatalog : IRequest>`; `SearchFieldCatalog.Fields` (static `List`). -- [ ] **Step 1: Confirm the test project name** +- [ ] **Step 1: (resolved) Test location** -Run: `ls /Users/timothy/ersatztv/.claude/worktrees/176-rule-builder | grep Tests` -Expected: a directory like `ErsatzTV.Application.Tests` (use its real name for the test path below). +Application handler tests live in `ErsatzTV.Tests/Application//` with namespace `ErsatzTV.Tests.Application.`, run via `dotnet test ErsatzTV.Tests`. The new test goes in `ErsatzTV.Tests/Application/Search/`. This repo uses **explicit `using`s** (no ImplicitUsings) — include the `System.*` usings shown in Step 5. Exemplar: `ErsatzTV.Tests/Application/Channels/PreviewAutoTuneChannelsHandlerTests.cs`. - [ ] **Step 2: Write the response model** @@ -176,15 +175,19 @@ public class GetSearchFieldCatalogHandler : IRequestHandler): Rule | if (negate) return null; // NOT only valid with quoted forms above - // Wildcards (text only) + // Wildcards (text only): *v* → contains, v* → startsWith if (raw.startsWith('*') && raw.endsWith('*') && raw.length >= 2) { if (type !== 'text') return null; - return mk(field, 'contains', unescape(raw.slice(1, -1))); + return mk(field, 'contains', unescapeWild(raw.slice(1, -1))); } - if (raw.endsWith('*') && !raw.includes('*', 0)) { /* handled below */ } if (/^[^*]+\*$/.test(raw)) { if (type !== 'text') return null; - return mk(field, 'startsWith', unescape(raw.slice(0, -1))); + return mk(field, 'startsWith', unescapeWild(raw.slice(0, -1))); } // Bare token → numeric eq only (reject fuzzy ~, boosts ^, etc.) @@ -658,7 +660,7 @@ function parseAtom(atom: string, fieldTypes: Record): Rule | return null; } -function unescape(value: string): string { +function unescapeWild(value: string): string { return value.replace(/\\([+\-!(){}[\]^"~:\\/])/g, '$1'); } @@ -697,7 +699,7 @@ export function parse(input: string, fieldTypes: Record): Gro - [ ] **Step 4: Run the parser test to verify it passes** Run: `cd web && npx vitest run src/builder/rules/parse.test.ts` -Expected: PASS. If the `startsWith` branch misbehaves, note the intent: a value that ends in a single trailing `*` and contains no other `*` is `startsWith`; the regex `/^[^*]+\*$/` captures exactly that. Remove the dead `if (raw.endsWith('*') && ...)` line if the linter flags it. +Expected: PASS. If the `startsWith` branch misbehaves, note the intent: a value that ends in a single trailing `*` and contains no other `*` is `startsWith`; the regex `/^[^*]+\*$/` captures exactly that. - [ ] **Step 5: Commit** @@ -1218,7 +1220,7 @@ Run: ```bash cd /Users/timothy/ersatztv/.claude/worktrees/176-rule-builder dotnet build ErsatzTV.sln 2>&1 | grep -E "error|Build succeeded" -dotnet test ErsatzTV.Application.Tests --filter GetSearchFieldCatalogHandlerTests +dotnet test ErsatzTV.Tests --filter GetSearchFieldCatalogHandlerTests cd web && npx vitest run && npx tsc -b --pretty false && npm run lint && npm run check:api ``` Expected: `Build succeeded`, all tests PASS, `check:api` clean (no uncommitted generated diff).