Update project plans and documentation for Phase 3 completion

Project plan updates:
- Mark all 14 Phase 3 tasks as completed
- Update acceptance criteria to met
- Update master plan status to Phase 4 next
- Add detailed deliverables list for Phase 3

Documentation updates:
- Add UNSET sentinel pattern to CLAUDE.md
- Document when to use UNSET vs None for nullable fields

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-28 15:26:01 -06:00
parent 7d397a2e22
commit ebe776d54d
3 changed files with 87 additions and 45 deletions

View File

@ -150,6 +150,42 @@ class CollectionService:
--- ---
## UNSET Sentinel Pattern
For nullable fields that can be explicitly cleared (set to `None`), use the `UNSET` sentinel to distinguish between "not provided" (keep existing) and "set to null" (clear the value).
### Pattern
```python
from app.repositories.protocols import UNSET
# In repository/service method signatures
async def update(
self,
deck_id: UUID,
name: str | None = None, # None means "don't change"
description: str | None = UNSET, # type: ignore[assignment]
# UNSET = keep existing, None = clear, str = set new value
) -> DeckEntry | None:
...
if description is not UNSET:
record.description = description # Could be None (clear) or string (set)
```
### API Layer Usage
```python
# Check if field was explicitly provided in request
description = deck_in.description if "description" in deck_in.model_fields_set else UNSET
```
### When to Use
- Fields that can be meaningfully set to `None` (descriptions, notes, optional refs)
- Not needed for fields where `None` means "don't update" (name, cards, etc.)
---
## Configuration Classes ## Configuration Classes
Game rules are defined in `app/core/config.py` as Pydantic models. These are passed from the frontend as request parameters. Game rules are defined in `app/core/config.py` as Pydantic models. These are passed from the frontend as request parameters.

View File

