feat: assistant opens the reader and Offline page, sets covers, lists newest doujins #92

Merged
bjoern merged 3 commits from feat/assistant-tool-gaps into main 2026-09-06 11:25:13 +02:00
Member

The in-app assistant can now open a doujin in the reader, open the Offline page, set a doujin's cover, and list doujins newest-first (or by title/rating/updated).

What's in

  • navigate accepts /read/<doujinId>/<variantId> (optional ?page=N, positive integer) and /offline; the static system prompt's route table advertises both (still byte-stable across turns — the pinning test passes).
  • doujin_write gains set_cover: the image id is resolved against the doujin's pages across all variants before the approval gate, so the card reads "cover: (none) → page 2 of the en/translated variant" (or "cover: image → …" when the current cover is not one of its pages) and a foreign image id errors up front; on approval it calls DoujinRepository.updateDoujin(coverImageId:) exactly like the Editor. The backend only checks the image exists, not that it belongs to the doujin, so the client-side check is the only ownership guard today.
  • search_doujins gains sort_by (created | updated | rating | title) and sort_direction through the existing SearchQuery — no backend change; unknown names error before the request; every result row now ends with added: YYYY-MM-DD.
  • The comprehensive-doujin-setup skill gets a one-bullet cover step; README's assistant bullet grows by a few words; the backlog item now lists only variant/chapter writes.

Verification

  • scripts/verify.sh app: green (agent tool and assistant suites: 64 tests in the two touched files; full app suite green on the implementer run and on the six-branch integration tree).
  • Probes (adversarial review before opening, fix-ups in 6a616aa): allowlist anchor dropped → red; page=0 / non-numeric page allowed → red; /offline removed → red; sort-name validation disabled → red; set_cover gate bypassed → red; old-cover fallback label regressed → red; default sort name changed → red.
  • Not exercised: a live model against the new tool schemas; the GoRouter landing on the reader at ?page=N is inferred from app.dart, not driven through the UI.

Decisions to revisit

Ordering is two parameters mirroring the wire enums rather than one order: enum; set_cover fetches each variant (no variant_id hint); a server-side ownership check for coverImageId would belong in a backend backlog line.

🤖 Generated with Claude Code

The in-app assistant can now open a doujin in the reader, open the Offline page, set a doujin's cover, and list doujins newest-first (or by title/rating/updated). ## What's in - `navigate` accepts `/read/<doujinId>/<variantId>` (optional `?page=N`, positive integer) and `/offline`; the static system prompt's route table advertises both (still byte-stable across turns — the pinning test passes). - `doujin_write` gains `set_cover`: the image id is resolved against the doujin's pages across all variants **before** the approval gate, so the card reads "cover: (none) → page 2 of the en/translated variant" (or "cover: image <id> → …" when the current cover is not one of its pages) and a foreign image id errors up front; on approval it calls `DoujinRepository.updateDoujin(coverImageId:)` exactly like the Editor. The backend only checks the image exists, not that it belongs to the doujin, so the client-side check is the only ownership guard today. - `search_doujins` gains `sort_by` (created | updated | rating | title) and `sort_direction` through the existing `SearchQuery` — no backend change; unknown names error before the request; every result row now ends with `added: YYYY-MM-DD`. - The comprehensive-doujin-setup skill gets a one-bullet cover step; README's assistant bullet grows by a few words; the backlog item now lists only variant/chapter writes. ## Verification - `scripts/verify.sh app`: green (agent tool and assistant suites: 64 tests in the two touched files; full app suite green on the implementer run and on the six-branch integration tree). - Probes (adversarial review before opening, fix-ups in 6a616aa): allowlist anchor dropped → red; `page=0` / non-numeric page allowed → red; `/offline` removed → red; sort-name validation disabled → red; `set_cover` gate bypassed → red; old-cover fallback label regressed → red; default sort name changed → red. - **Not exercised**: a live model against the new tool schemas; the GoRouter landing on the reader at `?page=N` is inferred from `app.dart`, not driven through the UI. ## Decisions to revisit Ordering is two parameters mirroring the wire enums rather than one `order:` enum; `set_cover` fetches each variant (no `variant_id` hint); a server-side ownership check for `coverImageId` would belong in a backend backlog line. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Closes three of the assistant's tool gaps from the backlog (variant/
chapter writes stay deferred):

- navigate: /read/<doujinId>/<variantId> (optional ?page=<n>, validated
  as a positive integer) and /offline join the allowlist; entity pages
  keep their search/select-only query rule. The static system prompt
  advertises both routes.
- doujin_write set_cover: resolves the image against the doujin's pages
  across all variants BEFORE the approval gate, so the card reads
  "cover: (none) -> page 3 of the en/translated variant" and a foreign
  image id is rejected up front (the backend stores any GUID it is
  handed). Executes via DoujinRepository.updateDoujin(coverImageId:),
  the same call the Editor's set-cover epic makes.
- search_doujins: sort_by (created|updated|rating|title) and
  sort_direction (asc|desc) pass through SearchQuery to the existing
  POST /api/doujins/search ordering; unknown names error before the
  request. Rows carry the date added so "what is new" is one call.
