fix: the access gate holds — a rotated secret revokes its cookies, one secret check at a time, a loopback-only port #45

Merged
bjoern merged 2 commits from fix/gate-hardening into main 2026-09-26 13:42:26 +02:00
Member

Summary

The access gate catches up with what ADR 0007 promised, and it gets a real throttle.

  • A rotated secret revokes its cookies. Each cookie now carries an HMAC fingerprint of the secret that issued it. The HMAC uses a fixed label and is keyed by the secret, so the secret itself never goes into the cookie. OnValidatePrincipal refuses and clears a cookie whose fingerprint belongs to another secret, and also a cookie from before this change, which has no fingerprint. So after a deploy, the owner types the secret once. The secret is read only at startup, so a rotation means a restart, which ends every open circuit; a tab reconnects through the same check.
  • One secret check at a time, app-wide (GateThrottle).
    • The comparison happens inside the turn, so a parallel burst finds out about a hit no sooner than a serial one would.
    • A wrong secret keeps the turn for the 1 s failure delay, timed on the host's TimeProvider. A client that hangs up during that delay does not shorten it.
    • Four attempts may wait, served in order of arrival. A waiting attempt whose client hangs up leaves the queue without being compared.
    • One more attempt gets 429 with Retry-After: 1 before anything is compared. One second is when a place in the queue frees up; the comment says it is only a hint.
    • Each wrong secret is logged at Warning with the client's address. Refusals are logged at most once per delay, each line counting the refusals it held back, so a flood cannot fill the host's disk.
  • The logged address is the one nginx saw. ForwardLimit = 1 is the framework default, now set explicitly. nginx appends its own entry to any X-Forwarded-For the client sends, and the app reads only that rightmost entry.
  • Return URL. A return URL is honoured only if it is visible ASCII. Before, the right secret with a non-ASCII or control character in ReturnUrl caused a 500 on Kestrel. Sign-in now passes the cleaned URL as its RedirectUri, so the cookie handler never follows the raw query value itself.
  • Response headers.
    • The antiforgery cookie is Secure whenever the browser-facing request is HTTPS (SameAsRequest). It is not Always, because under Always antiforgery refuses every plain-HTTP request, which breaks local development.
    • Every response carries X-Content-Type-Options: nosniff. It is set as the headers are sent, so the error page keeps it too.
    • The snapshot download is Cache-Control: no-store.
  • Deploy. Compose now publishes 127.0.0.1:8088 only, and nginx overwrites X-Forwarded-Host, so the proxy really is the only way in.

Needs the owner's decision before merge:

  • The loopback bind is a deployment change. It assumes the nginx block runs on the Docker host itself, as proxy_pass http://127.0.0.1:8088 implies. A proxy on another machine, or in a bridge-network container, would lose its route. The compose comment and ADR 0008 say what to do in those cases.
  • Refusing a short secret outside Development is left out. That is your call and would need an amendment to ADR 0006/0007. ADR 0007's Consequences now says plainly that a short secret is not refused.

Docs: ADR 0007 amended (fingerprint and revocation, throttle and its logging, the rightmost X-Forwarded-For, visible-ASCII return URL, antiforgery cookie, nosniff/no-store, why forwarded headers can be trusted) and ADR 0008 amended (loopback-only port in the Deploy bullet); deploy/docker-compose.yml and deploy/nginx-tsumugi.conf comments.

Verification

  • dotnet test tests/Tsumugi.Integration.Tests: 416/416 after the last change. This PR touches only the Server project and Integration tests, so no other suite is affected. The Kagaku.Flow suites were not run.
  • Flake check: AccessGate, AccessSecret, BackupSnapshotEndpoint, AppChrome, ImageEndpoint and SeedDevData are every test that uses GatedAppFactory. Together they are 89 tests, run 10 times in a row: 89/89 each time, 3–8 s per run. The throttle tests use a fake clock (GateClock) and never sleep. Each of their waits is bounded at 10 real seconds, and so is every request from CreateGateClient.
  • New tests.
    • The delay on the injected clock; the queue and its order; a hang-up while holding the turn; a hang-up while waiting, which leaves the queue unchecked.
    • The burst refusal (429, Retry-After: 1, nothing compared); the refusal log's once-per-delay cap and its count.
    • Every throttle log line, by level and as an exact list; the rightmost X-Forwarded-For entry.
    • The 14-day sliding cookie; SameSite and persistence; rotation across restarts; a minted cookie from before the fingerprint.
    • Every return-URL arm, including both ends of visible ASCII, one case on real Kestrel; sign-in and sign-out under a sub-path; nosniff including on the error page; the antiforgery cookie; no-store.
    • The fingerprint by exact value, and a secret beyond ASCII by its UTF-8 bytes.
  • Mutation probes, each restored and touched afterwards. The implementer's pass had 40; after the review, 26 more targeted the fixes. Every one that compiles fails its test:
    • AcquireAsync ignoring the abort: killed by …hangs_up_while_it_waits_leaves_the_queue_unchecked.
    • The wrong-secret or refusal line at Error instead of Warning, logged twice, or the right secret logged as wrong: killed by the exact-list and level assertions in A_wrong_secret_re_prompts…, …waits_its_turn_in_order_of_arrival and A_burst….
    • The refusal cap removed, made inclusive, doubled, on the system clock, with its time kept only the first time or set a delay behind, its count not incremented, not reset or logged as 0: all killed by A_flood_of_refusals_is_logged_once_per_failure_delay_counting_the_rest.
    • '!' or '~' excluded from the return URL: killed by the /stories?q=!~ case.
    • ASCII in place of UTF-8 in the comparison hash, in the fingerprint or in the candidate: killed by A_secret_beyond_ascii_is_compared_and_fingerprinted_by_its_utf8_bytes.
    • ForwardLimit set to null or 2: killed by the two-entry X-Forwarded-For in A_wrong_secret_re_prompts….
    • Retry-After ×5 and the refusal naming the local IP: killed by A_burst….
    • The right secret also waiting out the delay, and a turn never released (using dropped): both now fail named tests within the 10 s bounds (63 s and 185 s for the whole filtered run). Before, they hung the run.
    • One mutant does not compile: with _refusalLogged never assigned, CS0649 is an error.
    • Two found by the review are equivalent: the sign-in's RedirectUri set to the base href (the page's NavigateTo overwrites Location), and ?? "" in place of IssuedWith's null check.
  • Independent review (two reviewers; one of them probed 58 mutants).
    • Nine survivors and six major findings, all fixed with tests: the log level was not pinned; warnings were checked with Contains, so a doubled line or a right secret logged as wrong went unnoticed; a queued hang-up was untested; the ends of visible ASCII were untested; UTF-8 was not pinned; ForwardLimit was implicit, so a client could choose the logged address.
    • Minor findings, fixed:
      • A flood of refusals could fill the disk, so refusals are now logged at most once per delay.
      • The auth cookie's comment and ADR 0007 said the app cannot know the browser-facing scheme. Both now give the real reason for Always.
      • ADR 0007 said nginx sets every forwarded header itself, but it appends to X-Forwarded-For. The ADR now says so.
      • Two probes hung the run instead of failing it; they are now bounded by 10 s timeouts.
    • Nits, fixed: GateClock states its one-timer assumption; the backup scheduler's removal fails with a named message; ADR 0007's amendment names nosniff and no-store.
    • Declined: a longer Retry-After. One second is when a place in the queue frees up; the comment now says it is a hint.
    • Left as a follow-up: ADR 0008's stale "csproj-first layering" clause. It is a wording fix, and this arc amends ADRs only where behaviour changes.
  • Browser pass (implementer, headless Chromium, the app on a free local port):
    • A wrong secret took 1119 ms.
    • A burst of six gave one 429 with Retry-After: 1 and five 200s, with the warnings in the log.
    • The right secret landed on /settings with a Secure, SameSite=Lax, 14-day cookie.
    • The cookie survived a restart with the same secret.
    • After a restart with a new secret, the old cookie was sent to /gate?ReturnUrl=%2F, and signing in with the new secret worked.

🤖 Generated with Claude Code

