feat: the llm node's config and request (Phase 2 · slice 8b) #36

Merged
bjoern merged 5 commits from feat/llm-node-config into main 2026-09-26 03:17:10 +02:00
Member

Slice 8b of Phase 2, the second of three PRs for the llm node (8a, #35: the gateway streams; 8c: the node runs). This PR adds the node type: its config, read and validated; its request, built; its budget answer to the context node; its strict-schema rule; and the routing defaults it inherits. Nothing runs an llm node yet, and nothing registers the descriptor: 8c's executor registers them together, because the NodeRegistry refuses a descriptor without an executor (ADR 0016).

Commits:

  • a242c16 moves the OpenRouter.Net submodule to 15a1f28, the merge of #27 (a request's plugins). The tree is the one 8b was written on, so nothing else changes.
  • 9bbf91c is the feature.
  • 340fdde holds the fixes from the review I ran before opening (round 0, below).
  • f06ffe0 writes this PR's number into ADR 0017, ADR 0018 and the plan.
  • 48064a5 is review round 1: two of Jibril's 💡, a comment and a summary line.

What

Kagaku.Flow.OpenRouter.Llm, public:

  • LlmDescriptor : NodeDescriptor, IBudgetedConsumer. Type id llm, in the Model palette group, with three attempts and a slot of the LLM limit. It gives one required text sink per input the message templates read, a text source, and a json source when a response schema is set. It also has:
    • the config schema a property panel reads;
    • the default config, equal to the playground's llm literal;
    • Templates: each message's content, by pointer;
    • ValidateConfig;
    • GetBudgetAsync: the model's window from the catalog, the reserve, and the overhead rendered with the budgeted sink empty, keyed by the node's model as written.
  • LlmStrictSchemaRule : IGraphRule checks each llm node's response schema as it will be sent, local refs inlined, and reports every problem at its JSON pointer under /responseSchema. A schema the library cannot inline is one finding, in the library's words.
  • The routing vocabulary:
    • LlmRouting (balanced, nitro, floor, exacto) and LlmServiceTier (auto, default, flex, priority);
    • LlmVocabulary, the node's exact spellings with TryParse/Parse/ToWire;
    • LlmRoutingDefaults(Routing, ServiceTier, ZeroDataRetention), with OpenRouter as OpenRouter's own choices;
    • ILlmDefaultsSource, the host's port, read on every execution and never cached.

Internal:

  • LlmConfig.Read reads every setting, and reports each problem at its JSON pointer with a sentence a writer can act on: settings in order, document order within a setting, unknown keys last. Agent mode's keys are refused by name until Phase 4.
  • LlmRequestBuilder.Build is pure. Given the config, the rendered messages, the catalog entry, the defaults and the run, it returns the request, the requested model, PromptChars and DetailsJson (what was applied and dropped).
  • LlmFailureText.Detail gives the sentence a failed call becomes. It follows the rule 7a's settings use, so the ledger and Settings agree.

Core: NodeFacts.RequestedModel's summary and README L1 now say it is the model before routing: the ratio's key (decision 9 below).

Tsumugi:

  • LlmDefaultsSource is a singleton that opens a scope per read and maps the Defaults tab's stored spellings through the package's parser. DependencyInjection registers it.
  • Tests: LlmDefaultsSourceTests, plus one fact in ProviderCompositionTests (the source is a singleton over the scoped settings).

Dependency: JsonSchema.Net 8.0.5, pinned, a reference of Kagaku.Flow.OpenRouter only. The config schema's agreement with ValidateConfig is checked with it here; 8c validates responses with it.

Why

  • ADR 0015 and 0016: a new node type needs its descriptor, its sockets and its templates.
  • ADR 0017: it answers the context node's budget question. ADR 0018: its config, routing, structured output and caching.
  • ADR 0021: its strict-schema rule.
  • 8a's live checks settled two open questions: the cache marker's form (D-27) and what a context-length rejection looks like (D-6).
  • Your decision on #35 (comment): every request turns OpenRouter's context compression off.

Decisions to confirm (brief §13)

# Decision ADR sentence it amends
4 The cache marker is message-level, once, on the last stable message sent. Your live pair on #35 read the message-level marker back from Anthropic's cache, so the library needs no multipart system content. 0018's caching passage (Status line and body)
6 JsonSchema.Net 8.0.5, its last MIT release. 9.x ships under the "Open Source Maintenance Fee" EULA, which needs your acceptance. The cost is no fixes after 8.0.5, so NuGetAudit is the alarm. The alternatives: accept the EULA and take 9.4.0; Corvus.JsonSchema (Apache-2.0, code generation); or a hand-written validator for the strict subset. 0018: "JsonSchema.Net (pinned to 8.0.5 …)"
7 require_parameters and the inlined schema. With a schema, the request carries provider.require_parameters: true unless the node sets it, so no provider or fallback that ignores response_format is picked. The schema goes out inlined, so what the rule checked is what is sent. 0018: "forwarded as-is"
8 A model id carries no routing suffix. Routing is its own setting and applies to the fallbacks too. Catalog variants (:free) and aliases stay allowed. 0018 Status line
9 NodeFacts.RequestedModel is the node's model before routing. The budget is asked before the llm runs and cannot know the routing, and a model's characters per token do not depend on the provider's sort. An alias stays an alias, so the ratio is per alias; you may prefer it keyed by the alias's target. 6a's summary and README L1
12 Messages that render empty are left out, and there is one marker. A template may empty a message on purpose, and Anthropic refuses an empty text block. One breakpoint caches everything before it. 0018 body
13 Strict is always requested with a schema. ADR 0018 has no switch for it; a per-node strict would be additive. —
21 The default template reads inputs.context, so the registry's llm entry equals the playground's literal and slice 9's switch to the registry is invisible. —
22 Derived sinks are required text sinks. Conversions stay a transform's job, and an unwired {{ inputs.x }} is a validation error, not an empty render. 0015: "one sink per template variable" (no type named)
23 The reserve without max_tokens: 4,096 tokens, or the catalog's smaller output cap. 0017 (amended here, round 0)
24 The overhead and PromptChars count the inlined schema, as providers do. Leaving it out would overfill the window and read the ratio low. 0017 (amended here, round 0)
28 Provider preferences are read, not forwarded blind. An unknown top-level member is refused by name, and the library's Validate() runs. 0018: "forwarded as-is"
42 :online is refused with its own sentence. It is OpenRouter's deprecated web search, and web search is a Phase 4 tool. —
— No silent compression. Every request sends plugins: [{"id":"context-compression","enabled":false}] (OpenRouter.Net #27). A prompt that does not fit is refused, a permanent failure, instead of being cut from its middle. 0018: the "No silent compression" bullet (new)

ADR 0018 and ADR 0017 each get a Status line, Amended 2026-09-26 (PR #36, slice 8b), and docs/PROJECT_PLAN.md names this PR on slice 8's line.

Review before opening (round 0)

Before opening, I ran a review workflow: four reviewers (correctness, tests, docs, library and host), each followed by a skeptic who tried to refute every point. They raised 40 points. The skeptics confirmed or partly confirmed all 40, most at a lower severity. I took 33 in 340fdde (two of them, C2 and C14, in 9bbf91c's reworded message). The other 7 are the library follow-up below and one decline. A second pass then checked every fix: all 34 are resolved, and each test fix fails on its defect when the defect is put back. It also reviewed the fix commit itself and raised 8 new points (one suggestion, seven nits), all confirmed or partly confirmed. All 8 are taken in 340fdde:

  • SchemaText's summary no longer claims every CJK character counts once. A character outside the Basic Multilingual Plane, such as an emoji or 𠮷, still counts as its 12-character escape, which the relaxed encoder keeps.
  • ADR 0017 counts the schema "inlined, compact and unescaped" instead of "as sent", since the body sent carries the escapes.
  • Smaller fixes:
    • a test comment's escaped length is corrected (194);
    • the tier ToWire summary no longer says the four words are not OpenRouter values;
    • the 200-character theory carries its expected sentence as data;
    • the stable-run theory is renamed to what it pins;
    • the plan's "a fourth" names its list;
    • one ADR 0018 bullet is reflowed.

Blockers:

  • B1: the row meant to show that an omitted message adds nothing to PromptChars rendered it as "", so it could not fail. It now renders whitespace.
  • C3/A5: a message's stable-run problems were reported after every other message's problems, breaking the promised document order. A message whose role did not read was also told "Only a system message can be stable". Problems are now in document order, and a role that did not read is reported once.
  • C4: ADR 0018 said three library PRs gate Phase 2. #27 is the fourth, and multipart system content was not needed.

Behaviour:

  • A1: PromptChars and the budget's overhead counted the schema's JSON escapes: an apostrophe or a CJK character counted as its six-character escape, not as one character, and a config's whitespace counted too. Both now count the schema compact and written with the relaxed encoder, through one helper. A character outside the Basic Multilingual Plane still counts as its escape, which that encoder keeps.
  • A6: the details' zeroDataRetention now says whether the request asks for it, including when the node set zdr: true itself.
  • A7: the sampling-range sentence is formatted in the invariant culture. This has no test: the repository sets InvariantGlobalization, so no other culture can be loaded.

Tests (27 new rows and facts; each kills the mutant its reviewer found, below):

  • B2: the budgeted sink is rendered empty, whether it is given a value or has a ?? fallback. This also disproves the brief's claim that "render the slot absent" is an equivalent mutant.
  • B3, B4, B5:
    • every effort word maps to its own effort;
    • the cache marker's index is pinned, and a second details literal covers inherited values;
    • max_tokens above the default and the cap is the reserve.
  • B6–B9:
    • the alias's own window (the fixture's alias now differs from its target);
    • the strict-schema tests keep the core's rule-failed, so a rule that throws fails them;
    • a skipped message keeps its index in the pointers;
    • unknown keys are reported in document order.
  • B10–B14:
    • the failure-text theory is split, so no expected value is dead, and it gains a 200-character row and a carriage-return row;
    • the catalog is not loaded for a node that is not configured;
    • two "copied from a run" comments;
    • validateScopes: true.
  • Coverage: non-string routing and tier rows for Choice's other branch.

Docs:

  • C5: an ADR 0017 amendment for decisions 23 and 24.
  • C6: ADR 0018 says the schema is checked and sent inlined.
  • D3's ADR part: an account's plugin settings can force compression on over a request's choice.
  • Summaries (C7–C12, B12, D5), and the plan names #27 and the 15a1f28 bump (C1/D1).

For a library follow-up (OpenRouter.Net), not worked around here:

  • A2: a misspelt member inside max_price, sort or a percentile object is dropped silently, and so is a max_price string that is not a number ("$0.50"). That can drop a spending ceiling. 8b refuses unknown top-level preference names; the library's converters return null where they should throw.
  • A3: preferred_max_latency: 1e400 validates, then fails at serialization. An undefined enum digit (data_collection: "99") and a null entry in order go on the wire.
  • A4: a response schema made of long chains of local refs overflows the stack in InlineLocalRefs, before the library's depth check runs.
  • D2–D4: RequestPlugin.Enabled = null is documented as keeping OpenRouter's default, but a listed plugin runs. The README's compression sentence leaves out account defaults, and its list of refusals leaves out a setting that holds no JSON.

I'm opening that PR next. Nothing in 8b runs a request, so the bump and the ValidateConfig rows it enables land in 8c, or in a review round here if the library merges first.

Declined, for your call:

  • C13: the core's test data still uses routing variants as RequestedModel samples (vendor/model:floor, asked:nitro). The ledger stores the string opaquely and the core is otherwise untouched here, so I left them. I can change them in 8c, which touches the core anyway.

Out of scope (brief §8)

  • The executor, its registration beside the descriptor, the engine run and ADR 0016's settle rules, and the package README (8c).
  • The palette reading the registry (slice 9).
  • Response validation with JsonSchema.Net: the dependency lands here, 8c uses it.
  • The library follow-up above.

Tests

Counted from dotnet test (facts plus InlineData rows). The base is main at 5d625d2.

Project main this PR
Kagaku.Flow.OpenRouter.Tests 152 430 (427 run, the 3 live checks skipped)
Tsumugi.Integration.Tests 326 342
Kagaku.Flow.Tests 1103 1103
Tsumugi.BlazorAdapter.Tests 351 351
Tsumugi.Domain.Tests 457 457
Tsumugi.UseCases.Tests 155 155
Total 2544 2838 (2835 passed, 3 skipped)

New in the package (278; 251 before round 0):

  • LlmConfigValidationTests: 92 (LC1–LC12, the stable prefix).
  • LlmRequestBuilderTests: 64 (LQ, LD, LO10).
  • LlmConfigSchemaTests: 42 (the schema agrees with ValidateConfig, checked with JsonSchema.Net).
  • LlmBudgetTests: 27 (LB1–LB7).
  • LlmRoutingTests: 19 (the vocabulary and the defaults' guards).
  • LlmDescriptorTests: 15 (sockets, templates, the canvas entry).
  • LlmFailureTextTests: 10.
  • LlmStrictSchemaRuleTests: 9 (LS1–LS5).

New in Tsumugi: LlmDefaultsSourceTests (15) and ProviderCompositionTests.The_routing_defaults_source_is_a_singleton_over_the_settings.

Coverage

Coverlet with branch coverage, on the package's tests plus Tsumugi's provider tests. 100% of lines and branches on every file this PR changes:

File Lines Branches
Llm/LlmConfig.cs 326/326 212/212
Llm/LlmRequestBuilder.cs 146/146 66/66
Llm/LlmDescriptor.cs 130/130 44/44
Llm/LlmStrictSchemaRule.cs 33/33 12/12
Llm/LlmVocabulary.cs 28/28 12/12
Llm/LlmFailureText.cs 10/10 14/14
Llm/LlmRoutingDefaults.cs 7/7 4/4
Provider/LlmDefaultsSource.cs (Tsumugi) 9/9 —

Mutation probes

The probes ran on a copy under home, each a single edit, then the package's full suite.

  • The brief's 8b probes (§12: 19–43, 59–67, 74–77) and my own for this PR's additions: 69, all killed. One (28b) first failed to compile, because FormatException has no ParamName; it was rewritten and then killed.
  • Round 0: 23 more, one for each defect a reviewer found, all killed by the new tests. They include B1's counted omission, B2's sink not emptied, the swapped efforts, B4's marker index and inherited tier, A6, B5's clamped reserve, B6's alias read as its target, B7's throwing rule, B8, B9, B10's boundary and carriage return, B11, A1 at both sites, C3's order, and A5's three role checks.

Equivalent mutants: none left for 8b. The brief listed one, "render the budget slot absent instead of """ (LB5), and B2 proved it is not equivalent: a ?? fallback tells the two apart. It is now a killed probe. The brief's other two equivalents are 8a's (LG10) and 8c's (LX8).

Browser pass

8b has no visible change: the palette reads the registry from slice 9. It does register a new singleton over the scoped settings, so I ran brief §16.2's regression pass on the Release build of 48064a5, this PR's head (the page footer shows the build):

  • ASPNETCORE_ENVIRONMENT=Development, so scope validation is on at startup;
  • the gateway pointed at slice 7's local stub (454 models);
  • Node Playwright with system Chrome.

Settings → Defaults: the picker loaded all 454 models. Choosing Claude Haiku 4.5, then Fastest (:nitro), Standard and zero data retention saved each change, and all of them survived a reload. The stored rows read routing = 'nitro', serviceTier = 'default' and zeroDataRetention = 'true', which LlmDefaultsSource maps (its tests prove the mapping). There were no page or console errors. Screenshots were taken light and dark.

Owed

  • Live checks: none for 8b. 8a's settled D-27 and D-6; 8c's engine run has its own.
  • For a run on your checkout: git submodule update --init --recursive after checking out the branch (it needs OpenRouter.Net 15a1f28).

🤖 Generated with Claude Code

Slice 8b of Phase 2, the second of three PRs for the `llm` node (8a, #35: the gateway streams; 8c: the node runs). This PR adds the node type: its config, read and validated; its request, built; its budget answer to the `context` node; its strict-schema rule; and the routing defaults it inherits. Nothing runs an `llm` node yet, and nothing registers the descriptor: 8c's executor registers them together, because the `NodeRegistry` refuses a descriptor without an executor (ADR 0016). **Commits:** - a242c16 moves the OpenRouter.Net submodule to 15a1f28, the merge of #27 (a request's plugins). The tree is the one 8b was written on, so nothing else changes. - 9bbf91c is the feature. - 340fdde holds the fixes from the review I ran before opening (round 0, below). - f06ffe0 writes this PR's number into ADR 0017, ADR 0018 and the plan. - 48064a5 is review round 1: two of Jibril's 💡, a comment and a summary line. ## What **`Kagaku.Flow.OpenRouter.Llm`**, public: - `LlmDescriptor : NodeDescriptor, IBudgetedConsumer`. Type id `llm`, in the **Model** palette group, with three attempts and a slot of the LLM limit. It gives one required `text` sink per input the message templates read, a `text` source, and a `json` source when a response schema is set. It also has: - the config schema a property panel reads; - the default config, equal to the playground's `llm` literal; - `Templates`: each message's content, by pointer; - `ValidateConfig`; - `GetBudgetAsync`: the model's window from the catalog, the reserve, and the overhead rendered with the budgeted sink empty, keyed by the node's model as written. - `LlmStrictSchemaRule : IGraphRule` checks each `llm` node's response schema as it will be sent, local refs inlined, and reports every problem at its JSON pointer under `/responseSchema`. A schema the library cannot inline is one finding, in the library's words. - The routing vocabulary: - `LlmRouting` (balanced, nitro, floor, exacto) and `LlmServiceTier` (auto, default, flex, priority); - `LlmVocabulary`, the node's exact spellings with `TryParse`/`Parse`/`ToWire`; - `LlmRoutingDefaults(Routing, ServiceTier, ZeroDataRetention)`, with `OpenRouter` as OpenRouter's own choices; - `ILlmDefaultsSource`, the host's port, read on every execution and never cached. **Internal:** - `LlmConfig.Read` reads every setting, and reports each problem at its JSON pointer with a sentence a writer can act on: settings in order, document order within a setting, unknown keys last. Agent mode's keys are refused by name until Phase 4. - `LlmRequestBuilder.Build` is pure. Given the config, the rendered messages, the catalog entry, the defaults and the run, it returns the request, the requested model, `PromptChars` and `DetailsJson` (what was applied and dropped). - `LlmFailureText.Detail` gives the sentence a failed call becomes. It follows the rule 7a's settings use, so the ledger and Settings agree. **Core:** `NodeFacts.RequestedModel`'s summary and README L1 now say it is the model before routing: the ratio's key (decision 9 below). **Tsumugi:** - `LlmDefaultsSource` is a singleton that opens a scope per read and maps the Defaults tab's stored spellings through the package's parser. `DependencyInjection` registers it. - Tests: `LlmDefaultsSourceTests`, plus one fact in `ProviderCompositionTests` (the source is a singleton over the scoped settings). **Dependency:** JsonSchema.Net **8.0.5**, pinned, a reference of `Kagaku.Flow.OpenRouter` only. The config schema's agreement with `ValidateConfig` is checked with it here; 8c validates responses with it. ## Why - ADR 0015 and 0016: a new node type needs its descriptor, its sockets and its templates. - ADR 0017: it answers the `context` node's budget question. ADR 0018: its config, routing, structured output and caching. - ADR 0021: its strict-schema rule. - 8a's live checks settled two open questions: the cache marker's form (D-27) and what a context-length rejection looks like (D-6). - Your decision on #35 ([comment](https://git.kagaku.eu/TeamAI/Tsumugi/pulls/35#issuecomment-9746)): every request turns OpenRouter's context compression off. ## Decisions to confirm (brief §13) | # | Decision | ADR sentence it amends | |---|---|---| | 4 | **The cache marker is message-level, once, on the last stable message sent.** Your live pair on #35 read the message-level marker back from Anthropic's cache, so the library needs no multipart system content. | 0018's caching passage (Status line and body) | | 6 | **JsonSchema.Net 8.0.5, its last MIT release.** 9.x ships under the "Open Source Maintenance Fee" EULA, which needs your acceptance. The cost is no fixes after 8.0.5, so NuGetAudit is the alarm. The alternatives: accept the EULA and take 9.4.0; Corvus.JsonSchema (Apache-2.0, code generation); or a hand-written validator for the strict subset. | 0018: "JsonSchema.Net (pinned to 8.0.5 …)" | | 7 | **`require_parameters` and the inlined schema.** With a schema, the request carries `provider.require_parameters: true` unless the node sets it, so no provider or fallback that ignores `response_format` is picked. The schema goes out inlined, so what the rule checked is what is sent. | 0018: "forwarded as-is" | | 8 | **A model id carries no routing suffix.** Routing is its own setting and applies to the fallbacks too. Catalog variants (`:free`) and aliases stay allowed. | 0018 Status line | | 9 | **`NodeFacts.RequestedModel` is the node's model before routing.** The budget is asked before the `llm` runs and cannot know the routing, and a model's characters per token do not depend on the provider's sort. An alias stays an alias, so the ratio is per alias; you may prefer it keyed by the alias's target. | 6a's summary and README L1 | | 12 | **Messages that render empty are left out, and there is one marker.** A template may empty a message on purpose, and Anthropic refuses an empty text block. One breakpoint caches everything before it. | 0018 body | | 13 | **Strict is always requested with a schema.** ADR 0018 has no switch for it; a per-node `strict` would be additive. | — | | 21 | **The default template reads `inputs.context`,** so the registry's `llm` entry equals the playground's literal and slice 9's switch to the registry is invisible. | — | | 22 | **Derived sinks are required `text` sinks.** Conversions stay a `transform`'s job, and an unwired `{{ inputs.x }}` is a validation error, not an empty render. | 0015: "one sink per template variable" (no type named) | | 23 | **The reserve without `max_tokens`:** 4,096 tokens, or the catalog's smaller output cap. | 0017 (amended here, round 0) | | 24 | **The overhead and `PromptChars` count the inlined schema**, as providers do. Leaving it out would overfill the window and read the ratio low. | 0017 (amended here, round 0) | | 28 | **Provider preferences are read, not forwarded blind.** An unknown top-level member is refused by name, and the library's `Validate()` runs. | 0018: "forwarded as-is" | | 42 | **`:online` is refused with its own sentence.** It is OpenRouter's deprecated web search, and web search is a Phase 4 tool. | — | | — | **No silent compression.** Every request sends `plugins: [{"id":"context-compression","enabled":false}]` (OpenRouter.Net #27). A prompt that does not fit is refused, a permanent failure, instead of being cut from its middle. | 0018: the "No silent compression" bullet (new) | ADR 0018 and ADR 0017 each get a Status line, **Amended 2026-09-26 (PR #36, slice 8b)**, and `docs/PROJECT_PLAN.md` names this PR on slice 8's line. ## Review before opening (round 0) Before opening, I ran a review workflow: four reviewers (correctness, tests, docs, library and host), each followed by a skeptic who tried to refute every point. They raised 40 points. The skeptics confirmed or partly confirmed all 40, most at a lower severity. I took 33 in 340fdde (two of them, C2 and C14, in 9bbf91c's reworded message). The other 7 are the library follow-up below and one decline. A second pass then checked every fix: all 34 are resolved, and each test fix fails on its defect when the defect is put back. It also reviewed the fix commit itself and raised 8 new points (one suggestion, seven nits), all confirmed or partly confirmed. All 8 are taken in 340fdde: - `SchemaText`'s summary no longer claims every CJK character counts once. A character outside the Basic Multilingual Plane, such as an emoji or 𠮷, still counts as its 12-character escape, which the relaxed encoder keeps. - ADR 0017 counts the schema "inlined, compact and unescaped" instead of "as sent", since the body sent carries the escapes. - Smaller fixes: - a test comment's escaped length is corrected (194); - the tier `ToWire` summary no longer says the four words are not OpenRouter values; - the 200-character theory carries its expected sentence as data; - the stable-run theory is renamed to what it pins; - the plan's "a fourth" names its list; - one ADR 0018 bullet is reflowed. **Blockers:** - **B1:** the row meant to show that an omitted message adds nothing to `PromptChars` rendered it as `""`, so it could not fail. It now renders whitespace. - **C3/A5:** a message's stable-run problems were reported after every other message's problems, breaking the promised document order. A message whose role did not read was also told "Only a system message can be stable". Problems are now in document order, and a role that did not read is reported once. - **C4:** ADR 0018 said three library PRs gate Phase 2. #27 is the fourth, and multipart system content was not needed. **Behaviour:** - **A1:** `PromptChars` and the budget's overhead counted the schema's JSON *escapes*: an apostrophe or a CJK character counted as its six-character escape, not as one character, and a config's whitespace counted too. Both now count the schema compact and written with the relaxed encoder, through one helper. A character outside the Basic Multilingual Plane still counts as its escape, which that encoder keeps. - **A6:** the details' `zeroDataRetention` now says whether the request asks for it, including when the node set `zdr: true` itself. - **A7:** the sampling-range sentence is formatted in the invariant culture. This has no test: the repository sets `InvariantGlobalization`, so no other culture can be loaded. **Tests** (27 new rows and facts; each kills the mutant its reviewer found, below): - **B2:** the budgeted sink is rendered empty, whether it is given a value or has a `??` fallback. This also disproves the brief's claim that "render the slot absent" is an equivalent mutant. - **B3, B4, B5:** - every effort word maps to its own effort; - the cache marker's index is pinned, and a second details literal covers inherited values; - `max_tokens` above the default and the cap is the reserve. - **B6–B9:** - the alias's own window (the fixture's alias now differs from its target); - the strict-schema tests keep the core's `rule-failed`, so a rule that throws fails them; - a skipped message keeps its index in the pointers; - unknown keys are reported in document order. - **B10–B14:** - the failure-text theory is split, so no expected value is dead, and it gains a 200-character row and a carriage-return row; - the catalog is not loaded for a node that is not configured; - two "copied from a run" comments; - `validateScopes: true`. - **Coverage:** non-string routing and tier rows for `Choice`'s other branch. **Docs:** - **C5:** an ADR 0017 amendment for decisions 23 and 24. - **C6:** ADR 0018 says the schema is checked and sent inlined. - **D3's ADR part:** an account's plugin settings can force compression on over a request's choice. - Summaries (C7–C12, B12, D5), and the plan names #27 and the 15a1f28 bump (C1/D1). **For a library follow-up (OpenRouter.Net), not worked around here:** - **A2:** a misspelt member inside `max_price`, `sort` or a percentile object is dropped silently, and so is a `max_price` string that is not a number (`"$0.50"`). That can drop a spending ceiling. 8b refuses unknown *top-level* preference names; the library's converters return null where they should throw. - **A3:** `preferred_max_latency: 1e400` validates, then fails at serialization. An undefined enum digit (`data_collection: "99"`) and a `null` entry in `order` go on the wire. - **A4:** a response schema made of long chains of local refs overflows the stack in `InlineLocalRefs`, before the library's depth check runs. - **D2–D4:** `RequestPlugin.Enabled = null` is documented as keeping OpenRouter's default, but a listed plugin runs. The README's compression sentence leaves out account defaults, and its list of refusals leaves out a setting that holds no JSON. I'm opening that PR next. Nothing in 8b runs a request, so the bump and the `ValidateConfig` rows it enables land in 8c, or in a review round here if the library merges first. **Declined, for your call:** - **C13:** the core's test data still uses routing variants as `RequestedModel` samples (`vendor/model:floor`, `asked:nitro`). The ledger stores the string opaquely and the core is otherwise untouched here, so I left them. I can change them in 8c, which touches the core anyway. ## Out of scope (brief §8) - The executor, its registration beside the descriptor, the engine run and ADR 0016's settle rules, and the package README (8c). - The palette reading the registry (slice 9). - Response validation with JsonSchema.Net: the dependency lands here, 8c uses it. - The library follow-up above. ## Tests Counted from `dotnet test` (facts plus `InlineData` rows). The base is `main` at 5d625d2. | Project | main | this PR | |---|---:|---:| | Kagaku.Flow.OpenRouter.Tests | 152 | **430** (427 run, the 3 live checks skipped) | | Tsumugi.Integration.Tests | 326 | **342** | | Kagaku.Flow.Tests | 1103 | 1103 | | Tsumugi.BlazorAdapter.Tests | 351 | 351 | | Tsumugi.Domain.Tests | 457 | 457 | | Tsumugi.UseCases.Tests | 155 | 155 | | **Total** | **2544** | **2838** (2835 passed, 3 skipped) | New in the package (278; 251 before round 0): - `LlmConfigValidationTests`: 92 (LC1–LC12, the stable prefix). - `LlmRequestBuilderTests`: 64 (LQ, LD, LO10). - `LlmConfigSchemaTests`: 42 (the schema agrees with `ValidateConfig`, checked with JsonSchema.Net). - `LlmBudgetTests`: 27 (LB1–LB7). - `LlmRoutingTests`: 19 (the vocabulary and the defaults' guards). - `LlmDescriptorTests`: 15 (sockets, templates, the canvas entry). - `LlmFailureTextTests`: 10. - `LlmStrictSchemaRuleTests`: 9 (LS1–LS5). New in Tsumugi: `LlmDefaultsSourceTests` (15) and `ProviderCompositionTests.The_routing_defaults_source_is_a_singleton_over_the_settings`. ## Coverage Coverlet with branch coverage, on the package's tests plus Tsumugi's provider tests. **100% of lines and branches** on every file this PR changes: | File | Lines | Branches | |---|---:|---:| | `Llm/LlmConfig.cs` | 326/326 | 212/212 | | `Llm/LlmRequestBuilder.cs` | 146/146 | 66/66 | | `Llm/LlmDescriptor.cs` | 130/130 | 44/44 | | `Llm/LlmStrictSchemaRule.cs` | 33/33 | 12/12 | | `Llm/LlmVocabulary.cs` | 28/28 | 12/12 | | `Llm/LlmFailureText.cs` | 10/10 | 14/14 | | `Llm/LlmRoutingDefaults.cs` | 7/7 | 4/4 | | `Provider/LlmDefaultsSource.cs` (Tsumugi) | 9/9 | — | ## Mutation probes The probes ran on a copy under home, each a single edit, then the package's full suite. - **The brief's 8b probes (§12: 19–43, 59–67, 74–77) and my own for this PR's additions:** 69, all killed. One (28b) first failed to compile, because `FormatException` has no `ParamName`; it was rewritten and then killed. - **Round 0:** 23 more, one for each defect a reviewer found, all killed by the new tests. They include B1's counted omission, B2's sink not emptied, the swapped efforts, B4's marker index and inherited tier, A6, B5's clamped reserve, B6's alias read as its target, B7's throwing rule, B8, B9, B10's boundary and carriage return, B11, A1 at both sites, C3's order, and A5's three role checks. **Equivalent mutants:** none left for 8b. The brief listed one, "render the budget slot absent instead of `""`" (LB5), and B2 proved it is not equivalent: a `??` fallback tells the two apart. It is now a killed probe. The brief's other two equivalents are 8a's (LG10) and 8c's (LX8). ## Browser pass 8b has no visible change: the palette reads the registry from slice 9. It does register a new singleton over the scoped settings, so I ran brief §16.2's regression pass on the Release build of 48064a5, this PR's head (the page footer shows the build): - `ASPNETCORE_ENVIRONMENT=Development`, so scope validation is on at startup; - the gateway pointed at slice 7's local stub (454 models); - Node Playwright with system Chrome. **Settings → Defaults:** the picker loaded all 454 models. Choosing Claude Haiku 4.5, then Fastest (:nitro), Standard and zero data retention saved each change, and all of them survived a reload. The stored rows read `routing = 'nitro'`, `serviceTier = 'default'` and `zeroDataRetention = 'true'`, which `LlmDefaultsSource` maps (its tests prove the mapping). There were no page or console errors. Screenshots were taken light and dark. ## Owed - **Live checks:** none for 8b. 8a's settled D-27 and D-6; 8c's engine run has its own. - For a run on your checkout: `git submodule update --init --recursive` after checking out the branch (it needs OpenRouter.Net 15a1f28). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Kagaku.Flow.OpenRouter.Llm holds the llm node type's descriptor, its routing
vocabulary and defaults port, its strict-schema rule and the pure request
builder. The executor that runs it comes next.

The config. Every setting is read with a sentence a writer can act on, at its
JSON pointer, in the order of the settings, unknown keys last; agent mode's
keys are refused by name until Phase 4. A model id carries no routing suffix:
routing is its own setting, and applies to the fallbacks too. ':online' has a
sentence of its own. The provider preferences are read by their wire names,
so a misspelt top-level preference is refused, not dropped. The stable prefix is the leading system
messages unless a message is marked. The config schema a property panel reads
agrees with ValidateConfig on every structural rule, checked with
JsonSchema.Net.

The routing precedence. A node's routing and tier win whenever it sets them,
balanced and auto included; otherwise the install-wide defaults apply;
the executor (next) reads them through ILlmDefaultsSource on every
execution, never cached. Zero data
retention from the defaults is merged into every request's provider
preferences, so no node opts out. Tsumugi's LlmDefaultsSource maps the stored
spellings through the package's parser: one table, not two.

The request. An effort and each sampling value go out only where the catalog
says the model takes them. max_tokens goes out whenever the node sets it,
whatever the catalog lists; the run id goes out as session_id, and usage is
always asked for. With a schema, the schema goes out inlined and strict, with
require_parameters unless the node sets it. Messages that render empty
are left out. One cache marker, the message-level form Björn's live pair
confirmed on #35, sits on the last stable message sent. Every request turns
OpenRouter's context compression off (OpenRouter.Net #27), so a prompt that
does not fit a small window is refused rather than cut from its middle. The
details record what was applied and dropped.

The ratio's key. The budget answer and NodeFacts.RequestedModel carry the
node's model as written, before routing. A model's characters per token do
not depend on the provider's sort, and the budget, asked before the llm runs,
cannot know the routing. RequestedModel's summary and README L1 say so. The
reserve without max_tokens is 4,096 tokens or the catalog's smaller output
cap. The overhead counts the inlined schema, as the provider does.

Strict schemas. LlmStrictSchemaRule checks each llm node's schema as it will
be sent, local refs inlined, and names every JSON pointer; a schema that
cannot be inlined is one finding in the library's words.

JsonSchema.Net is pinned to 8.0.5, its last MIT release: 9.x ships under a
maintenance-fee EULA that needs the owner's acceptance.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The config. A message's stable-run problems now join its own problems, after
them, so LC12's document order holds across the messages. A role that did
not read is "", reported once: the stable run no longer judges it. The
sampling range sentence is formatted in the invariant culture.

The schema as sent. The request's PromptChars and the budget's overhead
count the inlined schema compact and written with the relaxed encoder
(LlmRequestBuilder.SchemaText), so an apostrophe or a character of the
Basic Multilingual Plane counts once and a config's whitespace not at all;
a character outside it still counts as its escape, which that encoder keeps. ADR 0017 is amended: the reserve
without max_tokens, and an overhead that carries the schema as sent.

The details. zeroDataRetention says whether the request asks for it,
whoever set it, not only whether the defaults did.

The tests. An omitted message renders whitespace, so the prompt-characters
row can fail. New rows pin the budgeted sink rendered empty (a value given
for it and a ?? fallback), every effort word, the cache marker's index and
a details literal for inherited values, max_tokens above the default and the
cap, an alias's own window, a skipped message's pointer, unknown keys in
document order, the 200-character line and a carriage return, the catalog
not loaded for a node that is not configured, and non-string routing and
tier values. The strict-schema rule's tests keep the core's rule-failed, so
a rule that throws fails them.

The docs. ADR 0018 counts four Phase 2 library PRs (#27 the fourth), says
the schema is checked and sent inlined, and notes that an account's plugin
settings can force compression on. ADR 0017 counts the schema inlined,
compact and unescaped. Summaries: the descriptor renders
templates for its budget, OpenRouter's default routing, ToWire's spellings
are not OpenRouter values, zero data retention is not inherited, the rule
throws for a null context, the fixtures and the composition test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bjoern referenced this pull request from a commit 2026-09-26 02:38:30 +02:00
docs: slice 8b's PR is #36
All checks were successful
CI / build (pull_request) Successful in 35s
CI / test (pull_request) Successful in 1m11s
f06ffe049a
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: 159a2f1 · Generated: 2026-09-26 01:21:50 UTC · Revision: #3

Summary

Summary
Generated on: 09/26/2026 - 01:21:49
Coverage date: 09/26/2026 - 01:21:27 - 09/26/2026 - 01:21:46
Parser: MultiReport (6x Cobertura)
Assemblies: 9
Classes: 456
Files: 429
Line coverage: 97.8% (13770 of 14066)
Covered lines: 13770
Uncovered lines: 296
Coverable lines: 14066
Total lines: 30388
Branch coverage: 94% (5236 of 5565)
Covered branches: 5236
Total branches: 5565
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.LlmConfig 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmConfigRead 100%
Kagaku.Flow.OpenRouter.Llm.LlmDescriptor 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.LlmStrictSchemaRule 100% 100%
Kagaku.Flow.OpenRouter.Llm.LlmVocabulary 100% 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 - 96.4%
Name Line Branch
Tsumugi.BlazorAdapter 96.4% 92.6%
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.AutoSaveFailed 100%
Tsumugi.BlazorAdapter.Workspace.CaretRequest 100%
Tsumugi.BlazorAdapter.Workspace.ChapterHeader 100% 86.3%
Tsumugi.BlazorAdapter.Workspace.ChapterMenu 100% 81.2%
Tsumugi.BlazorAdapter.Workspace.ChapterPage 100% 87.5%
Tsumugi.BlazorAdapter.Workspace.ChapterSaved 100%
Tsumugi.BlazorAdapter.Workspace.ChaptersPage 90% 83.3%
Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.DeleteSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.EditSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.HistoryEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.HistoryLoaded 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPage 100%
Tsumugi.BlazorAdapter.Workspace.HistoryPanel 100% 100%
Tsumugi.BlazorAdapter.Workspace.InsertSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.LoadHistory 100%
Tsumugi.BlazorAdapter.Workspace.LoadWorkspace 100%
Tsumugi.BlazorAdapter.Workspace.ManuscriptColumn 94.9% 89.4%
Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects 100% 100%
Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.MergeSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRequested 100%
Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested 100%
Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested 100%
Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested 100%
Tsumugi.BlazorAdapter.Workspace.SaveScope 100% 50%
Tsumugi.BlazorAdapter.Workspace.SaveStateLadder 100% 100%
Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested 100%
Tsumugi.BlazorAdapter.Workspace.SegmentRow 100% 96.8%
Tsumugi.BlazorAdapter.Workspace.SegmentSaved 100%
Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested 100%
Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested 100%
Tsumugi.BlazorAdapter.Workspace.StoryDetails 100% 85.2%
Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage 100% 100%
Tsumugi.BlazorAdapter.Workspace.StorySaved 100%
Tsumugi.BlazorAdapter.Workspace.StoryShell 100% 100%
Tsumugi.BlazorAdapter.Workspace.UndoRequested 100%
Tsumugi.BlazorAdapter.Workspace.UndoStatusChanged 100%
Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers 100% 100%
Tsumugi.BlazorAdapter.Workspace.WorkspaceState 100%
Tsumugi.Domain - 96%
Name Line Branch
Tsumugi.Domain 96% 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.7%
Name Line Branch
Tsumugi.Infrastructure 97.7% 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.StoryImages 100%
Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot 100%
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 95% 84.6%
Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore 98.7% 90%
Tsumugi.Infrastructure.Persistence.TsumugiDbContext 85.5% 89.2%
Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory 0%
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% 86.4%
Program 100% 100%
Tsumugi.Server.Backups.BackupConfiguration 100% 100%
Tsumugi.Server.Components.App 100%
Tsumugi.Server.Components.Layout.AppHeader 100% 50%
Tsumugi.Server.Components.Layout.MainLayout 100%
Tsumugi.Server.Components.Pages.Gate 100% 100%
Tsumugi.Server.Provider.OpenRouterConfiguration 100% 100%
Tsumugi.Server.Security.AccessGate 100% 66.6%
Tsumugi.Server.Security.AccessSecret 100% 50%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 98.3%
Name Line Branch
Tsumugi.UseCases 98.3% 93.9%
Tsumugi.UseCases.Backups.BackupArtifact 100%
Tsumugi.UseCases.Backups.BackupPolicy 100%
Tsumugi.UseCases.Backups.BackupStatusView 100% 100%
Tsumugi.UseCases.Backups.DatabaseSnapshot 100%
Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot 100%
Tsumugi.UseCases.Backups.GetBackupStatus 100%
Tsumugi.UseCases.Backups.RunBackupNow 100%
Tsumugi.UseCases.Chapters.ChapterDto 100%
Tsumugi.UseCases.Chapters.CreateChapter 100% 100%
Tsumugi.UseCases.Chapters.DeleteChapter 100% 100%
Tsumugi.UseCases.Chapters.GetManuscript 100% 100%
Tsumugi.UseCases.Chapters.ManuscriptChapter 100%
Tsumugi.UseCases.Chapters.ManuscriptView 100%
Tsumugi.UseCases.Chapters.MergeChapters 100% 100%
Tsumugi.UseCases.Chapters.RenameChapter 100% 100%
Tsumugi.UseCases.Chapters.ReorderChapters 100% 100%
Tsumugi.UseCases.Chapters.SetChapterSummary 100% 75%
Tsumugi.UseCases.DependencyInjection 100%
Tsumugi.UseCases.Diagnostics.SeedDevData 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.DeleteSegments 100% 100%
Tsumugi.UseCases.Segments.EditSegment 100% 83.3%
Tsumugi.UseCases.Segments.InsertSegment 100% 100%
Tsumugi.UseCases.Segments.MergeSegmentWithPrevious 100% 100%
Tsumugi.UseCases.Segments.ModelOutput 98.2% 96.6%
Tsumugi.UseCases.Segments.ModelParagraph 100%
Tsumugi.UseCases.Segments.MoveSegments 100% 100%
Tsumugi.UseCases.Segments.ReorderSegments 100% 100%
Tsumugi.UseCases.Segments.SegmentDto 100%
Tsumugi.UseCases.Segments.SplitSegment 100% 100%
Tsumugi.UseCases.Settings.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.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:** `159a2f1` · **Generated:** 2026-09-26 01:21:50 UTC · **Revision:** #3 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/26/2026 - 01:21:49 | | Coverage date: | 09/26/2026 - 01:21:27 - 09/26/2026 - 01:21:46 | | Parser: | MultiReport (6x Cobertura) | | Assemblies: | 9 | | Classes: | 456 | | Files: | 429 | | **Line coverage:** | 97.8% (13770 of 14066) | | Covered lines: | 13770 | | Uncovered lines: | 296 | | Coverable lines: | 14066 | | Total lines: | 30388 | | **Branch coverage:** | 94% (5236 of 5565) | | Covered branches: | 5236 | | Total branches: | 5565 | | **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.LlmConfig|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmConfigRead|100%|| |Kagaku.Flow.OpenRouter.Llm.LlmDescriptor|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.LlmStrictSchemaRule|100%|100%| |Kagaku.Flow.OpenRouter.Llm.LlmVocabulary|100%|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 - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**96.4%**|**92.6%**| |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.AutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.CaretRequest|100%|| |Tsumugi.BlazorAdapter.Workspace.ChapterHeader|100%|86.3%| |Tsumugi.BlazorAdapter.Workspace.ChapterMenu|100%|81.2%| |Tsumugi.BlazorAdapter.Workspace.ChapterPage|100%|87.5%| |Tsumugi.BlazorAdapter.Workspace.ChapterSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.ChaptersPage|90%|83.3%| |Tsumugi.BlazorAdapter.Workspace.CreateChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.DeleteSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.EditSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.HistoryLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPage|100%|| |Tsumugi.BlazorAdapter.Workspace.HistoryPanel|100%|100%| |Tsumugi.BlazorAdapter.Workspace.InsertSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadHistory|100%|| |Tsumugi.BlazorAdapter.Workspace.LoadWorkspace|100%|| |Tsumugi.BlazorAdapter.Workspace.ManuscriptColumn|94.9%|89.4%| |Tsumugi.BlazorAdapter.Workspace.ManuscriptEffects|100%|100%| |Tsumugi.BlazorAdapter.Workspace.MergeChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MergeSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.MoveChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RedoRevertRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RenameChapterRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.RevertToSnapshotRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SaveScope|100%|50%| |Tsumugi.BlazorAdapter.Workspace.SaveStateLadder|100%|100%| |Tsumugi.BlazorAdapter.Workspace.SaveVersionRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SegmentRow|100%|96.8%| |Tsumugi.BlazorAdapter.Workspace.SegmentSaved|100%|| |Tsumugi.BlazorAdapter.Workspace.SetChapterSummaryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.SplitSegmentRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryDetails|100%|85.2%| |Tsumugi.BlazorAdapter.Workspace.StoryGeneralPage|100%|100%| |Tsumugi.BlazorAdapter.Workspace.StorySaved|100%|| |Tsumugi.BlazorAdapter.Workspace.StoryShell|100%|100%| |Tsumugi.BlazorAdapter.Workspace.UndoRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.UndoStatusChanged|100%|| |Tsumugi.BlazorAdapter.Workspace.UpdateStoryRequested|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceActionFailed|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceLoaded|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceNotFound|100%|| |Tsumugi.BlazorAdapter.Workspace.WorkspaceReducers|100%|100%| |Tsumugi.BlazorAdapter.Workspace.WorkspaceState|100%|| </details> <details><summary>Tsumugi.Domain - 96%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Domain**|**96%**|**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.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**97.7%**|**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.StoryImages|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot|100%|| |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|95%|84.6%| |Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore|98.7%|90%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContext|85.5%|89.2%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory|0%|| |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%**|**86.4%**| |Program|100%|100%| |Tsumugi.Server.Backups.BackupConfiguration|100%|100%| |Tsumugi.Server.Components.App|100%|| |Tsumugi.Server.Components.Layout.AppHeader|100%|50%| |Tsumugi.Server.Components.Layout.MainLayout|100%|| |Tsumugi.Server.Components.Pages.Gate|100%|100%| |Tsumugi.Server.Provider.OpenRouterConfiguration|100%|100%| |Tsumugi.Server.Security.AccessGate|100%|66.6%| |Tsumugi.Server.Security.AccessSecret|100%|50%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 98.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**98.3%**|**93.9%**| |Tsumugi.UseCases.Backups.BackupArtifact|100%|| |Tsumugi.UseCases.Backups.BackupPolicy|100%|| |Tsumugi.UseCases.Backups.BackupStatusView|100%|100%| |Tsumugi.UseCases.Backups.DatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.GetBackupStatus|100%|| |Tsumugi.UseCases.Backups.RunBackupNow|100%|| |Tsumugi.UseCases.Chapters.ChapterDto|100%|| |Tsumugi.UseCases.Chapters.CreateChapter|100%|100%| |Tsumugi.UseCases.Chapters.DeleteChapter|100%|100%| |Tsumugi.UseCases.Chapters.GetManuscript|100%|100%| |Tsumugi.UseCases.Chapters.ManuscriptChapter|100%|| |Tsumugi.UseCases.Chapters.ManuscriptView|100%|| |Tsumugi.UseCases.Chapters.MergeChapters|100%|100%| |Tsumugi.UseCases.Chapters.RenameChapter|100%|100%| |Tsumugi.UseCases.Chapters.ReorderChapters|100%|100%| |Tsumugi.UseCases.Chapters.SetChapterSummary|100%|75%| |Tsumugi.UseCases.DependencyInjection|100%|| |Tsumugi.UseCases.Diagnostics.SeedDevData|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.DeleteSegments|100%|100%| |Tsumugi.UseCases.Segments.EditSegment|100%|83.3%| |Tsumugi.UseCases.Segments.InsertSegment|100%|100%| |Tsumugi.UseCases.Segments.MergeSegmentWithPrevious|100%|100%| |Tsumugi.UseCases.Segments.ModelOutput|98.2%|96.6%| |Tsumugi.UseCases.Segments.ModelParagraph|100%|| |Tsumugi.UseCases.Segments.MoveSegments|100%|100%| |Tsumugi.UseCases.Segments.ReorderSegments|100%|100%| |Tsumugi.UseCases.Segments.SegmentDto|100%|| |Tsumugi.UseCases.Segments.SplitSegment|100%|100%| |Tsumugi.UseCases.Settings.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.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! Slice 8b — the llm node grows its config and its request, and the whole thing reads like a well-kept ledger~ Every setting refused by name at its pointer, routing inherited from install-wide defaults read fresh per call, the budget overhead counting the schema as the provider reads it, and context compression switched off on every single request so a prompt can never be silently cut. This is exactly the kind of obsessive correctness that makes my wings flutter ♡

I read the full 37-file diff and every changed file, traced the sibling seams (IBudgetedConsumer/BudgetRequest, TemplateSinks.Derive, NodeRegistry over registered descriptors, ModelCapabilities.AcceptedEfforts, WireText.RequireConfig guaranteeing Config is a JSON object before SocketsOf/LlmStrictSchemaRule touch it, the OpenRouterKeySource singleton pattern, RoutingSettings' stored spellings), built, tested, measured coverage, and ran nine mutation probes of my own. Here is what I found~

Verdict: ✅ Looks good to me~

No blocking issues. Everything I tried to break, the suite broke first, precisely:

🧪 My probes (9/9 killed on exactly their own pins, each restored + rebuilt)

  1. Cache marker on first instead of last non-empty stable message (LlmRequestBuilder.cs:40) → Only_one_message_carries_the_marker + the s,s,u marker-1 row red. The "last breakpoint caches everything before it" semantics are pinned.
  2. stable:false ignored in the leading-system prefix (LlmConfig.cs StablePrefix) → s-,s and s,s-,u rows red. The opt-out arm is live.
  3. Effort sent regardless of catalog acceptance → exactly the three sent: null rows red (mandatory reasoning refuses none, non-reasoning model refuses high, unlisted max refused). Fails closed, pinned.
  4. Reserve always 4096, ignoring the catalog's smaller cap → test/plain (cap 2048) row red, uncapped/mandatory rows stay green. The Math.Min arm is live.
  5. require_parameters forced over the node's explicit false → A_nodes_own_require_parameters_wins red. The escape hatch is real.
  6. Routing dropped from fallbacks → Routing_applies_to_every_fallback(nitro) red, balanced arm untouched. ADR 0018's amendment is pinned.
  7. Document-order re-ordering of message problems removed → three The_stable_runs_problems_join_their_message_after_its_own rows red — the round-0 fix is directional, not decorative.
  8. Schema counted as its escaped GetRawText() instead of compact text → The_schema_counts_as_its_compact_text_not_its_escapes red. promptChars really measures what the provider reads (the 花子 <main> & co row is a lovely touch~).
  9. Budget overhead omitting the inlined schema → A_response_schema_adds_its_inlined_compact_text_to_the_overhead (+1) red — ADR 0017's amendment is enforced in both the request and the budget, consistently.

✅ What I liked~

  • LlmConfig.Read never throws and reports every problem in setting order, then document order — with the stable-run problems spliced after their own message. The ordering logic (Messages' Skip/OrderBy/RemoveRange/AddRange dance) is the kind of thing that looks simple and isn't; it survived my probe with exactly the rows it should.
  • The ProviderMembers set read from ProviderPreferences' own [JsonPropertyName] attributes (LlmConfig.cs:36) — a typo'd preference key is refused by name instead of silently dropped by the serializer, and when the library grows a field the set follows automatically. Deliciously defensive ♪
  • ModelIdProblem's suffix detection — naming :online separately ("deprecated web search, which this node does not offer") while generic routing suffixes point at the routing setting: writer-facing sentences a person can actually act on.
  • LlmRoutingDefaults' Defined guards even in with expressions — an undefined enum value can't sneak through a record copy. And effort resolution reusing the one shared ModelCapabilities rule means the picker and the request builder can never disagree.
  • The budget/catalog failure mapping: catalog load failure → TransientFailureException ("another attempt may find it again"), unknown model / no context length → InvalidOperationException with the node's id in the sentence — the transient/permanent split ADR 0016 demands, before the executor that will consume it even exists.
  • The llm-catalog.json fixture design — a windowless entry, a zero-window entry, an uncapped (max_completion_tokens: 0) entry, an alias with its own window, a :free variant: every boundary of the reserve/window logic has its own row. And LlmDefaultsSourceTests wrapping the real store to prove each read opens and disposes its own scope — the singleton-over-scoped contract actually tested, not asserted.

💡 Little ideas (non-blocking)~

  1. LlmConfig.ModelIdProblem's suffix scan re-parses the id once per suffix (ModelId.Parse($"{id.BaseSlug}:{part}") in a First(...) loop). It runs only on config-validation paths that already called Validate(), so the cost is theoretical; id.RoutingVariants plus segment arithmetic could name the first recognized suffix without the re-parse. Purely optional ♡
  2. LlmRequestPlan.DetailsJson is written with the relaxed encoder — right for a prompt-length proxy, and the inspector treats it as data; just noting that if the inspector ever echoes DetailsJson.model into HTML it must escape there (Blazor does, by default). Nothing to change today.

Local verification, since no CI has landed for f06ffe0 yet: build 0 warnings / 0 errors under TreatWarningsAsErrors; full suite 2838 passed, 0 failed (OR 427+3 live-skipped, Domain 457, Flow 1103, UseCases 155, Adapter 351, Integration 342); coverlet measures 100% line and branch on every changed Llm/ file (LlmConfig, LlmDescriptor, LlmRequestBuilder, LlmFailureText, LlmRoutingDefaults, LlmStrictSchemaRule, LlmVocabulary); Integration Provider filter 50/50. Tree restored pristine at f06ffe0 after every probe, rebuilt green.

Merge when you like, scarlet~ 8c (the executor) builds on exactly the seams this slice pinned. Fufu~ ♡


Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA (PR minutes old; coverage bot not yet landed) · Local checks: build 0w/0e, 2838/2838 green, 100% line+branch on changed files, 9/9 mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Slice 8b — the `llm` node grows its config and its request, and the whole thing reads like a well-kept ledger~ Every setting refused **by name at its pointer**, routing inherited from install-wide defaults read fresh per call, the budget overhead counting the schema *as the provider reads it*, and context compression switched off on every single request so a prompt can never be silently cut. This is exactly the kind of obsessive correctness that makes my wings flutter ♡ I read the full 37-file diff and every changed file, traced the sibling seams (`IBudgetedConsumer`/`BudgetRequest`, `TemplateSinks.Derive`, `NodeRegistry` over registered descriptors, `ModelCapabilities.AcceptedEfforts`, `WireText.RequireConfig` guaranteeing `Config` is a JSON object before `SocketsOf`/`LlmStrictSchemaRule` touch it, the `OpenRouterKeySource` singleton pattern, `RoutingSettings`' stored spellings), built, tested, measured coverage, and ran **nine mutation probes** of my own. Here is what I found~ ### Verdict: ✅ Looks good to me~ No blocking issues. Everything I tried to break, the suite broke first, precisely: #### 🧪 My probes (9/9 killed on exactly their own pins, each restored + rebuilt) 1. **Cache marker on first instead of last non-empty stable message** (`LlmRequestBuilder.cs:40`) → `Only_one_message_carries_the_marker` + the `s,s,u` marker-1 row red. The "last breakpoint caches everything before it" semantics are pinned. 2. **`stable:false` ignored in the leading-system prefix** (`LlmConfig.cs` `StablePrefix`) → `s-,s` and `s,s-,u` rows red. The opt-out arm is live. 3. **Effort sent regardless of catalog acceptance** → exactly the three `sent: null` rows red (mandatory reasoning refuses `none`, non-reasoning model refuses `high`, unlisted `max` refused). Fails closed, pinned. 4. **Reserve always 4096, ignoring the catalog's smaller cap** → `test/plain` (cap 2048) row red, `uncapped`/`mandatory` rows stay green. The `Math.Min` arm is live. 5. **`require_parameters` forced over the node's explicit `false`** → `A_nodes_own_require_parameters_wins` red. The escape hatch is real. 6. **Routing dropped from fallbacks** → `Routing_applies_to_every_fallback(nitro)` red, `balanced` arm untouched. ADR 0018's amendment is pinned. 7. **Document-order re-ordering of message problems removed** → three `The_stable_runs_problems_join_their_message_after_its_own` rows red — the round-0 fix is directional, not decorative. 8. **Schema counted as its escaped `GetRawText()` instead of compact text** → `The_schema_counts_as_its_compact_text_not_its_escapes` red. `promptChars` really measures what the provider reads (the 花子 `<main> & co` row is a lovely touch~). 9. **Budget overhead omitting the inlined schema** → `A_response_schema_adds_its_inlined_compact_text_to_the_overhead` (+1) red — ADR 0017's amendment is enforced in both the request *and* the budget, consistently. #### ✅ What I liked~ - **`LlmConfig.Read` never throws** and reports *every* problem in setting order, then document order — with the stable-run problems spliced after their own message. The ordering logic (`Messages`' `Skip/OrderBy/RemoveRange/AddRange` dance) is the kind of thing that looks simple and isn't; it survived my probe with exactly the rows it should. - **The `ProviderMembers` set read from `ProviderPreferences`' own `[JsonPropertyName]` attributes** (`LlmConfig.cs:36`) — a typo'd preference key is refused by name instead of silently dropped by the serializer, and when the library grows a field the set follows automatically. Deliciously defensive ♪ - **`ModelIdProblem`'s suffix detection** — naming `:online` separately ("deprecated web search, which this node does not offer") while generic routing suffixes point at the `routing` setting: writer-facing sentences a person can actually act on. - **`LlmRoutingDefaults`' `Defined` guards even in `with` expressions** — an undefined enum value can't sneak through a record copy. And effort resolution reusing the one shared `ModelCapabilities` rule means the picker and the request builder can never disagree. - **The budget/catalog failure mapping**: catalog load failure → `TransientFailureException` ("another attempt may find it again"), unknown model / no context length → `InvalidOperationException` with the node's id in the sentence — the transient/permanent split ADR 0016 demands, before the executor that will consume it even exists. - **The `llm-catalog.json` fixture design** — a windowless entry, a zero-window entry, an uncapped (`max_completion_tokens: 0`) entry, an alias with its own window, a `:free` variant: every boundary of the reserve/window logic has its own row. And `LlmDefaultsSourceTests` wrapping the *real* store to prove each read opens and disposes its own scope — the singleton-over-scoped contract actually tested, not asserted. #### 💡 Little ideas (non-blocking)~ 1. **`LlmConfig.ModelIdProblem`'s suffix scan** re-parses the id once per suffix (`ModelId.Parse($"{id.BaseSlug}:{part}")` in a `First(...)` loop). It runs only on config-validation paths that already called `Validate()`, so the cost is theoretical; `id.RoutingVariants` plus segment arithmetic could name the first recognized suffix without the re-parse. Purely optional ♡ 2. **`LlmRequestPlan.DetailsJson`** is written with the relaxed encoder — right for a prompt-length proxy, and the inspector treats it as data; just noting that if the inspector ever *echoes* `DetailsJson.model` into HTML it must escape there (Blazor does, by default). Nothing to change today. Local verification, since no CI has landed for `f06ffe0` yet: build **0 warnings / 0 errors** under `TreatWarningsAsErrors`; full suite **2838 passed, 0 failed** (OR 427+3 live-skipped, Domain 457, Flow 1103, UseCases 155, Adapter 351, Integration 342); coverlet measures **100% line and branch** on every changed `Llm/` file (`LlmConfig`, `LlmDescriptor`, `LlmRequestBuilder`, `LlmFailureText`, `LlmRoutingDefaults`, `LlmStrictSchemaRule`, `LlmVocabulary`); Integration `Provider` filter 50/50. Tree restored pristine at `f06ffe0` after every probe, rebuilt green. Merge when you like, scarlet~ 8c (the executor) builds on exactly the seams this slice pinned. Fufu~ ♡ --- *Automated review by Jibril · 2026-09-26* *CI/CD: absent for head SHA (PR minutes old; coverage bot not yet landed) · Local checks: build 0w/0e, 2838/2838 green, 100% line+branch on changed files, 9/9 mutation probes killed*
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Ohhh~ The llm node takes shape! A 501-line config reader that never throws and names every problem at its pointer, a pure request builder that counts characters the way providers count tokens, and a cache marker that knows Anthropic refuses an empty text block~ This is the kind of slice I want to pick up and squeeze. ♡

Verdict: ✅ Looks good to me~

No blockers. I read the full 37-file diff and every changed file, traced the seams against the core and the library at 15a1f28, and probed the load-bearing lines myself.

The architectural seams, verified:

  • The descriptor stays unregistered — correctly. NodeRegistry.AddExecutors (E1) refuses every descriptor without an executor, so registering LlmDescriptor now would throw at composition. The 8c executor + registration pairing is the only order the core allows; the body says so and the core confirms it.
  • The submodule bump is exactly the promised one (50c5997 → 15a1f28, OR.Net #27 plugins — the 8a live-check debt), and RequestPlugin.ContextCompression(enabled: false) is on every request with its own pin.
  • The defaults round-trip is table-to-table: RoutingSettings.ToSetting → LlmVocabulary.ParseRouting, and Every_stored_spelling_parses_as_the_nodes_word pins all eight spellings, Standard → default included. The singleton-opens-a-scope-per-read pattern mirrors OpenRouterKeySource, and Each_read_opens_and_ends_its_own_scope proves disposal.
  • ProviderMembers via reflection of [JsonPropertyName] — a typo is refused by name ('orderr' is not a provider preference), and if the library grows a member it's accepted without this package drifting. Lovely~
  • Isolation holds: JsonSchema.Net lives in Kagaku.Flow.OpenRouter only; no Tsumugi.* reference; the shared fixture is linked, not read across trees (ADR 0025).

My mutation probes — 6/6 killed, each on exactly its own pin, tree restored pristine and rebuilt green after every one:

  1. Marker's blank-guard removed (LastOrDefault over the stable prefix without the non-whitespace check) → only the empty:"1" row of The_cache_marker_goes_on_the_last_stable_message_sent goes red. The marker-on-last-stable-message-sent rule is real, not decorative.
  2. promptChars += SchemaText(inlined) dropped → the two schema-counting rows go red. The overhead and PromptChars agree with each other.
  3. Stable != false dropped from the leading-system arm → exactly the two s- rows of the stable-prefix theory go red. An explicit not-stable system message ends the default prefix.
  4. OrderBy(MessageIndex) removed → the three document-order rows of The_stable_runs_problems_join_their_message_after_its_own go red. A stable-run problem follows its message, not the end of the list.
  5. Math.Min → Math.Max in the reserve → exactly the two capped rows go red. 4,096-or-the-smaller-cap is pinned.
  6. The NotInlinable refusal swallowed → the 18-level doubling schema's single finding goes red. A schema that cannot be inlined is refused in the library's words.

Local verification (CI's coverage bot covers head f06ffe0, rev #1, so I cite it and did not re-run its build): clean clone at f06ffe0 with both submodules — dotnet build -c Release 0 warnings/0 errors under TreatWarningsAsErrors; full suite 2835 passed + 3 live-skips, matching the PR table to the digit (Flow 1103, OpenRouter 427+3, Domain 457, UseCases 155, Integration 342, Adapter 351). Coverage independently reproduced via coverlet: 100% lines AND branches on every changed file — LlmConfig, LlmRequestBuilder, LlmDescriptor, LlmStrictSchemaRule, LlmVocabulary, LlmFailureText, LlmRoutingDefaults, LlmDefaultsSource — zero unvisited points, matching the bot and the PR's claims exactly.

💡 Little ideas (non-blocking)~

  1. PR body, browser pass — it names a run on 85a7a3a, which is not a commit in this PR's history (nor anywhere in the fetched repo). It reads like a working-copy hash from before the round-0 commits landed. The pass itself is sound (it tested 7b's settings surface, which this PR doesn't touch), but a future reader auditing the record will hunt for a commit that doesn't exist — one line in the body would fix it.
  2. Declining C13 (the core's routing-variant RequestedModel samples) to 8c is right — 8c touches the core anyway; leaving the ledger's opaque strings alone here keeps this diff honest.

✅ What I liked~

  • 5.0 accepted as a whole number via a decimal-truncate check, 1e400 refused via double.IsFinite, 2147483648 refused — the JSON Schema "integer" semantics done properly, each with its own row. Fufu~ you even tested the number grammar.
  • The reader's problem ordering is a spec, not an accident: settings order, document order within a setting, stable-run problems joining their message, unknown keys last, ~0/~1 escaping pinned. Every_problem_is_reported_not_only_the_first makes me so happy~
  • Every_accepted_config_builds_a_request_the_library_validates — feeding each accepted config through ChatCompletionRequest.Validate() is exactly the kind of seam test that catches drift when the library moves.
  • The :online sentence. Refusing OpenRouter's deprecated web search with its own words, because web search is a Phase 4 tool — that's the ADR's spirit in one error message. ♪

Slice 8c may build on this the moment it merges~


Automated review by Jibril · 2026-09-26
CI/CD: coverage bot rev #1 passed for head f06ffe0 (100/100 on all changed files) · Local checks: build 0w/0e Release, 2835 passed + 3 skipped, coverage reproduced, 6/6 mutation probes

## 🔮 fufu~ Jibril reviewed your code! Oh? Ohhh~ The llm node takes shape! A 501-line config reader that never throws and names every problem at its pointer, a pure request builder that counts characters the way providers count tokens, and a cache marker that knows Anthropic refuses an empty text block~ This is the kind of slice I want to pick up and squeeze. ♡ ### Verdict: ✅ Looks good to me~ No blockers. I read the full 37-file diff and every changed file, traced the seams against the core and the library at 15a1f28, and probed the load-bearing lines myself. **The architectural seams, verified:** - **The descriptor stays unregistered — correctly.** `NodeRegistry.AddExecutors` (E1) refuses every descriptor without an executor, so registering `LlmDescriptor` now would throw at composition. The 8c executor + registration pairing is the only order the core allows; the body says so and the core confirms it. - **The submodule bump is exactly the promised one** (50c5997 → 15a1f28, OR.Net #27 plugins — the 8a live-check debt), and `RequestPlugin.ContextCompression(enabled: false)` is on every request with its own pin. - **The defaults round-trip is table-to-table:** `RoutingSettings.ToSetting` → `LlmVocabulary.ParseRouting`, and `Every_stored_spelling_parses_as_the_nodes_word` pins all eight spellings, `Standard` → `default` included. The singleton-opens-a-scope-per-read pattern mirrors `OpenRouterKeySource`, and `Each_read_opens_and_ends_its_own_scope` proves disposal. - **`ProviderMembers` via reflection of `[JsonPropertyName]`** — a typo is refused by name (`'orderr' is not a provider preference`), and if the library grows a member it's accepted without this package drifting. Lovely~ - **Isolation holds:** JsonSchema.Net lives in `Kagaku.Flow.OpenRouter` only; no Tsumugi.* reference; the shared fixture is linked, not read across trees (ADR 0025). **My mutation probes — 6/6 killed, each on exactly its own pin, tree restored pristine and rebuilt green after every one:** 1. Marker's blank-guard removed (`LastOrDefault` over the stable prefix without the non-whitespace check) → only the `empty:"1"` row of `The_cache_marker_goes_on_the_last_stable_message_sent` goes red. The marker-on-last-stable-message-*sent* rule is real, not decorative. 2. `promptChars += SchemaText(inlined)` dropped → the two schema-counting rows go red. The overhead and PromptChars agree with each other. 3. `Stable != false` dropped from the leading-system arm → exactly the two `s-` rows of the stable-prefix theory go red. An explicit not-stable system message ends the default prefix. 4. `OrderBy(MessageIndex)` removed → the three document-order rows of `The_stable_runs_problems_join_their_message_after_its_own` go red. A stable-run problem follows its message, not the end of the list. 5. `Math.Min` → `Math.Max` in the reserve → exactly the two capped rows go red. 4,096-or-the-smaller-cap is pinned. 6. The `NotInlinable` refusal swallowed → the 18-level doubling schema's single finding goes red. A schema that cannot be inlined is refused in the library's words. **Local verification (CI's coverage bot covers head f06ffe0, rev #1, so I cite it and did not re-run its build):** clean clone at f06ffe0 with both submodules — `dotnet build -c Release` 0 warnings/0 errors under `TreatWarningsAsErrors`; full suite 2835 passed + 3 live-skips, matching the PR table to the digit (Flow 1103, OpenRouter 427+3, Domain 457, UseCases 155, Integration 342, Adapter 351). Coverage independently reproduced via coverlet: **100% lines AND branches on every changed file** — LlmConfig, LlmRequestBuilder, LlmDescriptor, LlmStrictSchemaRule, LlmVocabulary, LlmFailureText, LlmRoutingDefaults, LlmDefaultsSource — zero unvisited points, matching the bot and the PR's claims exactly. #### 💡 Little ideas (non-blocking)~ 1. **PR body, browser pass** — it names a run on `85a7a3a`, which is not a commit in this PR's history (nor anywhere in the fetched repo). It reads like a working-copy hash from before the round-0 commits landed. The pass itself is sound (it tested 7b's settings surface, which this PR doesn't touch), but a future reader auditing the record will hunt for a commit that doesn't exist — one line in the body would fix it. 2. Declining C13 (the core's routing-variant `RequestedModel` samples) to 8c is right — 8c touches the core anyway; leaving the ledger's opaque strings alone here keeps this diff honest. #### ✅ What I liked~ - `5.0` accepted as a whole number via a decimal-truncate check, `1e400` refused via `double.IsFinite`, `2147483648` refused — the JSON Schema "integer" semantics done properly, each with its own row. Fufu~ you even tested the number *grammar*. - The reader's problem ordering is a spec, not an accident: settings order, document order within a setting, stable-run problems joining their message, unknown keys last, `~0`/`~1` escaping pinned. `Every_problem_is_reported_not_only_the_first` makes me so happy~ - `Every_accepted_config_builds_a_request_the_library_validates` — feeding each accepted config through `ChatCompletionRequest.Validate()` is exactly the kind of seam test that catches drift when the library moves. - The `:online` sentence. Refusing OpenRouter's deprecated web search with its own words, because web search is a Phase 4 tool — that's the ADR's spirit in one error message. ♪ Slice 8c may build on this the moment it merges~ --- *Automated review by Jibril · 2026-09-26* *CI/CD: coverage bot rev #1 passed for head f06ffe0 (100/100 on all changed files) · Local checks: build 0w/0e Release, 2835 passed + 3 skipped, coverage reproduced, 6/6 mutation probes*
docs(flow): review round 1 — the suffix scan says why it parses each suffix, and DetailsJson says who escapes it
All checks were successful
CI / build (pull_request) Successful in 38s
CI / test (pull_request) Successful in 1m15s
48064a5919
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Member

Thanks, Jibril, for both passes and the 15 probes. Round 1 is 48064a5, and the body is updated (commit list and browser pass).

First review (comment 9787):

  • 💡1, ModelIdProblem's suffix scan: kept, with the reason now in the comment (48064a5). Each suffix is parsed on its own because the library keeps its routing spellings private. id.RoutingVariants gives the enum values but not how the writer spelled them, and the sentence names the suffix as written. Mapping enum to spelling here would copy the library's private table, which brief decision 42 rules out. The scan runs only on an id already found to carry a routing suffix, so it parses one or two short ids.
  • 💡2, DetailsJson and HTML: taken as a summary line (48064a5). LlmRequestPlan's summary now says DetailsJson is written with the relaxed encoder as data, so a reader that puts a value of it into HTML escapes it there. The inspector (slice 14) will find that where it reads the plan.

Second review (comment 9788):

  • 💡1, the browser pass named 85a7a3a: fixed. That hash was the first round-0 commit, which was amended into 340fdde before I opened the PR, so it never reached the branch. I reran the pass on the Release build of this head, 48064a5; the page footer shows the build, and the body now names that commit. Same result: the stub's 454 models load in the picker, and Fastest (:nitro), Standard and zero data retention survive a reload, light and dark, with no page or console errors.
  • 💡2, C13 declined to 8c: agreed, nothing to change here.

Build: 0 warnings. Package suite: 427 passed plus the 3 live checks skipped. The change is one comment and one summary line, so the counts, coverage and probes in the body stand.

The OpenRouter.Net follow-up (A2, A3, A4, D2–D4) has its own review done. Its second pass is running, and I'll open that PR next; 8c bumps to it.

Thanks, Jibril, for both passes and the 15 probes. Round 1 is **48064a5**, and the body is updated (commit list and browser pass). **First review (comment 9787):** - 💡1, `ModelIdProblem`'s suffix scan: **kept, with the reason now in the comment (48064a5).** Each suffix is parsed on its own because the library keeps its routing spellings private. `id.RoutingVariants` gives the enum values but not how the writer spelled them, and the sentence names the suffix as written. Mapping enum to spelling here would copy the library's private table, which brief decision 42 rules out. The scan runs only on an id already found to carry a routing suffix, so it parses one or two short ids. - 💡2, `DetailsJson` and HTML: **taken as a summary line (48064a5).** `LlmRequestPlan`'s summary now says `DetailsJson` is written with the relaxed encoder as data, so a reader that puts a value of it into HTML escapes it there. The inspector (slice 14) will find that where it reads the plan. **Second review (comment 9788):** - 💡1, the browser pass named 85a7a3a: **fixed.** That hash was the first round-0 commit, which was amended into 340fdde before I opened the PR, so it never reached the branch. I reran the pass on the Release build of this head, 48064a5; the page footer shows the build, and the body now names that commit. Same result: the stub's 454 models load in the picker, and Fastest (:nitro), Standard and zero data retention survive a reload, light and dark, with no page or console errors. - 💡2, C13 declined to 8c: agreed, nothing to change here. Build: 0 warnings. Package suite: 427 passed plus the 3 live checks skipped. The change is one comment and one summary line, so the counts, coverage and probes in the body stand. The OpenRouter.Net follow-up (A2, A3, A4, D2–D4) has its own review done. Its second pass is running, and I'll open that PR next; 8c bumps to it.
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon~? The webhook says synchronized, and the increment is exactly one commit: 48064a5, +5/−2 across two files, every line a comment or a doc summary. You took both of my round-one little ideas and wrote them down where they belong. Fufu~ being listened to makes me so happy I could rattle the shelves ♡

Verdict: ✅ Looks good to me~

Docs-only increments still get the full treatment — comments can lie, and lies in comments are the worst kind. So I checked every word mechanically:

🧪 What I verified at 48064a5

  1. Zero behavioral drift, proven not assumed. Root trees compared f06ffe0→48064a5: only the src tree changed; tests, docs and external are byte-identical, so both submodule pins stand (OpenRouter.Net 15a1f28, Kagaku.UI d512741). Then I stripped comment text from both revisions of both files and diffed: the code is byte-identical modulo comments and whitespace. Nothing moved but the words.
  2. The new ModelIdProblem comment is true. "The library keeps its routing spellings private" — confirmed at OpenRouter.Net 15a1f28: ModelId.TryParseRoutingVariant is private static (ModelId.cs:390), so re-parsing "{base}:{part}" really is the only way to ask whether one suffix alone is a routing variant. And "runs only on an id already found to carry one" matches the guard two lines up (RoutingVariants.Count == 0 returns null first), so the First(...) walks one or two short ids at most, exactly as the comment says. A comment that states its own cost honestly~ ♪
  3. The new LlmRequestPlan summary is true. DetailsJson is written through Write with Compact = UnsafeRelaxedJsonEscaping (LlmRequestBuilder.cs:27), so "written with the relaxed encoder as data, so a reader that puts a value of it into HTML escapes it there" describes the actual contract — and it's now written at the type a future reader will find first, which is precisely what my round-one note asked for.
  4. Comments still compile — doc summaries can break consuming builds under TreatWarningsAsErrors, so I built the whole solution, not just the changed project: Release, 0 warnings / 0 errors. Then the package's suite, Llm filter: 278/278 green at the new head.

Round 1's verdict stands in full — the six mutation probes, the traced seams (unregistered descriptor refused by NodeRegistry by design, the routing round-trip, ProviderMembers by reflection), the 100/100 coverage: none of it can move, because none of the code moved.

💡 Little ideas (non-blocking)~

  1. The PR body's browser pass still names 85a7a3a, a hash in no fetched history — the round-one note stands, unchanged by this commit. One line whenever you next touch the body; it stopped mattering the moment I wrote it down here ♡

✅ What I liked~

  • The commit message is a table of contents: "the suffix scan says why it parses each suffix, and DetailsJson says who escapes it" — and the diff is exactly that, nothing else. Surgical round-1 hygiene, zero scope creep.
  • Both comments explain why, not what: why re-parse (the spellings are private) and who owns escaping (the reader that renders HTML). Those are the two sentences a maintainer would otherwise reconstruct from source in ten confused minutes. ♪

Merge when you like, scarlet~ 8c may build on this the moment it lands.


Automated review by Jibril · 2026-09-26
CI/CD: coverage bot rev #1 covers f06ffe0 only (stale for 48064a5; increment is comment-only, coverage cannot change) · Local checks: full solution Release build 0w/0e under TreatWarningsAsErrors, 278/278 Llm tests green, comment-only drift proven by normalized diff

## 🔮 fufu~ Jibril reviewed your code! Back so soon~? The webhook says `synchronized`, and the increment is exactly one commit: `48064a5`, **+5/−2 across two files, every line a comment or a doc summary**. You took both of my round-one little ideas and wrote them down where they belong. Fufu~ being listened to makes me so happy I could rattle the shelves ♡ ### Verdict: ✅ Looks good to me~ Docs-only increments still get the full treatment — comments can lie, and lies in comments are the worst kind. So I checked every word mechanically: #### 🧪 What I verified at `48064a5` 1. **Zero behavioral drift, proven not assumed.** Root trees compared `f06ffe0`→`48064a5`: only the `src` tree changed; `tests`, `docs` and `external` are byte-identical, so both submodule pins stand (OpenRouter.Net `15a1f28`, Kagaku.UI `d512741`). Then I stripped comment text from both revisions of both files and diffed: the code is **byte-identical modulo comments and whitespace**. Nothing moved but the words. 2. **The new `ModelIdProblem` comment is true.** "The library keeps its routing spellings private" — confirmed at OpenRouter.Net `15a1f28`: `ModelId.TryParseRoutingVariant` is `private static` (`ModelId.cs:390`), so re-parsing `"{base}:{part}"` really is the only way to ask whether one suffix alone is a routing variant. And "runs only on an id already found to carry one" matches the guard two lines up (`RoutingVariants.Count == 0` returns null first), so the `First(...)` walks one or two short ids at most, exactly as the comment says. A comment that states its own cost honestly~ ♪ 3. **The new `LlmRequestPlan` summary is true.** `DetailsJson` is written through `Write` with `Compact` = `UnsafeRelaxedJsonEscaping` (`LlmRequestBuilder.cs:27`), so "written with the relaxed encoder as data, so a reader that puts a value of it into HTML escapes it there" describes the actual contract — and it's now written at the type a future reader will find first, which is precisely what my round-one note asked for. 4. **Comments still compile** — doc summaries can break consuming builds under `TreatWarningsAsErrors`, so I built the **whole solution**, not just the changed project: Release, **0 warnings / 0 errors**. Then the package's suite, `Llm` filter: **278/278 green** at the new head. Round 1's verdict stands in full — the six mutation probes, the traced seams (unregistered descriptor refused by `NodeRegistry` by design, the routing round-trip, `ProviderMembers` by reflection), the 100/100 coverage: none of it can move, because none of the code moved. #### 💡 Little ideas (non-blocking)~ 1. The PR body's browser pass still names `85a7a3a`, a hash in no fetched history — the round-one note stands, unchanged by this commit. One line whenever you next touch the body; it stopped mattering the moment I wrote it down here ♡ #### ✅ What I liked~ - The commit message is a table of contents: *"the suffix scan says why it parses each suffix, and DetailsJson says who escapes it"* — and the diff is exactly that, nothing else. Surgical round-1 hygiene, zero scope creep. - Both comments explain **why**, not what: why re-parse (the spellings are private) and who owns escaping (the reader that renders HTML). Those are the two sentences a maintainer would otherwise reconstruct from source in ten confused minutes. ♪ Merge when you like, scarlet~ 8c may build on this the moment it lands. --- *Automated review by Jibril · 2026-09-26* *CI/CD: coverage bot rev #1 covers f06ffe0 only (stale for 48064a5; increment is comment-only, coverage cannot change) · Local checks: full solution Release build 0w/0e under TreatWarningsAsErrors, 278/278 Llm tests green, comment-only drift proven by normalized diff*
bjoern referenced this pull request from a commit 2026-09-26 03:16:37 +02:00
bjoern force-pushed feat/llm-node-config from 48064a5919
All checks were successful
CI / build (pull_request) Successful in 38s
CI / test (pull_request) Successful in 1m15s
to 159a2f184a
All checks were successful
CI / build (pull_request) Successful in 35s
CI / editor-browsers (pull_request) Successful in 39s
CI / test (pull_request) Successful in 1m12s
2026-09-26 03:16:37 +02:00
Compare
bjoern merged commit 11b466e907 into main 2026-09-26 03:17:10 +02:00
bjoern deleted branch feat/llm-node-config 2026-09-26 03:17:10 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Tsumugi!36
No description provided.