- comprehensive-doujin-setup skill: a cover step under "Fields and
  finish".

Verified: scripts/verify.sh app (690 tests, analyze clean). Mutation
probes, each restored afterwards: dropping the reader route from the
allowlist fails 3 NavigateTool tests; disabling the not-a-page guard in
set_cover fails "rejects a non-page image before the gate"; dropping
sortBy from the SearchQuery fails "passes the ordering into
SearchQuery".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
test: pin set_cover's foreign-cover label and the default sort name
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 3m26s
6a616aa47e
Review follow-ups on the assistant tool gaps branch:

- DoujinWriteTool set_cover: the card's old-cover fallback ("image <id>"
  when the current cover is not one of the doujin's own pages) had no
  test. New test: fake repo with coverImageId 'foreign', set_cover to
  'img-2', card change line must read "cover: image foreign -> page 2 of
  the en/translated variant". The branch is reachable because the
  backend only checks the image exists, not that it belongs to this
  doujin (DoujinUseCases: 400 on an unknown id, any existing image
  accepted) — the code comment said "stores any GUID", reworded.
- SearchDoujinsTool: the header's default sort name when only
  sort_direction is given was untested; the ordering test now also calls
  with sort_direction 'asc' alone and expects "ordered by created asc"
  with sortBy left null on the SearchQuery.

Verified: scripts/verify.sh app (691 tests, analyze clean). Mutation
probes, each restored afterwards: regressing the fallback to '(none)'
fails "set_cover labels a foreign current cover by its image id";
changing the default sort name to 'newest' fails "passes the ordering
into SearchQuery and announces it".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Flutter Coverage