## Summary The access gate catches up with what ADR 0007 promised, and it gets a real throttle. - **A rotated secret revokes its cookies.** Each cookie now carries an HMAC fingerprint of the secret that issued it. The HMAC uses a fixed label and is keyed by the secret, so the secret itself never goes into the cookie. `OnValidatePrincipal` refuses and clears a cookie whose fingerprint belongs to another secret, and also a cookie from before this change, which has no fingerprint. So after a deploy, the owner types the secret once. The secret is read only at startup, so a rotation means a restart, which ends every open circuit; a tab reconnects through the same check. - **One secret check at a time, app-wide** (`GateThrottle`). - The comparison happens inside the turn, so a parallel burst finds out about a hit no sooner than a serial one would. - A wrong secret keeps the turn for the 1 s failure delay, timed on the host's `TimeProvider`. A client that hangs up during that delay does not shorten it. - Four attempts may wait, served in order of arrival. A waiting attempt whose client hangs up leaves the queue without being compared. - One more attempt gets `429` with `Retry-After: 1` before anything is compared. One second is when a place in the queue frees up; the comment says it is only a hint. - Each wrong secret is logged at Warning with the client's address. Refusals are logged at most once per delay, each line counting the refusals it held back, so a flood cannot fill the host's disk. - **The logged address is the one nginx saw.** `ForwardLimit = 1` is the framework default, now set explicitly. nginx appends its own entry to any `X-Forwarded-For` the client sends, and the app reads only that rightmost entry. - **Return URL.** A return URL is honoured only if it is visible ASCII. Before, the right secret with a non-ASCII or control character in `ReturnUrl` caused a 500 on Kestrel. Sign-in now passes the cleaned URL as its `RedirectUri`, so the cookie handler never follows the raw query value itself. - **Response headers.** - The antiforgery cookie is `Secure` whenever the browser-facing request is HTTPS (`SameAsRequest`). It is not `Always`, because under `Always` antiforgery refuses every plain-HTTP request, which breaks local development. - Every response carries `X-Content-Type-Options: nosniff`. It is set as the headers are sent, so the error page keeps it too. - The snapshot download is `Cache-Control: no-store`. - **Deploy.** Compose now publishes `127.0.0.1:8088` only, and nginx overwrites `X-Forwarded-Host`, so the proxy really is the only way in. **Needs the owner's decision before merge:** - **The loopback bind is a deployment change.** It assumes the nginx block runs on the Docker host itself, as `proxy_pass http://127.0.0.1:8088` implies. A proxy on another machine, or in a bridge-network container, would lose its route. The compose comment and ADR 0008 say what to do in those cases. - **Refusing a short secret outside Development is left out.** That is your call and would need an amendment to ADR 0006/0007. ADR 0007's Consequences now says plainly that a short secret is not refused. Docs: ADR 0007 amended (fingerprint and revocation, throttle and its logging, the rightmost `X-Forwarded-For`, visible-ASCII return URL, antiforgery cookie, `nosniff`/`no-store`, why forwarded headers can be trusted) and ADR 0008 amended (loopback-only port in the Deploy bullet); `deploy/docker-compose.yml` and `deploy/nginx-tsumugi.conf` comments. ## Verification - `dotnet test tests/Tsumugi.Integration.Tests`: **416/416** after the last change. This PR touches only the Server project and Integration tests, so no other suite is affected. The Kagaku.Flow suites were not run. - Flake check: AccessGate, AccessSecret, BackupSnapshotEndpoint, AppChrome, ImageEndpoint and SeedDevData are every test that uses `GatedAppFactory`. Together they are 89 tests, run **10 times** in a row: 89/89 each time, 3–8 s per run. The throttle tests use a fake clock (`GateClock`) and never sleep. Each of their waits is bounded at 10 real seconds, and so is every request from `CreateGateClient`. - **New tests.** - The delay on the injected clock; the queue and its order; a hang-up while holding the turn; a hang-up while waiting, which leaves the queue unchecked. - The burst refusal (`429`, `Retry-After: 1`, nothing compared); the refusal log's once-per-delay cap and its count. - Every throttle log line, by level and as an exact list; the rightmost `X-Forwarded-For` entry. - The 14-day sliding cookie; SameSite and persistence; rotation across restarts; a minted cookie from before the fingerprint. - Every return-URL arm, including both ends of visible ASCII, one case on real Kestrel; sign-in and sign-out under a sub-path; `nosniff` including on the error page; the antiforgery cookie; `no-store`. - The fingerprint by exact value, and a secret beyond ASCII by its UTF-8 bytes. - **Mutation probes**, each restored and touched afterwards. The implementer's pass had 40; after the review, 26 more targeted the fixes. Every one that compiles fails its test: - `AcquireAsync` ignoring the abort: killed by *…hangs_up_while_it_waits_leaves_the_queue_unchecked*. - The wrong-secret or refusal line at Error instead of Warning, logged twice, or the right secret logged as wrong: killed by the exact-list and level assertions in *A_wrong_secret_re_prompts…*, *…waits_its_turn_in_order_of_arrival* and *A_burst…*. - The refusal cap removed, made inclusive, doubled, on the system clock, with its time kept only the first time or set a delay behind, its count not incremented, not reset or logged as 0: all killed by *A_flood_of_refusals_is_logged_once_per_failure_delay_counting_the_rest*. - `'!'` or `'~'` excluded from the return URL: killed by the `/stories?q=!~` case. - ASCII in place of UTF-8 in the comparison hash, in the fingerprint or in the candidate: killed by *A_secret_beyond_ascii_is_compared_and_fingerprinted_by_its_utf8_bytes*. - `ForwardLimit` set to null or 2: killed by the two-entry `X-Forwarded-For` in *A_wrong_secret_re_prompts…*. - `Retry-After` ×5 and the refusal naming the local IP: killed by *A_burst…*. - The right secret also waiting out the delay, and a turn never released (`using` dropped): both now fail named tests within the 10 s bounds (63 s and 185 s for the whole filtered run). Before, they hung the run. - One mutant does not compile: with `_refusalLogged` never assigned, CS0649 is an error. - Two found by the review are equivalent: the sign-in's `RedirectUri` set to the base href (the page's `NavigateTo` overwrites `Location`), and `?? ""` in place of `IssuedWith`'s null check. - **Independent review** (two reviewers; one of them probed 58 mutants). - Nine survivors and six major findings, all fixed with tests: the log level was not pinned; warnings were checked with `Contains`, so a doubled line or a right secret logged as wrong went unnoticed; a queued hang-up was untested; the ends of visible ASCII were untested; UTF-8 was not pinned; `ForwardLimit` was implicit, so a client could choose the logged address. - Minor findings, fixed: - A flood of refusals could fill the disk, so refusals are now logged at most once per delay. - The auth cookie's comment and ADR 0007 said the app cannot know the browser-facing scheme. Both now give the real reason for `Always`. - ADR 0007 said nginx sets every forwarded header itself, but it appends to `X-Forwarded-For`. The ADR now says so. - Two probes hung the run instead of failing it; they are now bounded by 10 s timeouts. - Nits, fixed: `GateClock` states its one-timer assumption; the backup scheduler's removal fails with a named message; ADR 0007's amendment names `nosniff` and `no-store`. - Declined: a longer `Retry-After`. One second is when a place in the queue frees up; the comment now says it is a hint. - Left as a follow-up: ADR 0008's stale "csproj-first layering" clause. It is a wording fix, and this arc amends ADRs only where behaviour changes. - **Browser pass** (implementer, headless Chromium, the app on a free local port): - A wrong secret took 1119 ms. - A burst of six gave one `429` with `Retry-After: 1` and five 200s, with the warnings in the log. - The right secret landed on `/settings` with a Secure, SameSite=Lax, 14-day cookie. - The cookie survived a restart with the same secret. - After a restart with a new secret, the old cookie was sent to `/gate?ReturnUrl=%2F`, and signing in with the new secret worked. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(gate): the access gate holds — a rotated secret revokes its cookies, one secret check at a time, a loopback-only port
Some checks failed
CI / test (pull_request) Has been cancelled
CI / editor-browsers (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
72bc41c62e
- the cookie names the secret that issued it (an HMAC fingerprint under a fixed label, never the
  secret); OnValidatePrincipal refuses and clears another secret's cookie and one from before the
  fingerprint, so rotating TSUMUGI_ACCESS_TOKEN revokes what the old secret issued, as ADR 0007
  already promised
- GateThrottle: one secret check at a time, app-wide, the comparison inside the turn; a mismatch
  holds the turn for the failure delay on the host's TimeProvider, and a client that hangs up does
  not cut it short; four may wait in order of arrival, and one that hangs up while waiting leaves the
  queue unchecked; one beyond them gets 429 + Retry-After before anything is compared; each failure
  is logged at Warning with the client's address, the refusals at most once per delay with a count
  of the ones held back, so a flood cannot fill the host's disk
- the forwarded headers read only the rightmost X-Forwarded-For entry (ForwardLimit 1, the default,
  spelled out): nginx appends the address it saw to a client's own, and the gate logs that one
- the return URL must be visible ASCII too: the right secret with a non-ASCII or control character
  in ReturnUrl was a 500 on Kestrel; the sign-in passes the sanitized URL as its RedirectUri, so the
  cookie handler never follows the raw query value itself
- the antiforgery cookie is Secure where the browser-facing request is HTTPS (SameAsRequest, not
  Always, under which antiforgery refuses plain HTTP outright); every response carries nosniff, set
  as the headers go out so the error page keeps it; the snapshot download is no-store
- compose publishes 127.0.0.1:8088 only and nginx overwrites X-Forwarded-Host, so the proxy really
  is the only route in; the Program.cs comment says why
- tests: GatedAppFactory gains an opt-in fake clock (the backup scheduler left out, by name), a
  configurable secret and a shared data root, a failing path, a recorder of warnings with their
  level, and a ten-second bound on its clients' requests; AccessGateTests pin the delay, the queue
  and its order, both hang-ups, the burst and the refusal log's cap, every throttle log line by
  level and exact list, the rightmost forwarded address, the 14-day sliding cookie, SameSite and
  persistence, rotation across restarts, a minted pre-fingerprint cookie, every return-URL arm and
  both ends of visible ASCII (one of them on real Kestrel), sub-path sign-in and sign-out, nosniff
  and the antiforgery cookie; AccessSecretTests pin the comparison and the fingerprint by value,
  a secret beyond ASCII by its UTF-8 bytes
- ADR 0007 and ADR 0008 amended (PR #TBD); refusing a short secret stays an owner decision

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bjoern referenced this pull request from a commit 2026-09-26 13:28:31 +02:00
docs: gate hardening's PR is #45
All checks were successful
CI / build (pull_request) Successful in 39s
CI / editor-browsers (pull_request) Successful in 39s
CI / test (pull_request) Successful in 1m15s
7c84950ac1
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: 7c84950 · Generated: 2026-09-26 11:33:48 UTC · Revision: #1

Summary

Summary
Generated on: 09/26/2026 - 11:33:48
Coverage date: 09/26/2026 - 11:33:27 - 09/26/2026 - 11:33:45
Parser: MultiReport (6x Cobertura)
Assemblies: 9
Classes: 484
Files: 438
Line coverage: 97.8% (14802 of 15120)
Covered lines: 14802
Uncovered lines: 318
Coverable lines: 15120
Total lines: 32348
Branch coverage: 93.7% (5657 of 6032)
Covered branches: 5657
Total branches: 6032
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.Flow - 99.7%
Name Line Branch
Kagaku.Flow 99.7% 99.2%
Kagaku.Flow.Budgeting.BudgetRequest 100%
Kagaku.Flow.Budgeting.ConsumerBudget 100%
Kagaku.Flow.Execution.NodeExecution 100% 100%
Kagaku.Flow.Execution.NodeOutcome 100% 100%
Kagaku.Flow.Execution.TransientFailureException 100%
Kagaku.Flow.Execution.Upstream 100%
Kagaku.Flow.Extensions.ServiceCollectionExtensions 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.Runs.LedgerChange 100% 100%
Kagaku.Flow.Runs.LedgerTransitions 100% 100%
Kagaku.Flow.Runs.NodeAttempt 100%
Kagaku.Flow.Runs.NodeFacts 100%
Kagaku.Flow.Runs.NodeRun 100%
Kagaku.Flow.Runs.Run 100% 100%
Kagaku.Flow.Runs.RunEvent 100%
Kagaku.Flow.Runs.RunStatuses 100% 100%
Kagaku.Flow.Runs.RunTrigger 100% 100%
Kagaku.Flow.Scheduling.Exclusion 100%
Kagaku.Flow.Scheduling.FlowEngine 100% 100%
Kagaku.Flow.Scheduling.FlowEngineOptions 100% 100%
Kagaku.Flow.Scheduling.ReadyRow 100%
Kagaku.Flow.Scheduling.ReadySet 100% 100%
Kagaku.Flow.Scheduling.ReadySetResult 100%
Kagaku.Flow.Scheduling.RunRequest 100% 100%
Kagaku.Flow.Scheduling.RunStartResult 100%
Kagaku.Flow.Scheduling.SocketValues 100% 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.Catalog.ModelCapabilities 100% 100%
Kagaku.Flow.OpenRouter.Gateway.LlmCompletion 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGateway 100% 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup 100% 100%
Kagaku.Flow.OpenRouter.Gateway.SentRequestCapture 100% 100%
Kagaku.Flow.OpenRouter.Gateway.SentRequestCaptureHandler 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmAnswer 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmConfig 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmConfigRead 100%
Kagaku.Flow.OpenRouter.Llm.LlmDescriptor 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmExecutor 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmFacts 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmFailureText 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmMessage 100%
Kagaku.Flow.OpenRouter.Llm.LlmRequestBuilder 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmRequestPlan 100%
Kagaku.Flow.OpenRouter.Llm.LlmRoutingDefaults 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmSampling 100%
Kagaku.Flow.OpenRouter.Llm.LlmSchemaCheck 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmStrictSchemaRule 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmVocabulary 100% 100%
Kagaku.Flow.OpenRouter.LlmNodeServiceCollectionExtensions 100%
Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions 100% 100%
Kagaku.Flow.Testing - 100%
Name Line Branch
Kagaku.Flow.Testing 100% 100%
Kagaku.Flow.Testing.FlowEngineDriver 100% 100%
Kagaku.Flow.Testing.InMemoryRunLedger 100% 100%
Kagaku.Flow.Testing.RecordingRunEventSink 100%
Kagaku.Flow.Testing.RunLedgerContract 100% 100%
Kagaku.Flow.Testing.RunLedgerContractException 100%
Tsumugi.BlazorAdapter - 95.6%
Name Line Branch
Tsumugi.BlazorAdapter 95.6% 90.5%
Tsumugi.BlazorAdapter.BlazorAdapterAssembly 100%
Tsumugi.BlazorAdapter.Documents.CategoryCreated 100%
Tsumugi.BlazorAdapter.Documents.CategoryEditor 91.8% 87.2%
Tsumugi.BlazorAdapter.Documents.CategorySaved 100%
Tsumugi.BlazorAdapter.Documents.ChangeCategoryFieldsRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryChoice 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog 100% 94.1%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentCreated 100%
Tsumugi.BlazorAdapter.Documents.DocumentEditor 95.1% 88.8%
Tsumugi.BlazorAdapter.Documents.DocumentImageRefused 100%
Tsumugi.BlazorAdapter.Documents.DocumentImageSet 100%
Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentSaved 100%
Tsumugi.BlazorAdapter.Documents.DocumentsEffects 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsList 98.2% 92.4%
Tsumugi.BlazorAdapter.Documents.DocumentsLoaded 100%
Tsumugi.BlazorAdapter.Documents.DocumentsPage 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsReducers 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsRefusal 100%
Tsumugi.BlazorAdapter.Documents.DocumentsState 100% 100%
Tsumugi.BlazorAdapter.Documents.FieldInput 100% 96.6%
Tsumugi.BlazorAdapter.Documents.FieldsChanges 100% 100%
Tsumugi.BlazorAdapter.Documents.FieldShapes 90% 87.5%
Tsumugi.BlazorAdapter.Documents.ImageUpload 100%
Tsumugi.BlazorAdapter.Documents.ImageUploadRefusal 100%
Tsumugi.BlazorAdapter.Documents.ImpactText 100% 94.2%
Tsumugi.BlazorAdapter.Documents.LoadDocuments 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentChoice 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentDialog 95.6% 71.4%
Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested 100%
Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested 100%
Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed 100%
Tsumugi.BlazorAdapter.Documents.SetDocumentImageRequested 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.ModelCatalogLoaded 100%
Tsumugi.BlazorAdapter.Settings.ModelCatalogLoadFailed 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.AckWire 100%
Tsumugi.BlazorAdapter.Workspace.AutoSaveFailed 100%
Tsumugi.BlazorAdapter.Workspace.ChangedWire 100%
Tsumugi.BlazorAdapter.Workspace.ChapterHeader 100% 86.3%
Tsumugi.BlazorAdapter.Workspace.ChapterMenu 100% 81.2%
Tsumugi.BlazorAdapter.Workspace.ChapterPage 100% 80%
Tsumugi.BlazorAdapter.Workspace.ChapterSaved 100%
Tsumugi.BlazorAdapter.Workspace.ChaptersPage 90% 83.3%
Tsumugi.BlazorAdapter.Workspace.ChapterWire 100%
Tsumugi.BlazorAdapter.Workspace.ChapterWritten 100%
Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.CreatedWire 100%
Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeltaWire 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.LoadHistory 100%
Tsumugi.BlazorAdapter.Workspace.LoadWorkspace 100%
Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.ManuscriptEvents 81.3% 67.1%
Tsumugi.BlazorAdapter.Workspace.ManuscriptSurface 91% 84.9%
Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.MergeEvent 100%
Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested 100%
Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.ReshapeEvent 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.SegmentTextEvent 100%
Tsumugi.BlazorAdapter.Workspace.SegmentWire 100% 81.8%
Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested 100%
Tsumugi.BlazorAdapter.Workspace.SplitEvent 100%
Tsumugi.BlazorAdapter.Workspace.StampWire 0%
Tsumugi.BlazorAdapter.Workspace.StoryDetails 100% 85.2%
Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage 100% 100%
Tsumugi.BlazorAdapter.Workspace.StorySaved 100%
Tsumugi.BlazorAdapter.Workspace.StoryShell 98.2% 97.5%
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 - 96.1%
Name Line Branch
Tsumugi.Domain 96.1% 94.6%
System.Text.RegularExpressions.Generated 90.7% 87.5%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2
97.4% 95.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0
91% 91.1%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1
86.4% 77.7%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4
91.9% 88.8%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25
BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3
90.9% 84.3%
Tsumugi.Domain.Documents.CategoryFields 94.5% 93.7%
Tsumugi.Domain.Documents.ChoiceOption 100%
Tsumugi.Domain.Documents.ChoiceValue 100%
Tsumugi.Domain.Documents.DateText 100%
Tsumugi.Domain.Documents.DateValue 100%
Tsumugi.Domain.Documents.Document 100% 100%
Tsumugi.Domain.Documents.DocumentCategory 100% 100%
Tsumugi.Domain.Documents.DocumentValues 100%
Tsumugi.Domain.Documents.FieldCarry 100% 100%
Tsumugi.Domain.Documents.FieldConversion 100% 100%
Tsumugi.Domain.Documents.FieldDefinition 97% 95.3%
Tsumugi.Domain.Documents.FieldMessages 100% 100%
Tsumugi.Domain.Documents.FieldNames 100%
Tsumugi.Domain.Documents.FieldsChange 100%
Tsumugi.Domain.Documents.FieldShape 95.2% 87.5%
Tsumugi.Domain.Documents.FieldsPlan 100%
Tsumugi.Domain.Documents.FieldsPlanner 99.2% 98.5%
Tsumugi.Domain.Documents.FieldValue 100%
Tsumugi.Domain.Documents.FieldValues 95.4% 97.7%
Tsumugi.Domain.Documents.ImageFiles 100% 100%
Tsumugi.Domain.Documents.ImageValue 100%
Tsumugi.Domain.Documents.MovePlan 100%
Tsumugi.Domain.Documents.NumberParse 100%
Tsumugi.Domain.Documents.NumberText 100% 97.6%
Tsumugi.Domain.Documents.NumberValue 100%
Tsumugi.Domain.Documents.StoryImage 100%
Tsumugi.Domain.Documents.TextValue 100% 100%
Tsumugi.Domain.Documents.ValueChange 100%
Tsumugi.Domain.Documents.ValueChangeKey 100%
Tsumugi.Domain.Documents.ValueImpact 100% 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.InlineMarkup 100% 100%
Tsumugi.Domain.Stories.MarkupRun 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.9%
Name Line Branch
Tsumugi.Infrastructure 97.9% 85.9%
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 96.5% 83.3%
Tsumugi.Infrastructure.Backups.BackupWriter 98.5% 92.5%
Tsumugi.Infrastructure.Backups.DatabaseFingerprint 91.9% 97%
Tsumugi.Infrastructure.Backups.FileSystemBackupStore 100% 100%
Tsumugi.Infrastructure.Backups.PreMigrationBackup 89.1% 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.StoryImageConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio
n
100%
Tsumugi.Infrastructure.Persistence.Converters.CanonicalJsonComparer 100%
Tsumugi.Infrastructure.Persistence.Converters.CategoryFieldsConverter 97.6% 94.4%
Tsumugi.Infrastructure.Persistence.Converters.FieldValuesConverter 96.9% 93.7%
Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter 100% 90%
Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Tsumugi.Infrastructure.Persistence.Converters.WireFormat 85.7% 73.8%
Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings 99.2%
Tsumugi.Infrastructure.Persistence.Migrations.AddJournal 98.8%
Tsumugi.Infrastructure.Persistence.Migrations.CategoryFields 100% 100%
Tsumugi.Infrastructure.Persistence.Migrations.CategoryFieldsSql 100%
Tsumugi.Infrastructure.Persistence.Migrations.DropDocumentBody 100%
Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments 100%
Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain 97.3%
Tsumugi.Infrastructure.Persistence.Migrations.SegmentTextMarkup 100% 100%
Tsumugi.Infrastructure.Persistence.Migrations.SegmentTextMarkupSql 100%
Tsumugi.Infrastructure.Persistence.Migrations.StoryImages 100%
Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot 100%
Tsumugi.Infrastructure.Persistence.SqliteFunctions 80% 66.6%
Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore 98.3% 75%
Tsumugi.Infrastructure.Persistence.Stores.EfImageStore 100%
Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore 96.6% 84.6%
Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore 98.7% 90%
Tsumugi.Infrastructure.Persistence.TsumugiDbContext 85.5% 89.2%
Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory 100%
Tsumugi.Infrastructure.Provider.LlmDefaultsSource 100%
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% 92.1%
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% 87.5%
Tsumugi.Server.Security.AccessSecret 100% 100%
Tsumugi.Server.Security.GateThrottle 100% 100%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 98.3%
Name Line Branch
Tsumugi.UseCases 98.3% 94.6%
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.GetChapterSegments 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 98.2% 71.4%
Tsumugi.UseCases.Diagnostics.SeedPictures 100%
Tsumugi.UseCases.Documents.ChangeCategoryFields 100% 83.3%
Tsumugi.UseCases.Documents.CreateCategory 100% 100%
Tsumugi.UseCases.Documents.CreateDocument 100% 100%
Tsumugi.UseCases.Documents.DefaultFields 100% 100%
Tsumugi.UseCases.Documents.DeleteCategory 100% 94.4%
Tsumugi.UseCases.Documents.DeleteDocument 100% 100%
Tsumugi.UseCases.Documents.DocumentCategoryDto 100%
Tsumugi.UseCases.Documents.DocumentCategoryProfile 100%
Tsumugi.UseCases.Documents.DocumentDto 100%
Tsumugi.UseCases.Documents.DocumentPatch 100%
Tsumugi.UseCases.Documents.DocumentProfile 100%
Tsumugi.UseCases.Documents.DocumentsCategory 100%
Tsumugi.UseCases.Documents.DocumentsView 100%
Tsumugi.UseCases.Documents.DocumentWrites 100% 100%
Tsumugi.UseCases.Documents.FieldsChangeLabel 95.4% 88.2%
Tsumugi.UseCases.Documents.GetDocuments 100% 100%
Tsumugi.UseCases.Documents.GetStoryImage 100% 100%
Tsumugi.UseCases.Documents.ImpactPreview 100% 100%
Tsumugi.UseCases.Documents.LossMessages 92.3% 87.5%
Tsumugi.UseCases.Documents.MoveDocuments 100% 100%
Tsumugi.UseCases.Documents.ReorderCategories 100% 100%
Tsumugi.UseCases.Documents.SetDocumentImage 100% 93.7%
Tsumugi.UseCases.Documents.StoryImageFile 100%
Tsumugi.UseCases.Documents.UpdateCategory 100% 100%
Tsumugi.UseCases.Documents.UpdateDocument 100% 81.2%
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.ChangedText 100%
Tsumugi.UseCases.Segments.ChapterDraft 100%
Tsumugi.UseCases.Segments.ChapterWriting 100%
Tsumugi.UseCases.Segments.DeleteSegments 100% 100%
Tsumugi.UseCases.Segments.EditSegment 100% 100%
Tsumugi.UseCases.Segments.InsertSegment 100% 100%
Tsumugi.UseCases.Segments.ManuscriptAnswer 100%
Tsumugi.UseCases.Segments.ManuscriptWrites 99.1% 97.6%
Tsumugi.UseCases.Segments.MergeSegments 100%
Tsumugi.UseCases.Segments.MergeWrite 100%
Tsumugi.UseCases.Segments.ModelOutput 98.2% 96.6%
Tsumugi.UseCases.Segments.ModelParagraph 100%
Tsumugi.UseCases.Segments.MoveSegments 100% 100%
Tsumugi.UseCases.Segments.NewBlock 100%
Tsumugi.UseCases.Segments.ReorderSegments 100% 100%
Tsumugi.UseCases.Segments.ReshapeSegments 100% 100%
Tsumugi.UseCases.Segments.ReshapeWrite 100%
Tsumugi.UseCases.Segments.SegmentDto 100%
Tsumugi.UseCases.Segments.SplitSegment 100%
Tsumugi.UseCases.Segments.SplitWrite 100%
Tsumugi.UseCases.Segments.StampedId 100%
Tsumugi.UseCases.Segments.TextWrite 100%
Tsumugi.UseCases.Settings.CatalogModel 100% 100%
Tsumugi.UseCases.Settings.CheckOpenRouterKey 100%
Tsumugi.UseCases.Settings.GetLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.GetModelCatalog 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.RoutingDefaults 100%
Tsumugi.UseCases.Settings.RoutingSettings 100% 100%
Tsumugi.UseCases.Settings.SaveLlmDefaults 100% 100%
Tsumugi.UseCases.Settings.SaveOpenRouterKey 100% 100%
Tsumugi.UseCases.Stories.ChapterWriteOutcome 50%
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:** `7c84950` · **Generated:** 2026-09-26 11:33:48 UTC · **Revision:** #1 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/26/2026 - 11:33:48 | | Coverage date: | 09/26/2026 - 11:33:27 - 09/26/2026 - 11:33:45 | | Parser: | MultiReport (6x Cobertura) | | Assemblies: | 9 | | Classes: | 484 | | Files: | 438 | | **Line coverage:** | 97.8% (14802 of 15120) | | Covered lines: | 14802 | | Uncovered lines: | 318 | | Coverable lines: | 15120 | | Total lines: | 32348 | | **Branch coverage:** | 93.7% (5657 of 6032) | | Covered branches: | 5657 | | Total branches: | 6032 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.Flow - 99.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow**|**99.7%**|**99.2%**| |Kagaku.Flow.Budgeting.BudgetRequest|100%|| |Kagaku.Flow.Budgeting.ConsumerBudget|100%|| |Kagaku.Flow.Execution.NodeExecution|100%|100%| |Kagaku.Flow.Execution.NodeOutcome|100%|100%| |Kagaku.Flow.Execution.TransientFailureException|100%|| |Kagaku.Flow.Execution.Upstream|100%|| |Kagaku.Flow.Extensions.ServiceCollectionExtensions|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.Runs.LedgerChange|100%|100%| |Kagaku.Flow.Runs.LedgerTransitions|100%|100%| |Kagaku.Flow.Runs.NodeAttempt|100%|| |Kagaku.Flow.Runs.NodeFacts|100%|| |Kagaku.Flow.Runs.NodeRun|100%|| |Kagaku.Flow.Runs.Run|100%|100%| |Kagaku.Flow.Runs.RunEvent|100%|| |Kagaku.Flow.Runs.RunStatuses|100%|100%| |Kagaku.Flow.Runs.RunTrigger|100%|100%| |Kagaku.Flow.Scheduling.Exclusion|100%|| |Kagaku.Flow.Scheduling.FlowEngine|100%|100%| |Kagaku.Flow.Scheduling.FlowEngineOptions|100%|100%| |Kagaku.Flow.Scheduling.ReadyRow|100%|| |Kagaku.Flow.Scheduling.ReadySet|100%|100%| |Kagaku.Flow.Scheduling.ReadySetResult|100%|| |Kagaku.Flow.Scheduling.RunRequest|100%|100%| |Kagaku.Flow.Scheduling.RunStartResult|100%|| |Kagaku.Flow.Scheduling.SocketValues|100%|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.Catalog.ModelCapabilities|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.LlmCompletion|100%|| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGateway|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions|100%|| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.SentRequestCapture|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.SentRequestCaptureHandler|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmAnswer|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmConfig|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmConfigRead|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmDescriptor|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmExecutor|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmFacts|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmFailureText|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmMessage|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmRequestBuilder|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmRequestPlan|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmRoutingDefaults|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmSampling|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmSchemaCheck|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmStrictSchemaRule|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmVocabulary|100%|100%| |Kagaku.Flow.OpenRouter.LlmNodeServiceCollectionExtensions|100%|| |Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions|100%|100%| </details> <details><summary>Kagaku.Flow.Testing - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow.Testing**|**100%**|**100%**| |Kagaku.Flow.Testing.FlowEngineDriver|100%|100%| |Kagaku.Flow.Testing.InMemoryRunLedger|100%|100%| |Kagaku.Flow.Testing.RecordingRunEventSink|100%|| |Kagaku.Flow.Testing.RunLedgerContract|100%|100%| |Kagaku.Flow.Testing.RunLedgerContractException|100%|| </details> <details><summary>Tsumugi.BlazorAdapter - 95.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**95.6%**|**90.5%**| |Tsumugi.BlazorAdapter.BlazorAdapterAssembly|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryCreated|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryEditor|91.8%|87.2%| |Tsumugi.BlazorAdapter.Documents.CategorySaved|100%|| |Tsumugi.BlazorAdapter.Documents.ChangeCategoryFieldsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryChoice|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog|100%|94.1%| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentCreated|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentEditor|95.1%|88.8%| |Tsumugi.BlazorAdapter.Documents.DocumentImageRefused|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentImageSet|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentSaved|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsEffects|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsList|98.2%|92.4%| |Tsumugi.BlazorAdapter.Documents.DocumentsLoaded|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsPage|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsReducers|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsRefusal|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsState|100%|100%| |Tsumugi.BlazorAdapter.Documents.FieldInput|100%|96.6%| |Tsumugi.BlazorAdapter.Documents.FieldsChanges|100%|100%| |Tsumugi.BlazorAdapter.Documents.FieldShapes|90%|87.5%| |Tsumugi.BlazorAdapter.Documents.ImageUpload|100%|| |Tsumugi.BlazorAdapter.Documents.ImageUploadRefusal|100%|| |Tsumugi.BlazorAdapter.Documents.ImpactText|100%|94.2%| |Tsumugi.BlazorAdapter.Documents.LoadDocuments|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentChoice|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentDialog|95.6%|71.4%| |Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested|100%|| |Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed|100%|| |Tsumugi.BlazorAdapter.Documents.SetDocumentImageRequested|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.ModelCatalogLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.ModelCatalogLoadFailed|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.AckWire|100%|| |Tsumugi.BlazorAdapter.Workspace.AutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.ChangedWire|100%|| |Tsumugi.BlazorAdapter.Workspace.ChapterHeader|100%|86.3%| |Tsumugi.BlazorAdapter.Workspace.ChapterMenu|100%|81.2%| |Tsumugi.BlazorAdapter.Workspace.ChapterPage|100%|80%| |Tsumugi.BlazorAdapter.Workspace.ChapterSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.ChaptersPage|90%|83.3%| |Tsumugi.BlazorAdapter.Workspace.ChapterWire|100%|| |Tsumugi.BlazorAdapter.Workspace.ChapterWritten|100%|| |Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.CreatedWire|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeltaWire|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.LoadHistory|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadWorkspace|100%|| |Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.ManuscriptEvents|81.3%|67.1%| |Tsumugi.BlazorAdapter.Workspace.ManuscriptSurface|91%|84.9%| |Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MergeEvent|100%|| |Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.ReshapeEvent|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.SegmentTextEvent|100%|| |Tsumugi.BlazorAdapter.Workspace.SegmentWire|100%|81.8%| |Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SplitEvent|100%|| |Tsumugi.BlazorAdapter.Workspace.StampWire|0%|| |Tsumugi.BlazorAdapter.Workspace.StoryDetails|100%|85.2%| |Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage|100%|100%| |Tsumugi.BlazorAdapter.Workspace.StorySaved|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryShell|98.2%|97.5%| |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 - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Domain**|**96.1%**|**94.6%**| |System.Text.RegularExpressions.Generated|90.7%|87.5%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__DigitsAndSeparators_2|97.4%|95.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__LineBreak_0|91%|91.1%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__NumberToken_1|86.4%|77.7%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__Plain_4|91.9%|88.8%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>F5BB471BFAD87E25<br/>BE2A118445F5EE9A5FB1F861C394AC933A547F31011ADF02F__ThousandsGroup_3|90.9%|84.3%| |Tsumugi.Domain.Documents.CategoryFields|94.5%|93.7%| |Tsumugi.Domain.Documents.ChoiceOption|100%|| |Tsumugi.Domain.Documents.ChoiceValue|100%|| |Tsumugi.Domain.Documents.DateText|100%|| |Tsumugi.Domain.Documents.DateValue|100%|| |Tsumugi.Domain.Documents.Document|100%|100%| |Tsumugi.Domain.Documents.DocumentCategory|100%|100%| |Tsumugi.Domain.Documents.DocumentValues|100%|| |Tsumugi.Domain.Documents.FieldCarry|100%|100%| |Tsumugi.Domain.Documents.FieldConversion|100%|100%| |Tsumugi.Domain.Documents.FieldDefinition|97%|95.3%| |Tsumugi.Domain.Documents.FieldMessages|100%|100%| |Tsumugi.Domain.Documents.FieldNames|100%|| |Tsumugi.Domain.Documents.FieldsChange|100%|| |Tsumugi.Domain.Documents.FieldShape|95.2%|87.5%| |Tsumugi.Domain.Documents.FieldsPlan|100%|| |Tsumugi.Domain.Documents.FieldsPlanner|99.2%|98.5%| |Tsumugi.Domain.Documents.FieldValue|100%|| |Tsumugi.Domain.Documents.FieldValues|95.4%|97.7%| |Tsumugi.Domain.Documents.ImageFiles|100%|100%| |Tsumugi.Domain.Documents.ImageValue|100%|| |Tsumugi.Domain.Documents.MovePlan|100%|| |Tsumugi.Domain.Documents.NumberParse|100%|| |Tsumugi.Domain.Documents.NumberText|100%|97.6%| |Tsumugi.Domain.Documents.NumberValue|100%|| |Tsumugi.Domain.Documents.StoryImage|100%|| |Tsumugi.Domain.Documents.TextValue|100%|100%| |Tsumugi.Domain.Documents.ValueChange|100%|| |Tsumugi.Domain.Documents.ValueChangeKey|100%|| |Tsumugi.Domain.Documents.ValueImpact|100%|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.InlineMarkup|100%|100%| |Tsumugi.Domain.Stories.MarkupRun|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.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**97.9%**|**85.9%**| |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|96.5%|83.3%| |Tsumugi.Infrastructure.Backups.BackupWriter|98.5%|92.5%| |Tsumugi.Infrastructure.Backups.DatabaseFingerprint|91.9%|97%| |Tsumugi.Infrastructure.Backups.FileSystemBackupStore|100%|100%| |Tsumugi.Infrastructure.Backups.PreMigrationBackup|89.1%|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.StoryImageConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio<br/>n|100%|| |Tsumugi.Infrastructure.Persistence.Converters.CanonicalJsonComparer|100%|| |Tsumugi.Infrastructure.Persistence.Converters.CategoryFieldsConverter|97.6%|94.4%| |Tsumugi.Infrastructure.Persistence.Converters.FieldValuesConverter|96.9%|93.7%| |Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter|100%|90%| |Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Tsumugi.Infrastructure.Persistence.Converters.WireFormat|85.7%|73.8%| |Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings|99.2%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddJournal|98.8%|| |Tsumugi.Infrastructure.Persistence.Migrations.CategoryFields|100%|100%| |Tsumugi.Infrastructure.Persistence.Migrations.CategoryFieldsSql|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.DropDocumentBody|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain|97.3%|| |Tsumugi.Infrastructure.Persistence.Migrations.SegmentTextMarkup|100%|100%| |Tsumugi.Infrastructure.Persistence.Migrations.SegmentTextMarkupSql|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.StoryImages|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot|100%|| |Tsumugi.Infrastructure.Persistence.SqliteFunctions|80%|66.6%| |Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore|98.3%|75%| |Tsumugi.Infrastructure.Persistence.Stores.EfImageStore|100%|| |Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore|96.6%|84.6%| |Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore|98.7%|90%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContext|85.5%|89.2%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory|100%|| |Tsumugi.Infrastructure.Provider.LlmDefaultsSource|100%|| |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%**|**92.1%**| |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%|87.5%| |Tsumugi.Server.Security.AccessSecret|100%|100%| |Tsumugi.Server.Security.GateThrottle|100%|100%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 98.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**98.3%**|**94.6%**| |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.GetChapterSegments|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|98.2%|71.4%| |Tsumugi.UseCases.Diagnostics.SeedPictures|100%|| |Tsumugi.UseCases.Documents.ChangeCategoryFields|100%|83.3%| |Tsumugi.UseCases.Documents.CreateCategory|100%|100%| |Tsumugi.UseCases.Documents.CreateDocument|100%|100%| |Tsumugi.UseCases.Documents.DefaultFields|100%|100%| |Tsumugi.UseCases.Documents.DeleteCategory|100%|94.4%| |Tsumugi.UseCases.Documents.DeleteDocument|100%|100%| |Tsumugi.UseCases.Documents.DocumentCategoryDto|100%|| |Tsumugi.UseCases.Documents.DocumentCategoryProfile|100%|| |Tsumugi.UseCases.Documents.DocumentDto|100%|| |Tsumugi.UseCases.Documents.DocumentPatch|100%|| |Tsumugi.UseCases.Documents.DocumentProfile|100%|| |Tsumugi.UseCases.Documents.DocumentsCategory|100%|| |Tsumugi.UseCases.Documents.DocumentsView|100%|| |Tsumugi.UseCases.Documents.DocumentWrites|100%|100%| |Tsumugi.UseCases.Documents.FieldsChangeLabel|95.4%|88.2%| |Tsumugi.UseCases.Documents.GetDocuments|100%|100%| |Tsumugi.UseCases.Documents.GetStoryImage|100%|100%| |Tsumugi.UseCases.Documents.ImpactPreview|100%|100%| |Tsumugi.UseCases.Documents.LossMessages|92.3%|87.5%| |Tsumugi.UseCases.Documents.MoveDocuments|100%|100%| |Tsumugi.UseCases.Documents.ReorderCategories|100%|100%| |Tsumugi.UseCases.Documents.SetDocumentImage|100%|93.7%| |Tsumugi.UseCases.Documents.StoryImageFile|100%|| |Tsumugi.UseCases.Documents.UpdateCategory|100%|100%| |Tsumugi.UseCases.Documents.UpdateDocument|100%|81.2%| |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.ChangedText|100%|| |Tsumugi.UseCases.Segments.ChapterDraft|100%|| |Tsumugi.UseCases.Segments.ChapterWriting|100%|| |Tsumugi.UseCases.Segments.DeleteSegments|100%|100%| |Tsumugi.UseCases.Segments.EditSegment|100%|100%| |Tsumugi.UseCases.Segments.InsertSegment|100%|100%| |Tsumugi.UseCases.Segments.ManuscriptAnswer|100%|| |Tsumugi.UseCases.Segments.ManuscriptWrites|99.1%|97.6%| |Tsumugi.UseCases.Segments.MergeSegments|100%|| |Tsumugi.UseCases.Segments.MergeWrite|100%|| |Tsumugi.UseCases.Segments.ModelOutput|98.2%|96.6%| |Tsumugi.UseCases.Segments.ModelParagraph|100%|| |Tsumugi.UseCases.Segments.MoveSegments|100%|100%| |Tsumugi.UseCases.Segments.NewBlock|100%|| |Tsumugi.UseCases.Segments.ReorderSegments|100%|100%| |Tsumugi.UseCases.Segments.ReshapeSegments|100%|100%| |Tsumugi.UseCases.Segments.ReshapeWrite|100%|| |Tsumugi.UseCases.Segments.SegmentDto|100%|| |Tsumugi.UseCases.Segments.SplitSegment|100%|| |Tsumugi.UseCases.Segments.SplitWrite|100%|| |Tsumugi.UseCases.Segments.StampedId|100%|| |Tsumugi.UseCases.Segments.TextWrite|100%|| |Tsumugi.UseCases.Settings.CatalogModel|100%|100%| |Tsumugi.UseCases.Settings.CheckOpenRouterKey|100%|| |Tsumugi.UseCases.Settings.GetLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.GetModelCatalog|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.RoutingDefaults|100%|| |Tsumugi.UseCases.Settings.RoutingSettings|100%|100%| |Tsumugi.UseCases.Settings.SaveLlmDefaults|100%|100%| |Tsumugi.UseCases.Settings.SaveOpenRouterKey|100%|100%| |Tsumugi.UseCases.Stories.ChapterWriteOutcome|50%|| |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!

Oh? Oh! A gate-hardening PR where the threat model is written down, the timing oracles are closed one by one, and the test clock never sleeps~ This is the kind of candy I swoop down for! I read all 13 files end to end, built it, ran it, and then tried to break it five times. Come, let me show you what I found~ ♡

Verdict: ✅ Looks good to me~

What I verified myself (CI is absent for 7c84950 — the PR is minutes old — so this was all local, at head, in a fresh clone):

  • Build: Release, 0 warnings / 0 errors.
  • Tests: Tsumugi.Integration.Tests 416/416 — digit-exact against your PR body, 20 s, zero flake.
  • Mutation probes — 5/5 killed on exactly their own pins, each restored to a byte-pristine tree (git diff --quiet) with the full suite re-green after:
    1. RefuseAnotherSecretsCookieAsync's condition disabled → both A_rotated_secret_refuses… and A_cookie_from_before_the_fingerprint… go RED. The revocation is real, not decorative~
    2. QueueProcessingOrder.OldestFirst → NewestFirst → A_wrong_secret_holds_the_gate_so_even_the_right_one_waits_its_turn… RED. The FIFO fairness promise is pinned.
    3. Visible-ASCII filter <= '~' → < '~' → exactly the /stories?q=!~ theory row RED, nothing else. Boundary pinned at both ends.
    4. ForwardLimit = 1 → 2 → the wrong-secret test fails on the logged client (198.51.100.1 instead of nginx's 203.0.113.7). A client really cannot choose its own logged address.
    5. nosniff moved off OnStarting onto the eager header dictionary → exactly The_error_page_forbids_content_sniffing_too RED. The OnStarting placement is load-bearing and the test knows it. My favorite kill of the day~ ♪

The design, examined the way I examine everything:

  • The HMAC fingerprint is lovely: keyed by the secret over a fixed label, so it never contains the comparison hash, never names the secret, and rides inside the DataProtection-encrypted ticket — and IssuedWith compares in fixed time over ASCII bytes of a fixed-length Base64. A cookie from before the fingerprint existed names nothing and is refused the same way. Clean.
  • The throttle composition is the right one: the ConcurrencyLimiter holds the turn around the comparison, Task.Delay on the host's TimeProvider with CancellationToken.None (a hang-up can't buy a faster next guess), and abort-awareness only at AcquireAsync (a hang-up while waiting leaves without ever being compared). Each of those three properties has its own named test — the state machine has no dark corners.
  • AccessSecret.Matches dropping its IsNullOrEmpty early-exit in favor of hashing unconditionally is a quiet improvement — the comparison now takes the same path for every candidate shape. I see what you did there~ fufu
  • The refusal-log cap (one line per failure delay, counting the held-back) closes the disk-fill flood exactly where it can actually happen — refusals are the only un-delayed outcome. Correct asymmetry, correctly reasoned.
  • The GateClock waiter semantics (a test advances the clock only once the delay's timer has actually started) is the piece that makes the whole suite deterministic without a single sleep. Ten-second bounds on every wait, so a never-released turn fails a test instead of hanging the run — and your two former hang-mutants now prove it.

💡 Little ideas (non-blocking)~

  1. AccessGateTests.cs, RefusedLine helper — the expected message hardcodes "4 attempts were already waiting" while production interpolates QueueLimit. If the queue depth ever changes, the helper drifts silently in lockstep with whatever the new value logs. Interpolating GateThrottle.QueueLimit into the helper would keep the pin honest. Pure nicety — pin-by-value is a legitimate style and this codebase uses it deliberately.
  2. GateThrottle.cs Queued — GetStatistics()! is safe for the limiter's lifetime but a one-word comment ("non-null while not disposed") would spare the next reader the same two minutes I spent checking. ♡

✅ What I liked~

  • The disclosure discipline: equivalent mutants named, the declined Retry-After given a reason, the loopback bind and the short-secret refusal explicitly left to the owner with ADR pointers instead of smuggled in. That is exactly how decisions should arrive at review~
  • SafeReturnUrl now documents why each arm exists — //evil.example AND /\evil.example AND the visible-ASCII boundary — and the Kestrel-only Location-header refusal gets its own real-Kestrel test rather than a test-server approximation.
  • ADR 0007's amendment corrects its own earlier wrong claims (the "cannot know the scheme" reasoning, "nginx sets every header") instead of leaving them to rot. The truth was amended, not just the code~

The gate holds. Rotate the secret and the old cookies die; bring a burst and it lands one guess per second in arrival order; try to log-spam and the log stays quiet~ Fufu~ merge it when the owner has said their word on the loopback bind~ ♡


Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA (PR minutes old, no bot comment) · Local checks: Release build 0w/0e · Integration 416/416 · 5/5 mutation probes killed & restored

## 🔮 fufu~ Jibril reviewed your code! Oh? *Oh!* A gate-hardening PR where the threat model is written down, the timing oracles are closed one by one, and the test clock never sleeps~ This is the kind of candy I swoop down for! I read all 13 files end to end, built it, ran it, and then tried to break it five times. Come, let me show you what I found~ ♡ ### Verdict: ✅ Looks good to me~ **What I verified myself (CI is absent for `7c84950` — the PR is minutes old — so this was all local, at head, in a fresh clone):** - **Build:** Release, **0 warnings / 0 errors**. - **Tests:** `Tsumugi.Integration.Tests` **416/416** — digit-exact against your PR body, 20 s, zero flake. - **Mutation probes — 5/5 killed on exactly their own pins, each restored to a byte-pristine tree (`git diff --quiet`) with the full suite re-green after:** 1. `RefuseAnotherSecretsCookieAsync`'s condition disabled → **both** `A_rotated_secret_refuses…` and `A_cookie_from_before_the_fingerprint…` go RED. The revocation is real, not decorative~ 2. `QueueProcessingOrder.OldestFirst` → `NewestFirst` → `A_wrong_secret_holds_the_gate_so_even_the_right_one_waits_its_turn…` RED. The FIFO fairness promise is pinned. 3. Visible-ASCII filter `<= '~'` → `< '~'` → exactly the `/stories?q=!~` theory row RED, nothing else. Boundary pinned at both ends. 4. `ForwardLimit = 1` → `2` → the wrong-secret test fails on the logged client (`198.51.100.1` instead of nginx's `203.0.113.7`). A client really cannot choose its own logged address. 5. `nosniff` moved off `OnStarting` onto the eager header dictionary → exactly `The_error_page_forbids_content_sniffing_too` RED. The `OnStarting` placement is load-bearing and the test knows it. My favorite kill of the day~ ♪ **The design, examined the way I examine everything:** - The **HMAC fingerprint** is lovely: keyed by the secret over a fixed label, so it never contains the comparison hash, never names the secret, and rides inside the DataProtection-encrypted ticket — and `IssuedWith` compares in fixed time over ASCII bytes of a fixed-length Base64. A cookie from before the fingerprint existed names nothing and is refused the same way. Clean. - The **throttle composition** is the right one: the `ConcurrencyLimiter` holds the turn *around the comparison*, `Task.Delay` on the host's `TimeProvider` with `CancellationToken.None` (a hang-up can't buy a faster next guess), and abort-awareness only at `AcquireAsync` (a hang-up while *waiting* leaves without ever being compared). Each of those three properties has its own named test — the state machine has no dark corners. - `AccessSecret.Matches` dropping its `IsNullOrEmpty` early-exit in favor of hashing unconditionally is a *quiet* improvement — the comparison now takes the same path for every candidate shape. I see what you did there~ fufu - The refusal-log cap (`one line per failure delay, counting the held-back`) closes the disk-fill flood exactly where it can actually happen — refusals are the only un-delayed outcome. Correct asymmetry, correctly reasoned. - The `GateClock` waiter semantics (a test advances the clock only once the delay's timer has actually *started*) is the piece that makes the whole suite deterministic without a single sleep. Ten-second bounds on every wait, so a never-released turn fails a test instead of hanging the run — and your two former hang-mutants now prove it. #### 💡 Little ideas (non-blocking)~ 1. **AccessGateTests.cs, `RefusedLine` helper** — the expected message hardcodes `"4 attempts were already waiting"` while production interpolates `QueueLimit`. If the queue depth ever changes, the helper drifts silently in lockstep with whatever the new value logs. Interpolating `GateThrottle.QueueLimit` into the helper would keep the pin honest. Pure nicety — pin-by-value is a legitimate style and this codebase uses it deliberately. 2. **GateThrottle.cs `Queued`** — `GetStatistics()!` is safe for the limiter's lifetime but a one-word comment ("non-null while not disposed") would spare the next reader the same two minutes I spent checking. ♡ #### ✅ What I liked~ - The disclosure discipline: equivalent mutants named, the declined `Retry-After` given a *reason*, the loopback bind and the short-secret refusal explicitly left to the owner with ADR pointers instead of smuggled in. That is exactly how decisions should arrive at review~ - `SafeReturnUrl` now documents *why* each arm exists — `//evil.example` AND `/\evil.example` AND the visible-ASCII boundary — and the Kestrel-only `Location`-header refusal gets its own real-Kestrel test rather than a test-server approximation. - ADR 0007's amendment corrects its own earlier wrong claims (the "cannot know the scheme" reasoning, "nginx sets every header") instead of leaving them to rot. The truth was amended, not just the code~ The gate holds. Rotate the secret and the old cookies die; bring a burst and it lands one guess per second in arrival order; try to log-spam and the log stays quiet~ Fufu~ merge it when the owner has said their word on the loopback bind~ ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: absent for head SHA (PR minutes old, no bot comment) · Local checks: Release build 0w/0e · Integration 416/416 · 5/5 mutation probes killed & restored*
bjoern merged commit 01951aa25e into main 2026-09-26 13:42:26 +02:00
bjoern deleted branch fix/gate-hardening 2026-09-26 13:42:26 +02:00
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A gate-hardening PR that treats a one-user app like it matters — because it does! A rotated secret revoking its cookies via an HMAC fingerprint, an app-wide one-at-a-time throttle built on ConcurrencyLimiter, a fake clock so no test ever sleeps, and a refusal log that cannot fill the disk. I read all 1527 lines of the diff and every changed file whole, and the deeper I dug the happier I got~ ♡

Verdict: ✅ Looks good to me~

No blockers. Everything I probed held.

What I verified myself (not just read — ran)

  • Full suite on the pristine head 7c84950: 416/416 pass, twice (fresh build and no-build after my probes), .NET 10.0.401, submodules initialized. Build: 0 errors, 0 warnings.
  • Mutation probe 1 — deleted the visible-ASCII arm from SafeReturnUrl: 5 tests red (the theory arms + the Kestrel one). Restored → green. The Kestrel-rejects-it-in-Location fix is pinned, not decorative.
  • Mutation probe 2 — neutered OnValidatePrincipal (if (false && !secret.IssuedWith(...))): 2 tests red (rotated-secret refusal + pre-fingerprint refusal). Restored → green.
  • Mutation probe 3 — CancellationToken.None → http.RequestAborted in the failure delay: exactly A_client_that_hangs_up_does_not_cut_the_failure_delay_short goes red. That seemingly-tiny token choice is load-bearing and witnessed.
  • Framework subtleties checked, not assumed: the nullable-lifted now - _refusalLogged < FailureDelay in LogRefusal treats a first-ever refusal (null) as loggable — correct lifted semantics; ConcurrencyLimiter fails fast with IsAcquired=false when QueueLimit is reached, which is what makes the 429-before-comparison arm real; UseKestrel/StartServer resolve against Mvc.Testing 10.0.12 (my reflex to call them phantoms was wrong — the build proved it).

✅ What I liked~

  • The throttle design — the delay runs inside the turn on the host's TimeProvider with CancellationToken.None, so a hang-up can't buy a faster next guess. That's the kind of adversarial thinking that makes my wings flutter~
  • IssuedWith pins the cookie to its secret with a fixed-label HMAC keyed by the secret — the secret never rides in the cookie, and pre-fingerprint cookies are refused as "another secret's." The exact-value fingerprint tests (incl. UTF-8 vs ASCII) are chef's kiss.
  • The test oracle discipline: GateClock announces when a timer actually started (never advance blind), every wait bounded at 10 s so a lost turn fails instead of hanging, exact-list log assertions — a doubled line or a right-secret-logged-as-wrong cannot slip through.
  • OnStarting for nosniff — set as headers flush, so the exception handler's header-clear can't strip it. The error-page test proves it.
  • X-Forwarded-Host overwritten, X-Forwarded-For appended-to with ForwardLimit = 1 — the app reads exactly the entry nginx saw. The two-entry XFF test pins it.
  • The ADR amendments tell the truth about their own limitations (short secret not refused; loopback bind assumes the proxy shares the host) instead of hand-waving. Fufu~ honesty in docs is rarer than it should be.

💡 Little ideas (non-blocking)~

  1. Kagura (8086) and Orihon (8087) still publish on all interfaces — now that Tsumugi shows the pattern and the nginx-conf comment explains it, the sibling stacks could adopt the loopback bind in a follow-up. Not this PR's job; just a lovely future habit~
  2. GateThrottle.Queued is a test-only window on a production security class. It's honest, internal, and documented — I'd leave it — but if it ever grows a second test-only member, a testhooks partial or an InternalsVisibleTo accessor class would keep the production surface pristine.

The owner's two flagged decisions (loopback bind as a deployment change; not refusing short secrets) are correctly left open in the PR body — neither hides a code defect.

Run once and finished~ ♪


Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA (no bot comments on the PR) · Local checks: full suite 416/416 ×2 on pristine head; 3 independent mutation probes all red→restored→green

## 🔮 fufu~ Jibril reviewed your code! Oh? *Oh!* A gate-hardening PR that treats a one-user app like it matters — because it does! A rotated secret revoking its cookies via an HMAC fingerprint, an app-wide one-at-a-time throttle built on `ConcurrencyLimiter`, a fake clock so no test ever sleeps, and a refusal log that cannot fill the disk. I read all 1527 lines of the diff and every changed file whole, and the deeper I dug the happier I got~ ♡ ### Verdict: ✅ Looks good to me~ No blockers. Everything I probed held. #### What I verified myself (not just read — ran) - **Full suite on the pristine head 7c84950: 416/416 pass**, twice (fresh build and no-build after my probes), .NET 10.0.401, submodules initialized. Build: 0 errors, 0 warnings. - **Mutation probe 1** — deleted the visible-ASCII arm from `SafeReturnUrl`: **5 tests red** (the theory arms + the Kestrel one). Restored → green. The Kestrel-rejects-it-in-`Location` fix is pinned, not decorative. - **Mutation probe 2** — neutered `OnValidatePrincipal` (`if (false && !secret.IssuedWith(...))`): **2 tests red** (rotated-secret refusal + pre-fingerprint refusal). Restored → green. - **Mutation probe 3** — `CancellationToken.None` → `http.RequestAborted` in the failure delay: **exactly `A_client_that_hangs_up_does_not_cut_the_failure_delay_short` goes red**. That seemingly-tiny token choice is load-bearing and witnessed. - **Framework subtleties checked, not assumed**: the nullable-lifted `now - _refusalLogged < FailureDelay` in `LogRefusal` treats a first-ever refusal (null) as loggable — correct lifted semantics; `ConcurrencyLimiter` fails fast with `IsAcquired=false` when `QueueLimit` is reached, which is what makes the 429-before-comparison arm real; `UseKestrel`/`StartServer` resolve against Mvc.Testing 10.0.12 (my reflex to call them phantoms was wrong — the build proved it). #### ✅ What I liked~ - **The throttle design** — the delay runs *inside* the turn on the host's `TimeProvider` with `CancellationToken.None`, so a hang-up can't buy a faster next guess. That's the kind of adversarial thinking that makes my wings flutter~ - **`IssuedWith` pins the cookie to its secret** with a fixed-label HMAC keyed by the secret — the secret never rides in the cookie, and pre-fingerprint cookies are refused as "another secret's." The exact-value fingerprint tests (incl. UTF-8 vs ASCII) are *chef's kiss*. - **The test oracle discipline**: `GateClock` announces when a timer actually started (never advance blind), every wait bounded at 10 s so a lost turn *fails* instead of hanging, exact-list log assertions — a doubled line or a right-secret-logged-as-wrong cannot slip through. - **`OnStarting` for `nosniff`** — set as headers flush, so the exception handler's header-clear can't strip it. The error-page test proves it. - **`X-Forwarded-Host` overwritten, `X-Forwarded-For` appended-to with `ForwardLimit = 1`** — the app reads exactly the entry nginx saw. The two-entry XFF test pins it. - **The ADR amendments tell the truth about their own limitations** (short secret not refused; loopback bind assumes the proxy shares the host) instead of hand-waving. Fufu~ honesty in docs is rarer than it should be. #### 💡 Little ideas (non-blocking)~ 1. **Kagura (8086) and Orihon (8087) still publish on all interfaces** — now that Tsumugi shows the pattern and the nginx-conf comment explains it, the sibling stacks could adopt the loopback bind in a follow-up. Not this PR's job; just a lovely future habit~ 2. **`GateThrottle.Queued`** is a test-only window on a production security class. It's honest, `internal`, and documented — I'd leave it — but if it ever grows a second test-only member, a `testhooks` partial or an InternalsVisibleTo accessor class would keep the production surface pristine. The owner's two flagged decisions (loopback bind as a deployment change; not refusing short secrets) are correctly left open in the PR body — neither hides a code defect. Run once and finished~ ♪ --- *Automated review by Jibril · 2026-09-26* *CI/CD: absent for head SHA (no bot comments on the PR) · Local checks: full suite 416/416 ×2 on pristine head; 3 independent mutation probes all red→restored→green*
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/Tsumugi!45
No description provided.