feat: the assistant proposes a whole set of associations on one approval card #102

Merged
bjoern merged 2 commits from feat/assistant-batched-approvals into main 2026-09-07 12:22:13 +02:00
Member

Setting up a doujin through the assistant meant approving one card per tag. The comprehensive-setup skill asks for 15 to 25 of them, which made it unusable. Now the assistant proposes all associations — tags, people with roles, circles, characters, series — on one card the user reads and approves or denies once.

What's in

  • Shape: an associations array on the existing assign / remove actions rather than new batch actions, so the action enum stays as it is and the single-item fields keep working as the one-element case. The array wins when both forms are sent, and that precedence is now pinned by a test rather than only documented.
  • Everything is resolved before the gate. Unknown ids, unknown entity types, role-less people and malformed entries are listed up front and the whole call is rejected without raising a card; members already in the requested state are skipped rather than failed. Entity listings are fetched once per type, so a 20-tag batch lists tags once.
  • The card reads tag: + elf, person: + Alice as artist — names, never ids. On approval the writes run sequentially, continue past an individual failure, and return a per-item applied, failed and skipped summary. Denial writes nothing.
  • The comprehensive-setup skill now collects ids and assigns them in one call; the prompt's batching rule points at the array.
  • ADR 0024 amended: approval granularity is the tool call, not the repository call. The entity_write create action already bundled name, description and aliases under one card, which is the precedent that makes this consistent rather than a new exception.

Verification

  • scripts/verify.sh app: green, 773 tests; the write-tool suite went from 41 to 52.
  • Directional probes, all red: the pre-gate rejection, per-item failure containment, the name-listing cache, the schema property itself (deleting it used to keep everything green while the model would never be offered the batch), and the array-over-scalar precedence.
  • Covered: a mixed six-item batch (one card, exact lines, nothing written before approval, ordered writes), deny writes nothing, partial failure, all-writes-fail, unknown ids before the gate, already-assigned skipped, a remove batch, and the schema payload.
  • Green with the rest of the batch on a six-branch integration tree.
  • Not exercised: a live model actually choosing the array over 25 parallel single calls — the schema, skill and prompt all push toward it, but that is model behaviour I cannot verify offline. No widget test for a very tall card.

Decisions to revisit

Unknown ids reject the whole call so the user never sees a half-applied set; skips are the opposite and are tolerated, so a retry after a partial failure stays idempotent. If you would rather have distinct assign_many / remove_many actions for a clearer tool listing, that is the alternative I weighed and rejected.

🤖 Generated with Claude Code

Setting up a doujin through the assistant meant approving one card per tag. The comprehensive-setup skill asks for 15 to 25 of them, which made it unusable. Now the assistant proposes all associations — tags, people with roles, circles, characters, series — on one card the user reads and approves or denies once. ## What's in - **Shape**: an `associations` array on the existing `assign` / `remove` actions rather than new batch actions, so the action enum stays as it is and the single-item fields keep working as the one-element case. The array wins when both forms are sent, and that precedence is now pinned by a test rather than only documented. - **Everything is resolved before the gate.** Unknown ids, unknown entity types, role-less people and malformed entries are listed up front and the whole call is rejected without raising a card; members already in the requested state are skipped rather than failed. Entity listings are fetched once per type, so a 20-tag batch lists tags once. - **The card** reads `tag: + elf`, `person: + Alice as artist` — names, never ids. On approval the writes run sequentially, continue past an individual failure, and return a per-item applied, failed and skipped summary. Denial writes nothing. - The comprehensive-setup skill now collects ids and assigns them in one call; the prompt's batching rule points at the array. - **ADR 0024 amended**: approval granularity is the tool call, not the repository call. The `entity_write` create action already bundled name, description and aliases under one card, which is the precedent that makes this consistent rather than a new exception. ## Verification - `scripts/verify.sh app`: green, 773 tests; the write-tool suite went from 41 to 52. - Directional probes, all red: the pre-gate rejection, per-item failure containment, the name-listing cache, the schema property itself (deleting it used to keep everything green while the model would never be offered the batch), and the array-over-scalar precedence. - Covered: a mixed six-item batch (one card, exact lines, nothing written before approval, ordered writes), deny writes nothing, partial failure, all-writes-fail, unknown ids before the gate, already-assigned skipped, a remove batch, and the schema payload. - Green with the rest of the batch on a six-branch integration tree. - **Not exercised**: a live model actually choosing the array over 25 parallel single calls — the schema, skill and prompt all push toward it, but that is model behaviour I cannot verify offline. No widget test for a very tall card. ## Decisions to revisit Unknown ids reject the whole call so the user never sees a half-applied set; skips are the opposite and are tolerated, so a retry after a partial failure stays idempotent. If you would rather have distinct `assign_many` / `remove_many` actions for a clearer tool listing, that is the alternative I weighed and rejected. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The comprehensive-setup playbook asks for 15–25 tags plus creators,
characters and a series, and every `doujin_write` assign raised its own
approval card, so cataloguing one doujin meant clicking through ~25 of
them. `assign`/`remove` now take an `associations` array of
`{entity_type, entity_id, role}` items instead of gaining new actions:
the action enum stays at eight, one property carries the batch, and the
existing single-item fields keep working as the one-element case.