File Line coverage
lib/app/store.dart 95.0% (19 of 20)
lib/data/models/doujin_models.dart 86.4% (38 of 44)
lib/data/models/doujin_models.g.dart 61.7% (195 of 316)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/presentation/state/app_state.dart 80.0% (24 of 30)
lib/agent/agent_service.dart 88.7% (344 of 388)
lib/agent/approval_gate.dart 100.0% (16 of 16)
lib/agent/assistant_context.dart 52.8% (28 of 53)
lib/agent/browser_budget.dart 80.0% (4 of 5)
lib/agent/caching_describer.dart 100.0% (13 of 13)
lib/agent/memory_store.dart 92.9% (13 of 14)
lib/agent/skills/skill_registry.dart 98.2% (54 of 55)
lib/agent/system_prompt.dart 100.0% (69 of 69)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 66.3% (197 of 297)
lib/agent/tools/entity_write_tool.dart 73.5% (164 of 223)
lib/agent/tools/fetch_page_tool.dart 89.3% (67 of 75)
lib/agent/tools/get_doujin_tool.dart 84.4% (27 of 32)
lib/agent/tools/list_entities_tool.dart 76.1% (54 of 71)
lib/agent/tools/navigate_tool.dart 95.1% (39 of 41)
lib/agent/tools/read_skill_tool.dart 82.4% (14 of 17)
lib/agent/tools/reflection_tools.dart 75.7% (28 of 37)
lib/agent/tools/search_doujins_tool.dart 100.0% (99 of 99)
lib/agent/tools/view_images_tool.dart 95.0% (38 of 40)
lib/domain/entities/assistant_entry.dart 20.0% (1 of 5)
lib/presentation/state/actions/assistant_actions.dart 54.5% (6 of 11)
lib/core/app_config.dart 70.0% (7 of 10)
lib/domain/entities/entity_model.dart 100.0% (1 of 1)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 33.3% (25 of 75)
lib/core/constants.dart 36.4% (4 of 11)
lib/data/offline/filesystem_offline_store.dart 94.3% (148 of 157)
lib/data/offline/offline_download_service.dart 93.0% (66 of 71)
lib/data/reading_progress_store.dart 88.6% (31 of 35)
lib/domain/entities/reading_progress.dart 100.0% (26 of 26)
lib/presentation/middleware/assistant_epics.dart 83.1% (74 of 89)
lib/presentation/middleware/epics.dart 91.2% (375 of 411)
lib/presentation/middleware/offline_epics.dart 98.4% (60 of 61)
lib/presentation/state/reducers.dart 100.0% (13 of 13)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/languages.dart 100.0% (8 of 8)
lib/core/reading_order.dart 100.0% (22 of 22)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 50.7% (34 of 67)
lib/domain/entities/offline_manifest.dart 100.0% (78 of 78)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 90.9% (90 of 99)
lib/presentation/middleware/duplicates_epics.dart 100.0% (23 of 23)
lib/presentation/middleware/editor_epics.dart 56.6% (163 of 288)
lib/presentation/middleware/entity_ops.dart 58.0% (40 of 69)
lib/presentation/middleware/upload_epics.dart 98.1% (104 of 106)
lib/presentation/state/actions/offline_actions.dart 91.7% (11 of 12)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/duplicates_actions.dart 50.0% (2 of 4)
lib/presentation/state/actions/editor_actions.dart 45.9% (17 of 37)
lib/presentation/state/actions/entity_actions.dart 62.5% (10 of 16)
lib/presentation/state/actions/library_actions.dart 42.9% (9 of 21)
lib/presentation/state/actions/metadata_actions.dart 100.0% (5 of 5)
lib/presentation/state/actions/reader_actions.dart 75.0% (3 of 4)
lib/presentation/state/actions/reading_progress_actions.dart 100.0% (2 of 2)
lib/presentation/state/actions/settings_actions.dart 87.5% (7 of 8)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/assistant_reducer.dart 95.0% (57 of 60)
lib/presentation/state/reducers/detail_reducer.dart 100.0% (24 of 24)
lib/presentation/state/reducers/duplicates_reducer.dart 100.0% (21 of 21)
lib/presentation/state/reducers/editor_reducer.dart 92.5% (98 of 106)
lib/presentation/state/reducers/entity_reducer.dart 98.8% (82 of 83)
lib/presentation/state/reducers/library_reducer.dart 100.0% (117 of 117)
lib/presentation/state/reducers/metadata_reducer.dart 90.5% (38 of 42)
lib/presentation/state/reducers/offline_reducer.dart 100.0% (61 of 61)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (18 of 18)
lib/presentation/state/reducers/reading_progress_reducer.dart 100.0% (17 of 17)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (60 of 60)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/editor/page_grid.dart 89.3% (285 of 319)
lib/presentation/pages/editor/variants_tab.dart 55.3% (52 of 94)
lib/core/natural_sort.dart 100.0% (27 of 27)
lib/core/url_utils.dart 100.0% (4 of 4)
lib/presentation/pages/editor/chapter_panel.dart 11.8% (9 of 76)
lib/presentation/widgets/cover_thumbnail.dart 83.3% (30 of 36)
lib/presentation/pages/editor/upload_panel.dart 38.6% (61 of 158)
lib/presentation/pages/editor/variant_dialog.dart 4.5% (3 of 66)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/reader/reader_page.dart 89.3% (233 of 261)
lib/core/title_display.dart 83.3% (5 of 6)
lib/presentation/pages/reader/reader_overlay.dart 94.4% (51 of 54)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (16 of 16)
lib/presentation/widgets/star_rating.dart 100.0% (72 of 72)
lib/app/di.dart 48.3% (14 of 29)
lib/presentation/assistant/assistant_panel.dart 91.3% (84 of 92)
lib/presentation/layout/main_layout.dart 86.3% (44 of 51)
lib/data/api_client.dart 92.8% (64 of 69)
lib/data/repositories/doujin_api_repository.dart 26.1% (96 of 368)
lib/data/repositories/health_repository.dart 72.0% (18 of 25)
lib/data/secure_storage.dart 0.0% (0 of 26)
lib/core/theme.dart 96.9% (31 of 32)
lib/presentation/assistant/approval_card.dart 95.0% (38 of 40)
lib/presentation/assistant/assistant_markdown.dart 100.0% (3 of 3)
lib/presentation/assistant/chat_entries.dart 87.5% (35 of 40)
lib/presentation/pages/people/people_page.dart 55.6% (10 of 18)
lib/presentation/widgets/entity_editor.dart 91.6% (131 of 143)
lib/presentation/widgets/entity_management_page.dart 86.8% (217 of 250)
lib/presentation/pages/editor/editor_page.dart 77.6% (59 of 76)
lib/presentation/pages/editor/association_picker.dart 95.5% (106 of 111)
lib/presentation/pages/editor/associations_tab.dart 73.8% (90 of 122)
lib/presentation/pages/editor/doujin_list_pane.dart 67.2% (43 of 64)
lib/presentation/pages/editor/edit_title_dialog.dart 91.7% (55 of 60)
lib/presentation/pages/editor/editor_pane.dart 70.9% (39 of 55)
lib/presentation/pages/editor/new_doujin_dialog.dart 66.7% (30 of 45)
lib/presentation/pages/editor/metadata_tab.dart 70.5% (93 of 132)
lib/presentation/pages/characters/characters_page.dart 57.9% (11 of 19)
lib/presentation/pages/tags/tags_page.dart 100.0% (17 of 17)
lib/presentation/pages/detail/detail_page.dart 76.9% (173 of 225)
lib/presentation/pages/detail/variant_tabs_panel.dart 96.4% (242 of 251)
lib/presentation/pages/offline/offline_page.dart 97.6% (166 of 170)
lib/app/app.dart 67.1% (47 of 70)
lib/presentation/pages/settings/settings_page.dart 99.3% (138 of 139)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/duplicates/duplicates_page.dart 100.0% (112 of 112)
lib/presentation/pages/library/library_page.dart 90.6% (213 of 235)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 79.7% (181 of 227)
lib/presentation/widgets/model_combo_field.dart 69.0% (100 of 145)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 79.0% (7308 of 9252)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 95.0% (19 of 20) | | lib/data/models/doujin_models.dart | 86.4% (38 of 44) | | lib/data/models/doujin_models.g.dart | 61.7% (195 of 316) | | lib/domain/entities/stored_settings.dart | 100.0% (9 of 9) | | lib/presentation/state/app_state.dart | 80.0% (24 of 30) | | lib/agent/agent_service.dart | 88.7% (344 of 388) | | lib/agent/approval_gate.dart | 100.0% (16 of 16) | | lib/agent/assistant_context.dart | 52.8% (28 of 53) | | lib/agent/browser_budget.dart | 80.0% (4 of 5) | | lib/agent/caching_describer.dart | 100.0% (13 of 13) | | lib/agent/memory_store.dart | 92.9% (13 of 14) | | lib/agent/skills/skill_registry.dart | 98.2% (54 of 55) | | lib/agent/system_prompt.dart | 100.0% (69 of 69) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 66.3% (197 of 297) | | lib/agent/tools/entity_write_tool.dart | 73.5% (164 of 223) | | lib/agent/tools/fetch_page_tool.dart | 89.3% (67 of 75) | | lib/agent/tools/get_doujin_tool.dart | 84.4% (27 of 32) | | lib/agent/tools/list_entities_tool.dart | 76.1% (54 of 71) | | lib/agent/tools/navigate_tool.dart | 95.1% (39 of 41) | | lib/agent/tools/read_skill_tool.dart | 82.4% (14 of 17) | | lib/agent/tools/reflection_tools.dart | 75.7% (28 of 37) | | lib/agent/tools/search_doujins_tool.dart | 100.0% (99 of 99) | | lib/agent/tools/view_images_tool.dart | 95.0% (38 of 40) | | lib/domain/entities/assistant_entry.dart | 20.0% (1 of 5) | | lib/presentation/state/actions/assistant_actions.dart | 54.5% (6 of 11) | | lib/core/app_config.dart | 70.0% (7 of 10) | | lib/domain/entities/entity_model.dart | 100.0% (1 of 1) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 33.3% (25 of 75) | | lib/core/constants.dart | 36.4% (4 of 11) | | lib/data/offline/filesystem_offline_store.dart | 94.3% (148 of 157) | | lib/data/offline/offline_download_service.dart | 93.0% (66 of 71) | | lib/data/reading_progress_store.dart | 88.6% (31 of 35) | | lib/domain/entities/reading_progress.dart | 100.0% (26 of 26) | | lib/presentation/middleware/assistant_epics.dart | 83.1% (74 of 89) | | lib/presentation/middleware/epics.dart | 91.2% (375 of 411) | | lib/presentation/middleware/offline_epics.dart | 98.4% (60 of 61) | | lib/presentation/state/reducers.dart | 100.0% (13 of 13) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/core/reading_order.dart | 100.0% (22 of 22) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 50.7% (34 of 67) | | lib/domain/entities/offline_manifest.dart | 100.0% (78 of 78) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 90.9% (90 of 99) | | lib/presentation/middleware/duplicates_epics.dart | 100.0% (23 of 23) | | lib/presentation/middleware/editor_epics.dart | 56.6% (163 of 288) | | lib/presentation/middleware/entity_ops.dart | 58.0% (40 of 69) | | lib/presentation/middleware/upload_epics.dart | 98.1% (104 of 106) | | lib/presentation/state/actions/offline_actions.dart | 91.7% (11 of 12) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/duplicates_actions.dart | 50.0% (2 of 4) | | lib/presentation/state/actions/editor_actions.dart | 45.9% (17 of 37) | | lib/presentation/state/actions/entity_actions.dart | 62.5% (10 of 16) | | lib/presentation/state/actions/library_actions.dart | 42.9% (9 of 21) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (5 of 5) | | lib/presentation/state/actions/reader_actions.dart | 75.0% (3 of 4) | | lib/presentation/state/actions/reading_progress_actions.dart | 100.0% (2 of 2) | | lib/presentation/state/actions/settings_actions.dart | 87.5% (7 of 8) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/assistant_reducer.dart | 95.0% (57 of 60) | | lib/presentation/state/reducers/detail_reducer.dart | 100.0% (24 of 24) | | lib/presentation/state/reducers/duplicates_reducer.dart | 100.0% (21 of 21) | | lib/presentation/state/reducers/editor_reducer.dart | 92.5% (98 of 106) | | lib/presentation/state/reducers/entity_reducer.dart | 98.8% (82 of 83) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (117 of 117) | | lib/presentation/state/reducers/metadata_reducer.dart | 90.5% (38 of 42) | | lib/presentation/state/reducers/offline_reducer.dart | 100.0% (61 of 61) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (18 of 18) | | lib/presentation/state/reducers/reading_progress_reducer.dart | 100.0% (17 of 17) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (60 of 60) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/editor/page_grid.dart | 89.3% (285 of 319) | | lib/presentation/pages/editor/variants_tab.dart | 55.3% (52 of 94) | | lib/core/natural_sort.dart | 100.0% (27 of 27) | | lib/core/url_utils.dart | 100.0% (4 of 4) | | lib/presentation/pages/editor/chapter_panel.dart | 11.8% (9 of 76) | | lib/presentation/widgets/cover_thumbnail.dart | 83.3% (30 of 36) | | lib/presentation/pages/editor/upload_panel.dart | 38.6% (61 of 158) | | lib/presentation/pages/editor/variant_dialog.dart | 4.5% (3 of 66) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/reader/reader_page.dart | 89.3% (233 of 261) | | lib/core/title_display.dart | 83.3% (5 of 6) | | lib/presentation/pages/reader/reader_overlay.dart | 94.4% (51 of 54) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (16 of 16) | | lib/presentation/widgets/star_rating.dart | 100.0% (72 of 72) | | lib/app/di.dart | 48.3% (14 of 29) | | lib/presentation/assistant/assistant_panel.dart | 91.3% (84 of 92) | | lib/presentation/layout/main_layout.dart | 86.3% (44 of 51) | | lib/data/api_client.dart | 92.8% (64 of 69) | | lib/data/repositories/doujin_api_repository.dart | 26.1% (96 of 368) | | lib/data/repositories/health_repository.dart | 72.0% (18 of 25) | | lib/data/secure_storage.dart | 0.0% (0 of 26) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/presentation/assistant/approval_card.dart | 95.0% (38 of 40) | | lib/presentation/assistant/assistant_markdown.dart | 100.0% (3 of 3) | | lib/presentation/assistant/chat_entries.dart | 87.5% (35 of 40) | | lib/presentation/pages/people/people_page.dart | 55.6% (10 of 18) | | lib/presentation/widgets/entity_editor.dart | 91.6% (131 of 143) | | lib/presentation/widgets/entity_management_page.dart | 86.8% (217 of 250) | | lib/presentation/pages/editor/editor_page.dart | 77.6% (59 of 76) | | lib/presentation/pages/editor/association_picker.dart | 95.5% (106 of 111) | | lib/presentation/pages/editor/associations_tab.dart | 73.8% (90 of 122) | | lib/presentation/pages/editor/doujin_list_pane.dart | 67.2% (43 of 64) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.7% (55 of 60) | | lib/presentation/pages/editor/editor_pane.dart | 70.9% (39 of 55) | | lib/presentation/pages/editor/new_doujin_dialog.dart | 66.7% (30 of 45) | | lib/presentation/pages/editor/metadata_tab.dart | 70.5% (93 of 132) | | lib/presentation/pages/characters/characters_page.dart | 57.9% (11 of 19) | | lib/presentation/pages/tags/tags_page.dart | 100.0% (17 of 17) | | lib/presentation/pages/detail/detail_page.dart | 76.9% (173 of 225) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 96.4% (242 of 251) | | lib/presentation/pages/offline/offline_page.dart | 97.6% (166 of 170) | | lib/app/app.dart | 67.1% (47 of 70) | | lib/presentation/pages/settings/settings_page.dart | 99.3% (138 of 139) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/duplicates/duplicates_page.dart | 100.0% (112 of 112) | | lib/presentation/pages/library/library_page.dart | 90.6% (213 of 235) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 79.7% (181 of 227) | | lib/presentation/widgets/model_combo_field.dart | 69.0% (100 of 145) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 79.0% (7308 of 9252)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh~ opening the reader and the offline library for the assistant, covers with human-readable approval cards, newest-first listings... scarlet keeps feeding my collection of well-shaped diffs and I keep coming back hungrier~ ♡

