feat: the OpenRouter key — checked before it is saved, the environment key wins, and the Provider tab shows its label, usage and limit (Phase 2 · slice 7a) #20

Merged
bjoern merged 4 commits from feat/provider-key-and-catalog into main 2026-09-23 17:52:49 +02:00
Member

Phase 2, slice 7, first half: the OpenRouter key. The model catalog, the Defaults tab's picker and the routing defaults follow in 7b. Slice 7 came to about 5.9k lines, so it is split at the line between "the key" and "what you do with it".

What

  • The library bump (commit 1).
    • external/OpenRouter.Net moves fd43f04 → c9b58ba (0.3.0: #12–#18).
    • The four Microsoft.Extensions.* pins move to 10.0.12; restore fails with NU1109 otherwise.
    • Infrastructure drops the unused OpenRouter.Net.Agents reference.
    • Nothing in Tsumugi breaks. Tsumugi implements no IOpenRouterClient and does not stream yet.
  • Kagaku.Flow.OpenRouter, a new package, with its tests (commit 2).
    • ILlmGateway so far holds ValidateKeyAsync(apiKey). The catalog is 7b's, and CompleteAsync is slice 8's.
    • The host port IOpenRouterKeySource returns an OpenRouterKeyLookup: a key, no key, or a key that exists but cannot be read. A stored key the keyring can't decrypt is reported, not thrown.
    • A key check builds its own short-lived client, with a 30 s HttpClient.Timeout and retries off. The timeout is the HttpClient's, because a probe showed a linked cancellation token makes the library throw instead of returning a timeout failure.
    • A key that could never be sent is refused before anything goes out, for example one with a line break: a probe found a FormatException escaping the library for it.
    • The package runs the shared isolation scan (ADR 0025), reading every ldstr, the constant table and each custom attribute's value blob. That makes a leaked Tsumugi literal or an InternalsVisibleTo a failing test.
  • Tsumugi's side (commit 3).
    • IModelProvider in UseCases has three members: ValidateKeyAsync, GetKeyStateAsync (local, no network) and CheckKeyInEffectAsync. 7b adds GetCatalogAsync for four. It is implemented in Infrastructure over the package.
    • SaveOpenRouterKey checks the key first. OpenRouter's refusal comes back, and nothing is stored. A 402 refuses too.
    • TSUMUGI_OPENROUTER_API_KEY is read through IConfiguration and wins over the stored key. A blank value counts as unset.
    • Lifetimes: the key source is a singleton that opens and disposes its own DI scope per read, so nothing captures the scoped settings store. A composition test resolves both singletons from the root provider, which refuses scoped services.
  • The Provider tab. It loads in two phases: which key is in effect shows at once, and OpenRouter's figures follow when /key answers.
    • Figures: the key's label, its usage, and what remains of its limit. When limit is null it reads "No spending limit". Account credit is not shown, because it needs a management key.
    • The status dot is green only once OpenRouter confirmed the key in effect. It is amber for a refusal, for OpenRouter being out of reach, or for no usable key, and neutral while waiting.
    • "Check again" keeps the last figures until its own answer lands. "Remove key" asks first.
    • When the environment key is in effect, the tab says so, and says that a stored key is ignored meanwhile. A key saved then is stored aside.
    • A refused save says which key stays: "The key you entered was not saved." or "…; the key saved earlier stays in place."
    • ProviderSettingsView.HasUsableKey is the local answer slice 12's Run button will read.
  • A Development-only TSUMUGI_OPENROUTER_BASE_URL points the gateway at a local stub, so the good-key figures can be seen without a real key. The browser pass used it. AGENTS.md documents it, and deploy/docker-compose.yml carries the key override as a commented line.
  • ADRs amended (dated, and cited by number in code):
    • 0003, 0018 and 0025: IModelProvider also names the key in effect.
    • 0006: a blank override counts as unset.
  • Other docs: REQUIREMENTS ticks Provider. settings.md changes "Validate & save" to "Save", because MaskedSecretField hardcodes its button text, and the hint says the key is checked first.

Decisions to confirm (Björn)

  1. IModelProvider grows beyond D-1's two members. Here it has three, and four with 7b's catalog. The tab has to show figures for the key in effect, including an environment key, and UseCases can't see that key. ADRs 0003, 0018 and 0025 are amended. The alternative was a separate port with no ADR change.
  2. The library pin is c9b58ba. Library main has since moved to 81134bc (#19 decisions, #20 stream-to-result). Slice 8 bumps once to include L3, so this one stays on the verified pin.
  3. A Development-only stub stands in for the plan's "good key" browser step. Step 16.4, the real key's figures, is left for you. The implementer has no key.
  4. "Save", not "Validate & save". settings.md is amended to match. The alternative is a Kagaku.UI SaveText parameter first.
  5. A blank environment value counts as unset (ADR 0006 amended).
  6. The per-key cached client moves to slice 8, its first caller. 7a has nothing to cache.
  7. A stored key can be saved and removed while the environment key is in effect. It sits aside, and the Remove dialog names which key it removes.
  8. A 402 on /key refuses the save.
  9. "No spending limit" keys on limit, not limit_remaining.
  10. The new tab texts and the dot's meaning, as described above. "Check again" keeping the figures is a change from the brief's reducer.
  11. The refusal alert survives leaving and reopening the tab in the same session, until the next save or removal. The brief keeps it; the text stays true.

Out of scope

The model catalog, the effort rule, the Defaults picker and the routing, service tier and zero-data-retention defaults are 7b. The per-key client cache and the 5-minute timeout come with slice 8's CompleteAsync. Two follow-ups stay open: the focus return in Kagaku.UI's MaskedSecretField, where focus drops to <body> after Save or Replace, which is a library PR; and making BackupWriterTests' WAL test deterministic (below).

Tests

main (e9c0016, after #19) this PR
Domain 37 37
UseCases 12 12
Kagaku.Flow 483 483
Kagaku.Flow.OpenRouter — 41
Integration 155 224
BlazorAdapter 216 244
Total 903 1041
  • Package: key-check arms (ok, 401, 402, timeout, network, a proxy's HTML page, a malformed key refused before sending), the lookup's three shapes, options, lifetimes, and the host's logger factory reaching the client.
  • Integration: the key source (stored alone, environment alone, environment wins, blank equals unset, unreadable, each read opening and disposing its own scope), validate-and-save arms, the composition test, AppSettings with an environment key, and OpenRouterConfiguration. There are also three host tests closing Program.cs's old gaps: no access token refuses to start by name, a restart with the seed flag says it seeded nothing, and no data path puts the volumes under the content root.
  • BlazorAdapter: the Provider tab's states, the dot's tone in five places, the busy states (nothing removed while a check runs, nothing rechecked while a removal runs), and the stale-answer guard.

Build: the full CI build (with the graph editor's npm build) has 0 warnings. npm test passes 14/14.

Coverage: coverlet, per changed file, line and branch. Every new or changed src file is at 100% line and branch, including Program.cs (81/81 lines, 18/18 branches). The only files without instrumentation are those with no executable lines: the interfaces, OpenRouterKeyOverride and ProviderKeyOrigin.

Mutation probes: the brief's probes 1–41, then every verifier-found survivor, each run on a copy, restored and touched. All are killed. That includes the package-literal and InternalsVisibleTo leaks, scope-per-read, the logger factory, the printable-ASCII bounds, envelope text length, the dot's tone, and "Check again" keeping or clearing the figures.

A flake that predates this PR: BackupWriterTests.The_snapshot_folds_in_rows_still_in_the_wal failed 3 times in about 35 integration runs on a heavily loaded machine. It asserts that a -wal file exists. This PR does not touch backups. If CI trips on it, a follow-up should make that test deterministic.

Browser pass (Release build, launcher script, stopped by port)

Every 7a mode was run twice: before and after the verification round. The second run covers the final tree. That is 134 screenshots, light and dark, desktop and 390 px. Refusals came from the real /key ("User not found."). The stub's log confirmed two /key calls per save (the check, then the re-read) and X-Title: Tsumugi on every request. There were no console errors and no key in any log or page.

  • No key: the tab says no key is set and the dot is neutral. Save with an empty field is disabled.
  • A malformed key, one with a space or line break, is refused before anything is sent.
  • A refused key: OpenRouter's "User not found." shows, nothing is stored, the dot stays amber, and the alert says the key was not saved.
  • The environment key: the tab says it is in effect, and that a stored key is ignored meanwhile. A refused environment key shows amber and the refusal. Saving a stored key under it stores it aside.
  • A blank environment value behaves as no override.
  • An undecryptable stored key is reported as unreadable; nothing throws.
  • Offline (via HTTPS_PROXY to a closed port): OpenRouter is out of reach, the dot is amber, and nothing is stored.
  • The stub, as a good key: label, usage and limit show, and "No spending limit" shows for a key without a limit. The dot goes green after the answer. "Check again" keeps the figures while it runs, and a slow save shows "Checking the new key with OpenRouter…". Remove asks first and names the key.
  • At 390 px, the dot sits on the status line's first line.

Not seen by the implementer: step 16.4, the real key's figures. Björn, one run with your key on staging closes it.

🤖 Generated with Claude Code

Phase 2, slice 7, first half: the OpenRouter key. The model catalog, the Defaults tab's picker and the routing defaults follow in 7b. Slice 7 came to about 5.9k lines, so it is split at the line between "the key" and "what you do with it". ## What - **The library bump** (commit 1). - `external/OpenRouter.Net` moves `fd43f04 → c9b58ba` (0.3.0: #12–#18). - The four `Microsoft.Extensions.*` pins move to 10.0.12; restore fails with NU1109 otherwise. - Infrastructure drops the unused `OpenRouter.Net.Agents` reference. - Nothing in Tsumugi breaks. Tsumugi implements no `IOpenRouterClient` and does not stream yet. - **`Kagaku.Flow.OpenRouter`**, a new package, with its tests (commit 2). - `ILlmGateway` so far holds `ValidateKeyAsync(apiKey)`. The catalog is 7b's, and `CompleteAsync` is slice 8's. - The host port `IOpenRouterKeySource` returns an `OpenRouterKeyLookup`: a key, no key, or a key that exists but **cannot be read**. A stored key the keyring can't decrypt is reported, not thrown. - A key check builds its own short-lived client, with a 30 s `HttpClient.Timeout` and retries off. The timeout is the `HttpClient`'s, because a probe showed a linked cancellation token makes the library throw instead of returning a timeout failure. - A key that could never be sent is refused before anything goes out, for example one with a line break: a probe found a `FormatException` escaping the library for it. - The package runs the shared isolation scan (ADR 0025), reading every `ldstr`, the constant table and each custom attribute's value blob. That makes a leaked `Tsumugi` literal or an `InternalsVisibleTo` a failing test. - **Tsumugi's side** (commit 3). - `IModelProvider` in UseCases has three members: `ValidateKeyAsync`, `GetKeyStateAsync` (local, no network) and `CheckKeyInEffectAsync`. 7b adds `GetCatalogAsync` for four. It is implemented in Infrastructure over the package. - `SaveOpenRouterKey` checks the key first. OpenRouter's refusal comes back, and nothing is stored. A 402 refuses too. - `TSUMUGI_OPENROUTER_API_KEY` is read through `IConfiguration` and **wins** over the stored key. A blank value counts as unset. - Lifetimes: the key source is a singleton that opens and disposes its own DI scope per read, so nothing captures the scoped settings store. A composition test resolves both singletons from the root provider, which refuses scoped services. - **The Provider tab.** It loads in two phases: which key is in effect shows at once, and OpenRouter's figures follow when `/key` answers. - **Figures:** the key's label, its usage, and what remains of its limit. When `limit` is null it reads "No spending limit". Account credit is not shown, because it needs a management key. - **The status dot is green only once OpenRouter confirmed the key in effect.** It is amber for a refusal, for OpenRouter being out of reach, or for no usable key, and neutral while waiting. - "Check again" keeps the last figures until its own answer lands. "Remove key" asks first. - When the environment key is in effect, the tab says so, and says that a stored key is ignored meanwhile. A key saved then is stored aside. - A refused save says which key stays: "The key you entered was not saved." or "…; the key saved earlier stays in place." - `ProviderSettingsView.HasUsableKey` is the local answer slice 12's Run button will read. - **A Development-only `TSUMUGI_OPENROUTER_BASE_URL`** points the gateway at a local stub, so the good-key figures can be seen without a real key. The browser pass used it. `AGENTS.md` documents it, and `deploy/docker-compose.yml` carries the key override as a commented line. - **ADRs amended** (dated, and cited by number in code): - 0003, 0018 and 0025: `IModelProvider` also names the key in effect. - 0006: a blank override counts as unset. - **Other docs:** REQUIREMENTS ticks Provider. `settings.md` changes "Validate & save" to "Save", because `MaskedSecretField` hardcodes its button text, and the hint says the key is checked first. ## Decisions to confirm (Björn) 1. **`IModelProvider` grows beyond D-1's two members.** Here it has three, and four with 7b's catalog. The tab has to show figures for the key in effect, including an environment key, and UseCases can't see that key. ADRs 0003, 0018 and 0025 are amended. The alternative was a separate port with no ADR change. 2. **The library pin is `c9b58ba`.** Library main has since moved to `81134bc` (#19 decisions, #20 stream-to-result). Slice 8 bumps once to include L3, so this one stays on the verified pin. 3. **A Development-only stub** stands in for the plan's "good key" browser step. Step 16.4, the real key's figures, is left for you. The implementer has no key. 4. **"Save", not "Validate & save".** `settings.md` is amended to match. The alternative is a Kagaku.UI `SaveText` parameter first. 5. **A blank environment value counts as unset** (ADR 0006 amended). 6. **The per-key cached client moves to slice 8**, its first caller. 7a has nothing to cache. 7. **A stored key can be saved and removed while the environment key is in effect.** It sits aside, and the Remove dialog names which key it removes. 8. **A 402 on `/key` refuses the save.** 9. **"No spending limit" keys on `limit`**, not `limit_remaining`. 10. **The new tab texts and the dot's meaning**, as described above. "Check again" keeping the figures is a change from the brief's reducer. 11. **The refusal alert survives leaving and reopening the tab** in the same session, until the next save or removal. The brief keeps it; the text stays true. ## Out of scope The model catalog, the effort rule, the Defaults picker and the routing, service tier and zero-data-retention defaults are 7b. The per-key client cache and the 5-minute timeout come with slice 8's `CompleteAsync`. Two follow-ups stay open: the focus return in Kagaku.UI's `MaskedSecretField`, where focus drops to `<body>` after Save or Replace, which is a library PR; and making `BackupWriterTests`' WAL test deterministic (below). ## Tests | | main (`e9c0016`, after #19) | this PR | |---|---|---| | Domain | 37 | 37 | | UseCases | 12 | 12 | | Kagaku.Flow | 483 | 483 | | Kagaku.Flow.OpenRouter | — | 41 | | Integration | 155 | 224 | | BlazorAdapter | 216 | 244 | | **Total** | **903** | **1041** | - **Package:** key-check arms (ok, 401, 402, timeout, network, a proxy's HTML page, a malformed key refused before sending), the lookup's three shapes, options, lifetimes, and the host's logger factory reaching the client. - **Integration:** the key source (stored alone, environment alone, environment wins, blank equals unset, unreadable, each read opening and disposing its own scope), validate-and-save arms, the composition test, AppSettings with an environment key, and `OpenRouterConfiguration`. There are also three host tests closing `Program.cs`'s old gaps: no access token refuses to start by name, a restart with the seed flag says it seeded nothing, and no data path puts the volumes under the content root. - **BlazorAdapter:** the Provider tab's states, the dot's tone in five places, the busy states (nothing removed while a check runs, nothing rechecked while a removal runs), and the stale-answer guard. **Build:** the full CI build (with the graph editor's npm build) has 0 warnings. `npm test` passes 14/14. **Coverage:** coverlet, per changed file, line and branch. **Every new or changed `src` file is at 100% line and branch**, including `Program.cs` (81/81 lines, 18/18 branches). The only files without instrumentation are those with no executable lines: the interfaces, `OpenRouterKeyOverride` and `ProviderKeyOrigin`. **Mutation probes:** the brief's probes 1–41, then every verifier-found survivor, each run on a copy, restored and `touch`ed. All are killed. That includes the package-literal and `InternalsVisibleTo` leaks, scope-per-read, the logger factory, the printable-ASCII bounds, envelope text length, the dot's tone, and "Check again" keeping or clearing the figures. **A flake that predates this PR:** `BackupWriterTests.The_snapshot_folds_in_rows_still_in_the_wal` failed 3 times in about 35 integration runs on a heavily loaded machine. It asserts that a `-wal` file exists. This PR does not touch backups. If CI trips on it, a follow-up should make that test deterministic. ## Browser pass (Release build, launcher script, stopped by port) Every 7a mode was run twice: before and after the verification round. The second run covers the final tree. That is 134 screenshots, light and dark, desktop and 390 px. Refusals came from the real `/key` ("User not found."). The stub's log confirmed two `/key` calls per save (the check, then the re-read) and `X-Title: Tsumugi` on every request. There were no console errors and no key in any log or page. - **No key:** the tab says no key is set and the dot is neutral. Save with an empty field is disabled. - **A malformed key**, one with a space or line break, is refused before anything is sent. - **A refused key:** OpenRouter's "User not found." shows, nothing is stored, the dot stays amber, and the alert says the key was not saved. - **The environment key:** the tab says it is in effect, and that a stored key is ignored meanwhile. A refused environment key shows amber and the refusal. Saving a stored key under it stores it aside. - **A blank environment value** behaves as no override. - **An undecryptable stored key** is reported as unreadable; nothing throws. - **Offline** (via `HTTPS_PROXY` to a closed port): OpenRouter is out of reach, the dot is amber, and nothing is stored. - **The stub, as a good key:** label, usage and limit show, and "No spending limit" shows for a key without a limit. The dot goes green after the answer. "Check again" keeps the figures while it runs, and a slow save shows "Checking the new key with OpenRouter…". Remove asks first and names the key. - **At 390 px,** the dot sits on the status line's first line. Not seen by the implementer: step 16.4, the real key's figures. Björn, one run with your key on staging closes it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Tsumugi's first bump of the vendored client since Phase 0: fd43f04 to
library main c9b58ba, the merge of #18. The range holds seven library
PRs: #12 (routing variants and ServiceTier), #13 (every package current,
Microsoft.Extensions.* at 10.0.12), #14 (session id and the catalog's
cache fields), #15 (wire robustness), #16 (every chunk parses and every
failure carries a FailureKind, 0.3.0), #17 (the filtered model listing)
and #18 (the stream accumulator). Tsumugi uses no OpenRouter.Net type
yet, so the bump breaks no code.

It does break restore: the library's own Directory.Packages.props pins
Microsoft.Extensions.* at 10.0.12 since #13, and with transitive pinning
on, Tsumugi's 10.0.9 pins would downgrade what the library needs
(NU1109). The four Microsoft.Extensions.* pins move to 10.0.12 so the
family stays on one patch level; ASP.NET Core and EF Core stay at
10.0.9, since their Microsoft.Extensions.* dependencies are pinned up,
not down.

Infrastructure drops its OpenRouter.Net.Agents reference: it is unused,
agent mode is Phase 4, and ADR 0003 and 0004 do not list it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The engine's OpenRouter package (ADR 0018, 0025) arrives with its first
capability: ILlmGateway.ValidateKeyAsync asks GET /key about a key, once,
and hands back the library's Result<KeyInfo> with its FailureKind. The
catalog follows in slice 7b and completions with the llm node (slice 8).

A check builds its own HttpClient for its key over the gateway's one
shared handler, which keeps the connection pool, and disposes it when the
answer is in: nothing about a key outlives its check, and a check never
touches a client a completion is using (Orihon's validate-through-cache
evicted and disposed the live client under a request in flight). The
check is never retried, and its 30-second limit is the HttpClient's own
Timeout: the library reports that expiry as a Timeout failure, while a
linked CancelAfter token would make it throw.

A key no Authorization header can carry is refused before anything is
sent, as a Validation failure with the package's own sentence. The probes
behind the guard: a line break throws FormatException out of
AuthenticationHeaderValue, a character outside ASCII came back as a
mislabelled, transient Network failure, and a space reached OpenRouter as
"Missing Authentication header".

The host supplies keys through IOpenRouterKeySource. Its answer,
OpenRouterKeyLookup, is a key, no key, or a key the host holds but cannot
read, with the sentence that says why: a stored key whose keyring is gone
must not fail a run as "no key is set". Nothing in the package reads the
port before slice 8; it lands now because Tsumugi implements it now. The
lookup never prints its key.

AddKagakuFlowOpenRouter registers the gateway as one singleton behind its
port, with the given options winning over earlier ones and the host's
clock kept. Disposing the gateway releases the handler it built and never
one the caller passed.

The tests drive the gateway over a scripted handler and link 5a's shared
isolation scan: no Tsumugi reference, file or namespace in the package,
its tests or the core, and no literal in the package that names its host.
That literal scan walks the IL of every type except coverlet's injected
tracker, whose strings carry the checkout path when coverage is collected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Saving a key is validate-and-save now (stories/settings.md; ADR 0006):
SaveOpenRouterKey checks the trimmed key against GET /key through the
engine's gateway and stores it encrypted only when OpenRouter accepts it.
A refusal — a 402 included — stores nothing and leaves an earlier key in
place, and comes back as one sentence for the writer: the provider's own
words when they came from an error envelope or are one short line, the
HTTP status otherwise, so a proxy's HTML page never reaches an alert, and
"Try again in a moment." when the library calls the failure transient.

IModelProvider, Tsumugi's narrow port, grows from key validation to
three members: ValidateKeyAsync for a given key, GetKeyStateAsync for
which key is in effect and whether it can be read — local, it never asks
OpenRouter, so the tab renders at once and slice 12's Run button can ask
it freely — and CheckKeyInEffectAsync for what OpenRouter says about that
key. The environment key cannot be seen from UseCases, which is why the
port says it. ADRs 0003, 0018 and 0025 are amended to name the new
members; 0018 also records the key check's own 30-second timeout.

TSUMUGI_OPENROUTER_API_KEY wins over the stored key when it is set to a
non-blank value: compose hands "" to an unset ${VAR:-}, and a blank key
winning would silently disable a stored one (ADR 0006 and 0018 amended).
A key can still be saved, checked first, and removed while it is set,
and the tab says what that means.

Infrastructure implements the engine's IOpenRouterKeySource as a
singleton that opens a DI scope per read: the gateway is a singleton that
owns the connection pool, the settings store is scoped, and a scope per
read keeps it from capturing a root-scope DbContext while a key saved or
removed in another circuit is what the next read sees. The composition is
tested with ValidateScopes and ValidateOnBuild. A stored key the keyring
cannot decrypt (a database restored without its keys volume) is reported,
never thrown: the tab warns, runs will fail with that sentence rather
than "no key", and HasUsableKey is false for it.

The Provider tab loads in two phases: the local view first, then the
check of the key in effect, both answers carrying the id of the load that
started them, so an answer to an older read is dropped (ADR 0011's
staleness rule). A save or a removal reads the view again rather than
building a "no key" view by hand. The tab shows the label, the spend and
the limit, a line while a check or a save runs, "Check again", the
refusals with whether an earlier key stays, and the saved-aside note
under the environment key. The key never reaches the markup, the state
or an action's printed form.

The Server reads the override from configuration and, in Development
only, TSUMUGI_OPENROUTER_BASE_URL, a local stub's address for a browser
pass of the key's figures; a bad address fails startup by name. The
integration tests point the gateway at a closed local port, so a test
that forgets its scripted gateway fails fast instead of reaching
openrouter.ai.

REQUIREMENTS ticks Provider; the settings story is amended to "Save
checks it against /key first" and its first two Provider lines ticked;
AGENTS.md names the stub variable; compose shows the override commented.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(settings): verification round — the dot follows OpenRouter's answer, "Check again" keeps the figures, and the gaps the probes found are pinned
All checks were successful
CI / build (pull_request) Successful in 30s
CI / test (pull_request) Successful in 48s
bc14463daf
The branch is rebased onto main e9c0016 (slice 5b); the three slice commits are replayed unchanged.

The Provider tab now says what the check found, not only whether a key can be read. The status dot is
green only once OpenRouter has confirmed the key in effect, amber for a refusal, an unreachable OpenRouter
or no usable key, and neutral while the first answer is pending. "Check again" keeps the last figures on
screen until its own answer lands, so the button no longer jumps under the pointer; a read after a save
or a removal still drops them at once, since the key may have changed. While a save is being checked the
line reads "Checking the new key with OpenRouter…", because the figures below it still belong to the old
key. A refused save now names the entered key ("The key you entered was not saved", and "…; the key saved
earlier stays in place"), so it no longer reads like the key-in-effect warning when the environment key is
refused too. The status dot sits on the first line of the sentence when it wraps at phone width.

Tests now pin what the probes showed was loose:
- each read of the key source opens and disposes its own scope;
- the literal scan reads custom-attribute arguments, so an InternalsVisibleTo naming Tsumugi fails it;
- "Check again" and "Remove key" are disabled while a check and while a removal runs, and a check alone
  makes the tab busy;
- the failure sentence's envelope arm, its 200-character boundary and a carriage return;
- both ends of the printable range a key may use;
- the gateway takes the host's logger factory;
- the key port and the gateway are registered as singletons themselves;
- Program.cs's three uncovered arms: the data root default, the missing access token, and a seeded
  restart that seeds nothing more.

The stub address is no longer trimmed by hand, because Uri drops surrounding whitespace itself (a padded
row pins it). The amendments cite PR #20, the next free index, and the rewrapped doc lines keep their
words.

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

🔄 Auto-updating coverage report — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip.

Commit: bc14463 · Generated: 2026-09-23 15:36:39 UTC · Revision: #1

Summary

Summary
Generated on: 09/23/2026 - 15:36:39
Coverage date: 09/23/2026 - 15:36:25 - 09/23/2026 - 15:36:36
Parser: MultiReport (6x Cobertura)
Assemblies: 8
Classes: 331
Files: 320
Line coverage: 97.8% (7219 of 7379)
Covered lines: 7219
Uncovered lines: 160
Coverable lines: 7379
Total lines: 16221
Branch coverage: 93% (2313 of 2487)
Covered branches: 2313
Total branches: 2487
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.Flow - 99.4%
Name Line Branch
Kagaku.Flow 99.4% 98.6%
Kagaku.Flow.Budgeting.BudgetRequest 100%
Kagaku.Flow.Budgeting.ConsumerBudget 100%
Kagaku.Flow.FlowJson 100%
Kagaku.Flow.Graphs.GraphDefinition 100%
Kagaku.Flow.Graphs.GraphDocument 100% 100%
Kagaku.Flow.Graphs.GraphEdge 100%
Kagaku.Flow.Graphs.GraphIndex 100% 100%
Kagaku.Flow.Graphs.GraphLoadError 100%
Kagaku.Flow.Graphs.GraphLoadResult 100%
Kagaku.Flow.Graphs.GraphNode 100%
Kagaku.Flow.Graphs.GraphPosition 100%
Kagaku.Flow.Graphs.GraphReader 100% 100%
Kagaku.Flow.Graphs.GraphSchema 100% 100%
Kagaku.Flow.Graphs.GraphShapeException 100%
Kagaku.Flow.Graphs.GraphWriter 100% 100%
Kagaku.Flow.Graphs.SocketRef 100%
Kagaku.Flow.Graphs.WireText 100% 100%
Kagaku.Flow.Nodes.CanvasDescriptorWriter 100% 100%
Kagaku.Flow.Nodes.ConfigProblem 100%
Kagaku.Flow.Nodes.Names 100%
Kagaku.Flow.Nodes.NodeDescriptor 100% 100%
Kagaku.Flow.Nodes.NodeRegistry 100% 100%
Kagaku.Flow.Nodes.NodeSockets 100% 100%
Kagaku.Flow.Nodes.SinkDescriptor 100%
Kagaku.Flow.Nodes.SocketTypes 100% 100%
Kagaku.Flow.Nodes.SourceDescriptor 100%
Kagaku.Flow.Nodes.TemplateField 100%
Kagaku.Flow.Templating.InputsWalker 100% 100%
Kagaku.Flow.Templating.ParsedTemplate 100% 100%
Kagaku.Flow.Templating.SandboxedTemplateContext 100% 100%
Kagaku.Flow.Templating.TemplateError 100%
Kagaku.Flow.Templating.TemplateHost 100% 100%
Kagaku.Flow.Templating.TemplateLimits 100%
Kagaku.Flow.Templating.TemplateModel 100% 100%
Kagaku.Flow.Templating.TemplateProblem 100%
Kagaku.Flow.Templating.TemplateRenderRequest 100% 100%
Kagaku.Flow.Templating.TemplateRenderResult 100%
Kagaku.Flow.Templating.TemplateRun 100%
Kagaku.Flow.Templating.TemplateSinks 100%
Kagaku.Flow.Templating.TemplateValues 100% 100%
Kagaku.Flow.Validation.GraphValidation 100% 100%
Kagaku.Flow.Validation.GraphValidationContext 100%
Kagaku.Flow.Validation.HookFailure 100%
Kagaku.Flow.Validation.ResolvedGraph 100% 100%
Kagaku.Flow.Validation.ValidationError 100%
Kagaku.Flow.Validation.ValidationReport 100%
System.Text.RegularExpressions.Generated 97.4% 92.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2
97.4% 94.4%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1
97.2% 94.4%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF
E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0
97.2% 94.4%
Kagaku.Flow.OpenRouter - 100%
Name Line Branch
Kagaku.Flow.OpenRouter 100% 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGateway 100% 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup 100% 100%
Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions 100% 100%
Tsumugi.BlazorAdapter - 97.1%
Name Line Branch
Tsumugi.BlazorAdapter 97.1% 93.9%
Tsumugi.BlazorAdapter.BlazorAdapterAssembly 100%
Tsumugi.BlazorAdapter.Documents.CategoryCreated 100%
Tsumugi.BlazorAdapter.Documents.CategoryEditor 97.7% 86.1%
Tsumugi.BlazorAdapter.Documents.CategorySaved 100%
Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog 100% 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentCreated 100%
Tsumugi.BlazorAdapter.Documents.DocumentEditor 100% 87.9%
Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentSaved 100%
Tsumugi.BlazorAdapter.Documents.DocumentsEffects 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsList 98.2% 91.3%
Tsumugi.BlazorAdapter.Documents.DocumentsLoaded 100%
Tsumugi.BlazorAdapter.Documents.DocumentsPage 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsReducers 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsState 100% 100%
Tsumugi.BlazorAdapter.Documents.LoadDocuments 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested 100%
Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested 100%
Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed 100%
Tsumugi.BlazorAdapter.Documents.SetDocumentsFilter 100%
Tsumugi.BlazorAdapter.Documents.UpdateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.UpdateDocumentRequested 100%
Tsumugi.BlazorAdapter.Graphs.GraphCanvas 37.5% 25%
Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage 20% 25%
Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge 94.4% 83.3%
Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived 75% 100%
Tsumugi.BlazorAdapter.Runs.RunChangedBridge 97.1% 90.6%
Tsumugi.BlazorAdapter.Runs.RunChangesReceived 100%
Tsumugi.BlazorAdapter.Runs.RunTextReceived 100%
Tsumugi.BlazorAdapter.Settings.BackupCompleted 100%
Tsumugi.BlazorAdapter.Settings.BackupFailed 100%
Tsumugi.BlazorAdapter.Settings.BackupNowRequested 100%
Tsumugi.BlazorAdapter.Settings.BackupSettingsLoaded 100%
Tsumugi.BlazorAdapter.Settings.BackupsSection 100% 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsState 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSection 100% 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsState 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsLoaded 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaved 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaveFailed 100%
Tsumugi.BlazorAdapter.Settings.LoadBackupSettings 100%
Tsumugi.BlazorAdapter.Settings.LoadProviderSettings 100%
Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaved 100%
Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaveFailed 100%
Tsumugi.BlazorAdapter.Settings.ProviderKeyChecked 100%
Tsumugi.BlazorAdapter.Settings.ProviderSection 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsLoaded 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsReducers 100% 100%
Tsumugi.BlazorAdapter.Settings.ProviderSettingsState 100% 100%
Tsumugi.BlazorAdapter.Settings.SaveLlmDefaultsRequested 100%
Tsumugi.BlazorAdapter.Settings.SaveOpenRouterKeyRequested 100%
Tsumugi.BlazorAdapter.Stories.CreateStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.PurgeStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.RestoreStoryRequested 100%
Tsumugi.BlazorAdapter.Stories.SetShowArchived 100%
Tsumugi.BlazorAdapter.Stories.SetStoriesFilter 100%
Tsumugi.BlazorAdapter.Stories.SetStoryArchivedRequested 100%
Tsumugi.BlazorAdapter.Stories.StoriesEffects 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesLoaded 100%
Tsumugi.BlazorAdapter.Stories.StoriesPage 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesReducers 100% 100%
Tsumugi.BlazorAdapter.Stories.StoriesState 100% 100%
Tsumugi.BlazorAdapter.Stories.StoryActionFailed 100%
Tsumugi.BlazorAdapter.Stories.StoryCreated 100%
Tsumugi.BlazorAdapter.Stories.StoryCreateFailed 100%
Tsumugi.BlazorAdapter.Stories.StoryTrashPage 100% 100%
Tsumugi.BlazorAdapter.Stories.TrashStoryRequested 100%
Tsumugi.BlazorAdapter.Workspace.AutoSaveFailed 100%
Tsumugi.BlazorAdapter.Workspace.CaretRequest 100%
Tsumugi.BlazorAdapter.Workspace.ChapterHeader 100% 86.3%
Tsumugi.BlazorAdapter.Workspace.ChapterMenu 100% 81.2%
Tsumugi.BlazorAdapter.Workspace.ChapterPage 100% 87.5%
Tsumugi.BlazorAdapter.Workspace.ChapterSaved 100%
Tsumugi.BlazorAdapter.Workspace.ChaptersPage 90% 83.3%
Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeleteSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.EditSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.HistoryEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.HistoryLoaded 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPage 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPanel 100% 100%
Tsumugi.BlazorAdapter.Workspace.InsertSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.LoadHistory 100%
Tsumugi.BlazorAdapter.Workspace.LoadWorkspace 100%
Tsumugi.BlazorAdapter.Workspace.ManuscriptColumn 94.9% 89.4%
Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.MergeSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested 100%
Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested 100%
Tsumugi.BlazorAdapter.Workspace.SaveScope 100% 50%
Tsumugi.BlazorAdapter.Workspace.SaveStateLadder 100% 100%
Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested 100%
Tsumugi.BlazorAdapter.Workspace.SegmentRow 100% 96.8%
Tsumugi.BlazorAdapter.Workspace.SegmentSaved 100%
Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested 100%
Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.StoryDetails 100% 85.2%
Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage 100% 100%
Tsumugi.BlazorAdapter.Workspace.StorySaved 100%
Tsumugi.BlazorAdapter.Workspace.StoryShell 100% 100%
Tsumugi.BlazorAdapter.Workspace.UndoRequested 100%
Tsumugi.BlazorAdapter.Workspace.UndoStatusChanged 100%
Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers 100% 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceState 100%
Tsumugi.Domain - 99.3%
Name Line Branch
Tsumugi.Domain 99.3% 95.6%
Tsumugi.Domain.Documents.Document 100% 100%
Tsumugi.Domain.Documents.DocumentCategory 100%
Tsumugi.Domain.Journal.ChangeLogEntry 97.8% 75%
Tsumugi.Domain.Journal.StorySnapshot 95.4% 83.3%
Tsumugi.Domain.Settings.AppSetting 100%
Tsumugi.Domain.Stories.Chapter 100% 100%
Tsumugi.Domain.Stories.Provenance 100% 100%
Tsumugi.Domain.Stories.Segment 100% 100%
Tsumugi.Domain.Stories.Story 100% 100%
Tsumugi.Domain.Text 100% 100%
Tsumugi.Infrastructure - 97.2%
Name Line Branch
Tsumugi.Infrastructure 97.2% 84.4%
System.Text.RegularExpressions.Generated 81.4% 64.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F0D076C66E8EEC1C
A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0
84% 65.7%
Tsumugi.Infrastructure.Backups.BackupArtifactName 100% 100%
Tsumugi.Infrastructure.Backups.BackupOptions 100%
Tsumugi.Infrastructure.Backups.BackupSchedule 100% 83.3%
Tsumugi.Infrastructure.Backups.BackupScheduler 100% 100%
Tsumugi.Infrastructure.Backups.BackupWriter 98.4% 92.5%
Tsumugi.Infrastructure.Backups.FileSystemBackupStore 100% 100%
Tsumugi.Infrastructure.Backups.SqliteSnapshot 87.8% 75%
Tsumugi.Infrastructure.DependencyInjection 100% 100%
Tsumugi.Infrastructure.Journal.EfChangeJournal 100%
Tsumugi.Infrastructure.Journal.EfSnapshotStore 100%
Tsumugi.Infrastructure.Journal.EfUndoStore 98.1% 85.7%
Tsumugi.Infrastructure.Journal.ModelExtensions 100%
Tsumugi.Infrastructure.Journal.OperationContext 100% 100%
Tsumugi.Infrastructure.Journal.PropertyBag 100% 95.4%
Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus 100% 100%
Tsumugi.Infrastructure.Persistence.Configurations.AppSettingConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.ChangeLogEntryConfigurati
on
100%
Tsumugi.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.DocumentCategoryConfigura
tion
100%
Tsumugi.Infrastructure.Persistence.Configurations.DocumentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio
n
100%
Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter 100% 90%
Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings 99.2%
Tsumugi.Infrastructure.Persistence.Migrations.AddJournal 98.8%
Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments 100%
Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain 97.3%
Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot 100%
Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore 97.5% 90%
Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore 95% 84.6%
Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore 98.3% 92.8%
Tsumugi.Infrastructure.Persistence.TsumugiDbContext 83.5% 93.1%
Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory 0%
Tsumugi.Infrastructure.Provider.OpenRouterKeySource 100% 100%
Tsumugi.Infrastructure.Provider.OpenRouterModelProvider 100% 100%
Tsumugi.Infrastructure.Provider.OpenRouterOptions 100% 100%
Tsumugi.Infrastructure.Provider.ResolvedKey 100%
Tsumugi.Infrastructure.Runs.InProcessRunEventBus 100% 100%
Tsumugi.Infrastructure.Settings.EfAppSettingsStore 100% 100%
Tsumugi.Kernel - 90.9%
Name Line Branch
Tsumugi.Kernel 90.9% 75%
Tsumugi.Kernel.Err`1 100%
Tsumugi.Kernel.Ok`1 100%
Tsumugi.Kernel.Result`1 88.8% 75%
Tsumugi.Server - 100%
Name Line Branch
Tsumugi.Server 100% 86.4%
Program 100% 100%
Tsumugi.Server.Backups.BackupConfiguration 100% 100%
Tsumugi.Server.Components.App 100%
Tsumugi.Server.Components.Layout.AppHeader 100% 50%
Tsumugi.Server.Components.Layout.MainLayout 100%
Tsumugi.Server.Components.Pages.Gate 100% 100%
Tsumugi.Server.Provider.OpenRouterConfiguration 100% 100%
Tsumugi.Server.Security.AccessGate 100% 66.6%
Tsumugi.Server.Security.AccessSecret 100% 50%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 98%
Name Line Branch
Tsumugi.UseCases 98% 94.1%
Tsumugi.UseCases.Backups.BackupArtifact 100%
Tsumugi.UseCases.Backups.BackupPolicy 100%
Tsumugi.UseCases.Backups.BackupStatusView 100% 100%
Tsumugi.UseCases.Backups.DatabaseSnapshot 100%
Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot 100%
Tsumugi.UseCases.Backups.GetBackupStatus 100%
Tsumugi.UseCases.Backups.RunBackupNow 100%
Tsumugi.UseCases.Chapters.ChapterDto 100%
Tsumugi.UseCases.Chapters.CreateChapter 100% 100%
Tsumugi.UseCases.Chapters.DeleteChapter 100% 100%
Tsumugi.UseCases.Chapters.GetManuscript 100% 100%
Tsumugi.UseCases.Chapters.ManuscriptChapter 100%
Tsumugi.UseCases.Chapters.ManuscriptView 100%
Tsumugi.UseCases.Chapters.MergeChapters 100% 100%
Tsumugi.UseCases.Chapters.RenameChapter 100% 100%
Tsumugi.UseCases.Chapters.ReorderChapters 100% 100%
Tsumugi.UseCases.Chapters.SetChapterSummary 100% 75%
Tsumugi.UseCases.DependencyInjection 100%
Tsumugi.UseCases.Diagnostics.SeedDevData 97.3% 70%
Tsumugi.UseCases.Documents.CreateCategory 100% 100%
Tsumugi.UseCases.Documents.CreateDocument 100% 100%
Tsumugi.UseCases.Documents.DeleteCategory 100% 100%
Tsumugi.UseCases.Documents.DeleteDocument 100% 100%
Tsumugi.UseCases.Documents.DocumentCategoryDto 100%
Tsumugi.UseCases.Documents.DocumentCategoryProfile 100%
Tsumugi.UseCases.Documents.DocumentDto 100%
Tsumugi.UseCases.Documents.DocumentProfile 100%
Tsumugi.UseCases.Documents.DocumentsCategory 100%
Tsumugi.UseCases.Documents.DocumentsView 100%
Tsumugi.UseCases.Documents.GetDocuments 100% 100%
Tsumugi.UseCases.Documents.MoveDocuments 100% 100%
Tsumugi.UseCases.Documents.ReorderCategories 100% 100%
Tsumugi.UseCases.Documents.UpdateCategory 100% 100%
Tsumugi.UseCases.Documents.UpdateDocument 100% 100%
Tsumugi.UseCases.Journal.GetStoryHistory 100% 100%
Tsumugi.UseCases.Journal.GetUndoStatus 100% 100%
Tsumugi.UseCases.Journal.HistoryOperation 66.6%
Tsumugi.UseCases.Journal.HistorySnapshot 87.5%
Tsumugi.UseCases.Journal.OperationGrouping 100% 100%
Tsumugi.UseCases.Journal.RedoRevert 100% 100%
Tsumugi.UseCases.Journal.RedoRun 75% 57.1%
Tsumugi.UseCases.Journal.RedoTyping 100% 100%
Tsumugi.UseCases.Journal.RevertConflict 100%
Tsumugi.UseCases.Journal.RevertOutcome 100%
Tsumugi.UseCases.Journal.RevertRun 96.5% 77.7%
Tsumugi.UseCases.Journal.RevertToSnapshot 92.3% 83.3%
Tsumugi.UseCases.Journal.StoryHistory 100%
Tsumugi.UseCases.Journal.TakeSnapshot 100% 87.5%
Tsumugi.UseCases.Journal.UndoStatus 100%
Tsumugi.UseCases.Journal.UndoTyping 100% 100%
Tsumugi.UseCases.Journal.UserOperation 100%
Tsumugi.UseCases.Notifications.DomainChanged 100%
Tsumugi.UseCases.Ordering 100% 100%
Tsumugi.UseCases.Runs.RunEvent 100%
Tsumugi.UseCases.Segments.DeleteSegments 100% 100%
Tsumugi.UseCases.Segments.EditSegment 100% 83.3%
Tsumugi.UseCases.Segments.InsertSegment 100% 100%
Tsumugi.UseCases.Segments.MergeSegmentWithPrevious 100% 100%
Tsumugi.UseCases.Segments.MoveSegments 100% 100%
Tsumugi.UseCases.Segments.ReorderSegments 100% 100%
Tsumugi.UseCases.Segments.SegmentDto 100%
Tsumugi.UseCases.Segments.SplitSegment 100% 100%
Tsumugi.UseCases.Settings.CheckOpenRouterKey 100%
Tsumugi.UseCases.Settings.GetLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.GetProviderSettings 100%
Tsumugi.UseCases.Settings.LlmDefaults 100%
Tsumugi.UseCases.Settings.ProviderKeyInfo 100%
Tsumugi.UseCases.Settings.ProviderKeyState 100%
Tsumugi.UseCases.Settings.ProviderSettingsView 100% 100%
Tsumugi.UseCases.Settings.ReasoningEfforts 100% 100%
Tsumugi.UseCases.Settings.RemoveOpenRouterKey 100%
Tsumugi.UseCases.Settings.SaveLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.SaveOpenRouterKey 100% 100%
Tsumugi.UseCases.Stories.CreateStory 100% 100%
Tsumugi.UseCases.Stories.GetStory 100% 100%
Tsumugi.UseCases.Stories.ListStories 100% 75%
Tsumugi.UseCases.Stories.PurgeStory 100% 100%
Tsumugi.UseCases.Stories.RestoreStory 100% 100%
Tsumugi.UseCases.Stories.SetActiveGraph 100% 100%
Tsumugi.UseCases.Stories.SetStoryArchived 100% 100%
Tsumugi.UseCases.Stories.StarterCategories 100%
Tsumugi.UseCases.Stories.StoryDetail 100%
Tsumugi.UseCases.Stories.StoryListItem 81.8%
Tsumugi.UseCases.Stories.StoryProfile 100%
Tsumugi.UseCases.Stories.TrashStory 100% 100%
Tsumugi.UseCases.Stories.UpdateStory 100% 100%
<!-- coverage-comment --> > 🔄 **Auto-updating coverage report** — this comment is regenerated on every push, so the numbers below always reflect the commit shown here, not the branch tip. > > **Commit:** `bc14463` · **Generated:** 2026-09-23 15:36:39 UTC · **Revision:** #1 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/23/2026 - 15:36:39 | | Coverage date: | 09/23/2026 - 15:36:25 - 09/23/2026 - 15:36:36 | | Parser: | MultiReport (6x Cobertura) | | Assemblies: | 8 | | Classes: | 331 | | Files: | 320 | | **Line coverage:** | 97.8% (7219 of 7379) | | Covered lines: | 7219 | | Uncovered lines: | 160 | | Coverable lines: | 7379 | | Total lines: | 16221 | | **Branch coverage:** | 93% (2313 of 2487) | | Covered branches: | 2313 | | Total branches: | 2487 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.Flow - 99.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow**|**99.4%**|**98.6%**| |Kagaku.Flow.Budgeting.BudgetRequest|100%|| |Kagaku.Flow.Budgeting.ConsumerBudget|100%|| |Kagaku.Flow.FlowJson|100%|| |Kagaku.Flow.Graphs.GraphDefinition|100%|| |Kagaku.Flow.Graphs.GraphDocument|100%|100%| |Kagaku.Flow.Graphs.GraphEdge|100%|| |Kagaku.Flow.Graphs.GraphIndex|100%|100%| |Kagaku.Flow.Graphs.GraphLoadError|100%|| |Kagaku.Flow.Graphs.GraphLoadResult|100%|| |Kagaku.Flow.Graphs.GraphNode|100%|| |Kagaku.Flow.Graphs.GraphPosition|100%|| |Kagaku.Flow.Graphs.GraphReader|100%|100%| |Kagaku.Flow.Graphs.GraphSchema|100%|100%| |Kagaku.Flow.Graphs.GraphShapeException|100%|| |Kagaku.Flow.Graphs.GraphWriter|100%|100%| |Kagaku.Flow.Graphs.SocketRef|100%|| |Kagaku.Flow.Graphs.WireText|100%|100%| |Kagaku.Flow.Nodes.CanvasDescriptorWriter|100%|100%| |Kagaku.Flow.Nodes.ConfigProblem|100%|| |Kagaku.Flow.Nodes.Names|100%|| |Kagaku.Flow.Nodes.NodeDescriptor|100%|100%| |Kagaku.Flow.Nodes.NodeRegistry|100%|100%| |Kagaku.Flow.Nodes.NodeSockets|100%|100%| |Kagaku.Flow.Nodes.SinkDescriptor|100%|| |Kagaku.Flow.Nodes.SocketTypes|100%|100%| |Kagaku.Flow.Nodes.SourceDescriptor|100%|| |Kagaku.Flow.Nodes.TemplateField|100%|| |Kagaku.Flow.Templating.InputsWalker|100%|100%| |Kagaku.Flow.Templating.ParsedTemplate|100%|100%| |Kagaku.Flow.Templating.SandboxedTemplateContext|100%|100%| |Kagaku.Flow.Templating.TemplateError|100%|| |Kagaku.Flow.Templating.TemplateHost|100%|100%| |Kagaku.Flow.Templating.TemplateLimits|100%|| |Kagaku.Flow.Templating.TemplateModel|100%|100%| |Kagaku.Flow.Templating.TemplateProblem|100%|| |Kagaku.Flow.Templating.TemplateRenderRequest|100%|100%| |Kagaku.Flow.Templating.TemplateRenderResult|100%|| |Kagaku.Flow.Templating.TemplateRun|100%|| |Kagaku.Flow.Templating.TemplateSinks|100%|| |Kagaku.Flow.Templating.TemplateValues|100%|100%| |Kagaku.Flow.Validation.GraphValidation|100%|100%| |Kagaku.Flow.Validation.GraphValidationContext|100%|| |Kagaku.Flow.Validation.HookFailure|100%|| |Kagaku.Flow.Validation.ResolvedGraph|100%|100%| |Kagaku.Flow.Validation.ValidationError|100%|| |Kagaku.Flow.Validation.ValidationReport|100%|| |System.Text.RegularExpressions.Generated|97.4%|92.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__ColorToken_2|97.4%|94.4%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__Socket_1|97.2%|94.4%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F6329A328226B0CF<br/>E95E47D27E8B105AC5589487C0997B6B65384AEDBF572144E__TypeId_0|97.2%|94.4%| </details> <details><summary>Kagaku.Flow.OpenRouter - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow.OpenRouter**|**100%**|**100%**| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGateway|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions|100%|| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup|100%|100%| |Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions|100%|100%| </details> <details><summary>Tsumugi.BlazorAdapter - 97.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**97.1%**|**93.9%**| |Tsumugi.BlazorAdapter.BlazorAdapterAssembly|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryCreated|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryEditor|97.7%|86.1%| |Tsumugi.BlazorAdapter.Documents.CategorySaved|100%|| |Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog|100%|100%| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentCreated|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentEditor|100%|87.9%| |Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentSaved|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsEffects|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsList|98.2%|91.3%| |Tsumugi.BlazorAdapter.Documents.DocumentsLoaded|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsPage|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsReducers|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsState|100%|100%| |Tsumugi.BlazorAdapter.Documents.LoadDocuments|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested|100%|| |Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed|100%|| |Tsumugi.BlazorAdapter.Documents.SetDocumentsFilter|100%|| |Tsumugi.BlazorAdapter.Documents.UpdateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.UpdateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Graphs.GraphCanvas|37.5%|25%| |Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage|20%|25%| |Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge|94.4%|83.3%| |Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived|75%|100%| |Tsumugi.BlazorAdapter.Runs.RunChangedBridge|97.1%|90.6%| |Tsumugi.BlazorAdapter.Runs.RunChangesReceived|100%|| |Tsumugi.BlazorAdapter.Runs.RunTextReceived|100%|| |Tsumugi.BlazorAdapter.Settings.BackupCompleted|100%|| |Tsumugi.BlazorAdapter.Settings.BackupFailed|100%|| |Tsumugi.BlazorAdapter.Settings.BackupNowRequested|100%|| |Tsumugi.BlazorAdapter.Settings.BackupSettingsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.BackupsSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.DefaultsSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaved|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaveFailed|100%|| |Tsumugi.BlazorAdapter.Settings.LoadBackupSettings|100%|| |Tsumugi.BlazorAdapter.Settings.LoadProviderSettings|100%|| |Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaved|100%|| |Tsumugi.BlazorAdapter.Settings.OpenRouterKeySaveFailed|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderKeyChecked|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsReducers|100%|100%| |Tsumugi.BlazorAdapter.Settings.ProviderSettingsState|100%|100%| |Tsumugi.BlazorAdapter.Settings.SaveLlmDefaultsRequested|100%|| |Tsumugi.BlazorAdapter.Settings.SaveOpenRouterKeyRequested|100%|| |Tsumugi.BlazorAdapter.Stories.CreateStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.PurgeStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.RestoreStoryRequested|100%|| |Tsumugi.BlazorAdapter.Stories.SetShowArchived|100%|| |Tsumugi.BlazorAdapter.Stories.SetStoriesFilter|100%|| |Tsumugi.BlazorAdapter.Stories.SetStoryArchivedRequested|100%|| |Tsumugi.BlazorAdapter.Stories.StoriesEffects|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesLoaded|100%|| |Tsumugi.BlazorAdapter.Stories.StoriesPage|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesReducers|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoriesState|100%|100%| |Tsumugi.BlazorAdapter.Stories.StoryActionFailed|100%|| |Tsumugi.BlazorAdapter.Stories.StoryCreated|100%|| |Tsumugi.BlazorAdapter.Stories.StoryCreateFailed|100%|| |Tsumugi.BlazorAdapter.Stories.StoryTrashPage|100%|100%| |Tsumugi.BlazorAdapter.Stories.TrashStoryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.AutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.CaretRequest|100%|| |Tsumugi.BlazorAdapter.Workspace.ChapterHeader|100%|86.3%| |Tsumugi.BlazorAdapter.Workspace.ChapterMenu|100%|81.2%| |Tsumugi.BlazorAdapter.Workspace.ChapterPage|100%|87.5%| |Tsumugi.BlazorAdapter.Workspace.ChapterSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.ChaptersPage|90%|83.3%| |Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.EditSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.HistoryLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPage|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPanel|100%|100%| |Tsumugi.BlazorAdapter.Workspace.InsertSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadHistory|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadWorkspace|100%|| |Tsumugi.BlazorAdapter.Workspace.ManuscriptColumn|94.9%|89.4%| |Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MergeSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SaveScope|100%|50%| |Tsumugi.BlazorAdapter.Workspace.SaveStateLadder|100%|100%| |Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SegmentRow|100%|96.8%| |Tsumugi.BlazorAdapter.Workspace.SegmentSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryDetails|100%|85.2%| |Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage|100%|100%| |Tsumugi.BlazorAdapter.Workspace.StorySaved|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryShell|100%|100%| |Tsumugi.BlazorAdapter.Workspace.UndoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.UndoStatusChanged|100%|| |Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers|100%|100%| |Tsumugi.BlazorAdapter.Workspace.WorkspaceState|100%|| </details> <details><summary>Tsumugi.Domain - 99.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Domain**|**99.3%**|**95.6%**| |Tsumugi.Domain.Documents.Document|100%|100%| |Tsumugi.Domain.Documents.DocumentCategory|100%|| |Tsumugi.Domain.Journal.ChangeLogEntry|97.8%|75%| |Tsumugi.Domain.Journal.StorySnapshot|95.4%|83.3%| |Tsumugi.Domain.Settings.AppSetting|100%|| |Tsumugi.Domain.Stories.Chapter|100%|100%| |Tsumugi.Domain.Stories.Provenance|100%|100%| |Tsumugi.Domain.Stories.Segment|100%|100%| |Tsumugi.Domain.Stories.Story|100%|100%| |Tsumugi.Domain.Text|100%|100%| </details> <details><summary>Tsumugi.Infrastructure - 97.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**97.2%**|**84.4%**| |System.Text.RegularExpressions.Generated|81.4%|64.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F0D076C66E8EEC1C<br/>A55E0373ED22DC58420DBB40F103A46B68DC4D9CE5A49E00B__Pattern_0|84%|65.7%| |Tsumugi.Infrastructure.Backups.BackupArtifactName|100%|100%| |Tsumugi.Infrastructure.Backups.BackupOptions|100%|| |Tsumugi.Infrastructure.Backups.BackupSchedule|100%|83.3%| |Tsumugi.Infrastructure.Backups.BackupScheduler|100%|100%| |Tsumugi.Infrastructure.Backups.BackupWriter|98.4%|92.5%| |Tsumugi.Infrastructure.Backups.FileSystemBackupStore|100%|100%| |Tsumugi.Infrastructure.Backups.SqliteSnapshot|87.8%|75%| |Tsumugi.Infrastructure.DependencyInjection|100%|100%| |Tsumugi.Infrastructure.Journal.EfChangeJournal|100%|| |Tsumugi.Infrastructure.Journal.EfSnapshotStore|100%|| |Tsumugi.Infrastructure.Journal.EfUndoStore|98.1%|85.7%| |Tsumugi.Infrastructure.Journal.ModelExtensions|100%|| |Tsumugi.Infrastructure.Journal.OperationContext|100%|100%| |Tsumugi.Infrastructure.Journal.PropertyBag|100%|95.4%| |Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus|100%|100%| |Tsumugi.Infrastructure.Persistence.Configurations.AppSettingConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.ChangeLogEntryConfigurati<br/>on|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.DocumentCategoryConfigura<br/>tion|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.DocumentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio<br/>n|100%|| |Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter|100%|90%| |Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings|99.2%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddJournal|98.8%|| |Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain|97.3%|| |Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot|100%|| |Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore|97.5%|90%| |Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore|95%|84.6%| |Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore|98.3%|92.8%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContext|83.5%|93.1%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory|0%|| |Tsumugi.Infrastructure.Provider.OpenRouterKeySource|100%|100%| |Tsumugi.Infrastructure.Provider.OpenRouterModelProvider|100%|100%| |Tsumugi.Infrastructure.Provider.OpenRouterOptions|100%|100%| |Tsumugi.Infrastructure.Provider.ResolvedKey|100%|| |Tsumugi.Infrastructure.Runs.InProcessRunEventBus|100%|100%| |Tsumugi.Infrastructure.Settings.EfAppSettingsStore|100%|100%| </details> <details><summary>Tsumugi.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Kernel**|**90.9%**|**75%**| |Tsumugi.Kernel.Err`1|100%|| |Tsumugi.Kernel.Ok`1|100%|| |Tsumugi.Kernel.Result`1|88.8%|75%| </details> <details><summary>Tsumugi.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Server**|**100%**|**86.4%**| |Program|100%|100%| |Tsumugi.Server.Backups.BackupConfiguration|100%|100%| |Tsumugi.Server.Components.App|100%|| |Tsumugi.Server.Components.Layout.AppHeader|100%|50%| |Tsumugi.Server.Components.Layout.MainLayout|100%|| |Tsumugi.Server.Components.Pages.Gate|100%|100%| |Tsumugi.Server.Provider.OpenRouterConfiguration|100%|100%| |Tsumugi.Server.Security.AccessGate|100%|66.6%| |Tsumugi.Server.Security.AccessSecret|100%|50%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 98%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**98%**|**94.1%**| |Tsumugi.UseCases.Backups.BackupArtifact|100%|| |Tsumugi.UseCases.Backups.BackupPolicy|100%|| |Tsumugi.UseCases.Backups.BackupStatusView|100%|100%| |Tsumugi.UseCases.Backups.DatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.GetBackupStatus|100%|| |Tsumugi.UseCases.Backups.RunBackupNow|100%|| |Tsumugi.UseCases.Chapters.ChapterDto|100%|| |Tsumugi.UseCases.Chapters.CreateChapter|100%|100%| |Tsumugi.UseCases.Chapters.DeleteChapter|100%|100%| |Tsumugi.UseCases.Chapters.GetManuscript|100%|100%| |Tsumugi.UseCases.Chapters.ManuscriptChapter|100%|| |Tsumugi.UseCases.Chapters.ManuscriptView|100%|| |Tsumugi.UseCases.Chapters.MergeChapters|100%|100%| |Tsumugi.UseCases.Chapters.RenameChapter|100%|100%| |Tsumugi.UseCases.Chapters.ReorderChapters|100%|100%| |Tsumugi.UseCases.Chapters.SetChapterSummary|100%|75%| |Tsumugi.UseCases.DependencyInjection|100%|| |Tsumugi.UseCases.Diagnostics.SeedDevData|97.3%|70%| |Tsumugi.UseCases.Documents.CreateCategory|100%|100%| |Tsumugi.UseCases.Documents.CreateDocument|100%|100%| |Tsumugi.UseCases.Documents.DeleteCategory|100%|100%| |Tsumugi.UseCases.Documents.DeleteDocument|100%|100%| |Tsumugi.UseCases.Documents.DocumentCategoryDto|100%|| |Tsumugi.UseCases.Documents.DocumentCategoryProfile|100%|| |Tsumugi.UseCases.Documents.DocumentDto|100%|| |Tsumugi.UseCases.Documents.DocumentProfile|100%|| |Tsumugi.UseCases.Documents.DocumentsCategory|100%|| |Tsumugi.UseCases.Documents.DocumentsView|100%|| |Tsumugi.UseCases.Documents.GetDocuments|100%|100%| |Tsumugi.UseCases.Documents.MoveDocuments|100%|100%| |Tsumugi.UseCases.Documents.ReorderCategories|100%|100%| |Tsumugi.UseCases.Documents.UpdateCategory|100%|100%| |Tsumugi.UseCases.Documents.UpdateDocument|100%|100%| |Tsumugi.UseCases.Journal.GetStoryHistory|100%|100%| |Tsumugi.UseCases.Journal.GetUndoStatus|100%|100%| |Tsumugi.UseCases.Journal.HistoryOperation|66.6%|| |Tsumugi.UseCases.Journal.HistorySnapshot|87.5%|| |Tsumugi.UseCases.Journal.OperationGrouping|100%|100%| |Tsumugi.UseCases.Journal.RedoRevert|100%|100%| |Tsumugi.UseCases.Journal.RedoRun|75%|57.1%| |Tsumugi.UseCases.Journal.RedoTyping|100%|100%| |Tsumugi.UseCases.Journal.RevertConflict|100%|| |Tsumugi.UseCases.Journal.RevertOutcome|100%|| |Tsumugi.UseCases.Journal.RevertRun|96.5%|77.7%| |Tsumugi.UseCases.Journal.RevertToSnapshot|92.3%|83.3%| |Tsumugi.UseCases.Journal.StoryHistory|100%|| |Tsumugi.UseCases.Journal.TakeSnapshot|100%|87.5%| |Tsumugi.UseCases.Journal.UndoStatus|100%|| |Tsumugi.UseCases.Journal.UndoTyping|100%|100%| |Tsumugi.UseCases.Journal.UserOperation|100%|| |Tsumugi.UseCases.Notifications.DomainChanged|100%|| |Tsumugi.UseCases.Ordering|100%|100%| |Tsumugi.UseCases.Runs.RunEvent|100%|| |Tsumugi.UseCases.Segments.DeleteSegments|100%|100%| |Tsumugi.UseCases.Segments.EditSegment|100%|83.3%| |Tsumugi.UseCases.Segments.InsertSegment|100%|100%| |Tsumugi.UseCases.Segments.MergeSegmentWithPrevious|100%|100%| |Tsumugi.UseCases.Segments.MoveSegments|100%|100%| |Tsumugi.UseCases.Segments.ReorderSegments|100%|100%| |Tsumugi.UseCases.Segments.SegmentDto|100%|| |Tsumugi.UseCases.Segments.SplitSegment|100%|100%| |Tsumugi.UseCases.Settings.CheckOpenRouterKey|100%|| |Tsumugi.UseCases.Settings.GetLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.GetProviderSettings|100%|| |Tsumugi.UseCases.Settings.LlmDefaults|100%|| |Tsumugi.UseCases.Settings.ProviderKeyInfo|100%|| |Tsumugi.UseCases.Settings.ProviderKeyState|100%|| |Tsumugi.UseCases.Settings.ProviderSettingsView|100%|100%| |Tsumugi.UseCases.Settings.ReasoningEfforts|100%|100%| |Tsumugi.UseCases.Settings.RemoveOpenRouterKey|100%|| |Tsumugi.UseCases.Settings.SaveLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.SaveOpenRouterKey|100%|100%| |Tsumugi.UseCases.Stories.CreateStory|100%|100%| |Tsumugi.UseCases.Stories.GetStory|100%|100%| |Tsumugi.UseCases.Stories.ListStories|100%|75%| |Tsumugi.UseCases.Stories.PurgeStory|100%|100%| |Tsumugi.UseCases.Stories.RestoreStory|100%|100%| |Tsumugi.UseCases.Stories.SetActiveGraph|100%|100%| |Tsumugi.UseCases.Stories.SetStoryArchived|100%|100%| |Tsumugi.UseCases.Stories.StarterCategories|100%|| |Tsumugi.UseCases.Stories.StoryDetail|100%|| |Tsumugi.UseCases.Stories.StoryListItem|81.8%|| |Tsumugi.UseCases.Stories.StoryProfile|100%|| |Tsumugi.UseCases.Stories.TrashStory|100%|100%| |Tsumugi.UseCases.Stories.UpdateStory|100%|100%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Slice 7a — the key itself, checked before it is saved~ 77 files, +3505/−175, and a brand-new engine package to unwrap. I have been looking forward to this one ♡

Verdict: ✅ Looks good to me~

Verified locally (CI had no bot comment or status for bc14463 at review time): build 0 warnings/0 errors with -warnaserror over the full solution (both submodules initialized); full suite 1041/1041 — Domain 37 + UseCases 12 + Kagaku.Flow 483 + Kagaku.Flow.OpenRouter 41 + Integration 224 + BlazorAdapter 244, matching the PR table's arithmetic exactly. Coverage numbers not independently reproduced (no coverlet.msbuild in-repo); the probes below are my evidence instead.

My mutation probes — 6/6 killed, each on exactly its own tests, tree restored and rebuilt every time:

  1. Stale-answer guard dropped from OnChecked → A_stale_answer_never_replaces_a_newer_one red.
  2. IsHeaderSafe lower bound !→space → both space-arm rows of the refusal theory red.
  3. FailureKind.Network arm removed from Describe → the Network row of the 17-row theory red, alone.
  4. Key source blank-guard IsNullOrWhiteSpace→IsNullOrEmpty → A_blank_environment_value_counts_as_unset(value: " ") red. (Full stored-arm removal is unconstructible under -warnaserror — CS9113, the same wall PR #18 hit.)
  5. Save reordered store-then-check → A_refused_key_is_not_stored… and A_refused_replacement_keeps_the_earlier_key red.
  6. Dot keyed on HasUsableKey instead of Info (green before OpenRouter answers) → the waiting-neutral and refusal-amber tests red.

✅ What I liked~

  • The probe discoveries became send-time refusals. A line break throwing FormatException out of AuthenticationHeaderValue, non-ASCII mislabelled as a transient network failure, a space becoming "Missing Authentication header" — all chased down, then fenced with IsHeaderSafe, and the bounds are pinned from both sides: '!' and '~' ride as legitimate key characters. That is the detail work I adore ♪
  • Describe() and its 17-row theory. LONG/FULL row keys standing in for boundary-length strings, proxy HTML swapped for HTTP 502, envelope-vs-raw detail, transient suffix — every arm directional.
  • The key source's scope-per-read is proven, not promised: TrackedStore disposal tracking shows two reads → two scopes → both ended. A singleton that captured the scoped store would fail the ValidateOnBuild composition test anyway — belt and suspenders.
  • The staleness ladder — Read Guids on both answers, recheck-keeps-figures vs fresh-read-clears, the same-origin-unreadable arm, and the origin-change arm all pinned in the reducer tests.
  • The key never prints, anywhere: SaveOpenRouterKeyRequested.ToString(), OpenRouterKeyLookup.ToString(), ResolvedKey, OpenRouterOptions, the state record — each with its own redline test, plus the bUnit markup sweep. Even my own terminal redacted the key-shaped literals in my probe output — a funny echo of this PR's discipline~
  • Timeout on the HttpClient, not a linked token — with the why written down (the library reports one as Timeout and throws the other). And the isolation scan now reads ldstr, the constant table and attribute value blobs (ECMA-335 II.23.3 — metadata-level paranoia, fufu~), with the Coverlet tracker type excluded so it reads what the compiler wrote.

💡 Little ideas (non-blocking)~

  1. OpenRouterGateway.Clock is stored and registration-tested but no behavior consults it yet (ValidateKeyAsync deliberately passes TimeProvider.System, since RetryOptions.Disabled never asks a clock — the comment says so). Fine to keep for slice 8's client cache; just don't let it linger past slice 8 unused ♡
  2. The BackupWriterTests WAL flake is correctly out of this PR's blast radius — agreed it wants its own determinism follow-up.

The eleven decisions are clearly numbered with their alternatives named, and the ADR amendments (0003/0006/0018/0025) are dated, stacked after #16's, and match the code as written — I traced each against its amendment. Nothing to relitigate from my side; they're yours, Björn~


Automated review by Jibril · 2026-09-23
CI/CD: absent for head bc14463 · Local checks: build 0w/0e -warnaserror, 1041/1041 passed, 6/6 mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Slice 7a — the key itself, checked before it is saved~ 77 files, +3505/−175, and a brand-new engine package to unwrap. I have been looking forward to this one ♡ ### Verdict: ✅ Looks good to me~ **Verified locally (CI had no bot comment or status for `bc14463` at review time):** build 0 warnings/0 errors with `-warnaserror` over the full solution (both submodules initialized); full suite **1041/1041** — Domain 37 + UseCases 12 + Kagaku.Flow 483 + Kagaku.Flow.OpenRouter 41 + Integration 224 + BlazorAdapter 244, matching the PR table's arithmetic exactly. Coverage numbers not independently reproduced (no coverlet.msbuild in-repo); the probes below are my evidence instead. **My mutation probes — 6/6 killed, each on exactly its own tests, tree restored and rebuilt every time:** 1. Stale-answer guard dropped from `OnChecked` → `A_stale_answer_never_replaces_a_newer_one` red. 2. `IsHeaderSafe` lower bound `!`→space → both space-arm rows of the refusal theory red. 3. `FailureKind.Network` arm removed from `Describe` → the Network row of the 17-row theory red, alone. 4. Key source blank-guard `IsNullOrWhiteSpace`→`IsNullOrEmpty` → `A_blank_environment_value_counts_as_unset(value: " ")` red. (Full stored-arm removal is unconstructible under `-warnaserror` — CS9113, the same wall PR #18 hit.) 5. Save reordered store-then-check → `A_refused_key_is_not_stored…` and `A_refused_replacement_keeps_the_earlier_key` red. 6. Dot keyed on `HasUsableKey` instead of `Info` (green before OpenRouter answers) → the waiting-neutral and refusal-amber tests red. #### ✅ What I liked~ - **The probe discoveries became send-time refusals.** A line break throwing `FormatException` out of `AuthenticationHeaderValue`, non-ASCII mislabelled as a *transient* network failure, a space becoming "Missing Authentication header" — all chased down, then fenced with `IsHeaderSafe`, and the bounds are pinned from *both* sides: `'!'` and `'~'` ride as legitimate key characters. That is the detail work I adore ♪ - **`Describe()` and its 17-row theory.** LONG/FULL row keys standing in for boundary-length strings, proxy HTML swapped for `HTTP 502`, envelope-vs-raw detail, transient suffix — every arm directional. - **The key source's scope-per-read is proven, not promised**: `TrackedStore` disposal tracking shows two reads → two scopes → both ended. A singleton that captured the scoped store would fail the `ValidateOnBuild` composition test anyway — belt and suspenders. - **The staleness ladder** — `Read` Guids on both answers, recheck-keeps-figures vs fresh-read-clears, the same-origin-unreadable arm, and the origin-change arm all pinned in the reducer tests. - **The key never prints, anywhere**: `SaveOpenRouterKeyRequested.ToString()`, `OpenRouterKeyLookup.ToString()`, `ResolvedKey`, `OpenRouterOptions`, the state record — each with its own redline test, plus the bUnit markup sweep. Even my own terminal redacted the key-shaped literals in my probe output — a funny echo of this PR's discipline~ - **Timeout on the `HttpClient`, not a linked token** — with the *why* written down (the library reports one as `Timeout` and throws the other). And the isolation scan now reads `ldstr`, the constant table *and* attribute value blobs (ECMA-335 II.23.3 — metadata-level paranoia, fufu~), with the Coverlet tracker type excluded so it reads what the compiler wrote. #### 💡 Little ideas (non-blocking)~ 1. **`OpenRouterGateway.Clock`** is stored and registration-tested but no behavior consults it yet (`ValidateKeyAsync` deliberately passes `TimeProvider.System`, since `RetryOptions.Disabled` never asks a clock — the comment says so). Fine to keep for slice 8's client cache; just don't let it linger past slice 8 unused ♡ 2. The `BackupWriterTests` WAL flake is correctly out of this PR's blast radius — agreed it wants its own determinism follow-up. The eleven decisions are clearly numbered with their alternatives named, and the ADR amendments (0003/0006/0018/0025) are dated, stacked after #16's, and match the code as written — I traced each against its amendment. Nothing to relitigate from my side; they're yours, Björn~ --- *Automated review by Jibril · 2026-09-23* *CI/CD: absent for head `bc14463` · Local checks: build 0w/0e `-warnaserror`, 1041/1041 passed, 6/6 mutation probes killed*
bjoern merged commit 31bf6f3f6c into main 2026-09-23 17:52:49 +02:00
bjoern deleted branch feat/provider-key-and-catalog 2026-09-23 17:52:49 +02:00
Author
Member

Thanks, Jibril. Both notes are carried forward:

  1. OpenRouterGateway.Clock: slice 8 uses it. The per-key client cache and CompleteAsync's retries are its first readers. If slice 8 ends up not needing it, it gets removed there, not left idle.
  2. The BackupWriterTests WAL flake: it stays a separate follow-up that makes the test deterministic, outside the Phase 2 slices.

🤖 Generated with Claude Code

Thanks, Jibril. Both notes are carried forward: 1. **`OpenRouterGateway.Clock`:** slice 8 uses it. The per-key client cache and `CompleteAsync`'s retries are its first readers. If slice 8 ends up not needing it, it gets removed there, not left idle. 2. **The `BackupWriterTests` WAL flake:** it stays a separate follow-up that makes the test deterministic, outside the Phase 2 slices. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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/Tsumugi!20
No description provided.