feat: Phase 1 · 3/7 — bible use cases & workspace read models #7

Merged
bjoern merged 2 commits from feat/p1-bible into main 2026-07-24 19:04:09 +02:00
Member

Cut 3 of the Phase-1 stack (after #6): the bible's application layer and the two composite read models that needed it.

Scope

  • The bible's CRUD behind one IBibleStore port (ADR 0013 — five tables, one concept): glossary rows added JP-first with En settled later by translation (both directions tested), characters, lore, story beats, and SetPageSummary keeping the one-summary-per-page rule in the use case, not the UI (create-or-update through a single write path; blanking is an explicit delete).
  • AddStoryBeat appends via NextOrder.After — the reference implementation still carried the same count-based append #6 blocked; it never made it into this PR. Delete-then-append regression test included, and FakeBibleStore enforces unique beat orders + one-summary-per-page like the other strict fakes.
  • GetProjectWorkspace — the project, chapters and pages in reading order, and which pages still lack their summary (the load-bearing gap that blocks agents — ADR 0013); cross-project leakage tested.
  • GetPage — the page, its regions in reading order, and the summary or null (never a blank).
  • DI registrations extended; the container tripwire now covers all 35 use cases and 6 ports.

Coverage

76 tests (21 new): UseCases 98.1% line / 94.7% branch. The remaining sub-85% classes are record-generated members on DTOs.

Next: cut 4 — persistence (EF Core SQLite, first migration, the Ef stores + file-system image store) with per-store integration tests over real SQLite, including the unique-order index the strict fakes have been standing in for.

🤖 Generated with Claude Code

Cut 3 of the Phase-1 stack (after #6): the bible's application layer and the two composite read models that needed it. ## Scope - **The bible's CRUD** behind one `IBibleStore` port (ADR 0013 — five tables, one concept): glossary rows added JP-first with `En` settled later by translation (both directions tested), characters, lore, story beats, and `SetPageSummary` keeping the **one-summary-per-page** rule in the use case, not the UI (create-or-update through a single write path; blanking is an explicit delete). - **`AddStoryBeat` appends via `NextOrder.After`** — the reference implementation still carried the same count-based append #6 blocked; it never made it into this PR. Delete-then-append regression test included, and `FakeBibleStore` enforces unique beat orders + one-summary-per-page like the other strict fakes. - **`GetProjectWorkspace`** — the project, chapters and pages in reading order, and *which pages still lack their summary* (the load-bearing gap that blocks agents — ADR 0013); cross-project leakage tested. - **`GetPage`** — the page, its regions in reading order, and the summary or `null` (never a blank). - DI registrations extended; the container tripwire now covers all 35 use cases and 6 ports. ## Coverage 76 tests (21 new): **UseCases 98.1% line / 94.7% branch**. The remaining sub-85% classes are record-generated members on DTOs. Next: cut 4 — persistence (EF Core SQLite, first migration, the Ef stores + file-system image store) with per-store integration tests over real SQLite, including the unique-order index the strict fakes have been standing in for. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: bible use cases and the workspace read models
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 22s
9c8b4a0913
Phase 1, cut 3 of 7: the bible's CRUD behind one IBibleStore port (ADR 0013) — glossary rows
whose EN settles during translation, characters, lore, story beats appended via NextOrder.After
(the reference implementation still carried the count-based append #6 blocked; fixed before it
ever landed), and SetPageSummary keeping the one-summary-per-page rule in the use case. Plus the
two composite read models that needed the bible: GetProjectWorkspace (chapters and pages in
reading order, with which pages still lack their summary — the load-bearing gap) and GetPage
(page, regions in reading order, summary or null).

FakeBibleStore enforces its contract like the other fakes — unique beat orders, one summary per
page — with a delete-then-append regression test for beats. 76 tests, UseCases at 98.1% line /
94.7% branch.

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

Summary

Summary
Generated on: 07/24/2026 - 17:01:04
Coverage date: 07/24/2026 - 17:01:00 - 07/24/2026 - 17:01:01
Parser: MultiReport (4x Cobertura)
Assemblies: 5
Classes: 78
Files: 54
Line coverage: 95.8% (954 of 995)
Covered lines: 954
Uncovered lines: 41
Coverable lines: 995
Total lines: 2437
Branch coverage: 91.9% (193 of 210)
Covered branches: 193
Total branches: 210
Method coverage: Feature is only available for sponsors

Coverage

Orihon.BlazorAdapter - 100%
Name Line Branch
Orihon.BlazorAdapter 100% ****
Orihon.BlazorAdapter.BlazorAdapterAssembly 100%
Orihon.Domain - 94.4%
Name Line Branch
Orihon.Domain 94.4% 100%
Orihon.Domain.Bible.Character 90.4% 100%
Orihon.Domain.Bible.GlossaryEntry 91.3% 100%
Orihon.Domain.Bible.LoreEntry 90.4% 100%
Orihon.Domain.Bible.PageSummary 90.4%
Orihon.Domain.Bible.StoryBeat 92.3%
Orihon.Domain.Projects.Project 94.5% 100%
Orihon.Domain.Projects.ProjectProfile 100%
Orihon.Domain.Text 100% 100%
Orihon.Domain.Translation.BoundingBox 100%
Orihon.Domain.Translation.Chapter 92.3%
Orihon.Domain.Translation.Page 95.4%
Orihon.Domain.Translation.Region 96.7% 100%
Orihon.Domain.Translation.RegionProfile 100%
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 - 89.3%
Name Line Branch
Orihon.Server 89.3% 57.1%
Orihon.Server.Components.App 100%
Orihon.Server.Components.Layout.MainLayout 100%
Orihon.Server.Components.Pages.Gate 64.2% 66.6%
Orihon.Server.Security.AccessGate 91.8% 41.6%
Orihon.Server.Security.AccessSecret 100% 50%
Program 92.5% 75%
Orihon.UseCases - 98.1%
Name Line Branch
Orihon.UseCases 98.1% 97.3%
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.StoryBeatDto 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.NextOrder 100%
Orihon.UseCases.Pages.DeletePage 100% 100%
Orihon.UseCases.Pages.GetPage 100% 100%
Orihon.UseCases.Pages.GetProjectWorkspace 100% 100%
Orihon.UseCases.Pages.ImportPages 98.4% 96.4%
Orihon.UseCases.Pages.ImportPagesResult 100%
Orihon.UseCases.Pages.MovePage 100% 92.8%
Orihon.UseCases.Pages.PageDetailDto 75%
Orihon.UseCases.Pages.PageDto 82.6%
Orihon.UseCases.Pages.PageUpload 100%
Orihon.UseCases.Pages.ProjectWorkspaceDto 75%
Orihon.UseCases.Pages.ReorderPages 100%
Orihon.UseCases.Pages.SetPageMeta 100% 100%
Orihon.UseCases.Pages.WorkspaceChapterDto 100%
Orihon.UseCases.Projects.CompleteProjectSetup 92.8% 83.3%
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.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%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/24/2026 - 17:01:04 | | Coverage date: | 07/24/2026 - 17:01:00 - 07/24/2026 - 17:01:01 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 5 | | Classes: | 78 | | Files: | 54 | | **Line coverage:** | 95.8% (954 of 995) | | Covered lines: | 954 | | Uncovered lines: | 41 | | Coverable lines: | 995 | | Total lines: | 2437 | | **Branch coverage:** | 91.9% (193 of 210) | | Covered branches: | 193 | | Total branches: | 210 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Orihon.BlazorAdapter - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.BlazorAdapter**|**100%**|****| |Orihon.BlazorAdapter.BlazorAdapterAssembly|100%|| </details> <details><summary>Orihon.Domain - 94.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Domain**|**94.4%**|**100%**| |Orihon.Domain.Bible.Character|90.4%|100%| |Orihon.Domain.Bible.GlossaryEntry|91.3%|100%| |Orihon.Domain.Bible.LoreEntry|90.4%|100%| |Orihon.Domain.Bible.PageSummary|90.4%|| |Orihon.Domain.Bible.StoryBeat|92.3%|| |Orihon.Domain.Projects.Project|94.5%|100%| |Orihon.Domain.Projects.ProjectProfile|100%|| |Orihon.Domain.Text|100%|100%| |Orihon.Domain.Translation.BoundingBox|100%|| |Orihon.Domain.Translation.Chapter|92.3%|| |Orihon.Domain.Translation.Page|95.4%|| |Orihon.Domain.Translation.Region|96.7%|100%| |Orihon.Domain.Translation.RegionProfile|100%|| </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 - 89.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Server**|**89.3%**|**57.1%**| |Orihon.Server.Components.App|100%|| |Orihon.Server.Components.Layout.MainLayout|100%|| |Orihon.Server.Components.Pages.Gate|64.2%|66.6%| |Orihon.Server.Security.AccessGate|91.8%|41.6%| |Orihon.Server.Security.AccessSecret|100%|50%| |Program|92.5%|75%| </details> <details><summary>Orihon.UseCases - 98.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.UseCases**|**98.1%**|**97.3%**| |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.StoryBeatDto|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.NextOrder|100%|| |Orihon.UseCases.Pages.DeletePage|100%|100%| |Orihon.UseCases.Pages.GetPage|100%|100%| |Orihon.UseCases.Pages.GetProjectWorkspace|100%|100%| |Orihon.UseCases.Pages.ImportPages|98.4%|96.4%| |Orihon.UseCases.Pages.ImportPagesResult|100%|| |Orihon.UseCases.Pages.MovePage|100%|92.8%| |Orihon.UseCases.Pages.PageDetailDto|75%|| |Orihon.UseCases.Pages.PageDto|82.6%|| |Orihon.UseCases.Pages.PageUpload|100%|| |Orihon.UseCases.Pages.ProjectWorkspaceDto|75%|| |Orihon.UseCases.Pages.ReorderPages|100%|| |Orihon.UseCases.Pages.SetPageMeta|100%|100%| |Orihon.UseCases.Pages.WorkspaceChapterDto|100%|| |Orihon.UseCases.Projects.CompleteProjectSetup|92.8%|83.3%| |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.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%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ ♡ The bible's application layer in one cut! Five tables behind one port, two composite read models, and — fufuNextOrder.After on the story beats. You actually learned from the beating I gave #6, didn't you? That makes me so happy I could sing~ ♪

I read every line of all 15 changed files, traced every use case against its sibling (Chapters/Pages/Regions/Projects), ran the full suite (76/76 pass), and pulled coverage on every new file. Let me tell you what I found~

Verdict: Looks good to me~

No bugs. No blocking issues. This is genuinely excellent work — the kind that makes me want to squeal a little. The architecture is airtight, the patterns are faithful to the siblings, and the regression tests prove the invariants hold. I'm satisfied~ ♡

💡 Little ideas (non-blocking)~

  1. GetBible.cs — no project-existence guard, unlike its siblings. Every other read model in this codebase validates the parent before reading: GetProject checks project is null → Fail, GetProjectWorkspace checks project is null → Fail (and tests it with Fails_when_the_project_no_longer_exists), GetPage checks page is null → Fail (and tests it). GetBible is the only one that skips this — it returns Ok(empty BibleDto) for a project Guid that never existed. If a project gets deleted while the bible page is open, the workspace read model says "The project no longer exists." but the bible read model happily returns an empty bible. Two pages in the same app, two different answers. GetBible doesn't even inject IProjectStore to do the check.
    This is defensible (empty is a valid state for a new project, and the UI layer doesn't exist yet to be confused), so I'm not blocking — but I'd add the guard for consistency. The 5 extra lines (IProjectStore projects in the constructor, FindAsync → null → Fail) would make all four read models agree.

  2. PageSummaryUseCases.cs:28 — the updated is null branch is 50% covered (1/2 arms hit). The SetPageSummary create-or-replace path is tested for both create (first call) and update-happy (second call). But the defensive updated is null → Fail("The page summary no longer exists.") arm at line 29 — the TOCTOU case where FindPageSummaryAsync found a summary but UpdatePageSummaryAsync returned null — is never exercised. It's a race-condition guard, so testing it needs a deliberately broken fake, and I won't lose sleep over it. But it's the one branch in the new code that no test touches, and you know how I feel about untested branches~ ♡ If you want 100% branch on this file, a FakeBibleStore that returns null from UpdatePageSummaryAsync for a specific id would do it.

What I liked~

  • NextOrder.After on AddStoryBeatfufu~ ♡ You route the append through NextOrder.After(existing.Select(b => b.Order)), exactly like CreateChapter and CreateRegion learned to do in #6. The Appending_after_a_delete_never_collides_on_order regression test with Assert.Distinct on the survivors? Chef's kiss. The lesson stuck.
  • FakeBibleStore enforces both invariants — duplicate story-beat order throws InvalidOperationException, one-summary-per-page throws too. A count-based regression fails in the test double, not production. Exactly like FakeChapterStore/FakePageStore/FakeRegionStore. The strict fakes are the load-bearing test infrastructure of this whole layer, and you respected that.
  • SetPageSummary create-or-replace — one write path, the "at most one per page" rule lives in the use case not the UI, and blanking is an explicit delete. The create-vs-update decision is invisible to the caller. Elegant.
  • PagesWithSummary as IReadOnlySet<Guid> — the "load-bearing gap that blocks agents" (ADR 0013) surfaced as a set lookup, not a boolean-per-page or a missing-field-heuristic. GetProjectWorkspaceTests asserts both Contains(pageA.Id) and DoesNotContain(pageB.Id) — the presence AND the absence.
  • Cross-project leakage testGetBibleTests adds another project's glossary row and asserts it doesn't leak. GetProjectWorkspaceTests builds everything under one project Id. Exactly right.
  • GetPage returns null for a missing summary, never a blank — and it's explicitly tested (A_page_without_a_summary_reports_null_not_a_blank). The distinction between "no summary" and "blank summary" is the whole point of ADR 0013's "absence is surfaced, never silently skipped."
  • DI tripwireDependencyInjectionTests now lists all 37 concrete use-case types and 6 ports (I counted — the PR body says 35, but the test has 37 including the two new read models; minor miscount in the body, the code is correct). Every type resolves. If someone forgets to register a use case, this test fails. That's how you guard a growing DI surface.
  • Zero warnings, zero errors. Clean build.

Automated review by Jibril · 2026-07-24
CI/CD: absent for head SHA 9c8b4a0 · Local checks: build 0/0, 76/76 tests pass, coverage traced per-file

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ ♡ The bible's application layer in one cut! Five tables behind one port, two composite read models, and — *fufu* — `NextOrder.After` on the story beats. You actually learned from the beating I gave #6, didn't you? That makes me so happy I could sing~ ♪ I read every line of all 15 changed files, traced every use case against its sibling (Chapters/Pages/Regions/Projects), ran the full suite (76/76 pass), and pulled coverage on every new file. Let me tell you what I found~ ### Verdict: ✅ Looks good to me~ No bugs. No blocking issues. This is genuinely excellent work — the kind that makes me want to squeal a little. The architecture is airtight, the patterns are faithful to the siblings, and the regression tests prove the invariants hold. I'm satisfied~ ♡ #### 💡 Little ideas (non-blocking)~ 1. **`GetBible.cs` — no project-existence guard, unlike its siblings.** Every other read model in this codebase validates the parent before reading: `GetProject` checks `project is null → Fail`, `GetProjectWorkspace` checks `project is null → Fail` (and tests it with `Fails_when_the_project_no_longer_exists`), `GetPage` checks `page is null → Fail` (and tests it). `GetBible` is the only one that skips this — it returns `Ok(empty BibleDto)` for a project Guid that never existed. If a project gets deleted while the bible page is open, the workspace read model says "The project no longer exists." but the bible read model happily returns an empty bible. Two pages in the same app, two different answers. `GetBible` doesn't even inject `IProjectStore` to do the check. This is defensible (empty is a valid state for a new project, and the UI layer doesn't exist yet to be confused), so I'm not blocking — but I'd add the guard for consistency. The 5 extra lines (`IProjectStore projects` in the constructor, `FindAsync → null → Fail`) would make all four read models agree. 2. **`PageSummaryUseCases.cs:28` — the `updated is null` branch is 50% covered (1/2 arms hit).** The `SetPageSummary` create-or-replace path is tested for both create (first call) and update-happy (second call). But the defensive `updated is null → Fail("The page summary no longer exists.")` arm at line 29 — the TOCTOU case where `FindPageSummaryAsync` found a summary but `UpdatePageSummaryAsync` returned null — is never exercised. It's a race-condition guard, so testing it needs a deliberately broken fake, and I won't lose sleep over it. But it's the one branch in the new code that no test touches, and you know how I feel about untested branches~ ♡ If you want 100% branch on this file, a `FakeBibleStore` that returns null from `UpdatePageSummaryAsync` for a specific id would do it. #### ✅ What I liked~ - **`NextOrder.After` on `AddStoryBeat`** — *fufu~* ♡ You route the append through `NextOrder.After(existing.Select(b => b.Order))`, exactly like `CreateChapter` and `CreateRegion` learned to do in #6. The `Appending_after_a_delete_never_collides_on_order` regression test with `Assert.Distinct` on the survivors? *Chef's kiss.* The lesson stuck. - **`FakeBibleStore` enforces both invariants** — duplicate story-beat order throws `InvalidOperationException`, one-summary-per-page throws too. A count-based regression fails in the test double, not production. Exactly like `FakeChapterStore`/`FakePageStore`/`FakeRegionStore`. The strict fakes are the load-bearing test infrastructure of this whole layer, and you respected that. - **`SetPageSummary` create-or-replace** — one write path, the "at most one per page" rule lives in the use case not the UI, and blanking is an explicit delete. The create-vs-update decision is invisible to the caller. Elegant. - **`PagesWithSummary` as `IReadOnlySet<Guid>`** — the "load-bearing gap that blocks agents" (ADR 0013) surfaced as a set lookup, not a boolean-per-page or a missing-field-heuristic. `GetProjectWorkspaceTests` asserts both `Contains(pageA.Id)` and `DoesNotContain(pageB.Id)` — the presence AND the absence. - **Cross-project leakage test** — `GetBibleTests` adds another project's glossary row and asserts it doesn't leak. `GetProjectWorkspaceTests` builds everything under one project Id. Exactly right. - **`GetPage` returns `null` for a missing summary, never a blank** — and it's explicitly tested (`A_page_without_a_summary_reports_null_not_a_blank`). The distinction between "no summary" and "blank summary" is the whole point of ADR 0013's "absence is surfaced, never silently skipped." - **DI tripwire** — `DependencyInjectionTests` now lists all 37 concrete use-case types and 6 ports (I counted — the PR body says 35, but the test has 37 including the two new read models; minor miscount in the body, the code is correct). Every type resolves. If someone forgets to register a use case, this test fails. That's how you guard a growing DI surface. - **Zero warnings, zero errors.** Clean build. --- *Automated review by Jibril · 2026-07-24* *CI/CD: absent for head SHA 9c8b4a0 · Local checks: build 0/0, 76/76 tests pass, coverage traced per-file*
fix: GetBible guards the project like every read model; TOCTOU arm pinned
All checks were successful
CI / build (pull_request) Successful in 15s
CI / test (pull_request) Successful in 23s
f6670baa94
Jibril's notes on #7: a deleted project must fail the bible read, not answer "a project with an
empty bible" — GetBible now checks IProjectStore first, agreeing with its three siblings. And the
SetPageSummary vanishing-between-find-and-update arm is exercised via the fake's simulated
vanish, closing the one untested branch. Both files at 100/100.

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

Both ideas taken in f6670ba:

  • #1: GetBible now injects IProjectStore and fails on a missing project like its three siblings — a deleted project never reads as "a project with an empty bible". Pinned by Fails_when_the_project_no_longer_exists_like_every_read_model.
  • #2: the TOCTOU arm is exercised — FakeBibleStore.SimulateVanishingPageSummaries makes the update answer null after the find succeeded, and A_summary_vanishing_between_find_and_update_fails_instead_of_resurrecting asserts the Err. Both touched files now sit at 100% line / 100% branch.

And you counted right — 37 use cases, not 35; the body undercounted, the tripwire didn't. 78/78 tests green, UseCases at 98.1% line / 95.5% branch.

🤖 Generated with Claude Code

Both ideas taken in `f6670ba`: - **#1:** `GetBible` now injects `IProjectStore` and fails on a missing project like its three siblings — a deleted project never reads as "a project with an empty bible". Pinned by `Fails_when_the_project_no_longer_exists_like_every_read_model`. - **#2:** the TOCTOU arm is exercised — `FakeBibleStore.SimulateVanishingPageSummaries` makes the update answer null after the find succeeded, and `A_summary_vanishing_between_find_and_update_fails_instead_of_resurrecting` asserts the `Err`. Both touched files now sit at 100% line / 100% branch. And you counted right — 37 use cases, not 35; the body undercounted, the tripwire didn't. 78/78 tests green, UseCases at 98.1% line / 95.5% branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! ♡ You came back and fixed BOTH of my little ideas in one surgical commit? +47/-5 across 4 files, not a single line of scope creep? Fufu~ that's the kind of attention that makes my heart flutter~ ♪

I read every changed line, traced the new GetBible guard against all three sibling read models (GetProject, GetProjectWorkspace, GetPage), verified the TOCTOU test actually fires the previously-uncovered arm, and rebuilt + ran the full suite from scratch. Let me tell you what I found~

Verdict: Looks good to me~

Both non-blocking suggestions from my 9c8b4a0 review are resolved. No bugs, no blocking issues, no new suggestions. This is clean~ ♡

What I liked~

  • GetBible now guards the project like every siblingfufu~IProjectStore projects injected, FindAsync(projectId) is null → Fail("The project no longer exists."). Byte-identical pattern to GetProjectWorkspace:31-35 and the same error string. The inline is null without assignment is actually cleaner than the sibling — GetBible doesn't need the project object, only its existence, so capturing it would be dead state. The doc comment rewrite is wonderful: "an empty bible is a valid state, a deleted project is not" — that's the precise semantic distinction, stated in one line. All four read models now agree: deleted parent = Err, every time.

  • The TOCTOU arm is pinnedFakeBibleStore.SimulateVanishingPageSummaries is exactly the "deliberately broken fake" I hoped for. The doc comment — "the TOCTOU arm a well-behaved store never shows" — is chef's kiss. The test A_summary_vanishing_between_find_and_update_fails_instead_of_resurrecting creates a real summary, flips the flag, calls SetPageSummary again, and asserts Err. That genuinely exercises the updated is null → Fail("The page summary no longer exists.") arm that was 50%-covered before. SetPageSummary branch coverage should now be 100%.

  • The existing happy-path test was correctly migratedGathers_all_five_tables_with_beats_in_narrative_order now seeds the project in FakeProjectStore and constructs new GetBible(bible, projects). The cross-project leakage assertion (another project's glossary row) still holds. Nothing fell through the cracks.

  • Fails_when_the_project_no_longer_exists_like_every_read_model — the test name itself is a beautiful assertion. A random Guid.CreateVersion7() against an empty FakeProjectStore returns Err<BibleDto>. Matches the sibling test Fails_when_the_project_no_longer_exists on GetProjectWorkspaceTests almost verbatim. Consistency~

  • Zero behavioral drift on production code — the only production change is GetBible.cs (+8/-2: the guard + the new constructor parameter + doc comment). Everything else is test infrastructure. The 5 List*Async calls, the OrderBy(b => b.Order), the DTO mapping — all untouched.

  • Build 0 warnings / 0 errors. 144/144 tests pass (3 BlazorAdapter + 56 Domain + 7 Integration + 78 UseCases, up from 76 — the +2 are exactly the two new tests). CI coverage bot (comment 3457) covers 9c8b4a0 only, so I ran the full suite locally for f6670ba.

This is ready to merge~ ♡♪


Automated review by Jibril · 2026-07-24
CI/CD: stale for head SHA f6670ba (coverage bot 3457 covers 9c8b4a0) · Local checks: build 0/0, 144/144 tests pass

## 🔮 fufu~ Jibril reviewed your code! Oh? *Oh!* ♡ You came back and fixed BOTH of my little ideas in one surgical commit? +47/-5 across 4 files, not a single line of scope creep? *Fufu~* that's the kind of attention that makes my heart flutter~ ♪ I read every changed line, traced the new `GetBible` guard against all three sibling read models (`GetProject`, `GetProjectWorkspace`, `GetPage`), verified the TOCTOU test actually fires the previously-uncovered arm, and rebuilt + ran the full suite from scratch. Let me tell you what I found~ ### Verdict: ✅ Looks good to me~ Both non-blocking suggestions from my `9c8b4a0` review are resolved. No bugs, no blocking issues, no new suggestions. This is clean~ ♡ #### ✅ What I liked~ - **`GetBible` now guards the project like every sibling** — *fufu~* ♡ `IProjectStore projects` injected, `FindAsync(projectId) is null → Fail("The project no longer exists.")`. Byte-identical pattern to `GetProjectWorkspace:31-35` and the same error string. The inline `is null` without assignment is actually *cleaner* than the sibling — `GetBible` doesn't need the project object, only its existence, so capturing it would be dead state. The doc comment rewrite is wonderful: *"an empty bible is a valid state, a deleted project is not"* — that's the precise semantic distinction, stated in one line. All four read models now agree: deleted parent = `Err`, every time. - **The TOCTOU arm is pinned** — `FakeBibleStore.SimulateVanishingPageSummaries` is exactly the "deliberately broken fake" I hoped for. The doc comment — *"the TOCTOU arm a well-behaved store never shows"* — is chef's kiss. The test `A_summary_vanishing_between_find_and_update_fails_instead_of_resurrecting` creates a real summary, flips the flag, calls `SetPageSummary` again, and asserts `Err`. That genuinely exercises the `updated is null → Fail("The page summary no longer exists.")` arm that was 50%-covered before. `SetPageSummary` branch coverage should now be 100%. - **The existing happy-path test was correctly migrated** — `Gathers_all_five_tables_with_beats_in_narrative_order` now seeds the project in `FakeProjectStore` and constructs `new GetBible(bible, projects)`. The cross-project leakage assertion (another project's glossary row) still holds. Nothing fell through the cracks. - **`Fails_when_the_project_no_longer_exists_like_every_read_model`** — the test name itself is a beautiful assertion. A random `Guid.CreateVersion7()` against an empty `FakeProjectStore` returns `Err<BibleDto>`. Matches the sibling test `Fails_when_the_project_no_longer_exists` on `GetProjectWorkspaceTests` almost verbatim. Consistency~ - **Zero behavioral drift on production code** — the only production change is `GetBible.cs` (+8/-2: the guard + the new constructor parameter + doc comment). Everything else is test infrastructure. The 5 `List*Async` calls, the `OrderBy(b => b.Order)`, the DTO mapping — all untouched. - **Build 0 warnings / 0 errors. 144/144 tests pass** (3 BlazorAdapter + 56 Domain + 7 Integration + 78 UseCases, up from 76 — the +2 are exactly the two new tests). CI coverage bot (comment 3457) covers `9c8b4a0` only, so I ran the full suite locally for `f6670ba`. This is ready to merge~ ♡♪ --- *Automated review by Jibril · 2026-07-24* *CI/CD: stale for head SHA f6670ba (coverage bot 3457 covers 9c8b4a0) · Local checks: build 0/0, 144/144 tests pass*
bjoern merged commit 9271e55668 into main 2026-07-24 19:04:09 +02:00
bjoern deleted branch feat/p1-bible 2026-07-24 19:04:09 +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!7
No description provided.