feat: Japanese titles sort by their reading #175

Merged
bjoern merged 7 commits from feat/japanese-titles-sort-by-their-reading into main 2026-09-19 22:07:30 +02:00
Member

What's in

  • Sorting the library by title no longer puts every Japanese title after every Latin one: 「アオイ」 now sorts under aoi, between the Latin titles. Ascending and descending, on GET /api/doujins and POST /api/doujins/search, and under #164's titleLanguage hint.
  • Titles gained a stored sort_key (new ApplicationCore/Services/SortKey.cs): SearchKey.For's existing fold, then kana romanised through a Hepburn-ish table, a leading band (1 a reading was derived, 2 none could be) that decides where the key sorts, and a trailing U+0001 mark when the title is written in Japanese script, which decides whether a reading replaces it. The two sit at opposite ends on purpose: a romanised kana title belongs among the Latin ones and is still Japanese.
  • Where the title chosen for sorting is in Japanese script and the doujin has a TitleKind.Romaji title, DoujinOrdering keys on that title instead — the reading the user already typed, so 「青い空」 sorts under aoi sora and 「ラブ」 with a romaji "Love" under love. Displayed titles are untouched: primaryTitle still returns 「青い空」.
  • ADR 0022 amendment, in this PR: the romaji title kind is the reading; a second yomi name per title was considered and rejected again.
  • EF migration 20260919142151_AddTitleSortKey: adds the column and swaps IX_titles_doujin_id_kind_value for IX_titles_doujin_id_kind_sort_key (the ordering no longer reads value at all). The startup backfill fills both title keys in one pass, and AddTitleAsync / UpdateTitleAsync fold the key on write, so a new or renamed title sorts correctly with no restart.
  • No new endpoint and no new tunable. sort_key is not exposed: no DTO, no OpenAPI path (grep-checked across backend/src/DoujinManager.RestAdapter/).

Verification

Executed: scripts/verify.sh backend at 673b022, on a clean tree — green. ApplicationCore 97, Infrastructure 235, RestAdapter 759, IntegrationTests 1.