@ -7,8 +7,8 @@
"projectName": "Mantimon TCG - Backend Services", "projectName": "Mantimon TCG - Backend Services",
"description": "Live service backend for play.mantimon.com - server-authoritative multiplayer TCG with campaign mode, free-play, and F2P monetization", "description": "Live service backend for play.mantimon.com - server-authoritative multiplayer TCG with campaign mode, free-play, and F2P monetization",
"totalPhases": 6, "totalPhases": 6,
"completedPhases": 3, "completedPhases": 4,
"status": "Phases 0-2 complete, Phase 3 (Collections + Decks) up next" "status": "Phases 0-3 complete, Phase 4 (Game Service + WebSocket) up next"
}, },
"architectureDecisions": { "architectureDecisions": {
@ -121,20 +121,26 @@
{ {
"id": "PHASE_3", "id": "PHASE_3",
"name": "Collections + Decks", "name": "Collections + Decks",
"status": "NOT_STARTED", "status": "COMPLETE",
"description": "Card ownership, deck building, starter deck selection", "description": "Card ownership, deck building, starter deck selection",
"planFile": "project_plans/PHASE_3_COLLECTION_DECKS.json", "planFile": "project_plans/PHASE_3_COLLECTION_DECKS.json",
"estimatedWeeks": "1-2", "estimatedWeeks": "1-2",
"dependencies": ["PHASE_2"], "dependencies": ["PHASE_2"],
"deliverables": [ "deliverables": [
"CollectionService (owned cards CRUD)", "CollectionService with repository pattern",
"DeckService (create, edit, validate, delete)", "DeckService with DI and DeckConfig from request",
"Mode-aware deck validation (owned vs full collection)", "DeckValidator pure function with comprehensive rules",
"Deck slot limits (free vs premium)", "Collections API (/collections/me, /collections/me/cards/{id}, admin endpoint)",
"Starter deck selection flow", "Decks API (/decks CRUD, /decks/validate)",
"5 starter deck definitions", "Starter deck endpoints (/users/me/starter-deck, /users/me/starter-status)",
"REST endpoints" "5 starter deck definitions (grass, fire, water, psychic, lightning)",
] "UNSET sentinel pattern for nullable field updates",
"Race condition protection via unique partial index",
"Admin API key authentication",
"Energy type validation",
"127 new tests, 1199 total tests"
],
"completedDate": "2026-01-28"
}, },
{ {
"id": "PHASE_4", "id": "PHASE_4",

View File

@ -9,8 +9,8 @@
"description": "Card ownership (collections), deck building, validation, and starter deck selection", "description": "Card ownership (collections), deck building, validation, and starter deck selection",
"totalEstimatedHours": 29, "totalEstimatedHours": 29,
"totalTasks": 14, "totalTasks": 14,
"completedTasks": 4, "completedTasks": 14,
"status": "in_progress", "status": "completed",
"masterPlan": "../PROJECT_PLAN_MASTER.json" "masterPlan": "../PROJECT_PLAN_MASTER.json"
}, },
@ -149,8 +149,8 @@
"description": "Service layer for card collection CRUD operations", "description": "Service layer for card collection CRUD operations",
"category": "critical", "category": "critical",
"priority": 4, "priority": 4,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-001"], "dependencies": ["COLL-001"],
"files": [ "files": [
{"path": "app/services/collection_service.py", "status": "create"} {"path": "app/services/collection_service.py", "status": "create"}
@ -182,8 +182,8 @@
"description": "Service layer for deck CRUD with validation", "description": "Service layer for deck CRUD with validation",
"category": "critical", "category": "critical",
"priority": 5, "priority": 5,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-002", "COLL-003", "COLL-004"], "dependencies": ["COLL-002", "COLL-003", "COLL-004"],
"files": [ "files": [
{"path": "app/services/deck_service.py", "status": "create"} {"path": "app/services/deck_service.py", "status": "create"}
@ -222,8 +222,8 @@
"description": "Define 5 starter decks with real card IDs from scraped data", "description": "Define 5 starter decks with real card IDs from scraped data",
"category": "high", "category": "high",
"priority": 6, "priority": 6,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": [], "dependencies": [],
"files": [ "files": [
{"path": "app/data/starter_decks.py", "status": "create"} {"path": "app/data/starter_decks.py", "status": "create"}
@ -273,8 +273,8 @@
"description": "REST endpoints for collection management", "description": "REST endpoints for collection management",
"category": "high", "category": "high",
"priority": 7, "priority": 7,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-004"], "dependencies": ["COLL-004"],
"files": [ "files": [
{"path": "app/api/collections.py", "status": "create"} {"path": "app/api/collections.py", "status": "create"}
@ -308,8 +308,8 @@
"description": "REST endpoints for deck CRUD and validation", "description": "REST endpoints for deck CRUD and validation",
"category": "high", "category": "high",
"priority": 8, "priority": 8,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-005"], "dependencies": ["COLL-005"],
"files": [ "files": [
{"path": "app/api/decks.py", "status": "create"} {"path": "app/api/decks.py", "status": "create"}
@ -359,8 +359,8 @@
"description": "Endpoint for new user starter deck selection flow", "description": "Endpoint for new user starter deck selection flow",
"category": "high", "category": "high",
"priority": 9, "priority": 9,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-004", "COLL-005", "COLL-006"], "dependencies": ["COLL-004", "COLL-005", "COLL-006"],
"files": [ "files": [
{"path": "app/api/users.py", "status": "modify"} {"path": "app/api/users.py", "status": "modify"}
@ -393,8 +393,8 @@
"description": "Mount collections and decks routers", "description": "Mount collections and decks routers",
"category": "high", "category": "high",
"priority": 10, "priority": 10,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-007", "COLL-008"], "dependencies": ["COLL-007", "COLL-008"],
"files": [ "files": [
{"path": "app/main.py", "status": "modify"} {"path": "app/main.py", "status": "modify"}
@ -441,8 +441,8 @@
"description": "Integration tests for collection operations", "description": "Integration tests for collection operations",
"category": "high", "category": "high",
"priority": 12, "priority": 12,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-004"], "dependencies": ["COLL-004"],
"files": [ "files": [
{"path": "tests/services/test_collection_service.py", "status": "create"} {"path": "tests/services/test_collection_service.py", "status": "create"}
@ -470,8 +470,8 @@
"description": "Integration tests for deck operations", "description": "Integration tests for deck operations",
"category": "high", "category": "high",
"priority": 13, "priority": 13,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-005"], "dependencies": ["COLL-005"],
"files": [ "files": [
{"path": "tests/services/test_deck_service.py", "status": "create"} {"path": "tests/services/test_deck_service.py", "status": "create"}
@ -504,8 +504,8 @@
"description": "Integration tests for collection and deck endpoints", "description": "Integration tests for collection and deck endpoints",
"category": "high", "category": "high",
"priority": 14, "priority": 14,
"completed": false, "completed": true,
"tested": false, "tested": true,
"dependencies": ["COLL-007", "COLL-008", "COLL-009"], "dependencies": ["COLL-007", "COLL-008", "COLL-009"],
"files": [ "files": [
{"path": "tests/api/test_collections_api.py", "status": "create"}, {"path": "tests/api/test_collections_api.py", "status": "create"},
@ -551,18 +551,18 @@
}, },
"acceptanceCriteria": [ "acceptanceCriteria": [
{"criterion": "User can view their card collection", "met": false}, {"criterion": "User can view their card collection", "met": true},
{"criterion": "User can create a deck with name, cards, and energy", "met": false}, {"criterion": "User can create a deck with name, cards, and energy", "met": true},
{"criterion": "Deck validation enforces all rules (count, copies, Basic Pokemon)", "met": false}, {"criterion": "Deck validation enforces all rules (count, copies, Basic Pokemon)", "met": true},
{"criterion": "Deck validation reports all errors (not just first)", "met": false}, {"criterion": "Deck validation reports all errors (not just first)", "met": true},
{"criterion": "Free users limited to 5 decks", "met": false}, {"criterion": "Free users limited to 5 decks", "met": true},
{"criterion": "Premium users have unlimited decks", "met": false}, {"criterion": "Premium users have unlimited decks", "met": true},
{"criterion": "New user can select a starter deck (one time)", "met": false}, {"criterion": "New user can select a starter deck (one time)", "met": true},
{"criterion": "Starter deck selection grants cards and creates deck", "met": false}, {"criterion": "Starter deck selection grants cards and creates deck", "met": true},
{"criterion": "Campaign mode validates card ownership", "met": false}, {"criterion": "Campaign mode validates card ownership", "met": true},
{"criterion": "Freeplay mode skips ownership validation", "met": false}, {"criterion": "Freeplay mode skips ownership validation", "met": true},
{"criterion": "All endpoints require authentication", "met": false}, {"criterion": "All endpoints require authentication", "met": true},
{"criterion": "All tests pass with high coverage", "met": false} {"criterion": "All tests pass with high coverage", "met": true}
], ],
"securityConsiderations": [ "securityConsiderations": [