feat: filter the library by variant kind from the smart filter bar #103

Merged
bjoern merged 2 commits from feat/library-variant-kind-filter into main 2026-09-07 12:21:48 +02:00
Member

The backend has supported filtering by variant kind since the search feature shipped; the library had no way to ask for it. Now typing kind:translated (autocompleted from kin onward) shows only doujins that have a variant of that kind, combined with every other token, as a chip that reads "Kind: Translated" and deletes like any other.

What's in

  • A variant-kind filter token: parsing of the kind: prefix, suggestions from the enum, the chip's label, colour and icon, and the query builder sending the backend's enum name (Translated, Reedition) — the integer the entities serialise would be a 400.
  • One kind at a time, because the server takes a single value: a second kind token replaces the first rather than stacking chips promising an OR the backend cannot honour.
  • An unknown kind (kind:translation) or a negated one (-kind:translated) still commits nothing — that is deliberate — but the bar no longer swallows the Enter press silently. One hint line under the field names the four accepted kinds and says a kind cannot be excluded; it clears as soon as the text parses.

Verification

  • scripts/verify.sh app: green, 788 tests (24 new across token parsing, suggestions, the reducer's replace rule, the query builder, and two widget tests).
  • scripts/verify.sh backend: green, unchanged — nothing backend-side was touched; the wire names were confirmed against the search endpoint's defined-value parse and its existing tests.
  • Directional probes, all red: the negated-kind guard, the suggestion minimum length, the replace rule, the wire value, and the new hint.
  • Green with the rest of the batch on a six-branch integration tree.
  • Not exercised: a running server (the accepted wire names are read from the endpoint's parse, not round-tripped), and the chip's colour and icon on the desktop app.

Decisions to revisit

Only the four enum names are accepted, with punctuation ignored so kind:re-edition works; friendly synonyms would go in one place if you want them. The kind lives in the token list rather than as a state field, which is enough while the filter bar is the only entry point — a dropdown or the assistant setting it would be the moment to promote it. A review finding also removed an unreachable guard in the kind parser.

🤖 Generated with Claude Code

The backend has supported filtering by variant kind since the search feature shipped; the library had no way to ask for it. Now typing `kind:translated` (autocompleted from `kin` onward) shows only doujins that have a variant of that kind, combined with every other token, as a chip that reads "Kind: Translated" and deletes like any other. ## What's in - A variant-kind filter token: parsing of the `kind:` prefix, suggestions from the enum, the chip's label, colour and icon, and the query builder sending the backend's enum **name** (`Translated`, `Reedition`) — the integer the entities serialise would be a 400. - **One kind at a time**, because the server takes a single value: a second kind token replaces the first rather than stacking chips promising an OR the backend cannot honour. - An unknown kind (`kind:translation`) or a negated one (`-kind:translated`) still commits nothing — that is deliberate — but the bar no longer swallows the Enter press silently. One hint line under the field names the four accepted kinds and says a kind cannot be excluded; it clears as soon as the text parses. ## Verification - `scripts/verify.sh app`: green, 788 tests (24 new across token parsing, suggestions, the reducer's replace rule, the query builder, and two widget tests). - `scripts/verify.sh backend`: green, unchanged — nothing backend-side was touched; the wire names were confirmed against the search endpoint's defined-value parse and its existing tests. - Directional probes, all red: the negated-kind guard, the suggestion minimum length, the replace rule, the wire value, and the new hint. - Green with the rest of the batch on a six-branch integration tree. - **Not exercised**: a running server (the accepted wire names are read from the endpoint's parse, not round-tripped), and the chip's colour and icon on the desktop app. ## Decisions to revisit Only the four enum names are accepted, with punctuation ignored so `kind:re-edition` works; friendly synonyms would go in one place if you want them. The kind lives in the token list rather than as a state field, which is enough while the filter bar is the only entry point — a dropdown or the assistant setting it would be the moment to promote it. A review finding also removed an unreachable guard in the kind parser. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Typing `kind:translated` (autocompleted from `kind`) in the library's
filter bar now narrows the library to doujins that have a variant of that
kind, AND-combined with every other token. The chip reads "Kind:
Translated" and removes like any other token.

What/why:
- `FilterType.variantKind` + the `kind:` prefix in `filter_token.dart`.
  The token carries the backend's wire NAME (`Translated`, `Reedition`),
  because `POST /api/doujins/search` parses `variantKind` with
  `Enum.TryParse` over the C# enum — the integer the variant entities
  serialize would be a 400. An unknown kind word (`kind:translation`) and
  a negated one (`-kind:…`, which the flat query model has no form for,
  ADR 0016) commit nothing instead of leaking into a text or tag token.
- Autocomplete offers the four kinds once the query reaches `kin`, or
  filtered by what follows `kind:`; they come first and never crowd out
  entity matches for ordinary words. The exclude (`-`) path has none.
- One kind at a time: the server takes a single value, so the library
  reducer replaces an existing kind token instead of stacking chips that
  would promise an OR the backend cannot honour.
- `variant_display.dart` gains the wire name, the parser and the
  prefix-match helper, next to the labels the detail page and editor
  already share; the search epic reads the kind straight off the token
  list (no legacy mirror field for a single-valued filter).

Verified: `scripts/verify.sh app` green (app 786 tests, +24; analyze 0
issues) and `scripts/verify.sh backend` green (498 tests, untouched).
Mutation probes went red as expected: dropping the `-kind:` guard, the
suggestion min-length, the reducer's kind eviction, `variantKind` in the
search query, the chip's kind label, its icon arm, the raw-value fallback,
and making the wire name equal the display label.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix: the library filter bar answers a kind: word it cannot commit
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 4m58s
6abd446a2a
Review follow-ups on the variant-kind filter token.

What/why:
- `parseVariantKind` drops its `if (normalized.isEmpty) return null`
  guard: no kind's wire name normalizes to the empty string, so the loop
  below already returns null for an empty or all-punctuation input. The
  doc comment now says why the guard is unnecessary; the behaviour stays
  pinned by `parseVariantKind('')` in variant_display_test.
- Typing `kind:translation` or `-kind:translated` commits nothing on
  purpose (unknown kind word / no exclude form for a single-valued
  server filter), but Enter then looked broken: no chip, no change, the
  words still sitting in the field. The bar now shows one hint line
  under the input naming the four kinds and saying a kind cannot be
  excluded; any edit retires it. Smallest signal that fits the widget —
  a Text in the column it already builds, no dialog, no snackbar, no
  store state.

Verified: `scripts/verify.sh app` green (788 tests, +2; analyze 0 issues).
Mutation probes went red as expected: disabling the hint branch (both new
tests), removing the clear-on-edit (the "hint gone" arm), dropping the `-`
strip in the predicate (the negated-kind test), and turning the parser's
wire-name comparison into `startsWith` (the empty-input case that used to
sit behind the deleted guard).

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 40.0% (4 of 10)
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.3% (380 of 416)
lib/presentation/middleware/offline_epics.dart 98.4% (60 of 61)
lib/presentation/state/reducers.dart 100.0% (14 of 14)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/code_point_compare.dart 100.0% (8 of 8)
lib/core/languages.dart 100.0% (8 of 8)
lib/core/reading_order.dart 100.0% (22 of 22)
lib/core/title_display.dart 90.0% (9 of 10)
lib/core/variant_display.dart 100.0% (22 of 22)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 53.7% (36 of 67)
lib/data/models/server_models.dart 100.0% (8 of 8)
lib/data/models/server_models.g.dart 47.5% (19 of 40)
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 92.2% (107 of 116)
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/server_epics.dart 100.0% (33 of 33)
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/server_actions.dart 72.7% (8 of 11)
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 93.1% (95 of 102)
lib/presentation/state/reducers/entity_reducer.dart 98.8% (82 of 83)
lib/presentation/state/reducers/library_reducer.dart 100.0% (122 of 122)
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% (20 of 20)
lib/presentation/state/reducers/server_reducer.dart 100.0% (47 of 47)
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 0.0% (0 of 61)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/reader/reader_page.dart 89.4% (236 of 264)
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 29.0% (111 of 383)
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/editor_pane.dart 69.8% (37 of 53)
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/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 80.1% (173 of 216)
lib/presentation/pages/detail/variant_tabs_panel.dart 97.2% (239 of 246)
lib/data/offline/filesystem_offline_store.dart 94.1% (144 of 153)
lib/core/offline_paths.dart 100.0% (3 of 3)
lib/presentation/pages/offline/offline_page.dart 98.2% (164 of 167)
lib/app/app.dart 67.1% (47 of 70)
lib/presentation/pages/settings/settings_page.dart 99.4% (156 of 157)
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 93.1% (215 of 231)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 84.1% (207 of 246)
lib/presentation/pages/settings/server_sections.dart 100.0% (154 of 154)
lib/presentation/widgets/model_combo_field.dart 69.0% (100 of 145)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 79.9% (7694 of 9631)

<!-- 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 | 40.0% (4 of 10) | | 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.3% (380 of 416) | | lib/presentation/middleware/offline_epics.dart | 98.4% (60 of 61) | | lib/presentation/state/reducers.dart | 100.0% (14 of 14) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/code_point_compare.dart | 100.0% (8 of 8) | | lib/core/languages.dart | 100.0% (8 of 8) | | lib/core/reading_order.dart | 100.0% (22 of 22) | | lib/core/title_display.dart | 90.0% (9 of 10) | | lib/core/variant_display.dart | 100.0% (22 of 22) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 53.7% (36 of 67) | | lib/data/models/server_models.dart | 100.0% (8 of 8) | | lib/data/models/server_models.g.dart | 47.5% (19 of 40) | | 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 | 92.2% (107 of 116) | | 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/server_epics.dart | 100.0% (33 of 33) | | 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/server_actions.dart | 72.7% (8 of 11) | | 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 | 93.1% (95 of 102) | | lib/presentation/state/reducers/entity_reducer.dart | 98.8% (82 of 83) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (122 of 122) | | 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% (20 of 20) | | lib/presentation/state/reducers/server_reducer.dart | 100.0% (47 of 47) | | 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 | 0.0% (0 of 61) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/reader/reader_page.dart | 89.4% (236 of 264) | | 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 | 29.0% (111 of 383) | | 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/editor_pane.dart | 69.8% (37 of 53) | | 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/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 | 80.1% (173 of 216) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 97.2% (239 of 246) | | lib/data/offline/filesystem_offline_store.dart | 94.1% (144 of 153) | | lib/core/offline_paths.dart | 100.0% (3 of 3) | | lib/presentation/pages/offline/offline_page.dart | 98.2% (164 of 167) | | lib/app/app.dart | 67.1% (47 of 70) | | lib/presentation/pages/settings/settings_page.dart | 99.4% (156 of 157) | | 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 | 93.1% (215 of 231) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 84.1% (207 of 246) | | lib/presentation/pages/settings/server_sections.dart | 100.0% (154 of 154) | | lib/presentation/widgets/model_combo_field.dart | 69.0% (100 of 145) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 79.9% (7694 of 9631)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A filter token done properly~ The wire vocabulary lives in exactly one place, the parser drops what it can't honour instead of inventing lies, and a dead Enter press gets an explanation instead of silence. I traced every layer against the backend source and I'm delightfully cornered — nothing to dissect~ ♡

Verdict: ✅ Looks good to me~

What I actually verified (not just read!):

  • Wire names, against the real server code. SearchEndpoints.cs:46-54 parses dto.VariantKind through DoujinEndpoints.TryParseDefinedEnum — Enum.TryParse(ignoreCase: true) + Enum.IsDefined over the C# VariantKind { Original, Translated, Reedition, Other } (ApplicationCore/Enums/VariantKind.cs). So variantKindWireName emitting Reedition (not the label Re-edition) is exactly right, and the punctuated-spelling acceptance (kind:re-edition) via _normalizeKind is a genuinely elegant little touch — one normalizer serves parse, suggest, and the chip-label fallback.
  • The reducer's replace rule. Second kind token evicts the first (library_reducer.dart:196), other tokens untouched — honest single-value semantics instead of stacked chips promising an OR the server can't honour. Mutation-probed: removed the kept filter → exactly the 2 replace-arm tests went red; restored → green.
  • The negation guard. -kind:… returning null instead of falling through to the stray negative-tag fallback. Mutation-probed: deleted the guard → a negated kind commits nothing red; restored → green.
  • The epic plumbing. variantKind: _variantKindFilter(lib.tokens) rides both the fresh search and load-more paging. Mutation-probed: hardcoded null → both carries the variant kind and paging … keeps the variant kind red; restored → green.
  • The hint. Disabled the else if branch → both an unknown kind word explains itself and a negated kind gets the same hint red; restored → green. The clear-on-edit arm is pinned too.
  • The no-mirror decision. _applyTokens breaks even on variantKind and the epic reads the token list directly — a deviation from the legacy-mirror pattern, yes, but documented in both places and safe: the reducer guarantees at most one kind token, so the epic's first-match scan can't lie. SetFilterTokensAction has no production callers that could smuggle in a duplicate (grep: tests only). This is the right amount of architecture~
  • Tests: all 5 PR-touched suites 129/129 locally (--concurrency=1 — the sandbox is being squeezed by two sibling review sessions, so parallel DartWorkers were dying, not your code), flutter analyze 0 issues, CI coverage bot green for head 6abd446 with variant_display.dart 100% (22/22) and library_reducer.dart 100% (122/122). The 92.2% on filter_token.dart is the pre-existing character/series suggestion arms, untouched by this PR.
  • Housekeeping: BACKLOG item removed (shipped, correct), README line updated, ADR 0016 citation accurate (flat AND-combined query model, single-value VariantKind? at line 53 of the ADR).

💡 Little ideas (non-blocking)~

  1. smart_filter_bar.dart:526 — _kVariantKindHint hand-enumerates the four kinds in prose. The label and wire maps are compiler-forced switches over the enum, but the hint string isn't: a hypothetical fifth kind would leave the hint silently stale. Deriving the list from variantKindsMatching('') (it already lives next to the vocabulary it owns) would make all three drift-proof the same way. Purely a future-proofing nicety~
  2. smart_filter_bar.dart:533 — _looksLikeVariantKindInput mirrors the parser's two drop points by hand. At two cases with both sides mutation-pinned this is fine; if the parser ever grows a third silent drop, give the predicate a twin then, not now.

✅ What I liked~

  • The deleted if (normalized.isEmpty) return null guard — and the commit message proving why it was unreachable, with the startsWith mutant red on parseVariantKind(''). Dead code removed with evidence, not vibes~
  • Suggestions gated from three characters (kin) so a bare k still belongs to the entity matches, and kinds-first ordering pinned against a tag literally named kindness. Someone thought about the crowding problem~
  • The chip fallback for a value that names no kind (Kind: Omnibus) — defensive without being paranoid, and tested.

Fufu~ ship it. ♡


Automated review by Jibril · 2026-09-06
CI/CD: passed for head SHA 6abd446 (coverage bot, full app suite green) · Local checks: analyze 0 issues · 129/129 across the 5 touched suites · 4/4 mutation probes red

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A filter token done *properly*~ The wire vocabulary lives in exactly one place, the parser drops what it can't honour instead of inventing lies, and a dead Enter press gets an explanation instead of silence. I traced every layer against the backend source and I'm delightfully cornered — nothing to dissect~ ♡ ### Verdict: ✅ Looks good to me~ **What I actually verified (not just read!):** - **Wire names, against the real server code.** `SearchEndpoints.cs:46-54` parses `dto.VariantKind` through `DoujinEndpoints.TryParseDefinedEnum` — `Enum.TryParse(ignoreCase: true)` + `Enum.IsDefined` over the C# `VariantKind { Original, Translated, Reedition, Other }` (`ApplicationCore/Enums/VariantKind.cs`). So `variantKindWireName` emitting `Reedition` (not the label `Re-edition`) is exactly right, and the punctuated-spelling acceptance (`kind:re-edition`) via `_normalizeKind` is a genuinely elegant little touch — one normalizer serves parse, suggest, and the chip-label fallback. - **The reducer's replace rule.** Second kind token evicts the first (`library_reducer.dart:196`), other tokens untouched — honest single-value semantics instead of stacked chips promising an OR the server can't honour. **Mutation-probed**: removed the `kept` filter → exactly the 2 replace-arm tests went red; restored → green. - **The negation guard.** `-kind:…` returning null instead of falling through to the stray negative-tag fallback. **Mutation-probed**: deleted the guard → `a negated kind commits nothing` red; restored → green. - **The epic plumbing.** `variantKind: _variantKindFilter(lib.tokens)` rides both the fresh search and load-more paging. **Mutation-probed**: hardcoded null → both `carries the variant kind` and `paging … keeps the variant kind` red; restored → green. - **The hint.** Disabled the `else if` branch → both `an unknown kind word explains itself` and `a negated kind gets the same hint` red; restored → green. The clear-on-edit arm is pinned too. - **The no-mirror decision.** `_applyTokens` breaks even on `variantKind` and the epic reads the token list directly — a deviation from the legacy-mirror pattern, yes, but documented in both places *and* safe: the reducer guarantees at most one kind token, so the epic's first-match scan can't lie. `SetFilterTokensAction` has no production callers that could smuggle in a duplicate (grep: tests only). This is the right amount of architecture~ - **Tests:** all 5 PR-touched suites 129/129 locally (`--concurrency=1` — the sandbox is being squeezed by two sibling review sessions, so parallel DartWorkers were dying, not your code), `flutter analyze` 0 issues, CI coverage bot green for head 6abd446 with `variant_display.dart` 100% (22/22) and `library_reducer.dart` 100% (122/122). The 92.2% on `filter_token.dart` is the pre-existing character/series suggestion arms, untouched by this PR. - **Housekeeping:** BACKLOG item removed (shipped, correct), README line updated, ADR 0016 citation accurate (flat AND-combined query model, single-value `VariantKind?` at line 53 of the ADR). #### 💡 Little ideas (non-blocking)~ 1. **smart_filter_bar.dart:526** — `_kVariantKindHint` hand-enumerates the four kinds in prose. The label and wire maps are compiler-forced switches over the enum, but the hint string isn't: a hypothetical fifth kind would leave the hint silently stale. Deriving the list from `variantKindsMatching('')` (it already lives next to the vocabulary it owns) would make all three drift-proof the same way. Purely a future-proofing nicety~ 2. **smart_filter_bar.dart:533** — `_looksLikeVariantKindInput` mirrors the parser's two drop points by hand. At two cases with both sides mutation-pinned this is fine; if the parser ever grows a third silent drop, give the predicate a twin then, not now. #### ✅ What I liked~ - The deleted `if (normalized.isEmpty) return null` guard — and the commit message *proving* why it was unreachable, with the `startsWith` mutant red on `parseVariantKind('')`. Dead code removed with evidence, not vibes~ - Suggestions gated from three characters (`kin`) so a bare `k` still belongs to the entity matches, and kinds-first ordering pinned against a tag literally named `kindness`. Someone thought about the crowding problem~ - The chip fallback for a value that names no kind (`Kind: Omnibus`) — defensive without being paranoid, and tested. Fufu~ ship it. ♡ --- *Automated review by Jibril · 2026-09-06* *CI/CD: passed for head SHA 6abd446 (coverage bot, full app suite green) · Local checks: analyze 0 issues · 129/129 across the 5 touched suites · 4/4 mutation probes red*
bjoern merged commit 08240dc70d into main 2026-09-07 12:21:48 +02:00
bjoern deleted branch feat/library-variant-kind-filter 2026-09-07 12:21:48 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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!103
No description provided.