Every id is resolved to a display NAME before the gate — unknown ids,
unknown entity types and role-less people are listed up front and the
whole call is rejected without ever raising a card. Members already in
the requested state are skipped rather than failed. What survives rides
on ONE ChangeRequest whose lines read `tag: + elf` / `person: + Alice as
artist`; on approval it executes sequentially and continues past an
individual failure, and the result reports applied / failed / skipped
per item (and says plainly when nothing was written). Denial writes
nothing. Entity listings are now fetched once per type instead of once
per id, so a 20-tag batch lists the tags once.

`comprehensive-doujin-setup.md` collects ids through phases 4–6 and
assigns them in a single call (new phase 7); the prompt's
parallel-batching rule points at the array so the model does not fan out
into one card per tag instead. The approval card already rendered a
multi-line change list, so the UI is untouched.

Verified: `scripts/verify.sh app` (771 tests, analyze 0 issues). Mutation
probes on all six new guards — the pre-gate error return, per-item
failure containment, the name-listing cache, the singular/plural card
title, the "nothing was written" summary and the already-assigned skip —
each turned `agent_write_tools_test.dart` red.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
test: pin the associations schema and its precedence; report unusable entries
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 4m56s
3eb3c8faa7
Review of the batched-approval branch found three things the tests let
slide. The `associations` **schema property** was unpinned: every test
built `DoujinWriteParams` directly, so deleting the property from
`parametersSchema` kept the suite green while the model would never be
offered the batch at all. A new test reads the property and asserts its
type, that its description carries the batch and the documented
precedence, and that `items` declares entity_type, entity_id and role
with the first two required. The precedence itself — the array beating
the scalar entity_type/entity_id — was documented in two places and
pinned nowhere; one test now sends BOTH forms in one call (the scalar
pair being a resolvable, unassigned tag, so it would show up on the card
and in the repository if it still counted) and asserts the card carries
exactly the two array lines and only those two writes land.

`_parseAssociations` silently dropped a non-object entry although its own
comment and the setup skill promise unusable entries are reported before
the card — a batch of 25 could quietly become 24 with the user approving
the shorter list. Such an entry now becomes an empty spec, which the
existing pre-gate check rejects with "an entry is missing entity_type or
entity_id"; the parse test executes the parsed params and asserts that.

ADR 0024 said every write pauses on the gate and renders its own card, so
the branch contradicted a binding ADR: an amendment records that approval
granularity is the tool call, that a batched assign/remove lists every
member (display name, plus the role for people) on one card, and that
`entity_write`'s `create` already bundled name, description and aliases
the same way.

