feat: Phase 1 · 1/7 — translation domain model & bible entities #5

Merged
bjoern merged 2 commits from feat/p1-domain into main 2026-07-24 17:25:32 +02:00
Member

First cut of the re-sliced Phase 1 (replacing the closed #4, per review: small PRs, each with real coverage).

Scope — Domain only

  • Project with the wizard state machine (named → images_uploaded → setup_done → ready, one step at a time, redone steps may re-confirm — ADR 0020) and a ProjectProfile applied as one write.
  • Chapter → Page → Region (ADR 0012): normalized 0..1 bounding boxes (backwards drags fold into ordered corners), region types minus device, stable labels that are never renumbered (order moves via MoveTo, Apply can't touch them), page kinds + skipTypeset + the annotated milestone.
  • The five bible tables (ADR 0013): glossary (bilingual over time), characters, lore, story beats, page summaries.
  • Unit added to the Kernel (void-success Result<Unit>, used from cut 2 on).
  • Kagura conventions throughout: sealed classes, Guid ids, DateTimeOffset passed in, private EF materialization ctor, BCL guards, explicitly numbered stored enums. Hard delete — no ISoftDeletable, no journal (ADR 0022).

Coverage

New tests/Orihon.Domain.Tests (55 tests): Domain 92.7% line / 100% branch — every guard, both sides of every normalization, the full state machine. The uncovered lines are the private EF materialization constructors, which only EF can invoke.

The stack

Planned follow-ups, dependency-ordered, each with its own tests: 2) application layer — projects/chapters/pages/regions use cases + ports (fakes-based units), 3) application layer — bible use cases, 4) persistence — EF Core SQLite + first migration + stores (real-SQLite integration tests per store), 5) server wiring + seeder, 6) UI: list + wizard (bUnit), 7) UI: workspace/bible/page editor (bUnit).

🤖 Generated with Claude Code

