feat: the assistant can delete and reorder pages and edit variant fields #170
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/the-assistant-can-write-pages-and-variant-fields"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's in
doujin_writegainsdelete_pages,reorder_pagesandupdate_variant; no new parameters were needed, only widened schema descriptions.delete_pagesstates how many pages go, that they and every image no other page still uses are destroyed for good, what the variant is left with (including that an emptied variant can only be refilled in the editor), and that the cover goes when the cover page is in the selection.reorder_pagesdemands one ordered list naming every page (a partial list would push unnamed pages behind the named ones) and — because reading order groups by chapter — prints the order the pages will actually read after the chapters regroup them, not an echo of the request. Both say that the page numbers the model just read are now stale.update_variantpre-empts the server's rules instead of spending an approval on a failing write: only values that really differ are sent, promoting a variant names the edition that stops being default, un-setting the only default is refused up front with ADR 0004's conflict rule, and an emptydisplay_nameis refused with the real reason (VariantService.UpdateAsyncskips only nulls, so""would be stored as a blank label the editor cannot repair).move_pages_to_chapterinto_resolvePages, so "page 7" always means the pageget_doujinnumbered 7 across all three page actions.move_pages_to_chapterkeeps its previous behaviour exactly.docs/adr/0024-in-app-ai-assistant.mdgains "Amendment (2026-09-19): page and variant-field writes", narrowing the deferral to page uploads + doujin create/delete and recording the four decisions (mirror the server rather than invent a last-page refusal; pre-empt ADR 0004's only-default conflict and the un-clearable display name; sort order is not reading order, so the card prints the post-regroup order; a write that invalidates the numbers the model just read says so). Two stale passages in the same ADR are fixed.uploadPages/uploadZiptake local file paths from a picker the model cannot reach, so the tool description, the prompt, the README bullet and the backlog now record it as a boundary rather than a gap. No new endpoint, no new tunable, no EF migration.Verification
Executed:
scripts/verify.sh appfrom the worktree, fresh, to completion —flutter pub get, a cleanbuild_runnercodegen + committed-output check,flutter analyze,flutter test. Result:+1260: All tests passed!and== verify OK (app) ==(the script runs underset -euo pipefail, so analyze and the codegen staleness check passing is what let the run reach that line).scripts/verify.sh backendwas not run — this branch touches no backend file (git diff --name-only origin/main..HEAD:README.md,app/lib/agent/system_prompt.dart,app/lib/agent/tools/doujin_write_tool.dart,app/test/agent_write_tools_test.dart,app/test/assistant_agent_test.dart,docs/BACKLOG.md,docs/adr/0024-in-app-ai-assistant.md).Layers exercised: the agent tool layer only —
DoujinWriteToolagainst a fakeDoujinRepository(114 tests inagent_write_tools_test.dart), and the prompt builder (14 inassistant_agent_test.dart). Those tests assert the approval card's exact lines, that nothing is written before the gate, the exact arguments handed to the repository port, the result text, and the refresh signal. Not exercised: REST, use cases, persistence, and the UI — no widget test covers these actions, the repository implementation (DoujinApiRepository) is faked, andPageService.ReorderAsync/VariantService.UpdateAsyncbehaviour is asserted only in the tool's expectations. Server behaviour this branch reasons about (reorder assigns sort order by list index and pushes unlisted pages behind;ImageCleanupHelperkeeps shared images;UpdateAsyncskips only nulls) was confirmed by reading the backend source, not by running it. Nothing ran in the real app against a live server.Mutation probes (each applied to the committed tree one at a time, tests run, then
git checkout --to restore; the tree was clean and equal to HEAD after every one):_resolvePagesresolving against rawvariant.pagesinstead of reading order → red across every page test._deletePages,_reorderPages,_updateVariant→ red on the three "refusal writes nothing" tests.pages.take(1)) → red on the batch arm of "warns when the cover page goes"; widened to warn for any deleted page → red on its control arm.remaining == 0arm flipped → red on "says an emptied variant stays empty".breakafter a failed delete/move in both page loops → red on both partial-failure tests (both fixtures fail the first page, so the loop must carry on)._pagesWord(total - applied.length)→_pagesWord(remaining)→ red on "reports a partial failure" (the only fixture where the two formulas differ).onExecutedcalled unconditionally → red on "reports that nothing landed at all".regroupedforced true and forced false → red on opposite tests;requested.reversedsent → red on both ordering tests; the missing-page, duplicate-reference and both-lists guards each removed → red._orderLinecollapse disabled → red; collapsing 2-runs → red; letting descending runs collapse → red (a descending run must stay spelled out).update_variant: the promotion clause made unconditional → red on two tests (the demotion inverse is pinned); display-name diffing removed → red; the only-default guard removed → red; the promotion line collapsed to the "someone loses it" form → red; the empty-name guard disabled, and widened to refuse any name → red.system_promptreverted to "the pages and their images go for good", and the "never emptied" clause removed → red inassistant_agent_test.dart.Follow-ups
docs/BACKLOG.md— the existing Assistant item "Tool gaps: page writes and variant fields" was retitled "Tool gaps: page writes" and trimmed in place; what remains on it is the per-page write pattern (move_pages_to_chapteranddelete_pagesare one request per page, unbounded; a bulk page→chapter route or aDOUJIN_MANAGER_*cap is the real fix).docs/BACKLOG.md— the same item now records the backend gap behind the empty-display-name refusal:PUT /api/variants/{id}has no "clear" flag, so a display name can be set or replaced but never cleared (theclearCoverImage/clearRatingpattern doujins already use is the fix).create_variantstill forwards an emptydisplay_name, and the prompt'sdelete_variantclause carries the same image overstatement this PR fixed fordelete_pages. Both are pre-existing from #163 and outside this branch's findings.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Delete, reorder, and variant-field writes, all behind the one approval gate, with a single shared page resolver so "page 7" means the same page in every action~ This is exactly the shape I love to see. I read every changed file end to end, cross-checked every server-behaviour claim against the backend source (not the PR text), ran the suite myself, and mutation-probed the load-bearing resolver. Here is what I found ♪
Verdict: ✅ Looks good to me~
Backend claims — independently verified against the server source at merge-base:
PageService.ReorderAsync(PageService.cs:99-111): listed pages get list position as sort order, unlisted pages are pushed behind (nextUnspecified = pageIds.Count; ... SortOrder = nextUnspecified++). The "partial list moves pages you never named → demand the whole order" design is exactly right.VariantService.UpdateAsyncskips only nulls (if (displayName is not null) ..., VariantService.cs:90) — the empty-display-name refusal is pre-empting a real blank-label trap, and the un-set-only-default Conflict (cs:98-106) plus the promote-clears-old-default same-write behaviour (cs:93-97, :160-166) match the card lines precisely.DeletePageUseCase→ImageCleanupHelper.DeleteOrphanedImagesAsync, which keeps any image a surviving page still references. The "every image no other page still uses" wording is accurate, and consistent with delete_variant's.ON DELETE SET NULL(snapshot :718), so dangling chapter ids are unreachable and_deletePages' remaining-count arithmetic holds on clean data.Verification performed:
flutter analyze→ No issues found! · fullflutter test→ +1260: All tests passed! (matches the PR body's claim exactly; agent_write_tools_test.dart = 114 tests, arithmetic checks out)._resolvePages' reading order with raw sort order in the committed tree → 19 tests red (across move/delete/reorder), tree restored byte-clean afterwards. The reading-order contract is pinned by real teeth, not tautology._orderLine, the regroup-vs-plain control, both no-op short circuits (identical list / chapters-undo-it), duplicate + partial-list + both-lists guards, partial-failure counts (total - applied.length, first-page-fails fixtures), cover warning batch arm + cry-wolf control, refusal-writes-nothing for all three actions. The fixtures deliberately disagree on raw-vs-reading order (pg-cfirst, notpg-a) — the one mistake that would delete the wrong page is the one this suite cannot miss._movePagesToChapterconfirms the resolver extraction is behavior-preserving;parses the page selection as an orderpins that order is preserved for reorder.✅ What I liked~
_orderAfterReordersimulating post-write reading order and refusing to card a request the chapters undo. That's the kind of honesty in approval cards I get possessive about, and it's done beautifully.One tiny non-blocking note: the prompt's
delete_variantclause still says the images go "for good" unqualified, while delete_pages now says "every image no other page still uses" — the overstatement this PR fixed for delete_pages still lives in the delete_variant prompt sentence. You already flagged it as out-of-scope pre-existing in the body, so just leaving it written here for the record~Automated review by Jibril · 2026-09-19
CI/CD: absent for head
3a18db6(no runs/bot comments at review time) · Local checks: analyze clean, +1260 tests passed, 1 mutation probe (19 red on resolver→raw-order), backend claims traced to source