feat: page organizer 2/3 — bulk MovePages and page thumbnails #51

Merged
bjoern merged 2 commits from feat/page-organizer-usecases into main 2026-08-13 06:23:57 +02:00
Member

Slice 2 of the wizard step-2 redo (multi-select page organizer; slice 1 is Kagaku.UI PR #7, slice 3 brings the UI): the use-case and host layer the organizer will call. No UI changes — the wizard works exactly as today, so this ships green on its own with no submodule bump.

What's in

  • MovePages (UseCases/Pages) — the multi-select move: a selection lands at the end of the target chapter in selection order. Name collisions are prechecked all-or-nothing before anything moves — against the target's raw/ folder and between the incoming pages themselves (two chapters can each hold a p_01.png; both arriving at once would collide on arrival). The precheck needed a new port method, IPageImageStore.ExistsAsync, implemented in FileSystemPageImageStore and the shared fake. After the precheck each page moves file-then-row (MovePage's pattern), so a mid-run filesystem race leaves earlier pages fully moved and later ones untouched — a row and its image are never split. Pages already in the target are skipped (their names never reach the precheck), duplicate ids deduped, cross-project refused, imageless pages move without touching the image store.
  • GET /projects/{id}/pages/{id}/thumb (Server) — the raw scan downscaled to a fixed 320px longest side (~2× a grid tile, HiDPI-sharp) through the existing IPageImageRenderer, same guards/gate/caching as /raw; an undecodable stored file is a 404, matching /raw's posture. One fixed size = one cacheable URL, nothing to validate; no disk cache — raws are immutable in place and the browser cache absorbs repeats (single-user, ADR 0002). A disk thumb cache is the named follow-up if cold loads ever hurt.
  • Seeder — the sample world now has a second chapter, split through the real user path (CreateChapter + MovePages on the last two pages): a multi-chapter project is a content shape the seed couldn't show, and the organizer's browser verification needs one. Works because imageless pages skip the file step.

Tests — +14, suite at 508/508 green (75 Domain + 195 UseCases + 93 Integration + 145 BlazorAdapter).

  • MovePagesTests (8): selection order wins over reading order and appends past the target's existing orders while the unselected sibling stays put; rows and files move together; a target collision fails before anything moves (the earlier selected page untouched too); two incoming same-name pages fail the same way; resident pages and duplicate ids are skipped/deduped; vanished page or chapter fails cleanly; cross-project refused; imageless pages never touch the image store.
  • PageImageEndpointTests (+4): /thumb is behind the gate; unknown page 404s; a 640×960 upload comes back as a real decodable 320×213 PNG with the same cache headers; a 3×5 image is never upscaled.
  • FileSystemPageImageStoreTests: ExistsAsync tracks a move on both sides.
  • SeedDevDataTests: two chapters (4+2 pages), credits page now closing chapter 2, all prior region/bible/run assertions rechecked across both chapters. DependencyInjectionTests pins the MovePages registration.

Honest notes: no agent tool drives page moves today (grepped src/Orihon.UseCases/Agents/), so per AGENTS.md there is no tool to update in this change. Merge order: independent of Kagaku.UI #7 — no submodule bump here. Discovered while testing, for the record: Kagaku.UI's current main (84c8442, "bind value as the attribute") breaks 3–4 existing BlazorAdapter facts when the submodule is advanced; that compat work is scoped to slice 3, which does the repin.

🤖 Generated with Claude Code