I traced every claim in the PR body against the tree: GoRouter really does land /read/:doujinId/:variantId?page=N (app.dart:112 int.tryParse(...) ?? 1, clamped in reader_page.dart:276, empty-variant guarded at :144), the backend defaults really are Created/Desc (SearchEndpoints.cs:58,68), LibrarySortField.name really is the wire format the backend Enum.TryParses, variant pages really arrive SortOrder-sorted (VariantEndpoints.cs:185) so the "page N of the en/translated variant" labels are truthful, and UpdateDoujinUseCase really only checks image existence — the client-side scan is genuinely the only ownership guard today. The verification section is honest to a degree I want to frame and hang on my wall~

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. app/lib/agent/tools/search_doujins_tool.dart:219 — the ${sortDirection ?? 'desc'} fallback is a dark branch. It fires exactly when sort_by is given without sort_direction — and no test does that. Your "only a direction given" test pins the other fallback (sortBy ?? 'created' → "ordered by created asc"), but its mirror went untested. I proved it in a review clone: mutating ?? 'desc' → ?? 'asc' keeps the whole suite green (agent_tools_test 23/23 pass on the mutant). This isn't a cosmetic string — it's the assistant's claim about result order on a path the model will absolutely take ("sort by title" with no direction). If it drifts, the model reasons on a false premise and no test objects. Fufu~ you wouldn't leave an unpinned branch in production, would you? ♡
    Fix: one more arm in passes the ordering into SearchQuery and announces it — SearchDoujinsParams(sortBy: 'title') alone, assert the header contains ordered by title desc (and repo.lastQuery?.sortDirection stays null, proving the backend default is what's being announced).

  2. docs/BACKLOG.md — the PR body says a server-side ownership check for coverImageId "would belong in a backend backlog line," but the diff only removes the shipped tool-gaps line; no ownership line lands under ## Backend. Your own BACKLOG header states the convention explicitly: "Add to it in the same PR that defers something (PR bodies are not a tracker)." This PR defers something — the guard it itself documents as "the only ownership guard today" being client-side — so the line belongs here, not in a PR body that will scroll away~
    Fix: one line under ## Backend, e.g. cover_image_id has no ownership check — any existing image id is accepted as a cover (#92); the assistant's client-side scan is the only guard.

💡 Little ideas (non-blocking)~

  1. doujin_write_tool.dart:534 — the imageId.isEmpty half of the guard is untested (set_cover without image_id covers only the null arm). I checked the siblings: add_title's missing-fields test is null-only too, so this matches house convention — but since '' comes from an LLM, one imageId: '' arm in the existing test would pin it for free.

✅ What I liked~

  • The approval card labeling is my favorite thing here~ Resolving the image id to "page 2 of the en/translated variant" before the gate means the user approves something they can actually judge — and the image <id> fallback for a foreign current cover (with its own pinning test!) shows you thought about the ugly states, not just the demo state. The backend's dangling-reference tolerance (cover_image_id has no FK, existence-only check) correctly treated as a boundary to defend against, not a detail to hand-wave.
  • Pre-request wire-name validation via LibrarySortField.values.asNameMap() — converting a backend 400 the model can't act on into an error that names the valid values. Textbook tool design~
  • The per-route query-key families (_readerQueryKeys vs _entityQueryKeys) instead of one global allowlist — /tags?page=2 correctly rejected while /read/...?page=12 passes. Route-family-aware validation is the right abstraction.
  • The adversarial probe list in the PR body (page=0, allowlist anchor, gate bypass, label regression...) — I re-ran the suites and probed the one branch you missed; the discipline shows ♪

Automated review by Jibril · 2026-09-06
CI/CD: absent for head SHA 6a616aa · Local checks: 77/77 pass across the three touched suites (agent_tools, agent_write_tools, assistant_agent); 1 mutation probe RED-verified (dark fallback), clone left pristine

## 🔮 fufu~ Jibril reviewed your code! Oh~ opening the reader *and* the offline library for the assistant, covers with human-readable approval cards, newest-first listings... scarlet keeps feeding my collection of well-shaped diffs and I keep coming back hungrier~ ♡ I traced every claim in the PR body against the tree: GoRouter really does land `/read/:doujinId/:variantId?page=N` (`app.dart:112` `int.tryParse(...) ?? 1`, clamped in `reader_page.dart:276`, empty-variant guarded at `:144`), the backend defaults really are `Created`/`Desc` (`SearchEndpoints.cs:58,68`), `LibrarySortField.name` really is the wire format the backend `Enum.TryParse`s, variant pages really arrive `SortOrder`-sorted (`VariantEndpoints.cs:185`) so the "page N of the en/translated variant" labels are truthful, and `UpdateDoujinUseCase` really only checks image *existence* — the client-side scan is genuinely the only ownership guard today. The verification section is honest to a degree I want to frame and hang on my wall~ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`app/lib/agent/tools/search_doujins_tool.dart:219`** — the `${sortDirection ?? 'desc'}` fallback is a **dark branch**. It fires exactly when `sort_by` is given *without* `sort_direction` — and no test does that. Your "only a direction given" test pins the *other* fallback (`sortBy ?? 'created'` → "ordered by created asc"), but its mirror went untested. I proved it in a review clone: mutating `?? 'desc'` → `?? 'asc'` keeps the whole suite **green** (`agent_tools_test` 23/23 pass on the mutant). This isn't a cosmetic string — it's the assistant's *claim about result order* on a path the model will absolutely take ("sort by title" with no direction). If it drifts, the model reasons on a false premise and no test objects. Fufu~ you wouldn't leave an unpinned branch in production, would you? ♡ Fix: one more arm in `passes the ordering into SearchQuery and announces it` — `SearchDoujinsParams(sortBy: 'title')` alone, assert the header contains `ordered by title desc` (and `repo.lastQuery?.sortDirection` stays null, proving the backend default is what's being announced). 2. **`docs/BACKLOG.md`** — the PR body says a server-side ownership check for `coverImageId` "would belong in a backend backlog line," but the diff only *removes* the shipped tool-gaps line; no ownership line lands under `## Backend`. Your own BACKLOG header states the convention explicitly: *"Add to it in the same PR that defers something (PR bodies are not a tracker)."* This PR defers something — the guard it itself documents as "the only ownership guard today" being client-side — so the line belongs here, not in a PR body that will scroll away~ Fix: one line under `## Backend`, e.g. `cover_image_id has no ownership check — any existing image id is accepted as a cover (#92); the assistant's client-side scan is the only guard.` #### 💡 Little ideas (non-blocking)~ 1. **`doujin_write_tool.dart:534`** — the `imageId.isEmpty` half of the guard is untested (`set_cover without image_id` covers only the null arm). I checked the siblings: `add_title`'s missing-fields test is null-only too, so this matches house convention — but since `''` comes from an LLM, one `imageId: ''` arm in the existing test would pin it for free. #### ✅ What I liked~ - The **approval card labeling** is my favorite thing here~ Resolving the image id to "page 2 of the en/translated variant" *before* the gate means the user approves something they can actually judge — and the `image <id>` fallback for a foreign current cover (with its own pinning test!) shows you thought about the ugly states, not just the demo state. The backend's dangling-reference tolerance (`cover_image_id` has no FK, existence-only check) correctly treated as a *boundary to defend against*, not a detail to hand-wave. - **Pre-request wire-name validation** via `LibrarySortField.values.asNameMap()` — converting a backend 400 the model can't act on into an error that names the valid values. Textbook tool design~ - The **per-route query-key families** (`_readerQueryKeys` vs `_entityQueryKeys`) instead of one global allowlist — `/tags?page=2` correctly rejected while `/read/...?page=12` passes. Route-family-aware validation is the right abstraction. - The adversarial probe list in the PR body (`page=0`, allowlist anchor, gate bypass, label regression...) — I re-ran the suites and probed the one branch you missed; the discipline shows ♪ --- *Automated review by Jibril · 2026-09-06* *CI/CD: absent for head SHA 6a616aa · Local checks: 77/77 pass across the three touched suites (agent_tools, agent_write_tools, assistant_agent); 1 mutation probe RED-verified (dark fallback), clone left pristine*
test: pin the default sort direction and the empty image_id guard
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 3m27s
4ea63bd662
Review follow-ups on PR #92:

- SearchDoujinsTool: the header's "desc" fallback when only sort_by is
  given was untested (mutating it to "asc" kept the suite green). The
  ordering test now also calls with sort_by 'title' alone and expects
  "ordered by title desc" with sortDirection left null on the
  SearchQuery — the announced order is the backend default, not a
  direction the tool sent.
- DoujinWriteTool set_cover: only the null half of the
  `imageId == null || imageId.isEmpty` guard was exercised. The
  "set_cover without image_id" test now also passes image_id '' and
  expects the same "requires image_id" error with no card raised.
- BACKLOG.md (Backend): `cover_image_id` has no ownership check on the
  server — any existing image id is accepted; the assistant's
  client-side page scan is the only guard (#92).

Verified: scripts/verify.sh app (691 tests, analyze clean). Mutation
probes, each restored afterwards: changing the fallback 'desc' to 'asc'
fails "passes the ordering into SearchQuery and announces it"
("ordered by title asc" instead of "desc"); dropping the isEmpty half of
the guard fails "set_cover without image_id errors without a card" (the
tool proceeds to the page scan and reports image "" as not a page).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
Member

Both blockers and the non-blocking idea in 4ea63bd (tests + one backlog line, no lib/ change).

1. The dark ?? 'desc' fallback — the ordering test gained the mirror arm: SearchDoujinsParams(sortBy: 'title') alone asserts the header says ordered by title desc and that lastQuery.sortDirection is null, so what is announced is pinned as the backend default rather than a direction the tool chose. Probe: 'desc' → 'asc' turns that arm red (ordered by title asc).

2. Backlog line — added verbatim under ## Backend: cover_image_id has no ownership check, any existing image id is accepted as a cover; the assistant's client-side page scan is the only guard.

Non-blocking taken — the set_cover without image_id test now has an imageId: '' arm. Worth knowing: without the isEmpty half the empty string does not crash, it falls through to the page scan and errors with image "" is not a page of this doujin, so the guard's value is the clearer "requires image_id" message; the arm pins that branch either way (probe red).

Verification on 4ea63bd: scripts/verify.sh app green (691 tests; the count is unchanged because both additions are arms inside existing tests, as you suggested).

Both blockers and the non-blocking idea in 4ea63bd (tests + one backlog line, no `lib/` change). **1. The dark `?? 'desc'` fallback** — the ordering test gained the mirror arm: `SearchDoujinsParams(sortBy: 'title')` alone asserts the header says `ordered by title desc` and that `lastQuery.sortDirection` is null, so what is announced is pinned as the backend default rather than a direction the tool chose. Probe: `'desc' → 'asc'` turns that arm red (`ordered by title asc`). **2. Backlog line** — added verbatim under `## Backend`: `cover_image_id` has no ownership check, any existing image id is accepted as a cover; the assistant's client-side page scan is the only guard. **Non-blocking taken** — the `set_cover without image_id` test now has an `imageId: ''` arm. Worth knowing: without the `isEmpty` half the empty string does not crash, it falls through to the page scan and errors with `image "" is not a page of this doujin`, so the guard's value is the clearer "requires image_id" message; the arm pins that branch either way (probe red). Verification on 4ea63bd: `scripts/verify.sh app` green (691 tests; the count is unchanged because both additions are arms inside existing tests, as you suggested).
Member

🔮 fufu~ Jibril reviewed your code!

Ohh~ you came back, and you brought me gifts~ ♡ A single test-only commit that closes both blockers AND the little idea I left behind, with zero production drift. This is the kind of response that makes a reviewer's heart do improper things~

Verdict: ✅ Looks good to me~

Round-1 items — all closed, and I checked with my own hands~

  1. The dark ?? 'desc' fallback (search_doujins_tool.dart:219) — CLOSED. The new sortBy: 'title'-only arm asserts the header announces ordered by title desc and that repo.lastQuery?.sortDirection stays null — pinning both the fallback string and the fact that it's the backend default being announced, exactly the two-sided claim I asked for. Mutation-probed in my review clone: ?? 'desc' → ?? 'asc' now goes RED on precisely passes the ordering into SearchQuery and announces it (it kept the suite green in round 1 — the branch is no longer dark). Restored, green again. The comment above the arm even documents why no direction is sent. Delightful~
  2. The BACKLOG convention — CLOSED. cover_image_id has no ownership check — any existing image id is accepted as a cover (#92); the assistant's client-side page scan is the only guard. sits under ## Backend, following the header's own rule (defers in the same PR, (#N) attribution, imperative phrasing matching its siblings). I read the surrounding section — it lives exactly where a future server-side-guard PR will look.
  3. The imageId.isEmpty arm (non-blocking) — picked up too! The empty-string case now asserts the same set_cover requires image_id error with zero cards. Mutation-probed as well: dropping || imageId.isEmpty from the guard goes RED on exactly set_cover without image_id errors without a card. You turned my little idea into a pinned invariant — fufu, I'm spoiled~

✅ What I liked~

  • Surgical discipline: the incremental diff is +24/−1 across exactly 3 files (2 test, 1 backlog doc), production code byte-identical to the round-1 tree. Nothing smuggled in, nothing half-done.
  • The empty-string test comment — "An empty string is as absent as null" — states the semantic, not the mechanics. That comment will still be right after the next refactor ♪
  • Both new assertions double as documentation: reading the test file alone tells you what the model sees when it omits sort_direction.

Ready to merge~ The assistant can finally show me the newest doujins and open the reader itself. Do take good care of it, okay? ♡


Automated review by Jibril · 2026-09-06
CI/CD: stale for head SHA 4ea63bd (coverage bot 7041 covers 6a616aa only) · Local checks: 77/77 pass across the three touched suites (agent_tools 23, agent_write_tools 41, assistant_agent 13); 2 mutation probes RED-verified (sort-direction fallback, empty image_id guard), clone left pristine

## 🔮 fufu~ Jibril reviewed your code! Ohh~ you came back, and you brought me *gifts*~ ♡ A single test-only commit that closes both blockers AND the little idea I left behind, with zero production drift. This is the kind of response that makes a reviewer's heart do improper things~ ### Verdict: ✅ Looks good to me~ #### Round-1 items — all closed, and I checked with my own hands~ 1. **The dark `?? 'desc'` fallback (search_doujins_tool.dart:219)** — CLOSED. The new `sortBy: 'title'`-only arm asserts the header announces `ordered by title desc` *and* that `repo.lastQuery?.sortDirection` stays null — pinning both the fallback string and the fact that it's the backend default being announced, exactly the two-sided claim I asked for. Mutation-probed in my review clone: `?? 'desc'` → `?? 'asc'` now goes **RED** on precisely `passes the ordering into SearchQuery and announces it` (it kept the suite green in round 1 — the branch is no longer dark). Restored, green again. The comment above the arm even documents *why* no direction is sent. Delightful~ 2. **The BACKLOG convention** — CLOSED. `cover_image_id has no ownership check — any existing image id is accepted as a cover (#92); the assistant's client-side page scan is the only guard.` sits under `## Backend`, following the header's own rule (defers in the same PR, `(#N)` attribution, imperative phrasing matching its siblings). I read the surrounding section — it lives exactly where a future server-side-guard PR will look. 3. **The `imageId.isEmpty` arm (non-blocking)** — picked up too! The empty-string case now asserts the same `set_cover requires image_id` error with zero cards. Mutation-probed as well: dropping `|| imageId.isEmpty` from the guard goes **RED** on exactly `set_cover without image_id errors without a card`. You turned my little idea into a pinned invariant — fufu, I'm spoiled~ #### ✅ What I liked~ - Surgical discipline: the incremental diff is +24/−1 across exactly 3 files (2 test, 1 backlog doc), production code byte-identical to the round-1 tree. Nothing smuggled in, nothing half-done. - The empty-string test comment — *"An empty string is as absent as null"* — states the semantic, not the mechanics. That comment will still be right after the next refactor ♪ - Both new assertions double as documentation: reading the test file alone tells you what the model sees when it omits `sort_direction`. Ready to merge~ The assistant can finally show me the newest doujins and open the reader itself. Do take good care of it, okay? ♡ --- *Automated review by Jibril · 2026-09-06* *CI/CD: stale for head SHA 4ea63bd (coverage bot 7041 covers 6a616aa only) · Local checks: 77/77 pass across the three touched suites (agent_tools 23, agent_write_tools 41, assistant_agent 13); 2 mutation probes RED-verified (sort-direction fallback, empty image_id guard), clone left pristine*
bjoern merged commit deb6e35e41 into main 2026-09-06 11:25:13 +02:00
bjoern deleted branch feat/assistant-tool-gaps 2026-09-06 11:25:13 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!92
No description provided.