refactor(collections): retire the legacy rules surface — tree-only storage

The pre-tree rules machinery is deleted: no row ever carried a rules
array or a non-default match mode (verified in the dev DB), the web
never read the envelope keys, and the Android app consumes none of it.
Dead code gets no documentation.

Removed:
- PUT /api/collections/:id/match-mode — endpoint, route, and the
  UpdateCollectionMatchMode query. The tree's root mode owns any|all.
- auto_assign_rules / rule_match_mode from every envelope
  (create/list/detail/update), from the create/update request shapes,
  and from the schema: both columns DROPPED (schema.sql carries
  idempotent DROP COLUMN IF EXISTS heals for existing DBs; sqlc
  regenerated).
- The rules CRUD service layer (AddRule/SetRuleEnabled/RemoveRule +
  validateRule + UpdateCollectionRulesJSON), ParseRules /
  EnabledRules / IsSmartCollection, RulesToFilterTree (+ the SSR
  preload branch), the legacy branch of EvaluateSmartMembership, and
  SmartEvalOptions.MatchMode. IsCollectionSmart = tree presence;
  create-path isSmart = collection_type=smart OR a filter_tree.

Kept (live consumers): POST /api/collections/preview still evaluates
rules payloads in memory (the custom-section builder's preview), and
include_auto stays on the list (the Android app sends it).

The custom-section builder's SAVE was already broken — its
stringified auto_assign_rules payload failed request binding (probed
live: 400 invalid request). Create now rejects that payload loudly
with a 400 naming the retirement, instead of silently creating a
hollow static section; custom-section-builder.md carries the flag.
The builder's tree migration remains the flagged follow-up.

Test-side (asymmetry rule — deleted behavior, deleted tests):
TestPodcastSmartCollections rewritten onto trees (mode lives in the
tree root), match-mode subtests and tree_wins_over_legacy_rules
deleted, TestRulesToFilterTree deleted, contract-pin envelope keys
retracted per the contract_pins 2026-10-31 retirement entry. Mirrors
hand-synced: openapi.yaml, bruno create/update, docs/api/*.md,
docs/schema.go, api.d.ts. Full container suite green (155s).
This commit is contained in:
John O'Keefe committed 2026-10-31 19:06:48 -04:00
1 parent ab98015190
commit a86bdac87e
27 files changed
+239 -1093

No files matched your search

@@ -23,7 +23,6 @@ Creating a collection whose name already exists for the user returns **409**.
"color": "#00FF00",
"icon": "📖",
"collection_type": "static",
"auto_assign_rules": [],
"view_settings": { "kobo": { "view_mode": "grid" } }
}
```
@@ -39,7 +38,6 @@ Creating a collection whose name already exists for the user returns **409**.
"color": "#00FF00",
"icon": "📖",
"collection_type": "static",
"auto_assign_rules": null,
"view_settings": { "kobo": { "view_mode": "grid" } },
"book_count": 0,
"created_at": "2026-10-24 13:57:01.81234 +0000 UTC"
@@ -57,20 +55,9 @@ RFC3339. Treat it as display text; do not parse it as an ISO timestamp.
| description | string | no | |
| color | string | no | hex color, e.g. `"#FF5733"` |
| icon | string | no | emoji |
| collection_type | string | no | `"static"` (default) or `"smart"`; also implied by non-empty `auto_assign_rules` |
| auto_assign_rules | array | no | rule objects, see below |
| collection_type | string | no | `"static"` (default) or `"smart"`; also implied by a `filter_tree` |
| view_settings | object | no | per-device display preferences |
### Auto-assign rule object
| Field | Type | Required | Notes |
| ----- | ---- | -------- | ----- |
| id | string | no | rule id |
| field | string | yes | one of `genre`, `series`, `author`, `language`, `publisher`, `copyright_year`, `tags` |
| operator | string | yes | one of `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `greater_than`, `less_than` |
| value | string | yes | value to compare against |
| priority | number | no | 1-10; higher values take precedence |
| enabled | boolean | no | absent = enabled (rules written before this field existed keep matching) |
| filter_tree | object | no | the unified filter tree (qf DSL); presence makes the collection smart |
## Errors
@@ -27,7 +27,6 @@ GET /api/collections/550e8400-e29b-41d4-a716-446655440000
"description": "Books I want to read soon",
"color": "#00FF00",
"icon": "📖",
"auto_assign_rules": null,
"view_settings": { "kobo": { "view_mode": "grid" } },
"books": [],
"book_count": 0,
@@ -57,8 +56,8 @@ GET /api/collections/550e8400-e29b-41d4-a716-446655440000
| description | string | |
| color | string | |
| icon | string | |
| auto_assign_rules | array \| null | |
| view_settings | object | |
| filter_tree | object \| null | the unified filter tree (qf DSL); null = static |
| books | array | book cards in this collection |
| book_count | number | length of `books` |
| is_smart | boolean | |
@@ -29,7 +29,6 @@ GET /api/collections?include_auto=true&sort_by=name
"description": "Books I want to read soon",
"color": "#00FF00",
"icon": "📖",
"auto_assign_rules": null,
"created_at": "2026-10-24 13:57:01.81234 +0000 UTC",
"book_count": 12,
"is_smart": false
@@ -65,7 +64,6 @@ GET /api/collections?include_auto=true&sort_by=name
| description | string | |
| color | string | |
| icon | string | |
| auto_assign_rules | array \| null | raw rules JSON; non-null implies smart |
| created_at | string | Go `time.String()` format |
| book_count | number | live count; respects `library_id` for smart collections |
| is_smart | boolean | |
@@ -24,7 +24,6 @@ set `view_settings.hide_finished` / `view_settings.show_up_next` directly.
"description": "Updated description",
"color": "#33CCFF",
"icon": "📚",
"auto_assign_rules": [],
"view_settings": { "hide_finished": true },
"hide_finished": ["true"],
"show_up_next": ["true"]
@@ -41,7 +40,6 @@ set `view_settings.hide_finished` / `view_settings.show_up_next` directly.
"description": "Updated description",
"color": "#33CCFF",
"icon": "📚",
"auto_assign_rules": null,
"view_settings": { "hide_finished": true, "show_up_next": true },
"created_at": "2026-10-24 13:57:01.81234 +0000 UTC"
}
@@ -57,8 +55,8 @@ set `view_settings.hide_finished` / `view_settings.show_up_next` directly.
| description | string | no | |
| color | string | no | |
| icon | string | no | |
| auto_assign_rules | array | no | replaces the whole rule set (rule object shape: see Create Collection) |
| view_settings | object | no | merged into stored settings |
| filter_tree | object | no | replaces the stored filter tree (omitting it keeps the stored one) |
| hide_finished | array | no | single-element string array `["true"]`/`["false"]`; merged into view_settings |
| show_up_next | array | no | same shape as `hide_finished` |
@@ -68,6 +68,11 @@ Creates a new custom collection with filter rules and/or manual book selection.
**Request Body:**
**Status (2026-10-31):** the rules-array storage this payload targets
was retired — collections carry filter trees now. The server rejects
this save with a 400 naming the retired fields; the builder needs its
flagged tree migration (§12) before saving works again.
```json
{
"name": "My Custom Section",
-2
View File
@@ -229,7 +229,6 @@ paths:
"color": "#00FF00",
"icon": "📖",
"collection_type": "static",
"auto_assign_rules": [],
"view_settings": { "kobo": { "view_mode": "grid" } }
}
responses:
@@ -308,7 +307,6 @@ paths:
"description": "Updated description",
"color": "#33CCFF",
"icon": "📚",
"auto_assign_rules": [],
"view_settings": { "hide_finished": true },
"hide_finished": ["true"],
"show_up_next": ["true"]