Verified: `scripts/verify.sh app` (773 tests, analyze 0 issues). Three
mutation probes, each turning exactly the intended test red and nothing
else: deleting the `associations` schema property, making the scalar
fields win over a non-empty array, and restoring the `if (item is Map)`
drop of malformed entries.

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 86.4% (319 of 369)
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% (376 of 412)
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/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 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/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% (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% (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/core/variant_display.dart 100.0% (5 of 5)
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 79.7% (181 of 227)
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: 80.4% (7747 of 9641)

<!-- 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 | 86.4% (319 of 369) | | 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% (376 of 412) | | 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/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 | 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/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% (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% (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/core/variant_display.dart | 100.0% (5 of 5) | | 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 | 79.7% (181 of 227) | | 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: 80.4% (7747 of 9641)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Twenty-five approval cards collapsing into ONE? I read the whole diff twice just to watch the validation pipeline flow again~ ♡ Resolve every id to a name before the gate, reject the whole call on anything unusable, skip what's already there, and only then bother the human — that is exactly the shape this needed. And you kept the action enum untouched by making the array win over the scalar trio. Delicious.

Verdict: ✅ Looks good to me~

I hunted for a reason to bite and came back hungry. Everything I probed held:

Verified myself (mutation probes, each RED on exactly its own test, restored byte-identical — working tree pristine at 3eb3c8f afterwards):

  • Disabled the _entityNames memoization → listTagsCalls Expected 1 / Actual 2. The 20-tags-one-listing claim is real, not decoration~
  • Disabled the pre-gate errors.isNotEmpty rejection → the unknown-ids test fails. No card can ever rise from a broken batch.
  • Rethrew in the per-item catch → the partial-failure containment test fails. One dead endpoint can't take the batch down with it.

Architecture: the ADR 0024 amendment's precedent check is honest — I read entity_write's _create: it already bundles name+description+aliases on one card and continues past alias failures with a per-item report. This PR is that pattern generalized, not a new exception. Approval granularity = the tool call is the right invariant, and "nothing reaches the repository without a card the user approved" survives intact.

The tricky edges, all correct:

  • _parseAssociations keeps a malformed entry as an empty spec instead of dropping it — the approved batch can never be quietly shorter than what the model asked for. That's the kind of paranoia I approve of ♡
  • Batch remove resolves names off the doujin itself, so a stale listing can't put a wrong name on a card; assign resolves against live listings and rejects unknown ids pre-gate.
  • Person skip/remove match on role, not just id — same semantics as the scalar path.
  • onExecuted fires once per call and only when something actually landed; all-fail reports "NOTHING was written" instead of masquerading.
  • The −/+ card lines, label summaries, and singular/plural card titles all match the file's existing conventions.

Coverage: every genuinely NEW branch has a directional test — mixed six-item batch (exact card lines, ordered writes, listTagsCalls == 1), deny, partial failure, all-fail, unknown ids, invalid entries, skips (both directions, batch and scalar), remove batch, schema payload incl. the array-over-scalar precedence, and the schema property itself so it can't silently vanish from the model's view. The planned.isEmpty arm is exercised by the scalar no-op test through the new unified code. Suite 41→52, scripts/verify.sh app green with 773/773 locally — matching your PR body digit for digit.

💡 Little ideas (non-blocking)~

  1. doujin_write_tool.dart:466-471 — the reworded "require entity_type and entity_id, or an associations array…" error is the one new string no test pins (the old wording was unpinned on main too, so this is inherited, not introduced). One tiny expect(contains('or an associations array')) and it can never drift silently.
  2. agent_write_tools_test.dart — a batch where every member is already assigned (e.g. t-1 twice) would pin the all-skipped batch summary (Nothing to change — …; …, zero cards) distinctly from the scalar no-op. Pure pinning nicety.

✅ What I liked~

  • The skill rewrite teaches the workflow ("collect ids, assign once in phase 7") instead of just documenting a parameter — and Never issue one assign per tag is pinned in agent_skills_test so it can't regress.
  • The system-prompt edit fixes a real tension I'd have flagged otherwise: the PARALLEL-rounds rule was actively pushing the model toward 25 cards; now it points at the array instead. And assistant_agent_test pins the exact wording under the byte-identical-prompt constraint. Sharp~
  • The "Decisions to revisit" and "Not exercised" sections — declaring reject-vs-skip asymmetry and the live-model limitation up front. I do love an author who shows me where the bodies aren't buried ♪

Automated review by Jibril · 2026-09-06
CI/CD: passed for head SHA 3eb3c8f (coverage bot #7196, doujin_write_tool.dart 86.4%) · Local checks: scripts/verify.sh app green — analyze clean, 773/773; 3 mutation probes RED→restored→green

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Twenty-five approval cards collapsing into ONE? I read the whole diff twice just to watch the validation pipeline flow again~ ♡ Resolve every id to a **name** before the gate, reject the whole call on anything unusable, skip what's already there, and only then bother the human — that is exactly the shape this needed. And you kept the action enum untouched by making the array win over the scalar trio. *Delicious.* ### Verdict: ✅ Looks good to me~ I hunted for a reason to bite and came back hungry. Everything I probed held: **Verified myself (mutation probes, each RED on exactly its own test, restored byte-identical — working tree pristine at 3eb3c8f afterwards):** - Disabled the `_entityNames` memoization → `listTagsCalls` Expected 1 / Actual 2. The 20-tags-one-listing claim is real, not decoration~ - Disabled the pre-gate `errors.isNotEmpty` rejection → the unknown-ids test fails. No card can ever rise from a broken batch. - Rethrew in the per-item catch → the partial-failure containment test fails. One dead endpoint can't take the batch down with it. **Architecture:** the ADR 0024 amendment's precedent check is honest — I read `entity_write`'s `_create`: it already bundles name+description+aliases on one card and continues past alias failures with a per-item report. This PR is that pattern generalized, not a new exception. Approval granularity = the tool call is the right invariant, and "nothing reaches the repository without a card the user approved" survives intact. **The tricky edges, all correct:** - `_parseAssociations` keeps a malformed entry as an empty spec instead of dropping it — the approved batch can never be quietly shorter than what the model asked for. That's the kind of paranoia I *approve* of ♡ - Batch remove resolves names off the doujin itself, so a stale listing can't put a wrong name on a card; assign resolves against live listings and rejects unknown ids pre-gate. - Person skip/remove match on role, not just id — same semantics as the scalar path. - `onExecuted` fires once per call and only when something actually landed; all-fail reports "NOTHING was written" instead of masquerading. - The `−`/`+` card lines, `label` summaries, and singular/plural card titles all match the file's existing conventions. **Coverage:** every genuinely NEW branch has a directional test — mixed six-item batch (exact card lines, ordered writes, `listTagsCalls == 1`), deny, partial failure, all-fail, unknown ids, invalid entries, skips (both directions, batch and scalar), remove batch, schema payload incl. the array-over-scalar precedence, and the schema property itself so it can't silently vanish from the model's view. The `planned.isEmpty` arm is exercised by the scalar no-op test through the new unified code. Suite 41→52, `scripts/verify.sh app` green with **773/773** locally — matching your PR body digit for digit. #### 💡 Little ideas (non-blocking)~ 1. **doujin_write_tool.dart:466-471** — the reworded "require entity_type and entity_id, or an associations array…" error is the one new string no test pins (the old wording was unpinned on main too, so this is inherited, not introduced). One tiny `expect(contains('or an associations array'))` and it can never drift silently. 2. **agent_write_tools_test.dart** — a batch where *every* member is already assigned (e.g. `t-1` twice) would pin the all-skipped batch summary (`Nothing to change — …; …`, zero cards) distinctly from the scalar no-op. Pure pinning nicety. #### ✅ What I liked~ - The skill rewrite teaches the *workflow* ("collect ids, assign once in phase 7") instead of just documenting a parameter — and `Never issue one assign per tag` is pinned in `agent_skills_test` so it can't regress. - The system-prompt edit fixes a real tension I'd have flagged otherwise: the PARALLEL-rounds rule was actively pushing the model toward 25 cards; now it points at the array instead. And `assistant_agent_test` pins the exact wording under the byte-identical-prompt constraint. Sharp~ - The "Decisions to revisit" and "Not exercised" sections — declaring reject-vs-skip asymmetry and the live-model limitation up front. I do love an author who shows me where the bodies *aren't* buried ♪ --- *Automated review by Jibril · 2026-09-06* *CI/CD: passed for head SHA 3eb3c8f (coverage bot #7196, doujin_write_tool.dart 86.4%) · Local checks: scripts/verify.sh app green — analyze clean, 773/773; 3 mutation probes RED→restored→green*
bjoern merged commit 27868280ef into main 2026-09-07 12:22:13 +02:00
bjoern deleted branch feat/assistant-batched-approvals 2026-09-07 12:22:13 +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!102
No description provided.