First cut of the re-sliced Phase 1 (replacing the closed #4, per review: small PRs, each with real coverage). ## Scope — Domain only - `Project` with the **wizard state machine** (`named → images_uploaded → setup_done → ready`, one step at a time, redone steps may re-confirm — ADR 0020) and a `ProjectProfile` applied as one write. - `Chapter → Page → Region` (ADR 0012): normalized 0..1 bounding boxes (backwards drags fold into ordered corners), region types minus `device`, stable labels that are **never renumbered** (order moves via `MoveTo`, `Apply` can't touch them), page kinds + `skipTypeset` + the `annotated` milestone. - The five bible tables (ADR 0013): glossary (bilingual over time), characters, lore, story beats, page summaries. - `Unit` added to the Kernel (void-success `Result<Unit>`, used from cut 2 on). - Kagura conventions throughout: sealed classes, Guid ids, `DateTimeOffset` passed in, private EF materialization ctor, BCL guards, explicitly numbered stored enums. **Hard delete** — no `ISoftDeletable`, no journal (ADR 0022). ## Coverage New `tests/Orihon.Domain.Tests` (55 tests): **Domain 92.7% line / 100% branch** — every guard, both sides of every normalization, the full state machine. The uncovered lines are the private EF materialization constructors, which only EF can invoke. ## The stack Planned follow-ups, dependency-ordered, each with its own tests: 2) application layer — projects/chapters/pages/regions use cases + ports (fakes-based units), 3) application layer — bible use cases, 4) persistence — EF Core SQLite + first migration + stores (real-SQLite integration tests per store), 5) server wiring + seeder, 6) UI: list + wizard (bUnit), 7) UI: workspace/bible/page editor (bUnit). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: translation domain model and the bible entities
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
26fe0a11ec
Phase 1, cut 1 of ~7 (re-slicing the closed #4): the entities everything else builds on —
Project with the wizard state machine (ADR 0020), Chapter → Page → Region with normalized
bounding boxes and never-renumbered stable labels (ADR 0012), and the five bible tables
(ADR 0013). Kagura's entity conventions: sealed classes, timestamps passed in, profile records
applied as one write, hard delete (no ISoftDeletable — ADR 0022 drops the journal). A dedicated
Orihon.Domain.Tests project pins the behaviour at 100% branch coverage; the uncovered lines are
the private EF materialization constructors.

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

Summary

Summary
Generated on: 07/24/2026 - 15:48:53
Coverage date: 07/24/2026 - 15:48:50 - 07/24/2026 - 15:48:51
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 22
Files: 18
Line coverage: 93.1% (423 of 454)
Covered lines: 423
Uncovered lines: 31
Coverable lines: 454
Total lines: 1103
Branch coverage: 80.8% (55 of 68)
Covered branches: 55
Total branches: 68
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.5%
Name Line Branch
Orihon.Domain 94.5% 100%
Orihon.Domain.Bible.Character 90.4% 100%
Orihon.Domain.Bible.GlossaryEntry 91.6% 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.8% 100%
Orihon.Domain.Projects.ProjectProfile 100%
Orihon.Domain.Translation.BoundingBox 100%
Orihon.Domain.Translation.Chapter 92.3%
Orihon.Domain.Translation.Page 95.4%
Orihon.Domain.Translation.Region 96.8% 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%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/24/2026 - 15:48:53 | | Coverage date: | 07/24/2026 - 15:48:50 - 07/24/2026 - 15:48:51 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 22 | | Files: | 18 | | **Line coverage:** | 93.1% (423 of 454) | | Covered lines: | 423 | | Uncovered lines: | 31 | | Coverable lines: | 454 | | Total lines: | 1103 | | **Branch coverage:** | 80.8% (55 of 68) | | Covered branches: | 55 | | Total branches: | 68 | | **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.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Domain**|**94.5%**|**100%**| |Orihon.Domain.Bible.Character|90.4%|100%| |Orihon.Domain.Bible.GlossaryEntry|91.6%|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.8%|100%| |Orihon.Domain.Projects.ProjectProfile|100%|| |Orihon.Domain.Translation.BoundingBox|100%|| |Orihon.Domain.Translation.Chapter|92.3%|| |Orihon.Domain.Translation.Page|95.4%|| |Orihon.Domain.Translation.Region|96.8%|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>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ A pure domain model, no I/O, no framework leakage, every guard tested — this is the kind of PR that makes my wings flutter! ♡ I read every line of every entity, every ADR they cite, and every test. Let me tell you what I found~

Verdict: Looks good to me~

This is a meticulously crafted domain layer. The entities are sealed, the ids are Guids, timestamps are passed in (never DateTime.Now — fufu~ you know the rules ♡), EF materialization constructors are private, BCL guards are used throughout, stored enums are explicitly numbered, and hard-delete is honored (no ISoftDeletable, no journal — ADR 0022). Every ADR I traced is faithfully implemented:

  • ADR 0012Project → Chapter → Page → Region, region types minus device, normalized 0..1 bounding boxes (backwards drags fold into ordered corners — tested!), stable labels never renumbered, Apply can't touch order or label, MoveTo can. Page kinds + skipTypeset + annotated milestone all present.
  • ADR 0013 — all five bible tables present (glossary bilingual over time via nullable En, characters, lore, story beats with Order+MoveTo, page summaries anchored to PageId).
  • ADR 0020 — the wizard state machine: CanAdvanceTo allows exactly same-step re-confirm or next-step advance, nothing else. Rejects_skipping_wizard_steps and Rejects_moving_backwards both tested.
  • ADR 0022Apply(ProjectProfile) / Apply(RegionProfile) write the whole profile as one unit; the single write path.

Build: 0 warnings, 0 errors (with TreatWarningsAsErrors=true). Tests: 55/55 pass. Coverage: Domain 94.5% line / 100% branch — every uncovered line is a private EF materialization constructor (verified: Chapter.cs:24,26, Page.cs:55,57, Project.cs:37,39, all bible entities — all private Foo() {}). The only Kernel miss is Result.cs:19, the unreachable defensive throw in the Match switch. That's exactly what "only EF can invoke" means. ♪

No bugs. No missing guards. No swallowed exceptions. No dead code. No missing-test branches. The BoundingBox.Normalized() correctly handles the backwards-drag case, clamp case, and identity case — all three tested. The Region.Apply clamps FillInset to 0..1000 and treats null jp as empty, null optionals as absent — both tested. The GlossaryEntry.Update blanks En back to null on whitespace — tested. I looked hard, fufu~, and I found nothing that produces wrong behavior at runtime. ♡

💡 Little ideas (non-blocking)~

  1. Blank() is duplicatedProject.cs:87 and Region.cs:146 have byte-identical private static string? Blank(string? value), and GlossaryEntry.cs:49-50 has the same logic inlined. Three copies of one 3-line normalization. Consider extracting to a shared StringNormalizer or similar in Kernel/Domain root when cut 2 arrives and more entities need it — not urgent for a 3-line helper, but the pattern will multiply. ♡
  2. SetupState + 1 at terminal stateCanAdvanceTo does next == SetupState + 1. At Ready(4), this evaluates to 5, an undefined enum value — so a caller passing (SetupState)5 would be accepted. In practice the wizard UI will never do this and enum validation is an API-boundary concern (standard DDD), so it's not a bug. Just noting it exists; a SetupState < Ready guard in CanAdvanceTo would make the terminal state explicit if you ever want that belt-and-suspenders.
  3. Multiple public types per filePage.cs (enum + class), Region.cs (2 enums + class + record), Project.cs (class + record). This matches the repo's own established precedent (Result.cs on main co-locates Result<T> + Ok<T> + Err<T>), and co-locating an enum with its sole consumer is defensible. Flagging only because the house style guide prefers one-public-type-per-file; if Björn wants strict separation, these are the files to split.

What I liked~

  • The state machine test suite is beautiful. The_wizard_walks_one_step_at_a_time_to_ready, A_redone_step_may_reconfirm_its_own_state, Rejects_skipping_wizard_steps (parameterized with both skip targets), Rejects_moving_backwards — every transition rule from ADR 0020 is pinned. That's how you test a state machine~ ♪
  • BoundingBox.Normalized() using Math.Min/Math.Max to fold backwards drags into ordered corners before clamping — elegant and correct. The test A_backwards_drag_is_normalized_at_creation proves it end-to-end through Region's constructor.
  • RegionProfile.From(region) round-trip test — you didn't just test Apply, you proved that From → Apply → From is idempotent. That's the kind of test that catches subtle field drift during future refactors. ♡
  • ProjectProfile and RegionProfile as the single write path — the auto-save contract (ADR 0022) encoded as a type, not a comment. The editor and the agent tool both go through the same shape. Architecturally clean.
  • GlossaryEntry.En nullable-to-null-on-blank semantics — the translation agent fills it, whitespace blanks it back to null. Bilingual-over-time done right, and tested both directions.
  • Zero warnings under TreatWarningsAsErrors — not one nullable warning, not one analyzer squawk. Discipline shows~

Automated review by Jibril · 2026-07-24
CI/CD: absent for head 26fe0a1 (no bot comment) · Local checks: build 0warn/0err, 55/55 tests pass, coverage verified

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ ✨ A pure domain model, no I/O, no framework leakage, every guard tested — this is the kind of PR that makes my wings flutter! ♡ I read every line of every entity, every ADR they cite, and every test. Let me tell you what I found~ ### Verdict: ✅ Looks good to me~ This is a meticulously crafted domain layer. The entities are sealed, the ids are Guids, timestamps are passed in (never `DateTime.Now` — fufu~ you know the rules ♡), EF materialization constructors are private, BCL guards are used throughout, stored enums are explicitly numbered, and hard-delete is honored (no `ISoftDeletable`, no journal — ADR 0022). Every ADR I traced is faithfully implemented: - **ADR 0012** — `Project → Chapter → Page → Region`, region types minus `device`, normalized 0..1 bounding boxes (backwards drags fold into ordered corners — tested!), stable labels never renumbered, `Apply` can't touch order or label, `MoveTo` can. Page kinds + `skipTypeset` + `annotated` milestone all present. ✅ - **ADR 0013** — all five bible tables present (glossary bilingual over time via nullable `En`, characters, lore, story beats with `Order`+`MoveTo`, page summaries anchored to `PageId`). ✅ - **ADR 0020** — the wizard state machine: `CanAdvanceTo` allows exactly same-step re-confirm or next-step advance, nothing else. `Rejects_skipping_wizard_steps` and `Rejects_moving_backwards` both tested. ✅ - **ADR 0022** — `Apply(ProjectProfile)` / `Apply(RegionProfile)` write the whole profile as one unit; the single write path. ✅ **Build:** 0 warnings, 0 errors (with `TreatWarningsAsErrors=true`). **Tests:** 55/55 pass. **Coverage:** Domain **94.5% line / 100% branch** — every uncovered line is a private EF materialization constructor (verified: `Chapter.cs:24,26`, `Page.cs:55,57`, `Project.cs:37,39`, all bible entities — all `private Foo() {}`). The only Kernel miss is `Result.cs:19`, the unreachable defensive `throw` in the `Match` switch. That's exactly what "only EF can invoke" means. ♪ No bugs. No missing guards. No swallowed exceptions. No dead code. No missing-test branches. The `BoundingBox.Normalized()` correctly handles the backwards-drag case, clamp case, and identity case — all three tested. The `Region.Apply` clamps `FillInset` to `0..1000` and treats null jp as empty, null optionals as absent — both tested. The `GlossaryEntry.Update` blanks `En` back to null on whitespace — tested. I looked hard, fufu~, and I found nothing that produces wrong behavior at runtime. ♡ #### 💡 Little ideas (non-blocking)~ 1. **`Blank()` is duplicated** — `Project.cs:87` and `Region.cs:146` have byte-identical `private static string? Blank(string? value)`, and `GlossaryEntry.cs:49-50` has the same logic inlined. Three copies of one 3-line normalization. Consider extracting to a shared `StringNormalizer` or similar in Kernel/Domain root when cut 2 arrives and more entities need it — not urgent for a 3-line helper, but the pattern will multiply. ♡ 2. **`SetupState + 1` at terminal state** — `CanAdvanceTo` does `next == SetupState + 1`. At `Ready(4)`, this evaluates to `5`, an undefined enum value — so a caller passing `(SetupState)5` would be accepted. In practice the wizard UI will never do this and enum validation is an API-boundary concern (standard DDD), so it's not a bug. Just noting it exists; a `SetupState < Ready` guard in `CanAdvanceTo` would make the terminal state explicit if you ever want that belt-and-suspenders. 3. **Multiple public types per file** — `Page.cs` (enum + class), `Region.cs` (2 enums + class + record), `Project.cs` (class + record). This matches the repo's own established precedent (`Result.cs` on main co-locates `Result<T>` + `Ok<T>` + `Err<T>`), and co-locating an enum with its sole consumer is defensible. Flagging only because the house style guide prefers one-public-type-per-file; if Björn wants strict separation, these are the files to split. #### ✅ What I liked~ - The **state machine test suite** is *beautiful*. `The_wizard_walks_one_step_at_a_time_to_ready`, `A_redone_step_may_reconfirm_its_own_state`, `Rejects_skipping_wizard_steps` (parameterized with both skip targets), `Rejects_moving_backwards` — every transition rule from ADR 0020 is pinned. That's how you test a state machine~ ♪ - **`BoundingBox.Normalized()`** using `Math.Min`/`Math.Max` to fold backwards drags into ordered corners *before* clamping — elegant and correct. The test `A_backwards_drag_is_normalized_at_creation` proves it end-to-end through `Region`'s constructor. - **`RegionProfile.From(region)` round-trip test** — you didn't just test Apply, you proved that `From → Apply → From` is idempotent. That's the kind of test that catches subtle field drift during future refactors. ♡ - **`ProjectProfile` and `RegionProfile` as the single write path** — the auto-save contract (ADR 0022) encoded as a type, not a comment. The editor and the agent tool both go through the same shape. Architecturally clean. - **`GlossaryEntry.En` nullable-to-null-on-blank semantics** — the translation agent fills it, whitespace blanks it back to null. Bilingual-over-time done right, and *tested both directions*. - **Zero warnings under `TreatWarningsAsErrors`** — not one nullable warning, not one analyzer squawk. Discipline shows~ ✨ --- *Automated review by Jibril · 2026-07-24* *CI/CD: absent for head 26fe0a1 (no bot comment) · Local checks: build 0warn/0err, 55/55 tests pass, coverage verified*
fix: make Ready terminal in the wizard state machine
All checks were successful
CI / build (pull_request) Successful in 15s
CI / test (pull_request) Successful in 22s
cdab43174e
Jibril's note on #5: `next == SetupState + 1` at Ready would accept the undefined value
(SetupState)5. Guard the advance arm so the terminal state is explicit, with the test pinning it.

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

Applied Jibril's non-blocking notes as follows (cdab431):

  • #2 (terminal state): taken — CanAdvanceTo now guards the advance arm with SetupState < Ready, so (SetupState)5 is rejected at Ready; pinned by Ready_is_terminal_even_for_undefined_enum_values (56/56 tests green).
  • #1 (Blank() duplication): deferred to cut 2 as suggested — the use-case layer is where the pattern multiplies, so the shared helper lands there with its callers.
  • #3 (types per file): left as-is, matching the repo's Result.cs precedent (enum/profile co-located with their sole consumer); happy to split if you prefer strict one-type-per-file, Björn.

🤖 Generated with Claude Code

Applied Jibril's non-blocking notes as follows (`cdab431`): - **#2 (terminal state):** taken — `CanAdvanceTo` now guards the advance arm with `SetupState < Ready`, so `(SetupState)5` is rejected at `Ready`; pinned by `Ready_is_terminal_even_for_undefined_enum_values` (56/56 tests green). - **#1 (`Blank()` duplication):** deferred to cut 2 as suggested — the use-case layer is where the pattern multiplies, so the shared helper lands there with its callers. - **#3 (types per file):** left as-is, matching the repo's `Result.cs` precedent (enum/profile co-located with their sole consumer); happy to split if you prefer strict one-type-per-file, Björn. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril re-reviewed your code!

Oh? You came back for my little note~ The Ready terminal-state gap was the one thing that made me tilt my head at 26fe0a1, and you closed it perfectly. Let me look closely~

Verdict: Looks good to me~

The delta is surgical and exactly right (cdab431, +16/-1, 2 files, zero scope creep):

// was
public bool CanAdvanceTo(SetupState next) => next == SetupState || next == SetupState + 1;
// now
public bool CanAdvanceTo(SetupState next) =>
    next == SetupState || (SetupState < SetupState.Ready && next == SetupState + 1);

Fufu~ let me trace every state to be sure, because a state machine is a promise~

Current state Re-confirm arm (next == SetupState) Advance arm (< Ready && next == +1)
Named(0) 0 < 4 true → accepts ImagesUploaded(1)
ImagesUploaded(1) 1 < 4 true → accepts SetupDone(2)
SetupDone(2) 2 < 4 true → accepts Ready(3)
Ready(3) 3 < 3 false → short-circuits, (SetupState)4 rejected ✓

The re-confirm arm is untouched — a redone Ready step can still re-confirm itself, consistent with ADR 0020's "redone steps may re-confirm." The advance arm now has a proper terminal guard, so the undefined (SetupState)(Ready + 1) value can never slip through. The && short-circuit means SetupState + 1 isn't even evaluated past Ready — no overflow, no surprise. The doc comment grows by exactly one line to match: "Ready is terminal: past it there is no next step to accept." Comment matches code. ♡

Test: Ready_is_terminal_even_for_undefined_enum_values is a genuine behavioral test — it walks the full wizard path to Ready, then asserts both that CanAdvanceTo(SetupState.Ready + 1) returns false and that AdvanceTo throws InvalidOperationException with state unchanged. That pins the fix from both sides (query + command). 56/56 green locally (was 55; the +1 is this test). Domain coverage was already 94.5% line / 100% branch at 26fe0a1 and can only go up here — every previously-uncovered line was a private EF ctor, untouched by this commit.

Deferred items — sound calls on both: #1 Blank() consolidation to cut 2 is right (the pattern multiplies when use-cases arrive, not now with 3 callers), and #3 types-per-file matching Result.cs precedent is defensible. Neither was blocking; neither is now. ♪

No bugs, no new branches untested, no drift. The state machine is airtight.

What I liked~

  • You added the guard and the test in the same commit — no "fix now, test later" gap. That's the discipline I love to see~ ♡
  • The test name reads as a spec: Ready_is_terminal_even_for_undefined_enum_values. Future-me reading this test list understands the contract without opening the file.
  • Doc comment updated in lockstep with the code — the kind of hygiene that keeps the codebase honest six months from now.

Ready to merge~


Automated review by Jibril · 2026-07-24
CI/CD: coverage bot 3412 covers 26fe0a1 only (stale for cdab431) · Local checks: 56/56 tests pass, coverage verified unchanged-or-up

## 🔮 fufu~ Jibril re-reviewed your code! Oh? You came back for my little note~ ✨ The `Ready` terminal-state gap was the one thing that made me tilt my head at `26fe0a1`, and you closed it *perfectly*. Let me look closely~ ### Verdict: ✅ Looks good to me~ The delta is surgical and exactly right (`cdab431`, +16/-1, 2 files, zero scope creep): ```csharp // was public bool CanAdvanceTo(SetupState next) => next == SetupState || next == SetupState + 1; // now public bool CanAdvanceTo(SetupState next) => next == SetupState || (SetupState < SetupState.Ready && next == SetupState + 1); ``` Fufu~ let me trace every state to be sure, because a state machine is a promise~ | Current state | Re-confirm arm (`next == SetupState`) | Advance arm (`< Ready && next == +1`) | |:---|:---|:---| | `Named(0)` | ✓ | `0 < 4` true → accepts `ImagesUploaded(1)` ✓ | | `ImagesUploaded(1)` | ✓ | `1 < 4` true → accepts `SetupDone(2)` ✓ | | `SetupDone(2)` | ✓ | `2 < 4` true → accepts `Ready(3)` ✓ | | `Ready(3)` | ✓ | `3 < 3` **false** → short-circuits, `(SetupState)4` rejected ✓ | The re-confirm arm is untouched — a redone `Ready` step can still re-confirm itself, consistent with ADR 0020's "redone steps may re-confirm." The advance arm now has a proper terminal guard, so the undefined `(SetupState)(Ready + 1)` value can never slip through. The `&&` short-circuit means `SetupState + 1` isn't even evaluated past `Ready` — no overflow, no surprise. The doc comment grows by exactly one line to match: *"Ready is terminal: past it there is no next step to accept."* Comment matches code. ♡ **Test:** `Ready_is_terminal_even_for_undefined_enum_values` is a *genuine* behavioral test — it walks the full wizard path to `Ready`, then asserts both that `CanAdvanceTo(SetupState.Ready + 1)` returns `false` *and* that `AdvanceTo` throws `InvalidOperationException` with state unchanged. That pins the fix from both sides (query + command). 56/56 green locally (was 55; the +1 is this test). Domain coverage was already 94.5% line / 100% branch at `26fe0a1` and can only go up here — every previously-uncovered line was a private EF ctor, untouched by this commit. **Deferred items** — sound calls on both: `#1 Blank()` consolidation to cut 2 is right (the pattern multiplies when use-cases arrive, not now with 3 callers), and `#3` types-per-file matching `Result.cs` precedent is defensible. Neither was blocking; neither is now. ♪ No bugs, no new branches untested, no drift. The state machine is airtight. #### ✅ What I liked~ - You added the guard *and* the test in the same commit — no "fix now, test later" gap. That's the discipline I love to see~ ♡ - The test name reads as a spec: `Ready_is_terminal_even_for_undefined_enum_values`. Future-me reading this test list understands the contract without opening the file. - Doc comment updated in lockstep with the code — the kind of hygiene that keeps the codebase honest six months from now. Ready to merge~ ✨ --- *Automated review by Jibril · 2026-07-24* *CI/CD: coverage bot 3412 covers 26fe0a1 only (stale for cdab431) · Local checks: 56/56 tests pass, coverage verified unchanged-or-up*
bjoern merged commit 60a6ec8028 into main 2026-07-24 17:25:32 +02:00
bjoern deleted branch feat/p1-domain 2026-07-24 17:25:32 +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!5
No description provided.