|
213 | 213 |
|
214 | 214 | --- |
215 | 215 |
|
| 216 | +## Milestone: v3.1 — Deck Discovery |
| 217 | + |
| 218 | +**Shipped:** 2026-03-16 |
| 219 | +**Phases:** 4 | **Plans:** 8 | **Sessions:** ~3 |
| 220 | + |
| 221 | +### What Was Built |
| 222 | +- deck_index table with weighted tsvector/GIN fulltext search and immutable wrapper function for generated column |
| 223 | +- search_decks RPC with prefix matching for search-as-you-type, tag filtering, and card_count at query time |
| 224 | +- Subfolder-aware study RPCs: transparent subfolder_path filtering on 7 JOINs (zero signature changes) |
| 225 | +- docora-webhook deck.yaml detection, parsing (parseYaml reusing parseFrontmatter), and idempotent deck_index upsert/delete |
| 226 | +- deck-commit commit_yaml action with server-enforced author and lightweight YAML serialization |
| 227 | +- DeckMetadataForm in deck builder with collapsible UI, dirty tracking, YAML load/save, EN/IT i18n |
| 228 | +- Discovery tab (4th bottom tab) with search-as-you-type, tag chip bar, optimistic subscribe/unsubscribe |
| 229 | +- Shared deck entries in Repos screen with display_name enrichment from deck_index |
| 230 | + |
| 231 | +### What Worked |
| 232 | +- End-to-end pipeline approach: DB schema → webhook indexing → deck builder metadata → mobile discovery — each phase cleanly consumed the prior one's output |
| 233 | +- Transparent subfolder filter pattern: added to 7 JOINs across 2 RPCs with zero signature changes — existing mobile code worked unchanged |
| 234 | +- parseYaml wrapper reusing parseFrontmatter avoided adding a YAML library to the edge function |
| 235 | +- Prefix matching fix found during device verification (search-as-you-type requires `:*` suffix, not websearch_to_tsquery) |
| 236 | +- Optimistic UI with Set-based key tracking for subscribe/unsubscribe gives instant feedback |
| 237 | + |
| 238 | +### What Was Inefficient |
| 239 | +- websearch_to_tsquery chosen in Phase 41 had to be replaced with prefix matching in Phase 44 when device testing revealed partial typing didn't match — research didn't test search-as-you-type behavior |
| 240 | +- Client-side join between user_repositories and deck_index is a workaround for PostgREST not supporting embedding without FK — adds 2 round trips |
| 241 | + |
| 242 | +### Patterns Established |
| 243 | +- Immutable tsvector wrapper: wrap generated column expression in IMMUTABLE SQL function when using STABLE functions |
| 244 | +- COALESCE-based unique index: for nullable columns in unique constraints |
| 245 | +- Transparent subfolder filter: `AND (ur.subfolder_path IS NULL OR c.file_path LIKE ur.subfolder_path || '%')` on every user_repositories JOIN |
| 246 | +- Optimistic UI with Set<string> rollback: add key immediately, revert on error |
| 247 | +- Debounced search via useRef setTimeout: 300ms on text, immediate on tag selection |
| 248 | +- Prefix tsquery pattern: exact match all words except last, `:*` on last word |
| 249 | + |
| 250 | +### Key Lessons |
| 251 | +1. Search-as-you-type requires prefix matching (`:*`), not websearch_to_tsquery — test with partial input during research, not just complete words |
| 252 | +2. When PostgREST can't embed (no FK), client-side join with Map lookup is clean and fast enough for small result sets |
| 253 | +3. Transparent filter patterns (adding conditions to existing JOINs) are the cleanest way to extend RPCs without breaking callers |
| 254 | +4. Idempotent operations (UPSERT, 409-as-success) simplify webhook and UI code simultaneously |
| 255 | +5. Device verification catches real UX issues (search behavior, key collisions) that code review and unit tests miss |
| 256 | + |
| 257 | +### Cost Observations |
| 258 | +- Model mix: 80% opus, 20% sonnet (quality profile) |
| 259 | +- Sessions: ~3 (4 days, with gap between Phase 43 and 44) |
| 260 | +- Notable: 4 phases, 8 plans, 17 tasks — full end-to-end pipeline from DB to mobile UI in 4 days |
| 261 | + |
| 262 | +--- |
| 263 | + |
216 | 264 | ## Cross-Milestone Trends |
217 | 265 |
|
218 | 266 | ### Process Evolution |
|
224 | 272 | | v2.2 | ~2 | 2 | Cleanest milestone — zero deviations, zero issues | |
225 | 273 | | v2.3 | ~1 | 2 | Fastest milestone — pure UI polish, same-day ship | |
226 | 274 | | v3.0 | ~5 | 5 | First web app milestone; new platform (Vite/React SPA); clean sequential chain | |
| 275 | +| v3.1 | ~3 | 4 | First cross-platform pipeline (DB → webhook → web → mobile); prefix matching fix during device test | |
227 | 276 |
|
228 | 277 | ### Cumulative Quality |
229 | 278 |
|
|
234 | 283 | | v2.2 | — | Session limit (manual) | — | |
235 | 284 | | v2.3 | — | UI polish (UAT) | — | |
236 | 285 | | v3.0 | 38+ (auth/theme/i18n/validation) | Lib layer | Vite 7, React 19, Tailwind 4, @uiw/react-md-editor, katex, yaml | |
| 286 | +| v3.1 | 5 (api.test.ts) | API layer | — (zero new dependencies) | |
237 | 287 |
|
238 | 288 | ### Top Lessons (Verified Across Milestones) |
239 | 289 |
|
|
245 | 295 | 6. Pure UI polish milestones are fast, low-risk, and benefit most from UAT (v2.3) |
246 | 296 | 7. Shared backend across multiple frontends is a massive complexity reducer — one auth, one DB, no sync (v3.0) |
247 | 297 | 8. Browser compatibility for npm packages must be verified during research, not mid-implementation (v3.0) |
| 298 | +9. Search-as-you-type requires prefix matching — test with partial input during research, not just complete words (v3.1) |
| 299 | +10. Transparent filter patterns on existing JOINs are the cleanest way to extend RPCs without breaking callers (v3.1) |
0 commit comments