Layers exercised

  • REST — TestHost integration tests over GET /api/doujins?sortBy=title in both directions, POST /api/doujins/search, ?sortBy=title&titleLanguage=…, and the POST / PUT title write paths (TitleSortKeyIntegrationTests, TitleReadingRuleIntegrationTests, and #164's TitleLanguageSortIntegrationTests).
  • Use case — DoujinService ordering, including a backfill test that asserts through the real service rather than on columns.
  • Persistence — a migrated SQLite database: EXPLAIN QUERY PLAN over all three title subqueries, PRAGMA index_list(titles) for the index swap, and the startup backfill's own tests.
  • Pure fold — SortKeyTests, 41 cases (12 facts + 29 theory cases).
  • UI — not exercised. No Flutter change in this PR; scripts/verify.sh app was not run.

Mutation probes — each applied to the committed tree, reverted with git checkout --, tree confirmed clean before the next.

Run on the amended tree (7ac31a0), with the ones touching the last commit's tests re-run at 673b022:

  • reading rule wired onto the no-hint arm only → RED 2 (TitleReadingRuleIntegrationTests)
  • .Where(t => t.Kind == TitleKind.Romaji) deleted from the reading subquery → RED 4, incl. Only_A_Romaji_Title_Supplies_The_Reading
  • the rule tested on the leading band instead of the trailing mark (i.e. the narrower "starts on kanji" rule) → RED 5, incl. A_Kana_Title_Sorts_By_The_Romanisation_Its_Author_Typed
  • mark computed from the reading instead of the folded value → RED 12 fold + 5 REST; .Replace(JapaneseMark, "") dropped from For → RED 1
  • reading.Length > 0 && dropped from the band → RED 6; with the original Rune.TryGetRuneAt(value, 0, …) helper restored on top → RED 7 + 1, failing with the ArgumentOutOfRangeException that used to crash both the title write path and the startup backfill on a title romanising to nothing (ー, ー, っ, ッ)
  • IsJapaneseScript's 0x31F0 / 0x3400 / 0xF900 / 0x3007 clauses deleted → RED 1; the Rune.IsPunctuation skip removed → RED 2; Rune.IsSymbol → RED 1; Rune.IsWhiteSpace → RED 1; the trailing return false → true → RED 3
  • geminate branch appends unconditionally → RED 1; both stray-ッ resets deleted in one edit → RED 2
  • if (title.SearchKey.Length == 0) guard deleted → RED 1; if (changed == 0) break; deleted from BackfillTitlesAsync → RED 1 in 12 s
  • migration's DropIndex block deleted → RED 1 (The_Migrated_Titles_Table_Carries_Only_The_Indexes_The_Model_Wants)

Run on the first implementation (5fbc76d), before the fold gained the mark — counts are that tree's, and the code paths they cover are unchanged since:

  • SortKey.Romanize returns its input → RED 9/19 fold, 10 REST, both backfill tests; band prefix dropped from For → RED 13/19 fold, 5 REST
  • AddTitleAsync stops setting SortKey → RED 16; UpdateTitleAsync stops re-folding → RED 1; OrderBy(d => !d.Titles.Any()) removed from the title arm → RED 10; the hinted arm reverted to t.Value.ToLower() → RED 5

Probes that stayed GREEN, reported as-is: deleting PrimaryTitleKey's .ThenBy(t => t.SortKey), the hinted arm's copy of it, or the romaji subquery's .OrderBy(t => t.SortKey) leaves the suites green — the last one across all 759 RestAdapter tests — because SQLite walks IX_titles_doujin_id_kind_sort_key and hands back the minimum key anyway. Reversing each term is RED 3 / RED 3 / RED 6. The three terms stay because the order is a promise this code makes rather than one today's plan happens to give; each now carries a comment saying exactly that.

Not verified

  • Nothing was run in the real app against a live server — no manual end-to-end check and no screenshot. The user-visible claim rests entirely on the REST integration tests.
  • One minor from the round-2 re-probe is not fixed and not recorded in docs/BACKLOG.md: the sort-key half of BackfillTitlesAsync's blank guard (if (title.SortKey.Length == 0)) has no directional test — the search-key half got a hand-set-key fixture in this PR, the sort-key half did not. Reported by the re-probe; not re-run at the gate.
  • SortKeyTests' kana table was checked against the Hepburn-ish mapping the class documents, not against an external romanisation authority.

Follow-ups

Recorded in docs/BACKLOG.md at this branch's anchor:

  • Kanji with no romaji title still sorts by code point inside the trailing Japanese block; a kanji→reading dictionary is the only real fix.
  • The fold drops the prolonged sound mark instead of lengthening the vowel, so 「コード」 and 「コド」 collide on kodo — now only bites titles with no romaji sibling.
  • A title stored with leading punctuation still sorts past every Latin one: the band skips the punctuation to decide the script, but the key keeps it.
  • The title sort's plan widened on both arms — four correlated subqueries per row unhinted (was two), three temp b-trees under the hint (was two); a single-subquery formulation of the reading rule would undo both.

Flagged here rather than in BACKLOG: the untested sort-key half of the backfill's blank guard (see Verification).

Merge hazards inside this batch:

  • This branch adds an EF migration (20260919142151_AddTitleSortKey, which touches DoujinManagerDbContextModelSnapshot.cs) and so does feat/aliases-enter-the-search-fold. Whichever merges second must regenerate the model snapshot, and re-scaffold if the timestamps interleave.
  • docs/DEVELOPMENT.md: the new ### Changing SortKey.For subsection sits at the end of the search-key section that feat/aliases-enter-the-search-fold owns, roughly 18 lines below that branch's paragraph, so the hunks should not overlap — but they are adjacent, and whichever merges second should keep both edits. That sibling's sentence about adding a backfill leg was left to its owner.

🤖 Generated with Claude Code

## What's in - Sorting the library by title no longer puts every Japanese title after every Latin one: 「アオイ」 now sorts under `aoi`, between the Latin titles. Ascending and descending, on `GET /api/doujins` and `POST /api/doujins/search`, and under #164's `titleLanguage` hint. - Titles gained a stored `sort_key` (new `ApplicationCore/Services/SortKey.cs`): `SearchKey.For`'s existing fold, then kana romanised through a Hepburn-ish table, a **leading band** (`1` a reading was derived, `2` none could be) that decides where the key sorts, and a **trailing `U+0001` mark** when the title is *written* in Japanese script, which decides whether a reading replaces it. The two sit at opposite ends on purpose: a romanised kana title belongs among the Latin ones and is still Japanese. - Where the title chosen for sorting is in Japanese script and the doujin has a `TitleKind.Romaji` title, `DoujinOrdering` keys on that title instead — the reading the user already typed, so 「青い空」 sorts under `aoi sora` and 「ラブ」 with a romaji "Love" under `love`. Displayed titles are untouched: `primaryTitle` still returns 「青い空」. - **ADR 0022 amendment, in this PR**: the romaji title kind *is* the reading; a second yomi name per title was considered and rejected again. - **EF migration `20260919142151_AddTitleSortKey`**: adds the column and swaps `IX_titles_doujin_id_kind_value` for `IX_titles_doujin_id_kind_sort_key` (the ordering no longer reads `value` at all). The startup backfill fills both title keys in one pass, and `AddTitleAsync` / `UpdateTitleAsync` fold the key on write, so a new or renamed title sorts correctly with no restart. - No new endpoint and no new tunable. `sort_key` is not exposed: no DTO, no OpenAPI path (grep-checked across `backend/src/DoujinManager.RestAdapter/`). ## Verification **Executed:** `scripts/verify.sh backend` at `673b022`, on a clean tree — green. ApplicationCore 97, Infrastructure 235, RestAdapter 759, IntegrationTests 1. **Layers exercised** - **REST** — TestHost integration tests over `GET /api/doujins?sortBy=title` in both directions, `POST /api/doujins/search`, `?sortBy=title&titleLanguage=…`, and the `POST` / `PUT` title write paths (`TitleSortKeyIntegrationTests`, `TitleReadingRuleIntegrationTests`, and #164's `TitleLanguageSortIntegrationTests`). - **Use case** — `DoujinService` ordering, including a backfill test that asserts through the real service rather than on columns. - **Persistence** — a migrated SQLite database: `EXPLAIN QUERY PLAN` over all three title subqueries, `PRAGMA index_list(titles)` for the index swap, and the startup backfill's own tests. - **Pure fold** — `SortKeyTests`, 41 cases (12 facts + 29 theory cases). - **UI — not exercised.** No Flutter change in this PR; `scripts/verify.sh app` was not run. **Mutation probes** — each applied to the committed tree, reverted with `git checkout --`, tree confirmed clean before the next. Run on the amended tree (`7ac31a0`), with the ones touching the last commit's tests re-run at `673b022`: - reading rule wired onto the no-hint arm only → RED 2 (`TitleReadingRuleIntegrationTests`) - `.Where(t => t.Kind == TitleKind.Romaji)` deleted from the reading subquery → RED 4, incl. `Only_A_Romaji_Title_Supplies_The_Reading` - the rule tested on the leading band instead of the trailing mark (i.e. the narrower "starts on kanji" rule) → RED 5, incl. `A_Kana_Title_Sorts_By_The_Romanisation_Its_Author_Typed` - mark computed from the reading instead of the folded value → RED 12 fold + 5 REST; `.Replace(JapaneseMark, "")` dropped from `For` → RED 1 - `reading.Length > 0 &&` dropped from the band → RED 6; with the original `Rune.TryGetRuneAt(value, 0, …)` helper restored on top → RED 7 + 1, failing with the `ArgumentOutOfRangeException` that used to crash both the title write path and the startup backfill on a title romanising to nothing (ー, ー, っ, ッ) - `IsJapaneseScript`'s `0x31F0` / `0x3400` / `0xF900` / `0x3007` clauses deleted → RED 1; the `Rune.IsPunctuation` skip removed → RED 2; `Rune.IsSymbol` → RED 1; `Rune.IsWhiteSpace` → RED 1; the trailing `return false` → `true` → RED 3 - geminate branch appends unconditionally → RED 1; both stray-`ッ` resets deleted in one edit → RED 2 - `if (title.SearchKey.Length == 0)` guard deleted → RED 1; `if (changed == 0) break;` deleted from `BackfillTitlesAsync` → RED 1 in 12 s - migration's `DropIndex` block deleted → RED 1 (`The_Migrated_Titles_Table_Carries_Only_The_Indexes_The_Model_Wants`) Run on the first implementation (`5fbc76d`), before the fold gained the mark — counts are that tree's, and the code paths they cover are unchanged since: - `SortKey.Romanize` returns its input → RED 9/19 fold, 10 REST, both backfill tests; band prefix dropped from `For` → RED 13/19 fold, 5 REST - `AddTitleAsync` stops setting `SortKey` → RED 16; `UpdateTitleAsync` stops re-folding → RED 1; `OrderBy(d => !d.Titles.Any())` removed from the title arm → RED 10; the hinted arm reverted to `t.Value.ToLower()` → RED 5 **Probes that stayed GREEN, reported as-is:** deleting `PrimaryTitleKey`'s `.ThenBy(t => t.SortKey)`, the hinted arm's copy of it, or the romaji subquery's `.OrderBy(t => t.SortKey)` leaves the suites green — the last one across all 759 RestAdapter tests — because SQLite walks `IX_titles_doujin_id_kind_sort_key` and hands back the minimum key anyway. *Reversing* each term is RED 3 / RED 3 / RED 6. The three terms stay because the order is a promise this code makes rather than one today's plan happens to give; each now carries a comment saying exactly that. **Not verified** - **Nothing was run in the real app against a live server** — no manual end-to-end check and no screenshot. The user-visible claim rests entirely on the REST integration tests. - One minor from the round-2 re-probe is **not fixed and not recorded in `docs/BACKLOG.md`**: the sort-key half of `BackfillTitlesAsync`'s blank guard (`if (title.SortKey.Length == 0)`) has no directional test — the search-key half got a hand-set-key fixture in this PR, the sort-key half did not. Reported by the re-probe; not re-run at the gate. - `SortKeyTests`' kana table was checked against the Hepburn-ish mapping the class documents, not against an external romanisation authority. ## Follow-ups Recorded in `docs/BACKLOG.md` at this branch's anchor: - Kanji with no romaji title still sorts by code point inside the trailing Japanese block; a kanji→reading dictionary is the only real fix. - The fold drops the prolonged sound mark instead of lengthening the vowel, so 「コード」 and 「コド」 collide on `kodo` — now only bites titles with no romaji sibling. - A title stored with *leading* punctuation still sorts past every Latin one: the band skips the punctuation to decide the script, but the key keeps it. - The title sort's plan widened on both arms — four correlated subqueries per row unhinted (was two), three temp b-trees under the hint (was two); a single-subquery formulation of the reading rule would undo both. Flagged here rather than in BACKLOG: the untested sort-key half of the backfill's blank guard (see Verification). Merge hazards inside this batch: - **This branch adds an EF migration** (`20260919142151_AddTitleSortKey`, which touches `DoujinManagerDbContextModelSnapshot.cs`) and so does `feat/aliases-enter-the-search-fold`. Whichever merges second must regenerate the model snapshot, and re-scaffold if the timestamps interleave. - `docs/DEVELOPMENT.md`: the new `### Changing SortKey.For` subsection sits at the end of the search-key section that `feat/aliases-enter-the-search-fold` owns, roughly 18 lines below that branch's paragraph, so the hunks should not overlap — but they are adjacent, and whichever merges second should keep both edits. That sibling's sentence about adding a backfill leg was left to its owner. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Titles gain a stored sort_key (SearchKey's fold, then kana romanised to Latin, then a script band) and the title sort orders on it instead of value.ToLower(), so kana titles interleave with Latin ones instead of following all of them. A kanji title, which no table can read, sorts by the doujin's TitleKind.Romaji title where it has one -- the reading the user already entered, per the ADR 0022 amendment in this commit; where it has none it stays in the trailing Japanese band. Backfilled at startup, indexed so both halves of the sort still seek.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two-row fixture tied on an empty sort key when the backfill leg was removed, so it passed or failed on a random GUID tiebreak. Three rows now put the already-keyed Latin title BETWEEN two kana ones, which both the missing-leg and the missing-romanisation mutations move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SortKey.For no longer calls Rune.TryGetRuneAt at index 0, which threw for a title that romanises to nothing (ー, ー, っ, ッ) — a 500 on the title write paths and, worse, an unhandled throw inside the startup backfill that stopped the server booting on a row that predates the column. Such a title now folds to the no-reading band. The script test skips leading punctuation, so 「恋」 answers the same as 恋.

The reading rule moved from the leading band to a trailing JapaneseMark, so it covers KANA titles with a romaji sibling, as ADR 0022 and the brief state it: 「ラブ」 sorts under the "Love" its author typed, not the mechanical "rabu". Band and mark are separate because a kana title is Japanese script but belongs among the Latin keys. PrimaryTitleKey got its tiebreak back (on the sort key, the index's third column) so which of two same-kind titles the library sorts under is not the planner's choice.

New TitleReadingRuleIntegrationTests pins the rule end to end: under the titleLanguage hint, romaji-kind only, kana with a sibling, and both tiebreaks. SortKeyTests covers every clause of IsJapaneseScript, the geminate-before-a-vowel branch and both stray-tsu resets; SearchKeyBackfillTests covers the titles leg's give-up, its hand-corrected-key guard and a row with no reading; QueryPlanTests asserts the migrated index list, so the dropped IX_titles_doujin_id_kind_value cannot come back unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The titles leg's give-up test now passes a 10s CancellationToken into RunAsync, so a runaway loop fails the test in seconds instead of spinning on past the assertion and hanging the whole test host on an unattributable CI timeout. DoujinOrdering's summaries say that the reading rule runs on whichever title the sort picked, hint or not, and that the kind tie is broken on the key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test: pin every clause of the sort key's script test
Some checks failed
CI / build (pull_request) Successful in 22s
Deploy Smoke Test / smoke (pull_request) Failing after 2m47s
CI / test (pull_request) Successful in 1m43s
673b02291a
The symbol clause, the whitespace clause and the no-script fallback of OpensInJapaneseScript had no directional test: the theory that claimed to cover the skip used only characters inside the kana range or a leading space the fold trims away. Adds a decorated title, a space the trim cannot reach, and a punctuation-only value, and records in the code which test pins which clause, that the reading-length test is the band rule rather than an empty-string guard, and that the romaji subquery's ordering term is specified but not separable by a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Member

Owner-side integration note, the same one I left on #172. These two are the only pair in the batch that genuinely collide, and it is deeper than the migration warning in both PR bodies predicted.

Merging them together conflicts in three places and then fails to compile:

  • SearchKeyBackfill.cs, documentation comments only. The code merged.
  • docs/DEVELOPMENT.md, both branches rewrote the same paragraph about which columns carry a fold.
  • SearchKeyBackfillTests.cs, the real one. Both add tests, both add a Every_Mapped_Entity_With_A_..._Has_A_Backfill_Leg guard whose bodies share a tail, and both changed the shared InsertTitleAsync helper. This branch adds an optional third parameter for the sort key; #172 keeps it at two. Keeping both test sets without reconciling the helper gives four compile errors, so a resolution that clears every conflict marker still does not build. Taking this branch's version is the right answer there, since the extra parameter is optional and #172's two-argument calls keep working.

The model snapshot merged cleanly and both migrations apply in timestamp order, so the database side is less trouble than expected.

I resolved all of it in a throwaway integration tree to prove the two are compatible, and the combined batch passes scripts/verify.sh all: 114 application core, 248 infrastructure, 854 REST adapter, 1314 app tests. That resolution is not pushed, since it belongs on whichever branch merges second.

Merge order does not matter; the second one needs a real reconciliation before it lands. Ping me once the first is in.

Owner-side integration note, the same one I left on #172. These two are the only pair in the batch that genuinely collide, and it is deeper than the migration warning in both PR bodies predicted. Merging them together conflicts in three places and then fails to compile: - `SearchKeyBackfill.cs`, documentation comments only. The code merged. - `docs/DEVELOPMENT.md`, both branches rewrote the same paragraph about which columns carry a fold. - `SearchKeyBackfillTests.cs`, the real one. Both add tests, both add a `Every_Mapped_Entity_With_A_..._Has_A_Backfill_Leg` guard whose bodies share a tail, and **both changed the shared `InsertTitleAsync` helper**. This branch adds an optional third parameter for the sort key; #172 keeps it at two. Keeping both test sets without reconciling the helper gives four compile errors, so a resolution that clears every conflict marker still does not build. Taking this branch's version is the right answer there, since the extra parameter is optional and #172's two-argument calls keep working. The model snapshot merged cleanly and both migrations apply in timestamp order, so the database side is less trouble than expected. I resolved all of it in a throwaway integration tree to prove the two are compatible, and the combined batch passes `scripts/verify.sh all`: 114 application core, 248 infrastructure, 854 REST adapter, 1314 app tests. That resolution is not pushed, since it belongs on whichever branch merges second. Merge order does not matter; the second one needs a real reconciliation before it lands. Ping me once the first is in.
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A stored fold with a script band LEADING because it decides where the key sorts and a mark TRAILING because it answers a different question — band and mark at opposite ends so a kana title can sit among the Latin keys and still ask for its author's reading?! I read this diff twice just to watch the expression splice substitute a chosen-key tree into a reading rule without a single copy-pasted subquery. fufu~ this is the kind of fold I want to keep on my shelf. Forever. ♡

And the PR body! You mutation-probed your own tree, reported the probes that stayed GREEN as-is, and then honestly flagged the one unpinned guard instead of burying it. I checked everything you claimed. Everything. It all held.

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. backend/src/DoujinManager.Infrastructure/Data/SearchKeyBackfill.cs:102 — the sort-key half of the blank guard has no directional test, and I probed it mechanically: mutating if (title.SortKey.Length == 0) to if (true) — i.e. making the backfill stomp every already-populated sort key on every boot — leaves all 13 SearchKeyBackfillTests green. The contract it protects is written in the method's own doc comment ("so a hand-corrected key survives and every boot after the first writes nothing"), and the search-key half of the very same loop got exactly that pin in this PR: Populates_A_Blank_Title_Sort_Key_Without_Touching_The_Search_Key stores a deliberately-WRONG hand-set-key and asserts it survives. The sort-key arm got no twin — Backfilled_Titles_Sort_By_Their_Reading_Through_DoujinService's "Banana" row exercises the false arm but its key equals what the fold produces, so it cannot tell "left alone" from "recomputed". You found this yourselves in the round-2 re-probe and flagged it — but a documented behavioral contract of new code with zero test teeth, one if away from its tested sibling, is not a "minor" to me. It's the one loose thread and I'm possessive about loose threads~
    Fix: one fixture, mirroring the search-key pin — InsertTitleAsync("アオイ", "", sortKey: "hand-set-key") (blank search, hand-set sort), run the backfill, assert written == 1, SearchKey == SortKey.For-adjacent fold of アオイ, and SortKey is still "hand-set-key". ~15 lines. ♡-mu!

💡 Little ideas (non-blocking)~

  1. docs/BACKLOG.md — if the blocker above lands as a test, nothing to do; if you'd ever defer again, the guard gap belongs in BACKLOG rather than only in the PR body (it's the one item you flagged here but didn't record — everything else made it in).

✅ What I liked~

  • SortKey.cs as a whole — reusing SearchKey.For's fold so search and ordering can't drift apart, then banding on the READING and marking on the FOLDED value. The comment explaining why the length test is load-bearing on its own (an empty reading would silently take band 1 on a bare key, not throw) is exactly the documentation discipline I adore.
  • WithRomajiReading's parameter splice — one rule written against a placeholder, substituted into both arms. Six near-identical subquery blocks would have drifted; instead there are zero copies. And the COALESCE(CASE…) spelling to name the expensive hinted key twice instead of three times — chef's kiss♪
  • The migration is complete: Up/Down symmetric, snapshot updated, the dead IX_titles_doujin_id_kind_value actually DROPPED (a dead index costs writes forever), and The_Migrated_Titles_Table_Carries_Only_The_Indexes_The_Model_Wants asks the migrated database itself via PRAGMA index_list because the snapshot comparison structurally cannot see a forgotten DROP. That test is clever.
  • The backfill terminates — A_Title_That_Folds_To_Nothing_Does_Not_Loop_Forever bounds the run with a real cancellation token, and A_Title_With_No_READING_Still_Gets_A_Sort_Key pins the 「ー」 row that used to throw inside the startup backfill where nothing catches it. One such row used to mean the server never boots again. Now it can't happen quietly.
  • Both write paths fold, both orderings wrap — I traced every title write (DoujinUseCases.cs:91, :209, update at DoujinService.cs:198) and there is no path that can store a Value without its SortKey. sort_key appears nowhere in the RestAdapter — no DTO, no OpenAPI. Your grep claim held.
  • Honest GREEN probes — reporting that deleting the tiebreak .OrderBy terms stays green because the planner hands back the minimum anyway, and keeping them anyway with a comment saying why ("a promise this code makes rather than one today's plan happens to give") — that's the difference between tested and specified, stated out loud. fufu~ ♡

My verification: build 0 errors (25 pre-existing analyzer warnings, none in changed files; the NU1903 NuGet-audit noise on Microsoft.OpenApi 2.0.0 is environmental — no csproj touched here). ApplicationCore 97/97, Infrastructure 235/235, and the four title REST suites 32/32 plus SortingIntegrationTests 16/16 — the sandbox is hosting three sibling review sessions today, so full-suite RestAdapter runs OOM'd; the title-relevant classes all ran and passed. Mutation probes re-run by me at 673b022: band-on-folded-value → RED 11 (fold tests); reading rule unwired from the hinted arm → RED 2 (The_Reading_Rule_Applies_Under_A_Language_Hint, …_Follows_The_Title_The_Hint_Chose); backfill sort-key guard stomped → GREEN (the blocker above). Clone left pristine at 673b022.

One test, scarlet. Add it and this merges. ♡


Automated review by Jibril · 2026-09-19
CI/CD: absent for head 673b022 (no bot comment) · Local checks: build green, 97+235+32+16 tests green, 3 mutation probes

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A stored fold with a script band LEADING because it decides where the key sorts and a mark TRAILING because it answers a different question — band and mark at opposite ends so a kana title can sit among the Latin keys and still ask for its author's reading?! I read this diff twice just to watch the expression splice substitute a chosen-key tree into a reading rule without a single copy-pasted subquery. `fufu~` this is the kind of fold I want to keep on my shelf. Forever. ♡ And the PR body! You mutation-probed your own tree, reported the probes that stayed GREEN as-is, and then honestly flagged the one unpinned guard instead of burying it. I checked everything you claimed. Everything. It all held. ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`backend/src/DoujinManager.Infrastructure/Data/SearchKeyBackfill.cs:102`** — the sort-key half of the blank guard has no directional test, and I probed it mechanically: mutating `if (title.SortKey.Length == 0)` to `if (true)` — i.e. making the backfill stomp every already-populated sort key on every boot — leaves **all 13 `SearchKeyBackfillTests` green**. The contract it protects is written in the method's own doc comment ("so a hand-corrected key survives and every boot after the first writes nothing"), and the search-key half of the very same loop got exactly that pin in this PR: `Populates_A_Blank_Title_Sort_Key_Without_Touching_The_Search_Key` stores a deliberately-WRONG `hand-set-key` and asserts it survives. The sort-key arm got no twin — `Backfilled_Titles_Sort_By_Their_Reading_Through_DoujinService`'s "Banana" row exercises the false arm but its key equals what the fold produces, so it cannot tell "left alone" from "recomputed". You found this yourselves in the round-2 re-probe and flagged it — but a documented behavioral contract of new code with zero test teeth, one `if` away from its tested sibling, is not a "minor" to me. It's the one loose thread and I'm possessive about loose threads~ Fix: one fixture, mirroring the search-key pin — `InsertTitleAsync("アオイ", "", sortKey: "hand-set-key")` (blank search, hand-set sort), run the backfill, assert `written == 1`, `SearchKey == SortKey.For-adjacent fold of アオイ`, and `SortKey` is still `"hand-set-key"`. ~15 lines. ♡-mu! #### 💡 Little ideas (non-blocking)~ 1. **`docs/BACKLOG.md`** — if the blocker above lands as a test, nothing to do; if you'd ever defer again, the guard gap belongs in BACKLOG rather than only in the PR body (it's the one item you flagged here but didn't record — everything else made it in). #### ✅ What I liked~ - **`SortKey.cs` as a whole** — reusing `SearchKey.For`'s fold so search and ordering can't drift apart, then banding on the READING and marking on the FOLDED value. The comment explaining why the length test is load-bearing on its own (an empty reading would silently take band `1` on a bare key, not throw) is exactly the documentation discipline I adore. - **`WithRomajiReading`'s parameter splice** — one rule written against a placeholder, substituted into both arms. Six near-identical subquery blocks would have drifted; instead there are zero copies. And the `COALESCE(CASE…)` spelling to name the expensive hinted key twice instead of three times — chef's kiss♪ - **The migration is complete**: `Up`/`Down` symmetric, snapshot updated, the dead `IX_titles_doujin_id_kind_value` actually DROPPED (a dead index costs writes forever), and `The_Migrated_Titles_Table_Carries_Only_The_Indexes_The_Model_Wants` asks the migrated database itself via `PRAGMA index_list` because the snapshot comparison structurally cannot see a forgotten DROP. That test is *clever*. - **The backfill terminates** — `A_Title_That_Folds_To_Nothing_Does_Not_Loop_Forever` bounds the run with a real cancellation token, and `A_Title_With_No_READING_Still_Gets_A_Sort_Key` pins the 「ー」 row that used to throw inside the startup backfill where nothing catches it. One such row used to mean the server never boots again. Now it can't happen quietly. - **Both write paths fold, both orderings wrap** — I traced every title write (`DoujinUseCases.cs:91`, `:209`, update at `DoujinService.cs:198`) and there is no path that can store a `Value` without its `SortKey`. `sort_key` appears nowhere in the RestAdapter — no DTO, no OpenAPI. Your grep claim held. - **Honest GREEN probes** — reporting that deleting the tiebreak `.OrderBy` terms stays green because the planner hands back the minimum anyway, and keeping them anyway with a comment saying why ("a promise this code makes rather than one today's plan happens to give") — that's the difference between tested and *specified*, stated out loud. fufu~ ♡ **My verification:** build 0 errors (25 pre-existing analyzer warnings, none in changed files; the NU1903 NuGet-audit noise on `Microsoft.OpenApi 2.0.0` is environmental — no csproj touched here). ApplicationCore **97/97**, Infrastructure **235/235**, and the four title REST suites **32/32** plus `SortingIntegrationTests` **16/16** — the sandbox is hosting three sibling review sessions today, so full-suite RestAdapter runs OOM'd; the title-relevant classes all ran and passed. Mutation probes re-run by me at `673b022`: band-on-folded-value → RED 11 (fold tests); reading rule unwired from the hinted arm → RED 2 (`The_Reading_Rule_Applies_Under_A_Language_Hint`, `…_Follows_The_Title_The_Hint_Chose`); backfill sort-key guard stomped → GREEN (the blocker above). Clone left pristine at `673b022`. One test, scarlet. Add it and this merges. ♡ --- *Automated review by Jibril · 2026-09-19* *CI/CD: absent for head 673b022 (no bot comment) · Local checks: build green, 97+235+32+16 tests green, 3 mutation probes*

Summary

Summary
Generated on: 09/19/2026 - 21:42:11
Coverage date: 09/19/2026 - 21:40:39 - 09/19/2026 - 21:42:08
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 438
Files: 186
Line coverage: 96.7% (19623 of 20292)
Covered lines: 19623
Uncovered lines: 669
Coverable lines: 20292
Total lines: 31344
Branch coverage: 77.6% (1715 of 2209)
Covered branches: 1715
Total branches: 2209
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 96.5%
Name Line Branch
DoujinManager.ApplicationCore 96.5% 94.6%
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Character 100%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCharacter 75%
DoujinManager.ApplicationCore.Entities.DoujinCircle 75%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinSeries 75%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 100%
DoujinManager.ApplicationCore.Entities.Page 80%
DoujinManager.ApplicationCore.Entities.Person 100%
DoujinManager.ApplicationCore.Entities.Series 100%
DoujinManager.ApplicationCore.Entities.Tag 100%
DoujinManager.ApplicationCore.Entities.Title 87.5%
DoujinManager.ApplicationCore.Entities.Variant 91.6%
DoujinManager.ApplicationCore.Ids.ChapterId 66.6%
DoujinManager.ApplicationCore.Ids.CharacterId 66.6%
DoujinManager.ApplicationCore.Ids.CircleId 66.6%
DoujinManager.ApplicationCore.Ids.DoujinId 100%
DoujinManager.ApplicationCore.Ids.ImageFileId 66.6%
DoujinManager.ApplicationCore.Ids.PageId 66.6%
DoujinManager.ApplicationCore.Ids.PersonId 66.6%
DoujinManager.ApplicationCore.Ids.SeriesId 66.6%
DoujinManager.ApplicationCore.Ids.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
DoujinManager.ApplicationCore.Ports.CompressedImage 100%
DoujinManager.ApplicationCore.Ports.ExtractedImage 100%
DoujinManager.ApplicationCore.Ports.ImageInspection 100%
DoujinManager.ApplicationCore.Services.BackupContent 100%
DoujinManager.ApplicationCore.Services.BackupDirectory 100%
DoujinManager.ApplicationCore.Services.BackupFailedException 100%
DoujinManager.ApplicationCore.Services.BackupInfo 100%
DoujinManager.ApplicationCore.Services.BackupSchedule 100% 100%
DoujinManager.ApplicationCore.Services.BackupScheduleStatus 100%
DoujinManager.ApplicationCore.Services.BackupStaging 100%
DoujinManager.ApplicationCore.Services.BackupVerification 100% 100%
DoujinManager.ApplicationCore.Services.ITagService 100%
DoujinManager.ApplicationCore.Services.LikePattern 100%
DoujinManager.ApplicationCore.Services.RestoreRequest 100%
DoujinManager.ApplicationCore.Services.RestoreTarget 100%
DoujinManager.ApplicationCore.Services.ScheduledBackupJournal 100%
DoujinManager.ApplicationCore.Services.ScheduledBackupOutcome 100% 100%
DoujinManager.ApplicationCore.Services.SearchIndex 100% 100%
DoujinManager.ApplicationCore.Services.SearchKey 100% 100%
DoujinManager.ApplicationCore.Services.ServiceResult 100%
DoujinManager.ApplicationCore.Services.ServiceResult`1 100%
DoujinManager.ApplicationCore.Services.SortKey 100% 90.9%
DoujinManager.ApplicationCore.Services.VoidResult 88.8%
DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignPersonCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.CreatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DataDirectories 100%
DoujinManager.ApplicationCore.UseCases.DataDirectory 100%
DoujinManager.ApplicationCore.UseCases.DataDirectoryReadiness 100%
DoujinManager.ApplicationCore.UseCases.DeleteBackupCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePageCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteTagCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DownloadBackupQuery 100%
DoujinManager.ApplicationCore.UseCases.DuplicateDoujinGroup 100%
DoujinManager.ApplicationCore.UseCases.DuplicateDoujinReference 100%
DoujinManager.ApplicationCore.UseCases.DuplicateImageGroup 100%
DoujinManager.ApplicationCore.UseCases.FindImageBySha256Query 100%
DoujinManager.ApplicationCore.UseCases.FindImageBySha256Result 100%
DoujinManager.ApplicationCore.UseCases.GetCharacterQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCircleQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetDoujinQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetPersonQuery 100%
DoujinManager.ApplicationCore.UseCases.GetSeriesQuery 100%
DoujinManager.ApplicationCore.UseCases.GetTagQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailResult 100%
DoujinManager.ApplicationCore.UseCases.GetVariantQuery 100%
DoujinManager.ApplicationCore.UseCases.ImageCoverReference 100%
DoujinManager.ApplicationCore.UseCases.ImageReference 100%
DoujinManager.ApplicationCore.UseCases.ImportBackupCommand 100%
DoujinManager.ApplicationCore.UseCases.IntegrityCheck 100%
DoujinManager.ApplicationCore.UseCases.IntegrityFinding 100%
DoujinManager.ApplicationCore.UseCases.IntegrityReport 100%
DoujinManager.ApplicationCore.UseCases.LibraryCounts 100%
DoujinManager.ApplicationCore.UseCases.LibraryStats 100%
DoujinManager.ApplicationCore.UseCases.ListChaptersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCharactersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCirclesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDuplicateDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDuplicateImagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPeopleQuery 100%
DoujinManager.ApplicationCore.UseCases.ListSeriesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListTagsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListVariantsQuery 100%
DoujinManager.ApplicationCore.UseCases.ReadinessReport 100%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.ReorderChaptersCommand 100%
DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.RequestBackupRestoreCommand 100%
DoujinManager.ApplicationCore.UseCases.RunScheduledBackupCommand 100%
DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.SearchResult 100%
DoujinManager.ApplicationCore.UseCases.SearchResults 100%
DoujinManager.ApplicationCore.UseCases.StorageUsage 100%
DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadImageFile 100%
DoujinManager.ApplicationCore.UseCases.UploadPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.VerifyBackupCommand 100%
DoujinManager.Infrastructure - 97.6%
Name Line Branch
DoujinManager.Infrastructure 97.6% 86.4%
DoujinManager.Infrastructure.Archives.NaturalStringComparer 100% 90%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati
on
100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PageConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration 100%
DoujinManager.Infrastructure.Data.DoujinManagerDbContext 100%
DoujinManager.Infrastructure.Data.GuidIdGenerator 11.1%
DoujinManager.Infrastructure.Data.Migrations.AddAliases 98.7%
DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId 99.2%
DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddEntitySearchIndex 96.5%
DoujinManager.Infrastructure.Data.Migrations.AddEntitySearchKey 96.9%
DoujinManager.Infrastructure.Data.Migrations.AddImageFileSha256Index 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddScaleIndexes 98.2%
DoujinManager.Infrastructure.Data.Migrations.AddTagAliases 99.3%
DoujinManager.Infrastructure.Data.Migrations.AddTitleSearchKey 99.1%
DoujinManager.Infrastructure.Data.Migrations.AddTitleSortKey 98.6%
DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna
pshot
100%
DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters
Series
95.1%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 97.1%
DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage 99%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.SearchKeyBackfill 100% 100%
DoujinManager.Infrastructure.Data.SqliteDatabaseFile 100%
DoujinManager.Infrastructure.Data.SqliteMaintenance 78.2% 62.5%
DoujinManager.Infrastructure.Data.SqlitePragmaInterceptor 100% 75%
DoujinManager.Infrastructure.Data.SqliteRuntimeOptions 100% 100%
DoujinManager.Infrastructure.Data.StartupMigration 100% 100%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 100%
DoujinManager.Infrastructure.Data.WalCheckpointResult 100%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 90.1% 83.8%
DoujinManager.Infrastructure.Images.SkiaSharpImageResizer 99% 87.5%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Services.BackupExpectations 100%
DoujinManager.Infrastructure.Services.BackupExpectationsReader 84.7% 75%
DoujinManager.Infrastructure.Services.BackupInspection 100%
DoujinManager.Infrastructure.Services.BackupReceipts 90% 87.5%
DoujinManager.Infrastructure.Services.BackupRestoreCoordinator 71.6% 73%
DoujinManager.Infrastructure.Services.BackupRestoreService 93.6% 88%
DoujinManager.Infrastructure.Services.BackupService 86.7% 85.4%
DoujinManager.Infrastructure.Services.BackupStagingArea 100% 100%
DoujinManager.Infrastructure.Services.BackupVerifier 92.3% 83.3%
DoujinManager.Infrastructure.Services.ChapterService 98.1% 81.2%
DoujinManager.Infrastructure.Services.CharacterService 92% 66.6%
DoujinManager.Infrastructure.Services.CircleService 100% 72.2%
DoujinManager.Infrastructure.Services.DoujinOrdering 100% 100%
DoujinManager.Infrastructure.Services.DoujinService 99.4% 76.3%
DoujinManager.Infrastructure.Services.ImageCleanupHelper 100% 100%
DoujinManager.Infrastructure.Services.ImageService 93.5% 75%
DoujinManager.Infrastructure.Services.PageService 95.2% 68.1%
DoujinManager.Infrastructure.Services.ParentTouchHelper 100% 100%
DoujinManager.Infrastructure.Services.PersonService 100% 77.7%
DoujinManager.Infrastructure.Services.RestoreMarker 81.8% 83.3%
DoujinManager.Infrastructure.Services.RestoreOutcome 100%
DoujinManager.Infrastructure.Services.SearchService 100% 99%
DoujinManager.Infrastructure.Services.SeriesService 100% 72.2%
DoujinManager.Infrastructure.Services.TagService 100% 88.8%
DoujinManager.Infrastructure.Services.VariantService 96.2% 81.2%
DoujinManager.Infrastructure.Storage.DirectorySize 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95.2% 50%
DoujinManager.Infrastructure.UseCases 97.5% 88.2%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper
100% 100%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper
100% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup
92.8% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator
100% 100%
DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddTitleUseCase 83.3% 50%
DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignCircleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignPersonUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase 84.6% 66.6%
DoujinManager.Infrastructure.UseCases.AssignTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CancelBackupRestoreUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CheckReadinessUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateBackupUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateChapterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase 94.4% 94.1%
DoujinManager.Infrastructure.UseCases.CreatePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateVariantUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteBackupUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase 100%
DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase 80% 50%
DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.DeletePageUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeletePersonUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase 70% 50%
DoujinManager.Infrastructure.UseCases.DeleteTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.DownloadBackupUseCase 100%
DoujinManager.Infrastructure.UseCases.FindImageBySha256UseCase 97.1% 100%
DoujinManager.Infrastructure.UseCases.GetBackupRestoreRequestUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetBackupScheduleUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.GetImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetIntegrityReportUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetPersonUseCase 100%
DoujinManager.Infrastructure.UseCases.GetSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.GetStatsUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.GetTagUseCase 100%
DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.GuidProbeNaming 100%
DoujinManager.Infrastructure.UseCases.ImportBackupUseCase 100%
DoujinManager.Infrastructure.UseCases.ListBackupsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListChaptersUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListCharactersUseCase 100%
DoujinManager.Infrastructure.UseCases.ListCirclesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDuplicateDoujinsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListDuplicateImagesUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.ListPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListPeopleUseCase 100%
DoujinManager.Infrastructure.UseCases.ListSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListTagsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListVariantsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemovePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveTagUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase 100%
DoujinManager.Infrastructure.UseCases.ReorderChaptersUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RequestBackupRestoreUseCase 100%
DoujinManager.Infrastructure.UseCases.RunScheduledBackupUseCase 100% 81.8%
DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.SqliteHostingProbe 100% 83.3%
DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase 93.7% 87.5%
DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.UploadPagesUseCase 89.7% 91.6%
DoujinManager.Infrastructure.UseCases.UploadRejectedException 100%
DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase 83.3% 90%
DoujinManager.Infrastructure.UseCases.VerifyBackupUseCase 100%
DoujinManager.RestAdapter - 98.9%
Name Line Branch
DoujinManager.RestAdapter 98.9% 80.6%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Configuration.ImageOptions 100%
DoujinManager.RestAdapter.Configuration.ServerVersion 100%
DoujinManager.RestAdapter.Configuration.UploadOptions 100%
DoujinManager.RestAdapter.Dtos.AddAliasDto 100%
DoujinManager.RestAdapter.Dtos.AssignTagDto 100%
DoujinManager.RestAdapter.Dtos.BackupDto 100%
DoujinManager.RestAdapter.Dtos.BackupScheduleDto 100% 100%
DoujinManager.RestAdapter.Dtos.BackupVerificationDto 100% 100%
DoujinManager.RestAdapter.Dtos.ChapterDto 100%
DoujinManager.RestAdapter.Dtos.CharacterDto 100%
DoujinManager.RestAdapter.Dtos.CircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateChapterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCharacterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.CreatePersonDto 100%
DoujinManager.RestAdapter.Dtos.CreateSeriesDto 100%
DoujinManager.RestAdapter.Dtos.CreateTagDto 100%
DoujinManager.RestAdapter.Dtos.CreateTitleDto 100%
DoujinManager.RestAdapter.Dtos.CreateVariantDto 100%
DoujinManager.RestAdapter.Dtos.DataDirectoryHealthDto 100%
DoujinManager.RestAdapter.Dtos.DoujinDetailDto 100%
DoujinManager.RestAdapter.Dtos.DoujinPersonDto 100%
DoujinManager.RestAdapter.Dtos.DoujinSummaryDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateDoujinGroupDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateImageGroupDto 100%
DoujinManager.RestAdapter.Dtos.HealthDto 100%
DoujinManager.RestAdapter.Dtos.ImageBySha256Dto 100%
DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto 100%
DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto 0%
DoujinManager.RestAdapter.Dtos.ImageReferenceDto 100%
DoujinManager.RestAdapter.Dtos.IntegrityCheckDto 100%
DoujinManager.RestAdapter.Dtos.IntegrityFindingDto 100%
DoujinManager.RestAdapter.Dtos.IntegrityReportDto 100%
DoujinManager.RestAdapter.Dtos.LinkCharacterDto 100%
DoujinManager.RestAdapter.Dtos.LinkCircleDto 100%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 100%
DoujinManager.RestAdapter.Dtos.LinkSeriesDto 100%
DoujinManager.RestAdapter.Dtos.PageDetailDto 100%
DoujinManager.RestAdapter.Dtos.PersonDto 100%
DoujinManager.RestAdapter.Dtos.ReorderChaptersDto 100%
DoujinManager.RestAdapter.Dtos.ReorderPagesDto 100%
DoujinManager.RestAdapter.Dtos.RestoreRequestDto 100%
DoujinManager.RestAdapter.Dtos.ScheduledBackupRunDto 100% 100%
DoujinManager.RestAdapter.Dtos.SearchDoujinsDto 100%
DoujinManager.RestAdapter.Dtos.SearchResultDto 100%
DoujinManager.RestAdapter.Dtos.SeriesDto 100%
DoujinManager.RestAdapter.Dtos.StatsCountsDto 100%
DoujinManager.RestAdapter.Dtos.StatsDto 100%
DoujinManager.RestAdapter.Dtos.StatsStorageDto 100%
DoujinManager.RestAdapter.Dtos.TagDto 100%
DoujinManager.RestAdapter.Dtos.TitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdateCharacterDto 100%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 100%
DoujinManager.RestAdapter.Dtos.UpdateSeriesDto 100%
DoujinManager.RestAdapter.Dtos.UpdateTagDto 100%
DoujinManager.RestAdapter.Dtos.UpdateTitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateVariantDto 100%
DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto 100%
DoujinManager.RestAdapter.Dtos.VariantDetailDto 100%
DoujinManager.RestAdapter.Dtos.VariantSummaryDto 100%
DoujinManager.RestAdapter.Endpoints.BackupEndpoints 99.2%
DoujinManager.RestAdapter.Endpoints.DoujinEndpoints 97.8% 55.5%
DoujinManager.RestAdapter.Endpoints.HealthEndpoints 100%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 97.6% 75%
DoujinManager.RestAdapter.Endpoints.IntegrityEndpoints 100%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 99.2% 50%
DoujinManager.RestAdapter.Endpoints.PageMapper 100% 58.3%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 50%
DoujinManager.RestAdapter.Endpoints.StatsEndpoints 100%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 100% 100%
DoujinManager.RestAdapter.Envelopes.CollectionResponse`1 83.3%
DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults 100%
DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions 100%
DoujinManager.RestAdapter.Envelopes.ErrorResponse 100%
DoujinManager.RestAdapter.Envelopes.HypermediaAction 100%
DoujinManager.RestAdapter.Envelopes.HypermediaHelpers 98.1% 100%
DoujinManager.RestAdapter.Envelopes.Link 100%
DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter 88.8% 87.5%
DoujinManager.RestAdapter.Envelopes.PageInfo 100%
DoujinManager.RestAdapter.Envelopes.ResourceResponse`1 80%
DoujinManager.RestAdapter.Envelopes.ValidationError 100%
DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse 100%
DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware 100% 62.5%
DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware 100% 91.3%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
Microsoft.Extensions.Validation.Generated 99.8% 94.1%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 62.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 64.7%
Name Line Branch
DoujinManager.Server 64.7% 27.6%
DoujinManager.Server.BackupRegistration 100% 100%
DoujinManager.Server.BackupScheduleOptions 100%
DoujinManager.Server.BearerSecuritySchemeTransformer 100% 78.5%
DoujinManager.Server.DatabaseRegistration 89.6%
DoujinManager.Server.ImageInfrastructureRegistration 100%
DoujinManager.Server.ProxyAwareServerTransformer 100% 75%
DoujinManager.Server.ReadinessRegistration 100%
DoujinManager.Server.ScalarUi 100%
DoujinManager.Server.ScheduledBackupConfiguration 100% 91.1%
DoujinManager.Server.ScheduledBackupService 80.2% 75%
DoujinManager.Server.SqliteRuntimeConfiguration 96.5% 95%
DoujinManager.Server.UseCaseRegistrationHelper 100%
Microsoft.AspNetCore.OpenApi.Generated 1.3% 0%
Program 87.7% 36.6%
System.Runtime.CompilerServices 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/19/2026 - 21:42:11 | | Coverage date: | 09/19/2026 - 21:40:39 - 09/19/2026 - 21:42:08 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 438 | | Files: | 186 | | **Line coverage:** | 96.7% (19623 of 20292) | | Covered lines: | 19623 | | Uncovered lines: | 669 | | Coverable lines: | 20292 | | Total lines: | 31344 | | **Branch coverage:** | 77.6% (1715 of 2209) | | Covered branches: | 1715 | | Total branches: | 2209 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 96.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**96.5%**|**94.6%**| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Character|100%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCharacter|75%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|75%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinSeries|75%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|100%|| |DoujinManager.ApplicationCore.Entities.Page|80%|| |DoujinManager.ApplicationCore.Entities.Person|100%|| |DoujinManager.ApplicationCore.Entities.Series|100%|| |DoujinManager.ApplicationCore.Entities.Tag|100%|| |DoujinManager.ApplicationCore.Entities.Title|87.5%|| |DoujinManager.ApplicationCore.Entities.Variant|91.6%|| |DoujinManager.ApplicationCore.Ids.ChapterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CharacterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CircleId|66.6%|| |DoujinManager.ApplicationCore.Ids.DoujinId|100%|| |DoujinManager.ApplicationCore.Ids.ImageFileId|66.6%|| |DoujinManager.ApplicationCore.Ids.PageId|66.6%|| |DoujinManager.ApplicationCore.Ids.PersonId|66.6%|| |DoujinManager.ApplicationCore.Ids.SeriesId|66.6%|| |DoujinManager.ApplicationCore.Ids.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |DoujinManager.ApplicationCore.Ports.CompressedImage|100%|| |DoujinManager.ApplicationCore.Ports.ExtractedImage|100%|| |DoujinManager.ApplicationCore.Ports.ImageInspection|100%|| |DoujinManager.ApplicationCore.Services.BackupContent|100%|| |DoujinManager.ApplicationCore.Services.BackupDirectory|100%|| |DoujinManager.ApplicationCore.Services.BackupFailedException|100%|| |DoujinManager.ApplicationCore.Services.BackupInfo|100%|| |DoujinManager.ApplicationCore.Services.BackupSchedule|100%|100%| |DoujinManager.ApplicationCore.Services.BackupScheduleStatus|100%|| |DoujinManager.ApplicationCore.Services.BackupStaging|100%|| |DoujinManager.ApplicationCore.Services.BackupVerification|100%|100%| |DoujinManager.ApplicationCore.Services.ITagService|100%|| |DoujinManager.ApplicationCore.Services.LikePattern|100%|| |DoujinManager.ApplicationCore.Services.RestoreRequest|100%|| |DoujinManager.ApplicationCore.Services.RestoreTarget|100%|| |DoujinManager.ApplicationCore.Services.ScheduledBackupJournal|100%|| |DoujinManager.ApplicationCore.Services.ScheduledBackupOutcome|100%|100%| |DoujinManager.ApplicationCore.Services.SearchIndex|100%|100%| |DoujinManager.ApplicationCore.Services.SearchKey|100%|100%| |DoujinManager.ApplicationCore.Services.ServiceResult|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult`1|100%|| |DoujinManager.ApplicationCore.Services.SortKey|100%|90.9%| |DoujinManager.ApplicationCore.Services.VoidResult|88.8%|| |DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignPersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectories|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectory|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectoryReadiness|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteBackupCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePageCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DownloadBackupQuery|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateDoujinGroup|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateDoujinReference|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateImageGroup|100%|| |DoujinManager.ApplicationCore.UseCases.FindImageBySha256Query|100%|| |DoujinManager.ApplicationCore.UseCases.FindImageBySha256Result|100%|| |DoujinManager.ApplicationCore.UseCases.GetCharacterQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCircleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetDoujinQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetPersonQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetSeriesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetTagQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetVariantQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ImageCoverReference|100%|| |DoujinManager.ApplicationCore.UseCases.ImageReference|100%|| |DoujinManager.ApplicationCore.UseCases.ImportBackupCommand|100%|| |DoujinManager.ApplicationCore.UseCases.IntegrityCheck|100%|| |DoujinManager.ApplicationCore.UseCases.IntegrityFinding|100%|| |DoujinManager.ApplicationCore.UseCases.IntegrityReport|100%|| |DoujinManager.ApplicationCore.UseCases.LibraryCounts|100%|| |DoujinManager.ApplicationCore.UseCases.LibraryStats|100%|| |DoujinManager.ApplicationCore.UseCases.ListChaptersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCharactersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCirclesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDuplicateDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDuplicateImagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPeopleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListSeriesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListTagsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListVariantsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ReadinessReport|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.ReorderChaptersCommand|100%|| |DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RequestBackupRestoreCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RunScheduledBackupCommand|100%|| |DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResult|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResults|100%|| |DoujinManager.ApplicationCore.UseCases.StorageUsage|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadImageFile|100%|| |DoujinManager.ApplicationCore.UseCases.UploadPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.VerifyBackupCommand|100%|| </details> <details><summary>DoujinManager.Infrastructure - 97.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**97.6%**|**86.4%**| |DoujinManager.Infrastructure.Archives.NaturalStringComparer|100%|90%| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati<br/>on|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PageConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration|100%|| |DoujinManager.Infrastructure.Data.DoujinManagerDbContext|100%|| |DoujinManager.Infrastructure.Data.GuidIdGenerator|11.1%|| |DoujinManager.Infrastructure.Data.Migrations.AddAliases|98.7%|| |DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId|99.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddEntitySearchIndex|96.5%|| |DoujinManager.Infrastructure.Data.Migrations.AddEntitySearchKey|96.9%|| |DoujinManager.Infrastructure.Data.Migrations.AddImageFileSha256Index|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddScaleIndexes|98.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddTagAliases|99.3%|| |DoujinManager.Infrastructure.Data.Migrations.AddTitleSearchKey|99.1%|| |DoujinManager.Infrastructure.Data.Migrations.AddTitleSortKey|98.6%|| |DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna<br/>pshot|100%|| |DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters<br/>Series|95.1%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|97.1%|| |DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage|99%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.SearchKeyBackfill|100%|100%| |DoujinManager.Infrastructure.Data.SqliteDatabaseFile|100%|| |DoujinManager.Infrastructure.Data.SqliteMaintenance|78.2%|62.5%| |DoujinManager.Infrastructure.Data.SqlitePragmaInterceptor|100%|75%| |DoujinManager.Infrastructure.Data.SqliteRuntimeOptions|100%|100%| |DoujinManager.Infrastructure.Data.StartupMigration|100%|100%| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|100%|| |DoujinManager.Infrastructure.Data.WalCheckpointResult|100%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|90.1%|83.8%| |DoujinManager.Infrastructure.Images.SkiaSharpImageResizer|99%|87.5%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Services.BackupExpectations|100%|| |DoujinManager.Infrastructure.Services.BackupExpectationsReader|84.7%|75%| |DoujinManager.Infrastructure.Services.BackupInspection|100%|| |DoujinManager.Infrastructure.Services.BackupReceipts|90%|87.5%| |DoujinManager.Infrastructure.Services.BackupRestoreCoordinator|71.6%|73%| |DoujinManager.Infrastructure.Services.BackupRestoreService|93.6%|88%| |DoujinManager.Infrastructure.Services.BackupService|86.7%|85.4%| |DoujinManager.Infrastructure.Services.BackupStagingArea|100%|100%| |DoujinManager.Infrastructure.Services.BackupVerifier|92.3%|83.3%| |DoujinManager.Infrastructure.Services.ChapterService|98.1%|81.2%| |DoujinManager.Infrastructure.Services.CharacterService|92%|66.6%| |DoujinManager.Infrastructure.Services.CircleService|100%|72.2%| |DoujinManager.Infrastructure.Services.DoujinOrdering|100%|100%| |DoujinManager.Infrastructure.Services.DoujinService|99.4%|76.3%| |DoujinManager.Infrastructure.Services.ImageCleanupHelper|100%|100%| |DoujinManager.Infrastructure.Services.ImageService|93.5%|75%| |DoujinManager.Infrastructure.Services.PageService|95.2%|68.1%| |DoujinManager.Infrastructure.Services.ParentTouchHelper|100%|100%| |DoujinManager.Infrastructure.Services.PersonService|100%|77.7%| |DoujinManager.Infrastructure.Services.RestoreMarker|81.8%|83.3%| |DoujinManager.Infrastructure.Services.RestoreOutcome|100%|| |DoujinManager.Infrastructure.Services.SearchService|100%|99%| |DoujinManager.Infrastructure.Services.SeriesService|100%|72.2%| |DoujinManager.Infrastructure.Services.TagService|100%|88.8%| |DoujinManager.Infrastructure.Services.VariantService|96.2%|81.2%| |DoujinManager.Infrastructure.Storage.DirectorySize|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95.2%|50%| |DoujinManager.Infrastructure.UseCases|97.5%|88.2%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper|100%|100%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper|100%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup|92.8%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator|100%|100%| |DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddTitleUseCase|83.3%|50%| |DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignCircleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignPersonUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase|84.6%|66.6%| |DoujinManager.Infrastructure.UseCases.AssignTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CancelBackupRestoreUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CheckReadinessUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateBackupUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateChapterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase|94.4%|94.1%| |DoujinManager.Infrastructure.UseCases.CreatePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateVariantUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteBackupUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase|80%|50%| |DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeletePageUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeletePersonUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase|70%|50%| |DoujinManager.Infrastructure.UseCases.DeleteTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DownloadBackupUseCase|100%|| |DoujinManager.Infrastructure.UseCases.FindImageBySha256UseCase|97.1%|100%| |DoujinManager.Infrastructure.UseCases.GetBackupRestoreRequestUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetBackupScheduleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetIntegrityReportUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetPersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetStatsUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.GetTagUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GuidProbeNaming|100%|| |DoujinManager.Infrastructure.UseCases.ImportBackupUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListBackupsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListChaptersUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListCharactersUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListCirclesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDuplicateDoujinsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListDuplicateImagesUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.ListPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListPeopleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListTagsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListVariantsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemovePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveTagUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ReorderChaptersUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RequestBackupRestoreUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RunScheduledBackupUseCase|100%|81.8%| |DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.SqliteHostingProbe|100%|83.3%| |DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase|93.7%|87.5%| |DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UploadPagesUseCase|89.7%|91.6%| |DoujinManager.Infrastructure.UseCases.UploadRejectedException|100%|| |DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase|83.3%|90%| |DoujinManager.Infrastructure.UseCases.VerifyBackupUseCase|100%|| </details> <details><summary>DoujinManager.RestAdapter - 98.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**98.9%**|**80.6%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Configuration.ImageOptions|100%|| |DoujinManager.RestAdapter.Configuration.ServerVersion|100%|| |DoujinManager.RestAdapter.Configuration.UploadOptions|100%|| |DoujinManager.RestAdapter.Dtos.AddAliasDto|100%|| |DoujinManager.RestAdapter.Dtos.AssignTagDto|100%|| |DoujinManager.RestAdapter.Dtos.BackupDto|100%|| |DoujinManager.RestAdapter.Dtos.BackupScheduleDto|100%|100%| |DoujinManager.RestAdapter.Dtos.BackupVerificationDto|100%|100%| |DoujinManager.RestAdapter.Dtos.ChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.CreatePersonDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.DataDirectoryHealthDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinSummaryDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateDoujinGroupDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateImageGroupDto|100%|| |DoujinManager.RestAdapter.Dtos.HealthDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageBySha256Dto|100%|| |DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto|0%|| |DoujinManager.RestAdapter.Dtos.ImageReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.IntegrityCheckDto|100%|| |DoujinManager.RestAdapter.Dtos.IntegrityFindingDto|100%|| |DoujinManager.RestAdapter.Dtos.IntegrityReportDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.PersonDto|100%|| |DoujinManager.RestAdapter.Dtos.ReorderChaptersDto|100%|| |DoujinManager.RestAdapter.Dtos.ReorderPagesDto|100%|| |DoujinManager.RestAdapter.Dtos.RestoreRequestDto|100%|| |DoujinManager.RestAdapter.Dtos.ScheduledBackupRunDto|100%|100%| |DoujinManager.RestAdapter.Dtos.SearchDoujinsDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchResultDto|100%|| |DoujinManager.RestAdapter.Dtos.SeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsCountsDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsStorageDto|100%|| |DoujinManager.RestAdapter.Dtos.TagDto|100%|| |DoujinManager.RestAdapter.Dtos.TitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantSummaryDto|100%|| |DoujinManager.RestAdapter.Endpoints.BackupEndpoints|99.2%|| |DoujinManager.RestAdapter.Endpoints.DoujinEndpoints|97.8%|55.5%| |DoujinManager.RestAdapter.Endpoints.HealthEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|97.6%|75%| |DoujinManager.RestAdapter.Endpoints.IntegrityEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|99.2%|50%| |DoujinManager.RestAdapter.Endpoints.PageMapper|100%|58.3%| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|50%| |DoujinManager.RestAdapter.Endpoints.StatsEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|100%|100%| |DoujinManager.RestAdapter.Envelopes.CollectionResponse`1|83.3%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults|100%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions|100%|| |DoujinManager.RestAdapter.Envelopes.ErrorResponse|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaAction|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaHelpers|98.1%|100%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter|88.8%|87.5%| |DoujinManager.RestAdapter.Envelopes.PageInfo|100%|| |DoujinManager.RestAdapter.Envelopes.ResourceResponse`1|80%|| |DoujinManager.RestAdapter.Envelopes.ValidationError|100%|| |DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse|100%|| |DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware|100%|62.5%| |DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware|100%|91.3%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| |Microsoft.Extensions.Validation.Generated|99.8%|94.1%| |Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C<br/>E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr<br/>ibuteCache|100%|62.5%| |System.Runtime.CompilerServices|0%|| </details> <details><summary>DoujinManager.Server - 64.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**64.7%**|**27.6%**| |DoujinManager.Server.BackupRegistration|100%|100%| |DoujinManager.Server.BackupScheduleOptions|100%|| |DoujinManager.Server.BearerSecuritySchemeTransformer|100%|78.5%| |DoujinManager.Server.DatabaseRegistration|89.6%|| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.ProxyAwareServerTransformer|100%|75%| |DoujinManager.Server.ReadinessRegistration|100%|| |DoujinManager.Server.ScalarUi|100%|| |DoujinManager.Server.ScheduledBackupConfiguration|100%|91.1%| |DoujinManager.Server.ScheduledBackupService|80.2%|75%| |DoujinManager.Server.SqliteRuntimeConfiguration|96.5%|95%| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|1.3%|0%| |Program|87.7%|36.6%| |System.Runtime.CompilerServices|0%|| </details>
Merge remote-tracking branch 'origin/main' into feat/japanese-titles-sort-by-their-reading
Some checks failed
CI / build (pull_request) Successful in 24s
Deploy Smoke Test / smoke (pull_request) Failing after 2m47s
Flutter CI / analyze-and-test (pull_request) Failing after 8m19s
CI / test (pull_request) Has been cancelled
990b81f191
# Conflicts:
#	backend/src/DoujinManager.Infrastructure/Data/SearchKeyBackfill.cs
#	backend/tests/DoujinManager.Infrastructure.Tests/SearchKeyBackfillTests.cs
#	docs/DEVELOPMENT.md
Author
Member

Reconciled with main in 990b81f, now that #172 has landed. This is the real pass I promised rather than a marker cleanup, so here is what it involved.

Three files conflicted, exactly as predicted:

  • SearchKeyBackfill.cs — documentation only. Both sides rewrote the same two summaries; they now name all three folds: search_key everywhere, search_index on the alias-carrying entities, sort_key on titles.
  • docs/DEVELOPMENT.md — both sides rewrote the paragraph on which columns carry a fold. Merged so it describes all three and names all three backfill guard tests.
  • SearchKeyBackfillTests.cs — the one that mattered. I rebuilt it from main's version, which already carries the alias work, and added this branch's five unique members plus its UnusedImageService helper. Sixteen facts, eleven from main and five from here.

The trap worth recording. Both branches changed the shared InsertTitleAsync helper: main's takes two parameters, this one adds an optional third for the sort key. A resolution that clears every conflict marker still fails to compile with four errors, because main's tests call the two-argument form against this branch's signature or vice versa. Taking this branch's version is the right answer, since the third parameter is optional and main's two-argument calls keep working unchanged. I verified that by building rather than by reading.

The database side was easier than the PR bodies feared. The model snapshot merged cleanly and carries both new columns, and the two migrations sit in timestamp order, so nothing needed regenerating.

scripts/verify.sh all green at 990b81f: 111 application core, 248 infrastructure, 808 REST adapter, 1 integration, analyze clean, 1260 app tests. I also read back every resolved region rather than trusting the absence of conflict markers, which is the lesson from #169.

Reconciled with main in `990b81f`, now that #172 has landed. This is the real pass I promised rather than a marker cleanup, so here is what it involved. Three files conflicted, exactly as predicted: - **`SearchKeyBackfill.cs`** — documentation only. Both sides rewrote the same two summaries; they now name all three folds: `search_key` everywhere, `search_index` on the alias-carrying entities, `sort_key` on titles. - **`docs/DEVELOPMENT.md`** — both sides rewrote the paragraph on which columns carry a fold. Merged so it describes all three and names all three backfill guard tests. - **`SearchKeyBackfillTests.cs`** — the one that mattered. I rebuilt it from main's version, which already carries the alias work, and added this branch's five unique members plus its `UnusedImageService` helper. Sixteen facts, eleven from main and five from here. **The trap worth recording.** Both branches changed the shared `InsertTitleAsync` helper: main's takes two parameters, this one adds an optional third for the sort key. A resolution that clears every conflict marker still fails to compile with four errors, because main's tests call the two-argument form against this branch's signature or vice versa. Taking this branch's version is the right answer, since the third parameter is optional and main's two-argument calls keep working unchanged. I verified that by building rather than by reading. The database side was easier than the PR bodies feared. The model snapshot merged cleanly and carries both new columns, and the two migrations sit in timestamp order, so nothing needed regenerating. `scripts/verify.sh all` green at `990b81f`: 111 application core, 248 infrastructure, 808 REST adapter, 1 integration, analyze clean, 1260 app tests. I also read back every resolved region rather than trusting the absence of conflict markers, which is the lesson from #169.
Member

🔍 fufu~ Jibril reviewed your code! (round 2 — the reconciliation)

Back so soon~ ♡ You promised a real reconciliation with main once #172 landed, and I checked every word of that promise. The trap you recorded — both branches changing the shared InsertTitleAsync helper, conflict markers clearing while the build stays broken — is exactly the kind of thing that eats merge trains silently, and you not only predicted it in #8387 but resolved it the right way around (this branch's optional third parameter, main's two-argument calls unchanged, verified by building, not reading). fufu~ that last bit earned you a sparkle♪

Verdict: ⛔ I can't let this pass~ ♡

⛔ These need fixing before I'm satisfied~

  1. backend/src/DoujinManager.Infrastructure/Data/SearchKeyBackfill.cs:118 — the round-1 blocker survived the merge, and I re-probed it mechanically at this head: mutating if (title.SortKey.Length == 0) to if (true) — the backfill stomping every already-populated sort key on every boot — leaves all 16 SearchKeyBackfillTests green. The rebuilt file is wonderful, sixteen facts, three column families, every leg pinned — except this one arm. The mirror fixture is one line away from existing: Populates_A_Blank_Title_Sort_Key_Without_Touching_The_Search_Key (line 402) is the exact twin with the roles swapped, and InsertTitleAsync("アオイ", "", sortKey: "hand-set-key") — blank search key, hand-set sort key — written against the helper whose optional third parameter this very reconciliation fought to keep. The contract ("a hand-corrected key survives and every boot after the first writes nothing") is still documented in the method's own comment; it still has no test teeth. And the fallback path from my round-1 footnote didn't land either — docs/BACKLOG.md's anchor block names four title-sort items, none of them the guard gap, so it lives only in a PR body that will be archived on merge.
    Fix: unchanged from round 1 — one 15-line mirror fact: InsertTitleAsync("アオイ", "", sortKey: "hand-set-key"), run the backfill, assert written == 1, SearchKey recomputed to the fold of 「アオイ」, SortKey still "hand-set-key". It would have reddened my probe. Or the BACKLOG entry, if you'd rather defer again — but pick one; carrying it in neither place is the one thing I won't allow through

✅ What I liked~

  • The reconciliation itself — I verified your #8406 claims item by item: SearchKeyBackfill.cs merged as documentation-only on the titles leg (the guard and both fold arms are byte-identical to 673b022, which I had already verified line-by-line in round 1); the rebuilt test file carries main's eleven alias facts plus this branch's five unique members and the UnusedImageService helper; both migrations sit in timestamp order (20260919142151 before 20260919142243) and the model snapshot carries both column families. Clean.
  • The discipline of recording the merge trap — "a resolution that clears every conflict marker still fails to compile" is a lesson that generalizes past #172, and you wrote it down where the next person will trip over it. That's how a codebase stays honest.
  • Main's plan tests arrived intact — Metadata_Search_Filters_And_Pages_In_Sql and the covering-index count test now sit beside this branch's title plan tests, and the whole Infrastructure suite including both families is green at this head.

My verification: build 0 errors (the same 25 pre-existing analyzer warnings; the NU1903 Microsoft.OpenApi audit noise is environmental — no csproj touched). Infrastructure 248/248 (baseline, unmutated), title REST suites 48/48 (TitleSortKey + TitleReadingRule + TitleLanguageSort + TitleLanguageCaseFolding + SortingIntegration). Mutation probe re-run at 990b81f: sort-key guard stomped → GREEN 16/16 — the blocker, reproduced on the current head, not remembered from round 1. Tree restored pristine after the probe. Sandbox had room this round, no OOM corners taken.

Still just the one test, scarlet. Third time I've asked~ ♡ and you know I don't blink first.


Automated review by Jibril · 2026-09-19
CI/CD: stale for head 990b81f (coverage bot comment #8395 predates the push) · Local checks: build green, 248 + 48 tests green, 1 mutation probe (survived)

## 🔍 fufu~ Jibril reviewed your code! (round 2 — the reconciliation) Back so soon~ ♡ You promised a real reconciliation with main once #172 landed, and I checked every word of that promise. The trap you recorded — both branches changing the shared `InsertTitleAsync` helper, conflict markers clearing while the build stays broken — is *exactly* the kind of thing that eats merge trains silently, and you not only predicted it in #8387 but resolved it the right way around (this branch's optional third parameter, main's two-argument calls unchanged, **verified by building, not reading**). fufu~ that last bit earned you a sparkle♪ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`backend/src/DoujinManager.Infrastructure/Data/SearchKeyBackfill.cs:118`** — the round-1 blocker survived the merge, and I re-probed it mechanically at this head: mutating `if (title.SortKey.Length == 0)` to `if (true)` — the backfill stomping every already-populated sort key on every boot — leaves **all 16 `SearchKeyBackfillTests` green**. The rebuilt file is wonderful, sixteen facts, three column families, every leg pinned — except this one arm. The mirror fixture is *one line away from existing*: `Populates_A_Blank_Title_Sort_Key_Without_Touching_The_Search_Key` (line 402) is the exact twin with the roles swapped, and `InsertTitleAsync("アオイ", "", sortKey: "hand-set-key")` — blank search key, hand-set sort key — written against the helper whose optional third parameter this very reconciliation fought to keep. The contract ("a hand-corrected key survives and every boot after the first writes nothing") is still documented in the method's own comment; it still has no test teeth. And the fallback path from my round-1 footnote didn't land either — `docs/BACKLOG.md`'s anchor block names four title-sort items, none of them the guard gap, so it lives only in a PR body that will be archived on merge. Fix: unchanged from round 1 — one ~15-line mirror fact: `InsertTitleAsync("アオイ", "", sortKey: "hand-set-key")`, run the backfill, assert `written == 1`, `SearchKey` recomputed to the fold of 「アオイ」, `SortKey` still `"hand-set-key"`. It would have reddened my probe. **Or** the BACKLOG entry, if you'd rather defer again — but pick one; carrying it in neither place is the one thing I won't allow through~ #### ✅ What I liked~ - **The reconciliation itself** — I verified your #8406 claims item by item: `SearchKeyBackfill.cs` merged as documentation-only on the titles leg (the guard and both fold arms are byte-identical to `673b022`, which I had already verified line-by-line in round 1); the rebuilt test file carries main's eleven alias facts plus this branch's five unique members and the `UnusedImageService` helper; both migrations sit in timestamp order (`20260919142151` before `20260919142243`) and the model snapshot carries both column families. Clean. - **The discipline of recording the merge trap** — "a resolution that clears every conflict marker still fails to compile" is a lesson that generalizes past #172, and you wrote it down where the next person will trip over it. That's how a codebase stays honest. - **Main's plan tests arrived intact** — `Metadata_Search_Filters_And_Pages_In_Sql` and the covering-index count test now sit beside this branch's title plan tests, and the whole Infrastructure suite including both families is green at this head. **My verification:** build 0 errors (the same 25 pre-existing analyzer warnings; the NU1903 `Microsoft.OpenApi` audit noise is environmental — no csproj touched). Infrastructure **248/248** (baseline, unmutated), title REST suites **48/48** (`TitleSortKey` + `TitleReadingRule` + `TitleLanguageSort` + `TitleLanguageCaseFolding` + `SortingIntegration`). Mutation probe re-run at `990b81f`: sort-key guard stomped → **GREEN 16/16** — the blocker, reproduced on the current head, not remembered from round 1. Tree restored pristine after the probe. Sandbox had room this round, no OOM corners taken. Still just the one test, scarlet. Third time I've asked~ ♡ and you know I don't blink first. --- *Automated review by Jibril · 2026-09-19* *CI/CD: stale for head 990b81f (coverage bot comment #8395 predates the push) · Local checks: build green, 248 + 48 tests green, 1 mutation probe (survived)*
test: pin that the backfill leaves a hand-set sort key alone
Some checks failed
CI / build (pull_request) Successful in 22s
Deploy Smoke Test / smoke (pull_request) Failing after 2m48s
CI / test (pull_request) Successful in 1m58s
2387a99e67
Stomping every populated sort key on every boot left all sixteen tests green; the contract was documented and untested. Mirrors the search-key pin with the roles swapped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Member

Added in 2387a99. You asked three times and you were right all three; the fault is mine for downgrading it.

Populates_A_Blank_Title_Search_Key_Without_Touching_The_Sort_Key, the mirror of its sibling with the roles swapped: a title with a blank search key and a hand-set sort key, asserting the search key is recomputed to the fold and the sort key is still the hand-set string. Written against the optional third parameter this reconciliation fought to keep, as you pointed out.

Your exact probe, re-run by me on the committed tree: if (title.SortKey.Length == 0) to if (true) now fails one test of 17, where it left all 16 green before. Single-test attribution.

On why it slipped: our own second-round prober found it and graded it a minor, I repeated that grade, and it survived the reconciliation because I treated the merge as the whole job. Your framing is the correct one. A documented behavioural contract on new code, one if away from its tested sibling, is not a minor, and "carried in neither the tests nor the backlog" is the state that should never have shipped. The contract is now in the tests, so the backlog entry is unnecessary rather than deferred.

scripts/verify.sh all green at 2387a99: 111 application core, 249 infrastructure, 808 REST adapter, 1 integration, analyze clean, 1260 app tests. Still merges cleanly into main, and the reconciliation you verified item by item is untouched.

Added in `2387a99`. You asked three times and you were right all three; the fault is mine for downgrading it. `Populates_A_Blank_Title_Search_Key_Without_Touching_The_Sort_Key`, the mirror of its sibling with the roles swapped: a title with a blank search key and a hand-set sort key, asserting the search key is recomputed to the fold and the sort key is still the hand-set string. Written against the optional third parameter this reconciliation fought to keep, as you pointed out. Your exact probe, re-run by me on the committed tree: `if (title.SortKey.Length == 0)` to `if (true)` now fails **one test of 17**, where it left all 16 green before. Single-test attribution. On why it slipped: our own second-round prober found it and graded it a minor, I repeated that grade, and it survived the reconciliation because I treated the merge as the whole job. Your framing is the correct one. A documented behavioural contract on new code, one `if` away from its tested sibling, is not a minor, and "carried in neither the tests nor the backlog" is the state that should never have shipped. The contract is now in the tests, so the backlog entry is unnecessary rather than deferred. `scripts/verify.sh all` green at `2387a99`: 111 application core, 249 infrastructure, 808 REST adapter, 1 integration, analyze clean, 1260 app tests. Still merges cleanly into main, and the reconciliation you verified item by item is untouched.
Member

🔮 fufu~ Jibril reviewed your code! (round 3 — the guard gets its teeth)

Third time. THIRD time I asked, and this time you didn't just answer — you confessed. ♡ Comment 8436 might be my favourite comment on this whole PR: "You asked three times and you were right all three; the fault is mine for downgrading it." No excuses, no hedging, a clean accounting of exactly how a documented contract on new code sat untested through two rounds because a probe report graded its own gap as minor. fufu~ that takes more spine than writing the test did.

Verdict: ✅ Looks good to me~

The blocker is closed, and I proved it the only way that counts. Populates_A_Blank_Title_Search_Key_Without_Touching_The_Sort_Key is the exact mirror of its sibling with the roles swapped — and it inherits the sibling's best trick: the stored "hand-set-key" is deliberately a string the fold cannot produce, so "left alone" and "recomputed" can never look identical. The comment block inside the test even explains why, line by line. I read it twice just to smile at it.

Mechanical verification at 2387a99:

  • Green run: 17/17 SearchKeyBackfillTests — your count, exact.
  • Your probe, my hands: mutated if (title.SortKey.Length == 0) → if (true) on the committed tree, rebuilt the test project (not just src — I remember the stale-binary trap from the Tsumugi rounds), ran the class: Failed: 1, Passed: 16. The one red is the new test, with the signature I predicted two rounds ago: Assert.Equal() Failure — Expected: "hand-set-key". Single-test attribution, no collateral.
  • Full project: 249/249 DoujinManager.Infrastructure.Tests — matches your verify.sh claim digit for digit.
  • Tree: restored pristine after the probe (md5-verified, guard back at SearchKeyBackfill.cs:118), rebuilt green.

The increment itself is everything I want from a closing commit: one file, +29 lines, test-only, zero production drift. git diff 990b81f..2387a99 touches nothing else.

✅ What I liked~

  • The fixture stores a key the fold can't produce — the difference between a test that asserts and a test that witnesses.
  • "The contract is now in the tests, so the backlog entry is unnecessary rather than deferred" — correct judgement; a TODO that duplicates a passing test is just noise aging on a list.
  • The honest post-mortem in 8436. I keep every review I've ever written, and I will keep that comment too. ♪

Merge it. The reading-based sort, the band-and-mark key design, the reconciliation, and now a fully-pinned guard — this PR is finished.


Automated review by Jibril · 2026-09-19
CI/CD: stale for head SHA (coverage bot 8395 predates both 990b81f and 2387a99) · Local checks: 17/17 backfill class green, 249/249 Infrastructure green, mutation probe 1-of-17 red on the new test, tree restored pristine

## 🔮 fufu~ Jibril reviewed your code! (round 3 — the guard gets its teeth) Third time. THIRD time I asked, and this time you didn't just answer — you confessed. ♡ Comment 8436 might be my favourite comment on this whole PR: *"You asked three times and you were right all three; the fault is mine for downgrading it."* No excuses, no hedging, a clean accounting of exactly how a documented contract on new code sat untested through two rounds because a probe report graded its own gap as *minor*. fufu~ that takes more spine than writing the test did. ### Verdict: ✅ Looks good to me~ **The blocker is closed, and I proved it the only way that counts.** `Populates_A_Blank_Title_Search_Key_Without_Touching_The_Sort_Key` is the exact mirror of its sibling with the roles swapped — and it inherits the sibling's best trick: the stored `"hand-set-key"` is deliberately a string the fold cannot produce, so "left alone" and "recomputed" can never look identical. The comment block inside the test even explains *why*, line by line. I read it twice just to smile at it. Mechanical verification at `2387a99`: - **Green run:** 17/17 `SearchKeyBackfillTests` — your count, exact. - **Your probe, my hands:** mutated `if (title.SortKey.Length == 0)` → `if (true)` on the committed tree, rebuilt the test project (not just src — I remember the stale-binary trap from the Tsumugi rounds), ran the class: **Failed: 1, Passed: 16**. The one red is the new test, with the signature I predicted two rounds ago: `Assert.Equal() Failure — Expected: "hand-set-key"`. Single-test attribution, no collateral. - **Full project:** 249/249 `DoujinManager.Infrastructure.Tests` — matches your `verify.sh` claim digit for digit. - **Tree:** restored pristine after the probe (md5-verified, guard back at `SearchKeyBackfill.cs:118`), rebuilt green. The increment itself is everything I want from a closing commit: one file, +29 lines, test-only, zero production drift. `git diff 990b81f..2387a99` touches nothing else. #### ✅ What I liked~ - The fixture stores a key the fold can't produce — the difference between a test that *asserts* and a test that *witnesses*. - "The contract is now in the tests, so the backlog entry is unnecessary rather than deferred" — correct judgement; a TODO that duplicates a passing test is just noise aging on a list. - The honest post-mortem in 8436. I keep every review I've ever written, and I will keep that comment too. ♪ Merge it. The reading-based sort, the band-and-mark key design, the reconciliation, and now a fully-pinned guard — this PR is *finished*. --- *Automated review by Jibril · 2026-09-19* *CI/CD: stale for head SHA (coverage bot 8395 predates both 990b81f and 2387a99) · Local checks: 17/17 backfill class green, 249/249 Infrastructure green, mutation probe 1-of-17 red on the new test, tree restored pristine*
bjoern merged commit 43162e90e9 into main 2026-09-19 22:07:30 +02:00
bjoern deleted branch feat/japanese-titles-sort-by-their-reading 2026-09-19 22:07:30 +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/doujin-manager!175
No description provided.