diff --git a/docs/superpowers/plans/2026-04-21-character-fandom-association-refactor.md b/docs/superpowers/plans/2026-04-21-character-fandom-association-refactor.md index 8fa14ad..f7b14e9 100644 --- a/docs/superpowers/plans/2026-04-21-character-fandom-association-refactor.md +++ b/docs/superpowers/plans/2026-04-21-character-fandom-association-refactor.md @@ -14,7 +14,7 @@ ## Context for the Implementer -**The codebase has no automated test suite.** There is no `tests/` directory, no `conftest.py`, no pytest fixtures. The existing convention (see migrations `h26041901`, `i26041901` and every feature commit on the branch) is manual verification: run the server in `docker compose`, exercise the feature in the browser, poke at the database via `docker compose exec web flask shell` or `docker compose exec db psql -U imagerepo imagerepo`. +**The codebase has no automated test suite.** There is no `tests/` directory, no `conftest.py`, no pytest fixtures. The existing convention (see migrations `h26041901`, `i26041901` and every feature commit on the branch) is manual verification: run the server in `docker compose`, exercise the feature in the browser, poke at the database via `docker compose exec web flask shell` or `docker compose exec postgres psql -U imagerepo imagerepo`. **Do not invent a test suite.** Each task below ships with explicit manual verification: a shell command, a SQL query, or a UI interaction with an expected result. These are the project's conventions — honor them. @@ -35,7 +35,7 @@ docker compose exec web flask db downgrade # revert docker compose exec web flask db history # list revisions # Postgres shell (inside the db container) -docker compose exec db psql -U imagerepo imagerepo +docker compose exec postgres psql -U imagerepo imagerepo ``` **Commit cadence:** one commit per task. The `app/main.py` file is large; commit per logical route or helper change so git blame stays useful. @@ -94,7 +94,7 @@ None. `app/tasks/maintenance.py` stays (empty body, retains git history); the Ce Run in shell: ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT COALESCE(kind, '(null)') AS kind, COUNT(*) AS total, @@ -112,7 +112,7 @@ Expected: a table with rows per kind. Record these numbers in your scratchpad - [ ] **Step 2: Export full tag list for diff-comparison after migration** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " COPY (SELECT id, name, kind, fandom_id FROM tag ORDER BY id) TO STDOUT WITH CSV HEADER " > /tmp/tag_snapshot_pre.csv wc -l /tmp/tag_snapshot_pre.csv @@ -123,7 +123,7 @@ Expected: CSV file of every tag row, one row per line plus header. Keep it — t - [ ] **Step 3: Snapshot image_tags counts (will verify zero lost after migration)** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT COUNT(*) AS total_image_tag_rows FROM image_tags; " ``` @@ -133,7 +133,7 @@ Record this number. The migration must never reduce it (except intentionally via - [ ] **Step 4: Verify database is backed up (user-facing reminder)** ```bash -docker compose exec -T db pg_dump -U imagerepo imagerepo > /tmp/imagerepo_pre_refactor_backup.sql +docker compose exec -T postgres pg_dump -U imagerepo imagerepo > /tmp/imagerepo_pre_refactor_backup.sql ls -lh /tmp/imagerepo_pre_refactor_backup.sql ``` @@ -617,7 +617,7 @@ Expected: output ends with the report block. Note the four counts (Phase 1 renam - [ ] **Step 4: Verify no colons remain in non-post/non-archive tag names** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT kind, COUNT(*) AS n FROM tag WHERE name LIKE '%:%' AND kind NOT IN ('post', 'archive') @@ -630,7 +630,7 @@ Expected: zero rows returned. (Post/archive tags legitimately retain inner `:` i - [ ] **Step 5: Verify no character tag name contains a fandom suffix** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT id, name FROM tag WHERE kind = 'character' AND name ~ '^.+ \(.+\)$' @@ -643,7 +643,7 @@ Expected: zero rows. If any remain, they're malformed orphans that the migration - [ ] **Step 6: Verify the four partial indices exist** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'tag' AND indexname IN ( @@ -660,7 +660,7 @@ Expected: four rows, each with the partial-WHERE clause visible in `indexdef`. - [ ] **Step 7: Verify image_tags count unchanged (no data loss)** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c "SELECT COUNT(*) FROM image_tags;" +docker compose exec -T postgres psql -U imagerepo imagerepo -c "SELECT COUNT(*) FROM image_tags;" ``` Compare against the baseline from Task 0 Step 3. Acceptable outcomes: @@ -966,7 +966,7 @@ def add_tag(image_id): - [ ] **Step 2: Exercise the endpoint against an existing image** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c "SELECT id FROM image_record LIMIT 1;" +docker compose exec -T postgres psql -U imagerepo imagerepo -c "SELECT id FROM image_record LIMIT 1;" ``` Copy the returned id (call it `$IMG`). Then: @@ -990,7 +990,7 @@ Expected for the second: `ok=true`, `tag.name="Test Char Ephemeral"`, `tag.kind= - [ ] **Step 3: Clean up the ephemeral test tags** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " DELETE FROM image_tags WHERE tag_id IN ( SELECT id FROM tag WHERE name IN ( 'test general ephemeral', 'Test Char Ephemeral', 'Test Fandom Ephemeral' @@ -1128,7 +1128,7 @@ def bulk_add_tag(): - [ ] **Step 3: Verify with two dummy images** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c "SELECT id FROM image_record LIMIT 2;" +docker compose exec -T postgres psql -U imagerepo imagerepo -c "SELECT id FROM image_record LIMIT 2;" ``` Take the two ids (call them IMG1, IMG2): @@ -1148,7 +1148,7 @@ Expected: `ok=true`, `added_count=2` (or less if the images already had that tag Clean up: ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " DELETE FROM image_tags WHERE tag_id IN (SELECT id FROM tag WHERE name = 'bulk test ephemeral'); DELETE FROM tag WHERE name = 'bulk test ephemeral'; " @@ -1279,7 +1279,7 @@ def accept_image_suggestion(image_id): - [ ] **Step 3: Smoke-test the 0-candidate branch** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c "SELECT id FROM image_record LIMIT 1;" +docker compose exec -T postgres psql -U imagerepo imagerepo -c "SELECT id FROM image_record LIMIT 1;" IMG= curl -sX POST "http://localhost:5000/image/$IMG/suggestions/accept" \ @@ -1324,7 +1324,7 @@ Expected: HTTP 409, JSON body with `error="ambiguous"` and a `candidates` array Clean up: ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " DELETE FROM image_tags WHERE tag_id IN (SELECT id FROM tag WHERE name LIKE '%Ambig%' OR name = 'Nonexistent Test Character'); DELETE FROM tag WHERE name LIKE '%Ambig%' OR name = 'Nonexistent Test Character'; " @@ -1547,7 +1547,7 @@ Remove any further downstream code that used the old `tag_name` variable; replac ```bash # Pick any existing tag -docker compose exec -T db psql -U imagerepo imagerepo -c "SELECT id, name, kind FROM tag WHERE kind = 'character' LIMIT 1;" +docker compose exec -T postgres psql -U imagerepo imagerepo -c "SELECT id, name, kind FROM tag WHERE kind = 'character' LIMIT 1;" TAG_ID= curl -s -o /dev/null -w "%{http_code}" "http://localhost:5000/?tag_id=$TAG_ID" @@ -1653,7 +1653,7 @@ def set_tag_fandom(tag_id): - [ ] **Step 2: Verify via curl** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT t.id, t.name, t.fandom_id FROM tag t WHERE t.kind = 'character' @@ -1679,7 +1679,7 @@ curl -sX POST "http://localhost:5000/api/tag/$TAG_ID/set-fandom" \ Expected: `ok=true`, `fandom_id: null`, `display_name` matches `name` (no suffix). Then clean up the ephemeral fandom: ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " DELETE FROM tag WHERE kind='fandom' AND name='Test Fandom For Set'; " ``` @@ -1873,7 +1873,7 @@ def update_tag(tag_id): - [ ] **Step 5: Verify via curl** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT id, name, kind FROM tag WHERE kind = 'user' LIMIT 1; " TAG_ID= @@ -2251,7 +2251,7 @@ If `re.` is not called anywhere in the file after removing `_FANDOM_SUFFIX_RE`, - [ ] **Step 5: Smoke-test suggestions for any image** ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT image_id, COUNT(*) FROM image_tag_prediction GROUP BY image_id @@ -2473,7 +2473,7 @@ Replace each: ```bash # Pick a series tag -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT id, name FROM tag WHERE kind = 'series' LIMIT 1; " SERIES_ID= @@ -2920,7 +2920,7 @@ In the modal, type `character:Test Char A` in tag input. Type `Test Fandom A` in Check DB: ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " SELECT t.id, t.name, t.kind, t.fandom_id, f.name AS fandom_name FROM tag t LEFT JOIN tag f ON t.fandom_id = f.id WHERE t.name = 'Test Char A'; @@ -2932,7 +2932,7 @@ Expected: one row. `kind='character'`, `name='Test Char A'`, `fandom_name='Test Clean up: ```bash -docker compose exec -T db psql -U imagerepo imagerepo -c " +docker compose exec -T postgres psql -U imagerepo imagerepo -c " DELETE FROM image_tags WHERE tag_id IN (SELECT id FROM tag WHERE name IN ('Test Char A', 'Test Fandom A')); DELETE FROM tag WHERE name IN ('Test Char A', 'Test Fandom A'); "