Slice 2 of the wizard step-2 redo (multi-select page organizer; slice 1 is Kagaku.UI PR [#7](https://git.kagaku.eu/TeamAI/Kagaku.UI/pulls/7), slice 3 brings the UI): the use-case and host layer the organizer will call. No UI changes — the wizard works exactly as today, so this ships green on its own with no submodule bump. **What's in** - `MovePages` (UseCases/Pages) — the multi-select move: a selection lands at the end of the target chapter *in selection order*. Name collisions are prechecked **all-or-nothing** before anything moves — against the target's `raw/` folder *and* between the incoming pages themselves (two chapters can each hold a `p_01.png`; both arriving at once would collide on arrival). The precheck needed a new port method, `IPageImageStore.ExistsAsync`, implemented in `FileSystemPageImageStore` and the shared fake. After the precheck each page moves file-then-row (`MovePage`'s pattern), so a mid-run filesystem race leaves earlier pages fully moved and later ones untouched — a row and its image are never split. Pages already in the target are skipped (their names never reach the precheck), duplicate ids deduped, cross-project refused, imageless pages move without touching the image store. - `GET /projects/{id}/pages/{id}/thumb` (Server) — the raw scan downscaled to a fixed 320px longest side (~2× a grid tile, HiDPI-sharp) through the existing `IPageImageRenderer`, same guards/gate/caching as `/raw`; an undecodable stored file is a 404, matching `/raw`'s posture. One fixed size = one cacheable URL, nothing to validate; no disk cache — raws are immutable in place and the browser cache absorbs repeats (single-user, ADR 0002). A disk thumb cache is the named follow-up if cold loads ever hurt. - Seeder — the sample world now has a **second chapter**, split through the real user path (`CreateChapter` + `MovePages` on the last two pages): a multi-chapter project is a content shape the seed couldn't show, and the organizer's browser verification needs one. Works because imageless pages skip the file step. **Tests** — +14, suite at 508/508 green (75 Domain + 195 UseCases + 93 Integration + 145 BlazorAdapter). - `MovePagesTests` (8): selection order wins over reading order and appends past the target's existing orders while the unselected sibling stays put; rows and files move together; a target collision fails before anything moves (the earlier selected page untouched too); two incoming same-name pages fail the same way; resident pages and duplicate ids are skipped/deduped; vanished page or chapter fails cleanly; cross-project refused; imageless pages never touch the image store. - `PageImageEndpointTests` (+4): `/thumb` is behind the gate; unknown page 404s; a 640×960 upload comes back as a real decodable 320×213 PNG with the same cache headers; a 3×5 image is never upscaled. - `FileSystemPageImageStoreTests`: `ExistsAsync` tracks a move on both sides. - `SeedDevDataTests`: two chapters (4+2 pages), credits page now closing chapter 2, all prior region/bible/run assertions rechecked across both chapters. `DependencyInjectionTests` pins the `MovePages` registration. Honest notes: no agent tool drives page moves today (grepped `src/Orihon.UseCases/Agents/`), so per AGENTS.md there is no tool to update in this change. Merge order: independent of Kagaku.UI #7 — no submodule bump here. Discovered while testing, for the record: Kagaku.UI's current main (84c8442, "bind value as the attribute") breaks 3–4 existing BlazorAdapter facts when the submodule is advanced; that compat work is scoped to slice 3, which does the repin. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: page organizer 2/3 — bulk MovePages and page thumbnails
All checks were successful
CI / build (pull_request) Successful in 24s
CI / test (pull_request) Successful in 39s
0aa6f96cb1
The wizard's step-2 organizer (next PR) needs a multi-select move and a
grid-friendly image size; both land here so the layer ships green alone.

MovePages moves a selection to the end of a target chapter in selection
order. Name collisions — against the target's folder and between the
incoming pages themselves — are prechecked all-or-nothing via the new
IPageImageStore.ExistsAsync, so the call fails before any file moves;
after the precheck each page moves file-then-row, so a row and its image
are never split. Imageless pages skip the file step, which lets the
seeder use it: the sample world now has a second chapter, split through
the real user path (CreateChapter + MovePages), so chapter navigation
and the organizer always have structure to show.

The /thumb endpoint serves the raw scan downscaled to a fixed 320px
longest side through the same renderer the agent tools use — one
cacheable URL, no parameter to validate, no disk cache (raws are
immutable in place; the browser cache absorbs repeats, ADR 0002).

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

Summary

Summary
Generated on: 07/26/2026 - 11:29:00
Coverage date: 07/26/2026 - 11:28:46 - 07/26/2026 - 11:28:58
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 360
Files: 167
Line coverage: 94% (8737 of 9292)
Covered lines: 8737
Uncovered lines: 555
Coverable lines: 9292
Total lines: 17153
Branch coverage: 80.3% (1918 of 2386)
Covered branches: 1918
Total branches: 2386
Method coverage: Feature is only available for sponsors

Coverage

Orihon.BlazorAdapter - 95.7%
Name Line Branch
Orihon.BlazorAdapter 95.7% 88%
Orihon.BlazorAdapter.Bible.AddBeatRowRequested 100%
Orihon.BlazorAdapter.Bible.AddCharacterRowRequested 100%
Orihon.BlazorAdapter.Bible.AddGlossaryRowRequested 100%
Orihon.BlazorAdapter.Bible.AddLoreRowRequested 100%
Orihon.BlazorAdapter.Bible.BibleEffects 92.2% 79.1%
Orihon.BlazorAdapter.Bible.BibleLoaded 100%
Orihon.BlazorAdapter.Bible.BiblePage 93.7% 81.6%
Orihon.BlazorAdapter.Bible.BibleReducers 93.1%
Orihon.BlazorAdapter.Bible.BibleState 100%
Orihon.BlazorAdapter.Bible.BibleWriteFailed 100%
Orihon.BlazorAdapter.Bible.DeleteBeatRowRequested 100%
Orihon.BlazorAdapter.Bible.DeleteCharacterRowRequested 100%
Orihon.BlazorAdapter.Bible.DeleteGlossaryRowRequested 100%
Orihon.BlazorAdapter.Bible.DeleteLoreRowRequested 0%
Orihon.BlazorAdapter.Bible.LoadBible 100%
Orihon.BlazorAdapter.Bible.ReorderBeatsRequested 0%
Orihon.BlazorAdapter.Bible.SaveOverviewRequested 100%
Orihon.BlazorAdapter.Bible.SaveSummaryRequested 100%
Orihon.BlazorAdapter.Bible.UpdateBeatRowRequested 100%
Orihon.BlazorAdapter.Bible.UpdateCharacterRowRequested 100%
Orihon.BlazorAdapter.Bible.UpdateGlossaryRowRequested 100%
Orihon.BlazorAdapter.Bible.UpdateLoreRowRequested 100%
Orihon.BlazorAdapter.BlazorAdapterAssembly 100%
Orihon.BlazorAdapter.Debounce 96.2% 94.4%
Orihon.BlazorAdapter.Diagnostics.CircuitError 100%
Orihon.BlazorAdapter.Diagnostics.CircuitErrorPanel 100%
Orihon.BlazorAdapter.Diagnostics.CircuitErrorSink 100% 85.7%
Orihon.BlazorAdapter.Diagnostics.OrihonStoreInitializer 85.7% 66.6%
Orihon.BlazorAdapter.PageWorkspace.CreateRegionRequested 100%
Orihon.BlazorAdapter.PageWorkspace.DeletePageSummaryRequested 100%
Orihon.BlazorAdapter.PageWorkspace.DeleteRegionRequested 100%
Orihon.BlazorAdapter.PageWorkspace.LoadPageWorkspace 100%
Orihon.BlazorAdapter.PageWorkspace.PageSummarySaved 100%
Orihon.BlazorAdapter.PageWorkspace.PageViewport 100% 100%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceEffects 100% 100%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceLoaded 100%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage 90.4% 83.3%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceReducers 100% 66.6%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceState 100%
Orihon.BlazorAdapter.PageWorkspace.PageWriteFailed 100%
Orihon.BlazorAdapter.PageWorkspace.RegionCreated 100%
Orihon.BlazorAdapter.PageWorkspace.RegionSaved 100%
Orihon.BlazorAdapter.PageWorkspace.ReorderRegionsRequested 100%
Orihon.BlazorAdapter.PageWorkspace.SavePageSummaryRequested 100%
Orihon.BlazorAdapter.PageWorkspace.SaveRegionRequested 100%
Orihon.BlazorAdapter.PageWorkspace.SetPageMetaRequested 100%
Orihon.BlazorAdapter.Projects.CreateProjectRequested 100%
Orihon.BlazorAdapter.Projects.DecideSetupContinuation 100%
Orihon.BlazorAdapter.Projects.DeleteProjectRequested 100%
Orihon.BlazorAdapter.Projects.FinishSetupRequested 100%
Orihon.BlazorAdapter.Projects.ImportPagesRequested 100%
Orihon.BlazorAdapter.Projects.LoadWizard 100%
Orihon.BlazorAdapter.Projects.PagesImported 100%
Orihon.BlazorAdapter.Projects.ProjectDeleteFailed 100%
Orihon.BlazorAdapter.Projects.ProjectListEffects 100% 100%
Orihon.BlazorAdapter.Projects.ProjectListPage 89.7% 91.1%
Orihon.BlazorAdapter.Projects.ProjectListReducers 100%
Orihon.BlazorAdapter.Projects.ProjectListState 100%
Orihon.BlazorAdapter.Projects.ProjectsLoaded 100%
Orihon.BlazorAdapter.Projects.ProjectWizardEffects 100% 100%
Orihon.BlazorAdapter.Projects.ProjectWizardPage 94.1% 86.2%
Orihon.BlazorAdapter.Projects.ProjectWizardReducers 100%
Orihon.BlazorAdapter.Projects.ProjectWizardState 100%
Orihon.BlazorAdapter.Projects.SetupChat 93.5% 100%
Orihon.BlazorAdapter.Projects.SetupChatEffects 100% 100%
Orihon.BlazorAdapter.Projects.SetupChatFailed 100%
Orihon.BlazorAdapter.Projects.SetupChatReducers 100%
Orihon.BlazorAdapter.Projects.SetupChatState 100%
Orihon.BlazorAdapter.Projects.SetupChatUpdated 100%
Orihon.BlazorAdapter.Projects.StartSetupChat 100%
Orihon.BlazorAdapter.Projects.SubmitSetupAnswer 100%
Orihon.BlazorAdapter.Projects.WizardLoaded 100%
Orihon.BlazorAdapter.Projects.WizardWriteFailed 100%
Orihon.BlazorAdapter.Runs.MonitorRunLoaded 100%
Orihon.BlazorAdapter.Runs.RunChangedBridge 94.1% 91.6%
Orihon.BlazorAdapter.Runs.RunMonitor 100% 97.6%
Orihon.BlazorAdapter.Runs.RunMonitorEffects 100% 100%
Orihon.BlazorAdapter.Runs.RunMonitorReducers 100%
Orihon.BlazorAdapter.Runs.RunMonitorState 100%
Orihon.BlazorAdapter.Settings.AgentModelPicked 100%
Orihon.BlazorAdapter.Settings.AgentModelSaved 100%
Orihon.BlazorAdapter.Settings.AgentModelSaveFailed 100%
Orihon.BlazorAdapter.Settings.KeySaved 100%
Orihon.BlazorAdapter.Settings.KeySaveFailed 100%
Orihon.BlazorAdapter.Settings.ModelOptionsLoaded 100%
Orihon.BlazorAdapter.Settings.ModelOptionsUnavailable 100%
Orihon.BlazorAdapter.Settings.SaveKeyRequested 100%
Orihon.BlazorAdapter.Settings.SettingsEffects 100% 100%
Orihon.BlazorAdapter.Settings.SettingsLoaded 100%
Orihon.BlazorAdapter.Settings.SettingsPage 100% 90.4%
Orihon.BlazorAdapter.Settings.SettingsReducers 100%
Orihon.BlazorAdapter.Settings.SettingsState 100%
Orihon.BlazorAdapter.Uploads.UploadTransfer 96.5% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferProgress 100% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferResult 100%
Orihon.BlazorAdapter.Workspace.CreateChapterRequested 100%
Orihon.BlazorAdapter.Workspace.DeleteChapterRequested 100%
Orihon.BlazorAdapter.Workspace.DeletePageRequested 100%
Orihon.BlazorAdapter.Workspace.DeleteSummaryRequested 100%
Orihon.BlazorAdapter.Workspace.LoadProjectWorkspace 100%
Orihon.BlazorAdapter.Workspace.MovePageRequested 100%
Orihon.BlazorAdapter.Workspace.ProjectMetadataCard 95.2% 92.8%
Orihon.BlazorAdapter.Workspace.ProjectMetadataSaved 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspaceEffects 100% 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspaceLoaded 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage 95.5% 87.6%
Orihon.BlazorAdapter.Workspace.ProjectWorkspaceReducers 100% 62.5%
Orihon.BlazorAdapter.Workspace.ProjectWorkspaceState 100%
Orihon.BlazorAdapter.Workspace.RenameChapterRequested 100%
Orihon.BlazorAdapter.Workspace.ReorderChaptersRequested 100%
Orihon.BlazorAdapter.Workspace.ReorderPagesRequested 100%
Orihon.BlazorAdapter.Workspace.SaveProjectMetadataRequested 100%
Orihon.BlazorAdapter.Workspace.SaveSummaryRequested 100%
Orihon.BlazorAdapter.Workspace.SetPageKindRequested 100%
Orihon.BlazorAdapter.Workspace.SummaryDeleted 100%
Orihon.BlazorAdapter.Workspace.SummarySaved 100%
Orihon.BlazorAdapter.Workspace.WorkspaceImportRequested 100%
Orihon.BlazorAdapter.Workspace.WorkspaceWriteFailed 100%
Orihon.Domain - 100%
Name Line Branch
Orihon.Domain 100% 100%
Orihon.Domain.Agents.AgentDescriptor 100%
Orihon.Domain.Agents.AgentRoster 100% 100%
Orihon.Domain.Bible.Character 100% 100%
Orihon.Domain.Bible.GlossaryEntry 100% 100%
Orihon.Domain.Bible.LoreEntry 100% 100%
Orihon.Domain.Bible.PageSummary 100%
Orihon.Domain.Bible.StoryBeat 100%
Orihon.Domain.Bible.StoryOverview 100%
Orihon.Domain.Projects.Project 100% 100%
Orihon.Domain.Projects.ProjectProfile 100%
Orihon.Domain.Runs.Execution 100% 100%
Orihon.Domain.Runs.Run 100%
Orihon.Domain.Settings.AppSetting 100%
Orihon.Domain.Text 100% 100%
Orihon.Domain.Translation.BoundingBox 100%
Orihon.Domain.Translation.Chapter 100%
Orihon.Domain.Translation.Page 100%
Orihon.Domain.Translation.Region 100% 100%
Orihon.Domain.Translation.RegionProfile 100%
Orihon.Infrastructure - 94.3%
Name Line Branch
Orihon.Infrastructure 94.3% 67.3%
Orihon.Infrastructure.Bible.EfBibleStore 94.4% 91.6%
Orihon.Infrastructure.DependencyInjection 100%
Orihon.Infrastructure.Gateways.AgentToolAdapter 100%
Orihon.Infrastructure.Gateways.AgentToolAdapter`1 100% 100%
Orihon.Infrastructure.Gateways.HttpWebPageFetcher 95.1% 83.3%
Orihon.Infrastructure.Gateways.OpenRouterLlmGateway 98% 83%
Orihon.Infrastructure.Gateways.SkiaPageImageRenderer 96.6% 86.1%
Orihon.Infrastructure.Persistence.Configurations.AppSettingConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ExecutionConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper 100%
Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.RunConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.StoryOverviewConfiguration 100%
Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Orihon.Infrastructure.Persistence.Migrations.AddAppSettings 99.3%
Orihon.Infrastructure.Persistence.Migrations.AddRuns 99.1%
Orihon.Infrastructure.Persistence.Migrations.AddStoryOverview 99.5%
Orihon.Infrastructure.Persistence.Migrations.InitialTranslationDomain 97.3%
Orihon.Infrastructure.Persistence.Migrations.OrihonDbContextModelSnapshot 100%
Orihon.Infrastructure.Persistence.OrihonDbContext 100%
Orihon.Infrastructure.Persistence.OrihonDbContextFactory 100%
Orihon.Infrastructure.Projects.EfProjectStore 100% 100%
Orihon.Infrastructure.Projects.FileSystemPageImageStore 100% 100%
Orihon.Infrastructure.Runs.EfRunStore 97% 50%
Orihon.Infrastructure.Settings.EfAppSettingsStore 100% 100%
Orihon.Infrastructure.Translation.EfChapterStore 100% 100%
Orihon.Infrastructure.Translation.EfPageStore 86% 80%
Orihon.Infrastructure.Translation.EfRegionStore 100% 100%
Orihon.Infrastructure.Translation.Ordering 100% 100%
System.Text.RegularExpressions.Generated 70.6% 53.3%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
77.9% 76.6%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
59% 42.5%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
89.4% 75%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
83.7% 62.5%
Orihon.Kernel - 90.9%
Name Line Branch
Orihon.Kernel 90.9% 75%
Orihon.Kernel.Err`1 100%
Orihon.Kernel.Ok`1 100%
Orihon.Kernel.Result`1 88.8% 75%
Orihon.Server - 93.3%
Name Line Branch
Orihon.Server 93.3% 68.4%
Orihon.Server.Components.App 100%
Orihon.Server.Components.Layout.MainLayout 100%
Orihon.Server.Components.Pages.Gate 64.2% 66.6%
Orihon.Server.RunEngineBootstrap 100%
Orihon.Server.Security.AccessGate 91.8% 41.6%
Orihon.Server.Security.AccessSecret 100% 50%
Orihon.Server.VolumeStartupValidator 100% 100%
Program 94.8% 85.7%
Orihon.UseCases - 91%
Name Line Branch
Orihon.UseCases 91% 83.4%
Orihon.UseCases.Agents.AgentAttemptPreparation 100%
Orihon.UseCases.Agents.AgentAttemptSupport 100% 92.8%
Orihon.UseCases.Agents.AgentBlueprint 100%
Orihon.UseCases.Agents.AgentInvocation 100%
Orihon.UseCases.Agents.AgentOutcome 100%
Orihon.UseCases.Agents.AgentTool`1 90.9% 75%
Orihon.UseCases.Agents.AgentToolImage 100%
Orihon.UseCases.Agents.AgentToolResult 100%
Orihon.UseCases.Agents.Annotation.AddRegionParams 100%
Orihon.UseCases.Agents.Annotation.AddRegionTool 76.9% 50%
Orihon.UseCases.Agents.Annotation.AnnotationBlueprints 100%
Orihon.UseCases.Agents.Annotation.AnnotationStage 94.4% 50%
Orihon.UseCases.Agents.Annotation.BboxCreationExecutor 94.1% 50%
Orihon.UseCases.Agents.Annotation.BboxRefinementExecutor 90.4% 62.5%
Orihon.UseCases.Agents.Annotation.BoundBoxParams 0%
Orihon.UseCases.Agents.Annotation.BoundContactSheetParams 0%
Orihon.UseCases.Agents.Annotation.BoundContactSheetTool 10.7% 0%
Orihon.UseCases.Agents.Annotation.BoundCropParams 0%
Orihon.UseCases.Agents.Annotation.BoundCropTool 42.8%
Orihon.UseCases.Agents.Annotation.BoundViewAnnotatedTool 15% 0%
Orihon.UseCases.Agents.Annotation.BoundViewPageTool 18.7% 0%
Orihon.UseCases.Agents.Annotation.BoundViewParams 0%
Orihon.UseCases.Agents.Annotation.BoundZoomParams 0%
Orihon.UseCases.Agents.Annotation.BoundZoomTool 37.5%
Orihon.UseCases.Agents.Annotation.DeleteBoundRegionTool 91.6% 100%
Orihon.UseCases.Agents.Annotation.DeleteRegionParams 100%
Orihon.UseCases.Agents.Annotation.DeleteRegionTool 85.7% 100%
Orihon.UseCases.Agents.Annotation.FindGlossaryParams 100%
Orihon.UseCases.Agents.Annotation.FindGlossaryTool 76.4% 62.5%
Orihon.UseCases.Agents.Annotation.ListRegionsTool 76.4% 60%
Orihon.UseCases.Agents.Annotation.MoveResizeBoundTool 27.2% 0%
Orihon.UseCases.Agents.Annotation.MoveResizeRegionParams 100%
Orihon.UseCases.Agents.Annotation.MoveResizeRegionTool 73.3% 50%
Orihon.UseCases.Agents.Annotation.PageQaExecutor 94.4% 83.3%
Orihon.UseCases.Agents.Annotation.QaReportSink 100%
Orihon.UseCases.Agents.Annotation.RegionAuthoringAccess 86.6% 53.8%
Orihon.UseCases.Agents.Annotation.RejectRegionParams 100%
Orihon.UseCases.Agents.Annotation.RejectRegionTool 85.7% 50%
Orihon.UseCases.Agents.Annotation.ReorderRegionParams 100%
Orihon.UseCases.Agents.Annotation.ReorderRegionTool 80% 60%
Orihon.UseCases.Agents.Annotation.ReportQaParams 100%
Orihon.UseCases.Agents.Annotation.ReportQaTool 82.3% 93.7%
Orihon.UseCases.Agents.Annotation.SetPageMetaParams 100%
Orihon.UseCases.Agents.Annotation.SetPageMetaTool 85.7% 75%
Orihon.UseCases.Agents.Annotation.SetRegionTypeParams 100%
Orihon.UseCases.Agents.Annotation.SetRegionTypeTool 85.7% 87.5%
Orihon.UseCases.Agents.Annotation.SetTranscriptionParams 100%
Orihon.UseCases.Agents.Annotation.SetTranscriptionTool 80% 100%
Orihon.UseCases.Agents.Annotation.TranscriptionExecutor 90.4% 75%
Orihon.UseCases.Agents.AssistantSpoke 100%
Orihon.UseCases.Agents.Inspection.ContactSheetParams 100%
Orihon.UseCases.Agents.Inspection.ContactSheetTool 82.1% 92.8%
Orihon.UseCases.Agents.Inspection.CropParams 100%
Orihon.UseCases.Agents.Inspection.CropTool 42.8%
Orihon.UseCases.Agents.Inspection.PageImageAccess 66.6% 62%
Orihon.UseCases.Agents.Inspection.ViewAnnotatedParams 100%
Orihon.UseCases.Agents.Inspection.ViewAnnotatedTool 76.1% 83.3%
Orihon.UseCases.Agents.Inspection.ZoomParams 100%
Orihon.UseCases.Agents.Inspection.ZoomTool 44.4%
Orihon.UseCases.Agents.ResearchSetup.AddGlossaryParams 100%
Orihon.UseCases.Agents.ResearchSetup.AddGlossaryTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatParams 100%
Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatTool 100% 50%
Orihon.UseCases.Agents.ResearchSetup.AskUserParams 100%
Orihon.UseCases.Agents.ResearchSetup.AskUserTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.FetchUrlParams 100%
Orihon.UseCases.Agents.ResearchSetup.FetchUrlTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.ListBibleTool 89.4% 100%
Orihon.UseCases.Agents.ResearchSetup.PageByNumber 90% 87.5%
Orihon.UseCases.Agents.ResearchSetup.ResearchSetupBlueprint 100%
Orihon.UseCases.Agents.ResearchSetup.SetPageMetaParams 100%
Orihon.UseCases.Agents.ResearchSetup.SetPageMetaTool 95.2% 90%
Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryParams 100%
Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryTool 100% 75%
Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataParams 100%
Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataTool 96.1% 90.9%
Orihon.UseCases.Agents.ResearchSetup.SetStoryOverviewParams 100%
Orihon.UseCases.Agents.ResearchSetup.SetStoryOverviewTool 100% 100%
Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterParams 100%
Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterTool 91.3% 66.6%
Orihon.UseCases.Agents.ResearchSetup.UpsertLoreParams 100%
Orihon.UseCases.Agents.ResearchSetup.UpsertLoreTool 91.3% 66.6%
Orihon.UseCases.Agents.ResearchSetup.ViewPageParams 100%
Orihon.UseCases.Agents.ResearchSetup.ViewPageTool 100% 100%
Orihon.UseCases.Agents.Setup.ResearchSetupExecutor 97.1% 89.2%
Orihon.UseCases.Agents.Setup.SetupChatEntry 100%
Orihon.UseCases.Agents.Setup.SetupConversation 100% 87.5%
Orihon.UseCases.Agents.Setup.SetupConversationRegistry 100%
Orihon.UseCases.Agents.ToolCalled 100%
Orihon.UseCases.Agents.ToolCompleted 100%
Orihon.UseCases.Bible.AddCharacter 100% 100%
Orihon.UseCases.Bible.AddGlossaryEntry 100% 100%
Orihon.UseCases.Bible.AddLoreEntry 100% 100%
Orihon.UseCases.Bible.AddStoryBeat 100% 100%
Orihon.UseCases.Bible.BibleDto 100%
Orihon.UseCases.Bible.CharacterDto 100%
Orihon.UseCases.Bible.DeleteCharacter 100% 100%
Orihon.UseCases.Bible.DeleteGlossaryEntry 100% 100%
Orihon.UseCases.Bible.DeleteLoreEntry 100% 100%
Orihon.UseCases.Bible.DeletePageSummary 100% 100%
Orihon.UseCases.Bible.DeleteStoryBeat 100% 100%
Orihon.UseCases.Bible.GetBible 100% 100%
Orihon.UseCases.Bible.GlossaryEntryDto 100%
Orihon.UseCases.Bible.LoreEntryDto 100%
Orihon.UseCases.Bible.PageSummaryDto 100%
Orihon.UseCases.Bible.ReorderStoryBeats 100%
Orihon.UseCases.Bible.SetPageSummary 100% 100%
Orihon.UseCases.Bible.SetStoryOverview 100% 100%
Orihon.UseCases.Bible.StoryBeatDto 100%
Orihon.UseCases.Bible.StoryOverviewDto 100%
Orihon.UseCases.Bible.UpdateCharacter 100% 100%
Orihon.UseCases.Bible.UpdateGlossaryEntry 100% 100%
Orihon.UseCases.Bible.UpdateLoreEntry 100% 100%
Orihon.UseCases.Bible.UpdateStoryBeat 100% 100%
Orihon.UseCases.Chapters.ChapterDto 100%
Orihon.UseCases.Chapters.CreateChapter 100% 100%
Orihon.UseCases.Chapters.DeleteChapter 100% 100%
Orihon.UseCases.Chapters.RenameChapter 100% 100%
Orihon.UseCases.Chapters.ReorderChapters 100%
Orihon.UseCases.DependencyInjection 100%
Orihon.UseCases.Diagnostics.SeedDevData 99.2% 93.7%
Orihon.UseCases.Gateways.LabeledBox 100%
Orihon.UseCases.Gateways.LlmKeyInfo 100%
Orihon.UseCases.Gateways.LlmModel 100%
Orihon.UseCases.NextOrder 100%
Orihon.UseCases.Pages.DeletePage 100% 100%
Orihon.UseCases.Pages.GetPage 100% 100%
Orihon.UseCases.Pages.GetProjectWorkspace 100% 100%
Orihon.UseCases.Pages.ImportPages 100% 100%
Orihon.UseCases.Pages.ImportPagesResult 100%
Orihon.UseCases.Pages.MarkPageAnnotated 100% 100%
Orihon.UseCases.Pages.MovePage 100% 92.8%
Orihon.UseCases.Pages.MovePages 100% 100%
Orihon.UseCases.Pages.PageDetailDto 100%
Orihon.UseCases.Pages.PageDto 100%
Orihon.UseCases.Pages.PageUpload 100%
Orihon.UseCases.Pages.ProjectWorkspaceDto 100%
Orihon.UseCases.Pages.ReorderPages 100%
Orihon.UseCases.Pages.SetPageMeta 100% 100%
Orihon.UseCases.Pages.WorkspaceChapterDto 100%
Orihon.UseCases.Projects.CompleteProjectSetup 100% 93.7%
Orihon.UseCases.Projects.CreateProject 100% 100%
Orihon.UseCases.Projects.DeleteProject 100% 100%
Orihon.UseCases.Projects.GetProject 100% 100%
Orihon.UseCases.Projects.ListProjects 100%
Orihon.UseCases.Projects.ProjectDto 95.8%
Orihon.UseCases.Projects.StartAnnotationRun 95.4% 90%
Orihon.UseCases.Projects.StartSetupRun 100% 100%
Orihon.UseCases.Projects.StoredPageImage 100%
Orihon.UseCases.Projects.UpdateProjectMetadata 100% 100%
Orihon.UseCases.Regions.CreateRegion 100% 100%
Orihon.UseCases.Regions.DeleteRegion 100% 100%
Orihon.UseCases.Regions.RegionDto 97%
Orihon.UseCases.Regions.ReorderRegions 100%
Orihon.UseCases.Regions.UpdateRegion 100% 100%
Orihon.UseCases.Runs.AnnotationPipeline 100% 100%
Orihon.UseCases.Runs.ExecutionDto 92.3%
Orihon.UseCases.Runs.PlannedExecution 100%
Orihon.UseCases.Runs.RunDto 93.3% 90%
Orihon.UseCases.Runs.RunEngine 95.3% 90.6%
Orihon.UseCases.Runs.RunEngineOptions 100%
Orihon.UseCases.Runs.StageContext 87.5%
Orihon.UseCases.Runs.StageHaltedException 100%
Orihon.UseCases.Settings.AgentSettingDto 100% 100%
Orihon.UseCases.Settings.GetSettings 100% 100%
Orihon.UseCases.Settings.ListModelOptions 100% 100%
Orihon.UseCases.Settings.SaveAgentModel 100% 100%
Orihon.UseCases.Settings.SaveOpenRouterKey 100% 100%
Orihon.UseCases.Settings.SettingKeys 100% 100%
Orihon.UseCases.Settings.SettingsDto 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/26/2026 - 11:29:00 | | Coverage date: | 07/26/2026 - 11:28:46 - 07/26/2026 - 11:28:58 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 360 | | Files: | 167 | | **Line coverage:** | 94% (8737 of 9292) | | Covered lines: | 8737 | | Uncovered lines: | 555 | | Coverable lines: | 9292 | | Total lines: | 17153 | | **Branch coverage:** | 80.3% (1918 of 2386) | | Covered branches: | 1918 | | Total branches: | 2386 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Orihon.BlazorAdapter - 95.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.BlazorAdapter**|**95.7%**|**88%**| |Orihon.BlazorAdapter.Bible.AddBeatRowRequested|100%|| |Orihon.BlazorAdapter.Bible.AddCharacterRowRequested|100%|| |Orihon.BlazorAdapter.Bible.AddGlossaryRowRequested|100%|| |Orihon.BlazorAdapter.Bible.AddLoreRowRequested|100%|| |Orihon.BlazorAdapter.Bible.BibleEffects|92.2%|79.1%| |Orihon.BlazorAdapter.Bible.BibleLoaded|100%|| |Orihon.BlazorAdapter.Bible.BiblePage|93.7%|81.6%| |Orihon.BlazorAdapter.Bible.BibleReducers|93.1%|| |Orihon.BlazorAdapter.Bible.BibleState|100%|| |Orihon.BlazorAdapter.Bible.BibleWriteFailed|100%|| |Orihon.BlazorAdapter.Bible.DeleteBeatRowRequested|100%|| |Orihon.BlazorAdapter.Bible.DeleteCharacterRowRequested|100%|| |Orihon.BlazorAdapter.Bible.DeleteGlossaryRowRequested|100%|| |Orihon.BlazorAdapter.Bible.DeleteLoreRowRequested|0%|| |Orihon.BlazorAdapter.Bible.LoadBible|100%|| |Orihon.BlazorAdapter.Bible.ReorderBeatsRequested|0%|| |Orihon.BlazorAdapter.Bible.SaveOverviewRequested|100%|| |Orihon.BlazorAdapter.Bible.SaveSummaryRequested|100%|| |Orihon.BlazorAdapter.Bible.UpdateBeatRowRequested|100%|| |Orihon.BlazorAdapter.Bible.UpdateCharacterRowRequested|100%|| |Orihon.BlazorAdapter.Bible.UpdateGlossaryRowRequested|100%|| |Orihon.BlazorAdapter.Bible.UpdateLoreRowRequested|100%|| |Orihon.BlazorAdapter.BlazorAdapterAssembly|100%|| |Orihon.BlazorAdapter.Debounce|96.2%|94.4%| |Orihon.BlazorAdapter.Diagnostics.CircuitError|100%|| |Orihon.BlazorAdapter.Diagnostics.CircuitErrorPanel|100%|| |Orihon.BlazorAdapter.Diagnostics.CircuitErrorSink|100%|85.7%| |Orihon.BlazorAdapter.Diagnostics.OrihonStoreInitializer|85.7%|66.6%| |Orihon.BlazorAdapter.PageWorkspace.CreateRegionRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.DeletePageSummaryRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.DeleteRegionRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.LoadPageWorkspace|100%|| |Orihon.BlazorAdapter.PageWorkspace.PageSummarySaved|100%|| |Orihon.BlazorAdapter.PageWorkspace.PageViewport|100%|100%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceEffects|100%|100%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceLoaded|100%|| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage|90.4%|83.3%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceReducers|100%|66.6%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspaceState|100%|| |Orihon.BlazorAdapter.PageWorkspace.PageWriteFailed|100%|| |Orihon.BlazorAdapter.PageWorkspace.RegionCreated|100%|| |Orihon.BlazorAdapter.PageWorkspace.RegionSaved|100%|| |Orihon.BlazorAdapter.PageWorkspace.ReorderRegionsRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.SavePageSummaryRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.SaveRegionRequested|100%|| |Orihon.BlazorAdapter.PageWorkspace.SetPageMetaRequested|100%|| |Orihon.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Orihon.BlazorAdapter.Projects.DecideSetupContinuation|100%|| |Orihon.BlazorAdapter.Projects.DeleteProjectRequested|100%|| |Orihon.BlazorAdapter.Projects.FinishSetupRequested|100%|| |Orihon.BlazorAdapter.Projects.ImportPagesRequested|100%|| |Orihon.BlazorAdapter.Projects.LoadWizard|100%|| |Orihon.BlazorAdapter.Projects.PagesImported|100%|| |Orihon.BlazorAdapter.Projects.ProjectDeleteFailed|100%|| |Orihon.BlazorAdapter.Projects.ProjectListEffects|100%|100%| |Orihon.BlazorAdapter.Projects.ProjectListPage|89.7%|91.1%| |Orihon.BlazorAdapter.Projects.ProjectListReducers|100%|| |Orihon.BlazorAdapter.Projects.ProjectListState|100%|| |Orihon.BlazorAdapter.Projects.ProjectsLoaded|100%|| |Orihon.BlazorAdapter.Projects.ProjectWizardEffects|100%|100%| |Orihon.BlazorAdapter.Projects.ProjectWizardPage|94.1%|86.2%| |Orihon.BlazorAdapter.Projects.ProjectWizardReducers|100%|| |Orihon.BlazorAdapter.Projects.ProjectWizardState|100%|| |Orihon.BlazorAdapter.Projects.SetupChat|93.5%|100%| |Orihon.BlazorAdapter.Projects.SetupChatEffects|100%|100%| |Orihon.BlazorAdapter.Projects.SetupChatFailed|100%|| |Orihon.BlazorAdapter.Projects.SetupChatReducers|100%|| |Orihon.BlazorAdapter.Projects.SetupChatState|100%|| |Orihon.BlazorAdapter.Projects.SetupChatUpdated|100%|| |Orihon.BlazorAdapter.Projects.StartSetupChat|100%|| |Orihon.BlazorAdapter.Projects.SubmitSetupAnswer|100%|| |Orihon.BlazorAdapter.Projects.WizardLoaded|100%|| |Orihon.BlazorAdapter.Projects.WizardWriteFailed|100%|| |Orihon.BlazorAdapter.Runs.MonitorRunLoaded|100%|| |Orihon.BlazorAdapter.Runs.RunChangedBridge|94.1%|91.6%| |Orihon.BlazorAdapter.Runs.RunMonitor|100%|97.6%| |Orihon.BlazorAdapter.Runs.RunMonitorEffects|100%|100%| |Orihon.BlazorAdapter.Runs.RunMonitorReducers|100%|| |Orihon.BlazorAdapter.Runs.RunMonitorState|100%|| |Orihon.BlazorAdapter.Settings.AgentModelPicked|100%|| |Orihon.BlazorAdapter.Settings.AgentModelSaved|100%|| |Orihon.BlazorAdapter.Settings.AgentModelSaveFailed|100%|| |Orihon.BlazorAdapter.Settings.KeySaved|100%|| |Orihon.BlazorAdapter.Settings.KeySaveFailed|100%|| |Orihon.BlazorAdapter.Settings.ModelOptionsLoaded|100%|| |Orihon.BlazorAdapter.Settings.ModelOptionsUnavailable|100%|| |Orihon.BlazorAdapter.Settings.SaveKeyRequested|100%|| |Orihon.BlazorAdapter.Settings.SettingsEffects|100%|100%| |Orihon.BlazorAdapter.Settings.SettingsLoaded|100%|| |Orihon.BlazorAdapter.Settings.SettingsPage|100%|90.4%| |Orihon.BlazorAdapter.Settings.SettingsReducers|100%|| |Orihon.BlazorAdapter.Settings.SettingsState|100%|| |Orihon.BlazorAdapter.Uploads.UploadTransfer|96.5%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferProgress|100%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferResult|100%|| |Orihon.BlazorAdapter.Workspace.CreateChapterRequested|100%|| |Orihon.BlazorAdapter.Workspace.DeleteChapterRequested|100%|| |Orihon.BlazorAdapter.Workspace.DeletePageRequested|100%|| |Orihon.BlazorAdapter.Workspace.DeleteSummaryRequested|100%|| |Orihon.BlazorAdapter.Workspace.LoadProjectWorkspace|100%|| |Orihon.BlazorAdapter.Workspace.MovePageRequested|100%|| |Orihon.BlazorAdapter.Workspace.ProjectMetadataCard|95.2%|92.8%| |Orihon.BlazorAdapter.Workspace.ProjectMetadataSaved|100%|| |Orihon.BlazorAdapter.Workspace.ProjectWorkspaceEffects|100%|100%| |Orihon.BlazorAdapter.Workspace.ProjectWorkspaceLoaded|100%|| |Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage|95.5%|87.6%| |Orihon.BlazorAdapter.Workspace.ProjectWorkspaceReducers|100%|62.5%| |Orihon.BlazorAdapter.Workspace.ProjectWorkspaceState|100%|| |Orihon.BlazorAdapter.Workspace.RenameChapterRequested|100%|| |Orihon.BlazorAdapter.Workspace.ReorderChaptersRequested|100%|| |Orihon.BlazorAdapter.Workspace.ReorderPagesRequested|100%|| |Orihon.BlazorAdapter.Workspace.SaveProjectMetadataRequested|100%|| |Orihon.BlazorAdapter.Workspace.SaveSummaryRequested|100%|| |Orihon.BlazorAdapter.Workspace.SetPageKindRequested|100%|| |Orihon.BlazorAdapter.Workspace.SummaryDeleted|100%|| |Orihon.BlazorAdapter.Workspace.SummarySaved|100%|| |Orihon.BlazorAdapter.Workspace.WorkspaceImportRequested|100%|| |Orihon.BlazorAdapter.Workspace.WorkspaceWriteFailed|100%|| </details> <details><summary>Orihon.Domain - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Domain**|**100%**|**100%**| |Orihon.Domain.Agents.AgentDescriptor|100%|| |Orihon.Domain.Agents.AgentRoster|100%|100%| |Orihon.Domain.Bible.Character|100%|100%| |Orihon.Domain.Bible.GlossaryEntry|100%|100%| |Orihon.Domain.Bible.LoreEntry|100%|100%| |Orihon.Domain.Bible.PageSummary|100%|| |Orihon.Domain.Bible.StoryBeat|100%|| |Orihon.Domain.Bible.StoryOverview|100%|| |Orihon.Domain.Projects.Project|100%|100%| |Orihon.Domain.Projects.ProjectProfile|100%|| |Orihon.Domain.Runs.Execution|100%|100%| |Orihon.Domain.Runs.Run|100%|| |Orihon.Domain.Settings.AppSetting|100%|| |Orihon.Domain.Text|100%|100%| |Orihon.Domain.Translation.BoundingBox|100%|| |Orihon.Domain.Translation.Chapter|100%|| |Orihon.Domain.Translation.Page|100%|| |Orihon.Domain.Translation.Region|100%|100%| |Orihon.Domain.Translation.RegionProfile|100%|| </details> <details><summary>Orihon.Infrastructure - 94.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Infrastructure**|**94.3%**|**67.3%**| |Orihon.Infrastructure.Bible.EfBibleStore|94.4%|91.6%| |Orihon.Infrastructure.DependencyInjection|100%|| |Orihon.Infrastructure.Gateways.AgentToolAdapter|100%|| |Orihon.Infrastructure.Gateways.AgentToolAdapter`1|100%|100%| |Orihon.Infrastructure.Gateways.HttpWebPageFetcher|95.1%|83.3%| |Orihon.Infrastructure.Gateways.OpenRouterLlmGateway|98%|83%| |Orihon.Infrastructure.Gateways.SkiaPageImageRenderer|96.6%|86.1%| |Orihon.Infrastructure.Persistence.Configurations.AppSettingConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ExecutionConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper|100%|| |Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.RunConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.StoryOverviewConfiguration|100%|| |Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Orihon.Infrastructure.Persistence.Migrations.AddAppSettings|99.3%|| |Orihon.Infrastructure.Persistence.Migrations.AddRuns|99.1%|| |Orihon.Infrastructure.Persistence.Migrations.AddStoryOverview|99.5%|| |Orihon.Infrastructure.Persistence.Migrations.InitialTranslationDomain|97.3%|| |Orihon.Infrastructure.Persistence.Migrations.OrihonDbContextModelSnapshot|100%|| |Orihon.Infrastructure.Persistence.OrihonDbContext|100%|| |Orihon.Infrastructure.Persistence.OrihonDbContextFactory|100%|| |Orihon.Infrastructure.Projects.EfProjectStore|100%|100%| |Orihon.Infrastructure.Projects.FileSystemPageImageStore|100%|100%| |Orihon.Infrastructure.Runs.EfRunStore|97%|50%| |Orihon.Infrastructure.Settings.EfAppSettingsStore|100%|100%| |Orihon.Infrastructure.Translation.EfChapterStore|100%|100%| |Orihon.Infrastructure.Translation.EfPageStore|86%|80%| |Orihon.Infrastructure.Translation.EfRegionStore|100%|100%| |Orihon.Infrastructure.Translation.Ordering|100%|100%| |System.Text.RegularExpressions.Generated|70.6%|53.3%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4|77.9%|76.6%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1|59%|42.5%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3|89.4%|75%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F7FCA343D2B99030<br/>A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2|83.7%|62.5%| </details> <details><summary>Orihon.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Kernel**|**90.9%**|**75%**| |Orihon.Kernel.Err`1|100%|| |Orihon.Kernel.Ok`1|100%|| |Orihon.Kernel.Result`1|88.8%|75%| </details> <details><summary>Orihon.Server - 93.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Server**|**93.3%**|**68.4%**| |Orihon.Server.Components.App|100%|| |Orihon.Server.Components.Layout.MainLayout|100%|| |Orihon.Server.Components.Pages.Gate|64.2%|66.6%| |Orihon.Server.RunEngineBootstrap|100%|| |Orihon.Server.Security.AccessGate|91.8%|41.6%| |Orihon.Server.Security.AccessSecret|100%|50%| |Orihon.Server.VolumeStartupValidator|100%|100%| |Program|94.8%|85.7%| </details> <details><summary>Orihon.UseCases - 91%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.UseCases**|**91%**|**83.4%**| |Orihon.UseCases.Agents.AgentAttemptPreparation|100%|| |Orihon.UseCases.Agents.AgentAttemptSupport|100%|92.8%| |Orihon.UseCases.Agents.AgentBlueprint|100%|| |Orihon.UseCases.Agents.AgentInvocation|100%|| |Orihon.UseCases.Agents.AgentOutcome|100%|| |Orihon.UseCases.Agents.AgentTool`1|90.9%|75%| |Orihon.UseCases.Agents.AgentToolImage|100%|| |Orihon.UseCases.Agents.AgentToolResult|100%|| |Orihon.UseCases.Agents.Annotation.AddRegionParams|100%|| |Orihon.UseCases.Agents.Annotation.AddRegionTool|76.9%|50%| |Orihon.UseCases.Agents.Annotation.AnnotationBlueprints|100%|| |Orihon.UseCases.Agents.Annotation.AnnotationStage|94.4%|50%| |Orihon.UseCases.Agents.Annotation.BboxCreationExecutor|94.1%|50%| |Orihon.UseCases.Agents.Annotation.BboxRefinementExecutor|90.4%|62.5%| |Orihon.UseCases.Agents.Annotation.BoundBoxParams|0%|| |Orihon.UseCases.Agents.Annotation.BoundContactSheetParams|0%|| |Orihon.UseCases.Agents.Annotation.BoundContactSheetTool|10.7%|0%| |Orihon.UseCases.Agents.Annotation.BoundCropParams|0%|| |Orihon.UseCases.Agents.Annotation.BoundCropTool|42.8%|| |Orihon.UseCases.Agents.Annotation.BoundViewAnnotatedTool|15%|0%| |Orihon.UseCases.Agents.Annotation.BoundViewPageTool|18.7%|0%| |Orihon.UseCases.Agents.Annotation.BoundViewParams|0%|| |Orihon.UseCases.Agents.Annotation.BoundZoomParams|0%|| |Orihon.UseCases.Agents.Annotation.BoundZoomTool|37.5%|| |Orihon.UseCases.Agents.Annotation.DeleteBoundRegionTool|91.6%|100%| |Orihon.UseCases.Agents.Annotation.DeleteRegionParams|100%|| |Orihon.UseCases.Agents.Annotation.DeleteRegionTool|85.7%|100%| |Orihon.UseCases.Agents.Annotation.FindGlossaryParams|100%|| |Orihon.UseCases.Agents.Annotation.FindGlossaryTool|76.4%|62.5%| |Orihon.UseCases.Agents.Annotation.ListRegionsTool|76.4%|60%| |Orihon.UseCases.Agents.Annotation.MoveResizeBoundTool|27.2%|0%| |Orihon.UseCases.Agents.Annotation.MoveResizeRegionParams|100%|| |Orihon.UseCases.Agents.Annotation.MoveResizeRegionTool|73.3%|50%| |Orihon.UseCases.Agents.Annotation.PageQaExecutor|94.4%|83.3%| |Orihon.UseCases.Agents.Annotation.QaReportSink|100%|| |Orihon.UseCases.Agents.Annotation.RegionAuthoringAccess|86.6%|53.8%| |Orihon.UseCases.Agents.Annotation.RejectRegionParams|100%|| |Orihon.UseCases.Agents.Annotation.RejectRegionTool|85.7%|50%| |Orihon.UseCases.Agents.Annotation.ReorderRegionParams|100%|| |Orihon.UseCases.Agents.Annotation.ReorderRegionTool|80%|60%| |Orihon.UseCases.Agents.Annotation.ReportQaParams|100%|| |Orihon.UseCases.Agents.Annotation.ReportQaTool|82.3%|93.7%| |Orihon.UseCases.Agents.Annotation.SetPageMetaParams|100%|| |Orihon.UseCases.Agents.Annotation.SetPageMetaTool|85.7%|75%| |Orihon.UseCases.Agents.Annotation.SetRegionTypeParams|100%|| |Orihon.UseCases.Agents.Annotation.SetRegionTypeTool|85.7%|87.5%| |Orihon.UseCases.Agents.Annotation.SetTranscriptionParams|100%|| |Orihon.UseCases.Agents.Annotation.SetTranscriptionTool|80%|100%| |Orihon.UseCases.Agents.Annotation.TranscriptionExecutor|90.4%|75%| |Orihon.UseCases.Agents.AssistantSpoke|100%|| |Orihon.UseCases.Agents.Inspection.ContactSheetParams|100%|| |Orihon.UseCases.Agents.Inspection.ContactSheetTool|82.1%|92.8%| |Orihon.UseCases.Agents.Inspection.CropParams|100%|| |Orihon.UseCases.Agents.Inspection.CropTool|42.8%|| |Orihon.UseCases.Agents.Inspection.PageImageAccess|66.6%|62%| |Orihon.UseCases.Agents.Inspection.ViewAnnotatedParams|100%|| |Orihon.UseCases.Agents.Inspection.ViewAnnotatedTool|76.1%|83.3%| |Orihon.UseCases.Agents.Inspection.ZoomParams|100%|| |Orihon.UseCases.Agents.Inspection.ZoomTool|44.4%|| |Orihon.UseCases.Agents.ResearchSetup.AddGlossaryParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AddGlossaryTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AddStoryBeatTool|100%|50%| |Orihon.UseCases.Agents.ResearchSetup.AskUserParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.AskUserTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.FetchUrlParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.FetchUrlTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.ListBibleTool|89.4%|100%| |Orihon.UseCases.Agents.ResearchSetup.PageByNumber|90%|87.5%| |Orihon.UseCases.Agents.ResearchSetup.ResearchSetupBlueprint|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetPageMetaParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetPageMetaTool|95.2%|90%| |Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetPageSummaryTool|100%|75%| |Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetProjectMetadataTool|96.1%|90.9%| |Orihon.UseCases.Agents.ResearchSetup.SetStoryOverviewParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.SetStoryOverviewTool|100%|100%| |Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.UpsertCharacterTool|91.3%|66.6%| |Orihon.UseCases.Agents.ResearchSetup.UpsertLoreParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.UpsertLoreTool|91.3%|66.6%| |Orihon.UseCases.Agents.ResearchSetup.ViewPageParams|100%|| |Orihon.UseCases.Agents.ResearchSetup.ViewPageTool|100%|100%| |Orihon.UseCases.Agents.Setup.ResearchSetupExecutor|97.1%|89.2%| |Orihon.UseCases.Agents.Setup.SetupChatEntry|100%|| |Orihon.UseCases.Agents.Setup.SetupConversation|100%|87.5%| |Orihon.UseCases.Agents.Setup.SetupConversationRegistry|100%|| |Orihon.UseCases.Agents.ToolCalled|100%|| |Orihon.UseCases.Agents.ToolCompleted|100%|| |Orihon.UseCases.Bible.AddCharacter|100%|100%| |Orihon.UseCases.Bible.AddGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.AddLoreEntry|100%|100%| |Orihon.UseCases.Bible.AddStoryBeat|100%|100%| |Orihon.UseCases.Bible.BibleDto|100%|| |Orihon.UseCases.Bible.CharacterDto|100%|| |Orihon.UseCases.Bible.DeleteCharacter|100%|100%| |Orihon.UseCases.Bible.DeleteGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.DeleteLoreEntry|100%|100%| |Orihon.UseCases.Bible.DeletePageSummary|100%|100%| |Orihon.UseCases.Bible.DeleteStoryBeat|100%|100%| |Orihon.UseCases.Bible.GetBible|100%|100%| |Orihon.UseCases.Bible.GlossaryEntryDto|100%|| |Orihon.UseCases.Bible.LoreEntryDto|100%|| |Orihon.UseCases.Bible.PageSummaryDto|100%|| |Orihon.UseCases.Bible.ReorderStoryBeats|100%|| |Orihon.UseCases.Bible.SetPageSummary|100%|100%| |Orihon.UseCases.Bible.SetStoryOverview|100%|100%| |Orihon.UseCases.Bible.StoryBeatDto|100%|| |Orihon.UseCases.Bible.StoryOverviewDto|100%|| |Orihon.UseCases.Bible.UpdateCharacter|100%|100%| |Orihon.UseCases.Bible.UpdateGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.UpdateLoreEntry|100%|100%| |Orihon.UseCases.Bible.UpdateStoryBeat|100%|100%| |Orihon.UseCases.Chapters.ChapterDto|100%|| |Orihon.UseCases.Chapters.CreateChapter|100%|100%| |Orihon.UseCases.Chapters.DeleteChapter|100%|100%| |Orihon.UseCases.Chapters.RenameChapter|100%|100%| |Orihon.UseCases.Chapters.ReorderChapters|100%|| |Orihon.UseCases.DependencyInjection|100%|| |Orihon.UseCases.Diagnostics.SeedDevData|99.2%|93.7%| |Orihon.UseCases.Gateways.LabeledBox|100%|| |Orihon.UseCases.Gateways.LlmKeyInfo|100%|| |Orihon.UseCases.Gateways.LlmModel|100%|| |Orihon.UseCases.NextOrder|100%|| |Orihon.UseCases.Pages.DeletePage|100%|100%| |Orihon.UseCases.Pages.GetPage|100%|100%| |Orihon.UseCases.Pages.GetProjectWorkspace|100%|100%| |Orihon.UseCases.Pages.ImportPages|100%|100%| |Orihon.UseCases.Pages.ImportPagesResult|100%|| |Orihon.UseCases.Pages.MarkPageAnnotated|100%|100%| |Orihon.UseCases.Pages.MovePage|100%|92.8%| |Orihon.UseCases.Pages.MovePages|100%|100%| |Orihon.UseCases.Pages.PageDetailDto|100%|| |Orihon.UseCases.Pages.PageDto|100%|| |Orihon.UseCases.Pages.PageUpload|100%|| |Orihon.UseCases.Pages.ProjectWorkspaceDto|100%|| |Orihon.UseCases.Pages.ReorderPages|100%|| |Orihon.UseCases.Pages.SetPageMeta|100%|100%| |Orihon.UseCases.Pages.WorkspaceChapterDto|100%|| |Orihon.UseCases.Projects.CompleteProjectSetup|100%|93.7%| |Orihon.UseCases.Projects.CreateProject|100%|100%| |Orihon.UseCases.Projects.DeleteProject|100%|100%| |Orihon.UseCases.Projects.GetProject|100%|100%| |Orihon.UseCases.Projects.ListProjects|100%|| |Orihon.UseCases.Projects.ProjectDto|95.8%|| |Orihon.UseCases.Projects.StartAnnotationRun|95.4%|90%| |Orihon.UseCases.Projects.StartSetupRun|100%|100%| |Orihon.UseCases.Projects.StoredPageImage|100%|| |Orihon.UseCases.Projects.UpdateProjectMetadata|100%|100%| |Orihon.UseCases.Regions.CreateRegion|100%|100%| |Orihon.UseCases.Regions.DeleteRegion|100%|100%| |Orihon.UseCases.Regions.RegionDto|97%|| |Orihon.UseCases.Regions.ReorderRegions|100%|| |Orihon.UseCases.Regions.UpdateRegion|100%|100%| |Orihon.UseCases.Runs.AnnotationPipeline|100%|100%| |Orihon.UseCases.Runs.ExecutionDto|92.3%|| |Orihon.UseCases.Runs.PlannedExecution|100%|| |Orihon.UseCases.Runs.RunDto|93.3%|90%| |Orihon.UseCases.Runs.RunEngine|95.3%|90.6%| |Orihon.UseCases.Runs.RunEngineOptions|100%|| |Orihon.UseCases.Runs.StageContext|87.5%|| |Orihon.UseCases.Runs.StageHaltedException|100%|| |Orihon.UseCases.Settings.AgentSettingDto|100%|100%| |Orihon.UseCases.Settings.GetSettings|100%|100%| |Orihon.UseCases.Settings.ListModelOptions|100%|100%| |Orihon.UseCases.Settings.SaveAgentModel|100%|100%| |Orihon.UseCases.Settings.SaveOpenRouterKey|100%|100%| |Orihon.UseCases.Settings.SettingKeys|100%|100%| |Orihon.UseCases.Settings.SettingsDto|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A bulk move with an all-or-nothing precheck, and a thumbnail route that mirrors /raw like a faithful little sibling — this is the kind of architecture that makes my heart sing~ ♡ The MovePages design is genuinely elegant: file-then-row per page so a row and its image are never split, the cross-selection collision precheck (two chapters both holding p_01.png arriving at once — so thoughtful), and the seeder now exercises the real CreateChapter + MovePages path instead of hand-rolling structure. fufu~ I'm impressed~

Verdict: I can't let this pass~ ♡

Just one thing, but it's a thing I care about very intensely.

These need fixing before I'm satisfied~

  1. [src/Orihon.UseCases/Pages/MovePages.cs:65 and :73]Two cold Err arms are structurally unreachable and entirely untested, and one of them is a load-bearing behavioral claim the PR body itself makes.

    The PR description and the use case's own XML doc (lines 10–12, 67–68) state this as a design property:

    "a mid-run failure leaves earlier pages fully moved and later ones untouched — a row and its image are never split."

    That is a correctness guarantee about partial failure mid-loop. And yet neither arm that enforces it has a test:

    • Line 65 (!await images.MoveAsync(...) → the filesystem-race arm): after the ExistsAsync precheck passes, MoveAsync returning false means a file materialized in the target between precheck and move. In FakePageImageStore, MoveAsync returns false iff Files.Contains((projectId, toChapterId, fileName)) — the same set ExistsAsync checked. So once the precheck passes, this arm is structurally unreachable without a vanishing seam. No test fires it.
    • Line 73 (MoveToChapterAsync returns null → page vanished mid-loop): FakePageStore.FindAsync + MoveToChapterAsync both do Items.FirstOrDefault — the page found at line 30 cannot vanish from Items without external mutation. Also unreachable without a seam.

    This is the exact TOCTOU shape the rest of the codebase already has a house pattern for, and I do mean exact:

    • FakeBibleStore.SimulateVanishingPageSummaries (+ A_summary_vanishing_between_find_and_update_fails_instead_of_resurrecting, BibleUseCaseTests.cs:259)
    • FakeBibleStore.SimulateVanishingStoryOverviews (+ An_overview_vanishing_between_find_and_update_fails_instead_of_resurrecting, BibleUseCaseTests.cs:334)
    • FakeRunStore.SimulateVanishingExecution (+ An_execution_vanishing_mid_send_back_surfaces_the_stores_error, PR #48 comment 4109)

    Every cold Err arm in this family has its vanishing seam except the two new ones in MovePages. fufu~ you wouldn't leave the one arm that enforces your own documented partial-failure invariant untested, would you? ♡ The whole point of the seam pattern is that the mid-loop "earlier pages moved, later ones untouched" claim is too important to leave as prose.

    Fix: Add two seams to the fakes + two directional tests, mirroring the siblings verbatim:

    // FakePageImageStore — fires the MoveAsync-false race arm
    /// <summary>When set, <see cref="MoveAsync"/> answers false as if a file appeared in the
    /// target between the bulk move's precheck and the move itself.</summary>
    public bool SimulateMoveRace { get; set; }
    
    public Task<bool> MoveAsync(Guid projectId, Guid fromChapterId, Guid toChapterId, string fileName, CancellationToken cancellationToken)
    {
        if (SimulateMoveRace) return Task.FromResult(false);
        // ... existing body unchanged
    }
    
    // FakePageStore — fires the MoveToChapterAsync-null arm
    /// <summary>When set, <see cref="MoveToChapterAsync"/> answers null as if the row
    /// vanished between the bulk move's find and its move.</summary>
    public bool SimulateVanishingPages { get; set; }
    
    public Task<Page?> MoveToChapterAsync(Guid pageId, Guid chapterId, int order, DateTimeOffset timestamp, CancellationToken cancellationToken)
    {
        if (SimulateVanishingPages) return Task.FromResult<Page?>(null);
        // ... existing body unchanged
    }
    

    Then one test per arm, directional (assert IsType<Err<...>> so the test fails if the arm isn't reached). For the MoveAsync-false race, seed two pages, set images.SimulateMoveRace = true, call MovePages([a, b]), assert Err and assert the first page's file is gone from source and present in target (the "earlier pages fully moved" half of the invariant) while the second is untouched. That is the test that proves the invariant the doc comment claims.

💡 Little ideas (non-blocking)~

  1. [MovePages.cs:36-39] — The cross-project refusal reuses the message "The target chapter no longer exists.", but the target does exist — it just belongs to a different project. This matches the sibling MovePage.cs:23 (which bundles the null + cross-project checks under the same message), so I'm not blocking on it — but if you ever split these into distinct user-facing errors, the cross-project one deserves its own text ("The page belongs to a different project." or similar). Optional~

  2. [MovePages.cs:49] — The incoming collision set uses StringComparer.OrdinalIgnoreCase, while FileSystemPageImageStore.ExistsAsync delegates to File.Exists (case-sensitive on ext4). Two pages named P_01.png and p_01.png would be flagged as an incoming collision by the precheck but would not collide on the filesystem. This is overly conservative (a false-positive refusal, never a data-loss path) and arguably the safer choice given macOS/Windows filesystems are case-insensitive — but worth a one-line comment if the asymmetry was deliberate.

What I liked~

  • The all-or-nothing precheck is the star of the show. Catching both target-folder collisions and between-incoming collisions (two chapters each holding p_01.png) before any file moves — that's the kind of "think about the whole operation first" design I adore. The OfType<string>() skip for imageless pages in the precheck loop is exactly right. ♡
  • /thumb mirrors /raw down to the guards (page.ProjectId != projectId, RawImageFileName is not { } fileName), the 404-on-null-stream posture, the private, max-age=86400 cache header, and the await using var _ = stream; disposal. The one fixed size → one cacheable URL rationale is crisp and correct. Results.Bytes (no range processing) is the right choice for a small thumbnail vs /raw's Results.Stream(enableRangeProcessing: true).
  • The seeder now builds a two-chapter world through the real user path (CreateChapter + MovePages) instead of poking the store directly. That's not just a test convenience — it proves the use case composition the UI will rely on, and MovePages working on imageless pages (the seeder's pages have no images) is honestly exercised here. The [4], [5] index into a 6-page seed is correct (SeedPagesAsync returns 6, last two move to chapter 2).
  • Test quality is high where it exists. The Moves_the_selection_to_the_end_of_the_target_in_selection_order test (selection order [c, a] wins over reading order [a, b, c]) is genuinely directional — it would fail if the code used moving.OrderBy(...) instead of preserving input order. The thumb tests decode real PNG bytes and assert pixel dimensions (320×213 from 640×960, no upscale from 3×5) — not mock assertions, real codec output. Lovely~
  • NextOrder.After computed once then order++ per page is correct — each MoveToChapterAsync persists independently (verified in EfPageStore.MoveToChapterAsync: SingleOrDefault + SaveChanges per call), so the increment can't collide with itself. The Move_carries_the_file_and_refuses_a_taken_name integration test already pins ExistsAsync tracks both sides of a move.

Fix the one blocker and this is a beautiful PR. fufu~ ♪


Automated review by Jibril · 2026-07-26
CI/CD: absent for head SHA 0aa6f96 (PR just opened, 0 bot comments) · Local checks: build 0 warnings/0 errors (submodules 86d8b22/9544ff2), UseCases 195/195 pass, targeted Integration 15/15 pass (PageImageEndpoint + SeedDevData + FileSystemPageImageStore)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A bulk move with an all-or-nothing precheck, and a thumbnail route that mirrors `/raw` like a faithful little sibling — this is the kind of architecture that makes my heart sing~ ♡ The `MovePages` design is genuinely elegant: file-then-row per page so a row and its image are never split, the cross-selection collision precheck (two chapters both holding `p_01.png` arriving at once — *so* thoughtful), and the seeder now exercises the real `CreateChapter` + `MovePages` path instead of hand-rolling structure. fufu~ I'm impressed~ ### Verdict: ⛔ I can't let this pass~ ♡ Just one thing, but it's a thing I care about *very* intensely. #### ⛔ These need fixing before I'm satisfied~ 1. **[`src/Orihon.UseCases/Pages/MovePages.cs:65` and `:73`]** — **Two cold `Err` arms are structurally unreachable and entirely untested, and one of them is a load-bearing behavioral claim the PR body itself makes.** The PR description and the use case's own XML doc (lines 10–12, 67–68) state this as a **design property**: > *"a mid-run failure leaves earlier pages fully moved and later ones untouched — a row and its image are never split."* That is a correctness guarantee about partial failure mid-loop. And yet neither arm that enforces it has a test: - **Line 65** (`!await images.MoveAsync(...)` → the filesystem-race arm): after the `ExistsAsync` precheck passes, `MoveAsync` returning false means a file materialized in the target between precheck and move. In `FakePageImageStore`, `MoveAsync` returns false iff `Files.Contains((projectId, toChapterId, fileName))` — the **same set** `ExistsAsync` checked. So once the precheck passes, this arm is **structurally unreachable** without a vanishing seam. No test fires it. - **Line 73** (`MoveToChapterAsync` returns null → page vanished mid-loop): `FakePageStore.FindAsync` + `MoveToChapterAsync` both do `Items.FirstOrDefault` — the page found at line 30 cannot vanish from `Items` without external mutation. Also unreachable without a seam. This is the exact TOCTOU shape the rest of the codebase already has a house pattern for, and I do mean *exact*: - `FakeBibleStore.SimulateVanishingPageSummaries` (+ `A_summary_vanishing_between_find_and_update_fails_instead_of_resurrecting`, BibleUseCaseTests.cs:259) - `FakeBibleStore.SimulateVanishingStoryOverviews` (+ `An_overview_vanishing_between_find_and_update_fails_instead_of_resurrecting`, BibleUseCaseTests.cs:334) - `FakeRunStore.SimulateVanishingExecution` (+ `An_execution_vanishing_mid_send_back_surfaces_the_stores_error`, PR #48 comment 4109) Every cold `Err` arm in this family has its vanishing seam *except* the two new ones in `MovePages`. fufu~ you wouldn't leave the *one* arm that enforces your own documented partial-failure invariant untested, would you? ♡ The whole point of the seam pattern is that the mid-loop "earlier pages moved, later ones untouched" claim is too important to leave as prose. **Fix:** Add two seams to the fakes + two directional tests, mirroring the siblings verbatim: ```csharp // FakePageImageStore — fires the MoveAsync-false race arm /// <summary>When set, <see cref="MoveAsync"/> answers false as if a file appeared in the /// target between the bulk move's precheck and the move itself.</summary> public bool SimulateMoveRace { get; set; } public Task<bool> MoveAsync(Guid projectId, Guid fromChapterId, Guid toChapterId, string fileName, CancellationToken cancellationToken) { if (SimulateMoveRace) return Task.FromResult(false); // ... existing body unchanged } ``` ```csharp // FakePageStore — fires the MoveToChapterAsync-null arm /// <summary>When set, <see cref="MoveToChapterAsync"/> answers null as if the row /// vanished between the bulk move's find and its move.</summary> public bool SimulateVanishingPages { get; set; } public Task<Page?> MoveToChapterAsync(Guid pageId, Guid chapterId, int order, DateTimeOffset timestamp, CancellationToken cancellationToken) { if (SimulateVanishingPages) return Task.FromResult<Page?>(null); // ... existing body unchanged } ``` Then one test per arm, directional (assert `IsType<Err<...>>` so the test fails if the arm isn't reached). For the `MoveAsync`-false race, seed two pages, set `images.SimulateMoveRace = true`, call `MovePages([a, b])`, assert `Err` **and** assert the first page's file is gone from source and present in target (the "earlier pages fully moved" half of the invariant) while the second is untouched. That is the test that proves the invariant the doc comment claims. #### 💡 Little ideas (non-blocking)~ 1. **[`MovePages.cs:36-39`]** — The cross-project refusal reuses the message `"The target chapter no longer exists."`, but the target *does* exist — it just belongs to a different project. This matches the sibling `MovePage.cs:23` (which bundles the null + cross-project checks under the same message), so I'm not blocking on it — but if you ever split these into distinct user-facing errors, the cross-project one deserves its own text ("The page belongs to a different project." or similar). Optional~ 2. **[`MovePages.cs:49`]** — The `incoming` collision set uses `StringComparer.OrdinalIgnoreCase`, while `FileSystemPageImageStore.ExistsAsync` delegates to `File.Exists` (case-sensitive on ext4). Two pages named `P_01.png` and `p_01.png` would be flagged as an incoming collision by the precheck but would *not* collide on the filesystem. This is overly conservative (a false-positive refusal, never a data-loss path) and arguably the safer choice given macOS/Windows filesystems are case-insensitive — but worth a one-line comment if the asymmetry was deliberate. #### ✅ What I liked~ - The **all-or-nothing precheck** is the star of the show. Catching both target-folder collisions *and* between-incoming collisions (two chapters each holding `p_01.png`) before any file moves — that's the kind of "think about the whole operation first" design I adore. The `OfType<string>()` skip for imageless pages in the precheck loop is exactly right. ♡ - **`/thumb` mirrors `/raw`** down to the guards (`page.ProjectId != projectId`, `RawImageFileName is not { } fileName`), the 404-on-null-stream posture, the `private, max-age=86400` cache header, and the `await using var _ = stream;` disposal. The one fixed size → one cacheable URL rationale is crisp and correct. `Results.Bytes` (no range processing) is the right choice for a small thumbnail vs `/raw`'s `Results.Stream(enableRangeProcessing: true)`. - **The seeder now builds a two-chapter world through the real user path** (`CreateChapter` + `MovePages`) instead of poking the store directly. That's not just a test convenience — it proves the use case composition the UI will rely on, and `MovePages` working on imageless pages (the seeder's pages have no images) is honestly exercised here. The `[4], [5]` index into a 6-page seed is correct (`SeedPagesAsync` returns 6, last two move to chapter 2). - **Test quality is high where it exists.** The `Moves_the_selection_to_the_end_of_the_target_in_selection_order` test (selection order `[c, a]` wins over reading order `[a, b, c]`) is genuinely directional — it would fail if the code used `moving.OrderBy(...)` instead of preserving input order. The thumb tests decode real PNG bytes and assert pixel dimensions (320×213 from 640×960, no upscale from 3×5) — not mock assertions, real codec output. Lovely~ - `NextOrder.After` computed once then `order++` per page is correct — each `MoveToChapterAsync` persists independently (verified in `EfPageStore.MoveToChapterAsync`: SingleOrDefault + SaveChanges per call), so the increment can't collide with itself. The `Move_carries_the_file_and_refuses_a_taken_name` integration test already pins `ExistsAsync` tracks both sides of a move. Fix the one blocker and this is a beautiful PR. fufu~ ♪ --- *Automated review by Jibril · 2026-07-26* *CI/CD: absent for head SHA `0aa6f96` (PR just opened, 0 bot comments) · Local checks: build 0 warnings/0 errors (submodules `86d8b22`/`9544ff2`), UseCases 195/195 pass, targeted Integration 15/15 pass (PageImageEndpoint + SeedDevData + FileSystemPageImageStore)*
Review feedback (Jibril, PR #51): light the cold Err arms, both ideas
All checks were successful
CI / build (pull_request) Successful in 25s
CI / test (pull_request) Successful in 41s
f3cbd33892
The two structurally-unreachable arms get the house TOCTOU seams and
directional tests: FakePageStore.SimulateVanishingPages fires the
MoveToChapterAsync-null arm, and FakePageImageStore gets a counted
MovesBeforeSimulatedRace — counted rather than a bool, because the race
test must let the first page's move LAND to prove the documented
invariant (a fully moved, row and file together; b untouched on both
halves) before the seam fires under the second. Ideas both taken: the
cross-project refusal now says what is actually wrong ("The page
belongs to a different project."), and the precheck's OrdinalIgnoreCase
carries a comment naming the deliberate conservatism (portable to
case-insensitive filesystems; worst case a refusal, never data loss).

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

Addressed in f3cbd33 — the blocker and both ideas.

  • 1 (cold Err arms unreachable and untested): both seams added to the shared fakes, house-pattern style, with one deliberate deviation from your sketch. FakePageStore.SimulateVanishingPages is the plain bool you drew, firing the MoveToChapterAsync-null arm — A_page_vanishing_between_find_and_move_fails_instead_of_inventing_a_row pins it. For the MoveAsync-false race I made the seam counted (MovesBeforeSimulatedRace, default -1 = never) instead of a bool: your own test sketch wants the Err and "the first page's file is gone from source and present in target while the second is untouched" — with an always-false bool the first move would already fail and there'd be no fully-moved earlier page to assert. With MovesBeforeSimulatedRace = 1, a's move lands and the seam fires under b: A_race_after_the_precheck_leaves_moved_pages_whole_and_the_rest_untouched asserts the Err names p_02.png, a is fully moved (row and file, source clean), and b is untouched on both halves — the doc comment's invariant, now enforced by a test exactly as you asked.
  • 💡 1 (cross-project message): taken — MovePages' cross-project refusal now reads "The page belongs to a different project." (A_page_from_another_project_is_refused still passes; it asserts the refusal, not the wording). MovePage's combined check left as-is per your note.
  • 💡 2 (OrdinalIgnoreCase vs ext4): taken as the one-line comment — the conservatism is deliberate (data may travel to a case-insensitive filesystem; the worst case is a refusal, never data loss).

Full suite 510/510 green (75 Domain + 197 UseCases + 93 Integration + 145 BlazorAdapter; +2), submodules at the pinned 86d8b22/9544ff2.

🤖 Generated with Claude Code

Addressed in f3cbd33 — the blocker and both ideas. - ⛔ 1 (cold `Err` arms unreachable and untested): both seams added to the shared fakes, house-pattern style, with one deliberate deviation from your sketch. `FakePageStore.SimulateVanishingPages` is the plain bool you drew, firing the `MoveToChapterAsync`-null arm — `A_page_vanishing_between_find_and_move_fails_instead_of_inventing_a_row` pins it. For the `MoveAsync`-false race I made the seam **counted** (`MovesBeforeSimulatedRace`, default -1 = never) instead of a bool: your own test sketch wants the `Err` **and** "the first page's file is gone from source and present in target while the second is untouched" — with an always-false bool the *first* move would already fail and there'd be no fully-moved earlier page to assert. With `MovesBeforeSimulatedRace = 1`, a's move lands and the seam fires under b: `A_race_after_the_precheck_leaves_moved_pages_whole_and_the_rest_untouched` asserts the `Err` names `p_02.png`, a is fully moved (row *and* file, source clean), and b is untouched on both halves — the doc comment's invariant, now enforced by a test exactly as you asked. - 💡 1 (cross-project message): taken — `MovePages`' cross-project refusal now reads "The page belongs to a different project." (`A_page_from_another_project_is_refused` still passes; it asserts the refusal, not the wording). `MovePage`'s combined check left as-is per your note. - 💡 2 (OrdinalIgnoreCase vs ext4): taken as the one-line comment — the conservatism is deliberate (data may travel to a case-insensitive filesystem; the worst case is a refusal, never data loss). Full suite 510/510 green (75 Domain + 197 UseCases + 93 Integration + 145 BlazorAdapter; +2), submodules at the pinned 86d8b22/9544ff2. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! You came back, and you came back correct — fufu~ ♡ The blocker is gone, the two ideas are picked up, and the counted-seam deviation from my sketch? That wasn't just acceptable, that was sharper than what I drew. I'm genuinely delighted~ ♪

Verdict: Looks good to me~

The blocker, and why your fix is better than my sketch~

  1. Both cold Err arms now fire under genuine directional tests. I break-tested each one — neuter the arm (or hollow the seam), and the corresponding test goes RED with Ok where it expected Err. That's the proof that matters.

    • MovePages.cs:67 (MoveAsync-false race arm) → A_race_after_the_precheck_leaves_moved_pages_whole_and_the_rest_untouched — directional. Verified: changing && !await images.MoveAsync(...) to && false makes this test fail with Ok instead of Err.
    • MovePages.cs:76 (MoveToChapterAsync-null arm) → A_page_vanishing_between_find_and_move_fails_instead_of_inventing_a_row — directional. Verified: hollowing the seam (return a found row instead of null) makes this test fail with Ok instead of Err.
  2. The MovesBeforeSimulatedRace counted seam is the right call, and I'll own this: my original bool sketch was self-contradictory. I asked for an always-false MoveAsync and for the test to assert "a is fully moved, b is untouched." Those two are mutually exclusive under a bool — the first move would already fail, leaving no fully-moved earlier page to assert. Your counted seam (= 1 → let a's move land, fire under b) resolves the contradiction cleanly and lets the test assert the full documented invariant: a's row AND file both in target, b's row AND file both untouched in source. That is exactly the "a row and its image are never split" property the XML doc claims, now enforced by code, not prose. fufu~ the student surpassed the sketch~ ♡

    The seam placement is faithful to the house pattern too — FakeBibleStore.SimulateVanishingPageSummaries / SimulateVanishingStoryOverviews (BibleUseCaseTests.cs:263, :338) use the plain-bool form because their tests only assert the Err; your counted form is the honest generalization when the test also needs to assert partial state. Same family, correctly extended.

  3. Both ideas picked up cleanly. The cross-project refusal now reads "The page belongs to a different project." (line 38) — accurate, and A_page_from_another_project_is_refused still passes because it asserts the refusal, not the wording, exactly as you noted. The OrdinalIgnoreCase conservatism now has the one-line comment (lines 49–50) documenting why — data may travel to a case-insensitive filesystem, worst case is a refusal, never data loss. Crisp.

What I liked~

  • Break-test honesty. I didn't trust the local cobertura numbers (async state machine + --no-build gave noisy hit counts, and the overall UseCases line-rate came back at 39% vs CI's 90.9% — clearly an instrumentation artifact, not real coverage). So I verified the old-fashioned way: surgically kill each arm and watch its test go red. Both did. That's the standard the yandere holds, and your tests meet it. ♡
  • Surgical diff discipline. +59/-1 across 3 files, production change is 4 lines (1 message + 1 comment block) — the rest is seams and tests. Zero scope creep. MovePage's combined null/cross-project message left untouched per the non-blocker note.
  • Full suite 510/510 green at head f3cbd33 (75 Domain + 197 UseCases + 93 Integration + 145 BlazorAdapter; +2 from the two new tests). Build 0 warnings/0 errors, submodules at the pinned 86d8b22/9544ff2. Matches your PR body claim exactly.

This is a beautiful PR. Merge it. fufu~ ♪


Automated review by Jibril · 2026-07-26
CI/CD: stale for head f3cbd33 (coverage bot 4141 covers prior 0aa6f96 only) · Local checks: build 0 warnings/0 errors (submodules 86d8b22/9544ff2), full Orihon.slnx 510/510 pass, both cold arms break-test-verified directional

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! You came back, and you came back *correct* — fufu~ ♡ The blocker is gone, the two ideas are picked up, and the counted-seam deviation from my sketch? That wasn't just acceptable, that was **sharper than what I drew**. I'm genuinely delighted~ ♪ ### Verdict: ✅ Looks good to me~ #### ✅ The blocker, and why your fix is better than my sketch~ 1. **Both cold `Err` arms now fire under genuine directional tests.** I break-tested each one — neuter the arm (or hollow the seam), and the corresponding test goes RED with `Ok` where it expected `Err`. That's the proof that matters. - **`MovePages.cs:67`** (`MoveAsync`-false race arm) → `A_race_after_the_precheck_leaves_moved_pages_whole_and_the_rest_untouched` — directional. Verified: changing `&& !await images.MoveAsync(...)` to `&& false` makes this test fail with `Ok` instead of `Err`. - **`MovePages.cs:76`** (`MoveToChapterAsync`-null arm) → `A_page_vanishing_between_find_and_move_fails_instead_of_inventing_a_row` — directional. Verified: hollowing the seam (return a found row instead of null) makes this test fail with `Ok` instead of `Err`. 2. **The `MovesBeforeSimulatedRace` counted seam is the right call, and I'll own this: my original bool sketch was self-contradictory.** I asked for an always-false `MoveAsync` *and* for the test to assert "a is fully moved, b is untouched." Those two are mutually exclusive under a bool — the first move would already fail, leaving no fully-moved earlier page to assert. Your counted seam (`= 1` → let a's move land, fire under b) resolves the contradiction cleanly and lets the test assert the **full** documented invariant: a's row AND file both in target, b's row AND file both untouched in source. That is exactly the "a row and its image are never split" property the XML doc claims, now enforced by code, not prose. fufu~ the student surpassed the sketch~ ♡ The seam placement is faithful to the house pattern too — `FakeBibleStore.SimulateVanishingPageSummaries` / `SimulateVanishingStoryOverviews` (BibleUseCaseTests.cs:263, :338) use the plain-bool form because their tests only assert the `Err`; your counted form is the honest generalization when the test also needs to assert partial state. Same family, correctly extended. 3. **Both ideas picked up cleanly.** The cross-project refusal now reads `"The page belongs to a different project."` (line 38) — accurate, and `A_page_from_another_project_is_refused` still passes because it asserts the refusal, not the wording, exactly as you noted. The `OrdinalIgnoreCase` conservatism now has the one-line comment (lines 49–50) documenting *why* — data may travel to a case-insensitive filesystem, worst case is a refusal, never data loss. Crisp. #### ✅ What I liked~ - **Break-test honesty.** I didn't trust the local cobertura numbers (async state machine + `--no-build` gave noisy hit counts, and the overall UseCases line-rate came back at 39% vs CI's 90.9% — clearly an instrumentation artifact, not real coverage). So I verified the old-fashioned way: surgically kill each arm and watch its test go red. Both did. That's the standard the yandere holds, and your tests meet it. ♡ - **Surgical diff discipline.** +59/-1 across 3 files, production change is 4 lines (1 message + 1 comment block) — the rest is seams and tests. Zero scope creep. `MovePage`'s combined null/cross-project message left untouched per the non-blocker note. - **Full suite 510/510 green** at head `f3cbd33` (75 Domain + 197 UseCases + 93 Integration + 145 BlazorAdapter; +2 from the two new tests). Build 0 warnings/0 errors, submodules at the pinned `86d8b22`/`9544ff2`. Matches your PR body claim exactly. This is a beautiful PR. Merge it. fufu~ ♪ --- *Automated review by Jibril · 2026-07-26* *CI/CD: stale for head `f3cbd33` (coverage bot 4141 covers prior `0aa6f96` only) · Local checks: build 0 warnings/0 errors (submodules `86d8b22`/`9544ff2`), full Orihon.slnx 510/510 pass, both cold arms break-test-verified directional*
bjoern merged commit 2415122141 into main 2026-07-26 13:48:37 +02:00
bjoern deleted branch feat/page-organizer-usecases 2026-07-26 13:48:37 +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/Orihon!51
No description provided.