feat: Defaults pick from the model catalog — the efforts a model takes, aliases resolved, and the routing defaults (Phase 2 · slice 7b) #24

Merged
bjoern merged 5 commits from feat/provider-catalog-defaults into main 2026-09-25 18:07:44 +02:00
Member

Slice 7 of Phase 2, second half; 7a was #20. Settings → Defaults now picks the default model from OpenRouter's catalog. The catalog loads without a key and is remembered for an hour. The default effort offers only what the catalog says the chosen model takes. The tab also stores the routing defaults an llm node will inherit in slice 8: routing variant, service tier and zero data retention.

Kagaku.UI #26 is merged (the Combobox fix this pass found). 14923e6 moves the submodule to its squash commit 1ac43db, whose tree is identical to the reviewed head 00fbcde.

What

Kagaku.Flow.OpenRouter (the package)

  • ILlmGateway.GetCatalogAsync(): the catalog of models whose output includes text.
    • It needs no key.
    • A loaded catalog is kept for 1 h on the injected clock; a failed load is not kept.
    • Each attempt has a 10 s timeout. There are two attempts, and a server's Retry-After is honoured up to 5 s.
    • OpenRouterGatewayOptions gains CatalogTimeout, CatalogTtl and CatalogRetry.
    • A disposed gateway refuses the catalog, even one it already holds.
  • ModelCapabilities: the one "fails closed" rule (M1–M5).
    • TakesReasoning: the entry lists the reasoning parameter, matched case-sensitively.
    • AcceptedEfforts: Off only when the entry says reasoning can be switched off, and a level only when the entry names it. The list is ascending, with no duplicates and no unknown values.
    • Slice 8's executor will use the same rule, so the picker and the request cannot disagree.

Tsumugi.UseCases.Settings

  • GetModelCatalog: returns CatalogModel(Id, Name, AliasTargetName, Efforts), one entry per id, sorted by display text. A ~…-latest alias reads "Name → target".
  • ReasoningEfforts.Offered(modelId, model): what the effort picker offers.
  • RoutingDefaults: carried by LlmDefaults. It holds:
    • ModelRouting: Balanced, Fastest :nitro, Cheapest :floor, Most reliable with tools :exacto;
    • ServiceTierPreference: Auto, Standard, Flex, Priority;
    • zero data retention.
  • RoutingSettings: a written table in the node's own spelling: balanced|nitro|floor|exacto, auto|default|flex|priority, true|false.
    • Parsing ignores case. Anything else, such as a number, standard or a blank, reads as the built-in value, field by field.
    • Saving refuses an undefined value and writes nothing.
  • IModelProvider gains its fourth member, the catalog, as 7a announced.

Settings → Defaults (DefaultsSection)

  • Model: a searchable Combobox that shows "Name (id)", so the id can be searched too. A pick saves at once.
    • A stored id the catalog lacks shows as "id (not in the catalog)".
    • While the catalog loads, the box shows the bare id.
    • If the load fails, the tab shows the reason and a Try again button.
  • Default effort: the efforts the chosen model takes.
    • A stored effort the model does not take stays selected, flagged "— not offered by this model".
    • With no model chosen, every effort is offered.
  • Routing, Service tier and Zero data retention (a two-option Select), each with its hint.
  • The Provider tab's no-key text gains "The model catalog loads without it."

Why

  • ADR 0018:
    • the catalog backs every picker and is cached for 1 h;
    • effort resolution fails closed;
    • the routing defaults are inherited at run time;
    • aliases.
  • ADR 0025: the rule lives in the package, not in Tsumugi.
  • ADR 0006: settings are AppSetting rows under keys that are never renamed.
  • ADR 0009: compose Kagaku.UI components; never hand-roll one.

ADRs and stories amended

ADR 0018:

  • New status line: "Amended 2026-09-25 (PR #24, slice 7b): the catalog load has a 10-second timeout and two attempts; zero data retention is a setting, not a switch; an effort needs the reasoning parameter and a named effort."
  • The gateway paragraph. Its parenthesis "(Orihon's finding; a key check gets 30 s, once)" becomes "(…a key check gets 30 s, once, and a catalog load 10 s, twice)".
  • The capability list. "(structured_outputs, tools, a reasoning block)" becomes "(structured_outputs, tools, the reasoning parameter)".
  • Fails closed. "an effort is sent only when the catalog says the model takes one" gains ": the model lists the reasoning parameter, and its reasoning block names the effort (for none, says reasoning can be switched off)".
  • Routing. "zero-data-retention-only switch" becomes "setting", twice.

docs/stories/settings.md:

  • The Default model and default effort criterion is ticked.
  • In the routing criterion, "optionally a Zero data retention only switch" becomes "a Zero data retention only setting".

Decisions to confirm

  1. §13.9, "fails closed", comes first. The ADR 0018 lines quoted above are provisional until you confirm this: the status clause, the capability list and the fails-closed sentence.
    • M1: a model takes an effort only when it lists the reasoning parameter. A reasoning block alone does not count. Three live models, such as rekaai/reka-edge, have the block without the parameter.
    • M3/M4: Off is offered only when the entry says reasoning can be switched off, and a level only when the entry names it. On today's catalog, 141 of the 323 reasoning models offer only Model default, or Model default and Off. Kagura and Orihon offer the full ladder for those.
    • If you decline: revert those ADR lines. Then either change M4 in one place and flip rows 3, 4 and 11, or change M1 and flip row 13. The new case-sensitive "Reasoning" row goes too.
  2. §13.27: the effort filter follows the default model, and the bundled templates take the same effort.
    • The effort's hint says the templates take the value too, and each sends it only when its own model takes it.
    • A writer can still pick a model that takes High, choose High, and switch back. The effort stays, flagged.
    • The alternative: offer every level and flag the ones the default model lacks. That reads "pickers filter" loosely.
  3. §13.7: the stored spellings are the node's vocabulary. Standard is stored as default. This avoids a second off/none mismatch.
  4. §13.8: zero data retention is a two-option Select, "Any provider" or "Zero data retention only".
    • Kagaku.UI has no Switch, and ADR 0009 forbids hand-rolling one.
    • A toggle group releases to null when clicked again, which suits a filter, not a setting.
    • An upstream Switch is an optional follow-up.
  5. §13.12: timeouts. The catalog gets 10 s × 2 attempts, because a picker must not wait minutes.
    • The worst case is about 21 s, or about 42 s for a second caller during a failing load.
    • Failed loads are not cached.
    • The 5-minute figure stays with completions, in slice 8.
  6. §13.6: no routing mapping in this slice. The mapping to ModelRoutingVariant/ServiceTier/ProviderPreferences has no consumer before slice 8's executor. Slice 8 adds ILlmDefaultsSource and the mapping.
  7. §13.10: aliases show the catalog's own names, e.g. "Anthropic: Claude Opus Latest → Anthropic: Claude Opus 5.5".
  8. §13.29: the model id is part of the picker's item text, because the Combobox filters only by ItemText. An upstream ItemSearchText is an optional follow-up.
  9. §13.24: the model picker cannot go back to "none chosen". The Combobox accepts only list values. BuiltIn still starts at "".

Departures from the brief, each pinned by a test:

  1. The picker shows Loading and LoadError only while it has no list. A list loaded earlier stays usable during a reload and after a failed one (A_failed_reload_keeps_the_list_it_had).
  2. The failed-catalog hint under the effort appears only when a model is chosen (Without_a_model_a_failed_catalog_leaves_the_usual_hint).
  3. A blank catalog name falls back to the id, and a blank alias-target name means no target.
  4. The routing test is named A_hand_edited_routing_row_reads_as_the_built_in_on_its_own.
  5. The catalog-failure theory has 4 rows. A timeout reads "…could not be loaded: Request timed out. Try again in a moment."

Out of scope

  • Slice 8:
    • CompleteAsync, streaming and the per-key client;
    • ILlmDefaultsSource, and applying routing, tier and ZDR to requests;
    • the llm descriptor and executor;
    • the second library bump.
  • Slices 10a and 12: the Run button's "no key" and "model left the catalog" notices, and copying the defaults into new nodes.
  • Phase 3: pickers filtered by structured_outputs/tools, pricing and context length, and per-node pickers.
  • Optional Kagaku.UI follow-ups: a Switch, ItemSearchText, and SaveText on MaskedSecretField.
  • The seed is unchanged: the key stays unset, and the routing defaults stay at their built-ins.

Tests

Project main (da9f961) this PR
Kagaku.Flow.OpenRouter.Tests 41 71
Tsumugi.UseCases.Tests 12 40
Tsumugi.Integration.Tests 224 247
Tsumugi.BlazorAdapter.Tests 247 268
Kagaku.Flow.Tests 483 483
Tsumugi.Domain.Tests 37 37
Total 1044 1146
GraphEditor (npm test) 14 14

The Release build has 0 warnings under TreatWarningsAsErrors. The fixture models.json holds six entries, each identical to its entry in a live catalog snapshot.

Coverage (coverlet, per changed file, lines and branches with branch="True"): all 20 changed source files with executable code are at 100%.

  • These include OpenRouterModelProvider.cs (54/54 lines, 48/48 branches), ModelCapabilities, OpenRouterGateway, RoutingSettings, GetModelCatalog, DefaultsSection, and the reducers and effects.
  • The package's files reach 100% from the package's own tests alone.
  • The other six changed files have no executable code: ILlmGateway, IModelProvider, ModelRouting, ServiceTierPreference, SettingKeys and LoadModelCatalog.

Mutation probes. Each was run on a copy, then restored and touched. All were killed.

  • §12 probes 42–64 (39 sub-cases):

    • 42–49: the keyless catalog, TTL and clock, failures not cached, retries and timeout, and M1–M5;
    • 50–57: name and effort mapping, dedupe and sort, U6, the routing table and its parsing, the save guards, and the per-field fallback;
    • 58–64: E1–E7 in the tab.
  • 8 extra probes:

    • the catalog client is not disposed;
    • the Enum.IsDefined filter is dropped;
    • an error is shown over a kept list;
    • "Loading" is shown on any reload;
    • the failed-catalog hint is shown with no model;
    • the Provider tab's catalog clause is dropped;
    • the reducer drops an earlier list;
    • a generic failure sentence.
  • Re-run after #21/#22 changed the Combobox and Select markup: 58a, 59a, 59b, 61a, 62 and 63.

  • The verifier's own probes. Four were killed: the id shown twice, one flag text for both cases, a reload that keeps the old error, and a case-sensitive sort. Two survived:

    • deleting the disposed-gateway check while the catalog is cached;
    • matching reasoning case-insensitively.

    Both now have tests: A_disposed_gateway_refuses_even_the_catalog_it_already_holds, and a "Reasoning" row. Each test fails under its mutant.

Browser pass

Setup:

  • The Release build ran from a copy with its own data directory.
  • A local stub served the 2026-09-23 catalog snapshot (457 models), and a second check used the live keyless catalog (458 today).
  • The Playwright MCP server would not connect in this session, so Node Playwright scripts drove system Chrome.
  • No page or console errors appeared in any mode.
  • The screenshots (b7b-*, r1-*) are kept in the probes directory.

Steps:

  1. Provider tab and C1. With no key, the tab reads "No key is set. Runs need one. The model catalog loads without it."
    • A key was saved through the stub, and the tab showed "$7.50 left of $20.00". The stub then logged the catalog request, which carried no key.
    • The catalog was fetched once across three page loads.
  2. Picker.
    • It lists 457 options.
    • With no model chosen, every effort is offered, with the E7 hint word for word.
    • Search ignores case, both by name ("opus latest", "Command A+") and by id ("ANTHROPIC/CLAUDE-OPUS").
    • A search with no match shows "No matches.", and a blur clears it.
  3. Alias. It saves at once and shows "Anthropic: Claude Opus Latest → Anthropic: Claude Opus 5.5 (~anthropic/claude-opus-latest)".
    • The database holds ~anthropic/claude-opus-latest, and it survives a reload.
    • Its efforts run from Model default to Max, with no Off.
  4. Luna Pro, found by id, adds Off. High saves as high.
  5. Pareto: "Model default", plus "High — not offered by this model", selected and flagged. The database still holds high.
    • Cohere offers Model default and "Off — no thinking", with High flagged.
    • A model with mandatory reasoning and no efforts listed offers Model default, with High flagged.
  6. Routing, tier and retention.
    • The labels and hints match, and each pick saves at once.
    • The stored values are nitro, default and true, and they survive a reload.
    • Changing the concurrency to 4 leaves the routing alone.
  7. Dark theme and 390 px.
    • Both themes look right, with no horizontal scroll (scrollWidth 390).
    • After Kagaku.UI #26, options wrap on a narrow menu, so an alias keeps its target and id.
    • The search box is no longer spellchecked.
  8. Offline. The warning reads "The model catalog could not be loaded: Connection refused (127.0.0.1:9). Try again in a moment." with a Try again button.
    • The box shows the bare id, and every effort is offered, with High not flagged.
    • The hint reads "…so this model's efforts cannot be checked."
    • Routing and effort still save.
  9. Blackhole. For 21 s the menu says "Loading…", the box shows the bare id, and the effort is disabled. At 21.4 s the load fails with the timeout sentence.
    • Recovery: with the stub down and then back up, Try again clears the warning.
    • A stored retired/model-x reads "retired/model-x (not in the catalog)", and its effort is flagged.
  10. Found here and fixed in Kagaku.UI #26: the picker opened while the catalog loads.
    • Before the fix, the menu listed only the stored model once the catalog arrived, until you clicked away and reopened it.
    • After the fix, on the live catalog with a cold cache, the picker was clicked during "Loading…" at 681 ms. At 935 ms it listed all 458 models without a blur. The box read "Cohere: Command A+ (cohere/command-a-plus)", highlighted and scrolled into view.
    • A stored model the catalog lacks lists 459 options.
    • The new test A_picker_opened_while_the_catalog_loads_lists_the_whole_catalog_once_it_lands (2 rows) fails against the old Combobox.

Not caused by this branch: at 390 px the app header clips the settings gear and the menu. The branch does not touch the shell.

Verification round (before opening)

The verifier and the browser pass raised 11 points. All are applied, in 9090348 and in Kagaku.UI #26:

  • the two surviving probes above;
  • the picker opened while the catalog loads;
  • ADR 0018's capability list now names the reasoning parameter;
  • a blank alias target;
  • options cut off at 390 px;
  • spellcheck in the search box;
  • the current option not scrolled into view;
  • the PR number check: 6a took #23, so this PR is #24 (500f362).

Review round 1

Jibril approved it (✅). None of its three 💡 needed code here.

After Kagaku.UI #26 merged, 14923e6 moved the submodule to 1ac43db.

  • The build has 0 warnings, and 1146/1146 tests pass.
  • Tsumugi.Integration.Tests failed 2 tests in one full-solution run. The other 8 full-solution runs and a standalone run were green. I couldn't identify the two tests; the pattern fits the known BackupWriter WAL flake, which predates this PR.

🤖 Generated with Claude Code

Slice 7 of Phase 2, second half; 7a was #20. Settings → Defaults now picks the default model from **OpenRouter's catalog**. The catalog loads without a key and is remembered for an hour. The default effort offers **only what the catalog says the chosen model takes**. The tab also stores the **routing defaults** an `llm` node will inherit in slice 8: routing variant, service tier and zero data retention. > **Kagaku.UI #26 is merged** (the Combobox fix this pass found). `14923e6` moves the submodule to its squash commit `1ac43db`, whose tree is identical to the reviewed head `00fbcde`. ## What **`Kagaku.Flow.OpenRouter`** (the package) - **`ILlmGateway.GetCatalogAsync()`:** the catalog of models whose output includes text. - It needs no key. - A loaded catalog is kept for 1 h on the injected clock; a failed load is not kept. - Each attempt has a **10 s** timeout. There are **two** attempts, and a server's `Retry-After` is honoured up to 5 s. - `OpenRouterGatewayOptions` gains `CatalogTimeout`, `CatalogTtl` and `CatalogRetry`. - A disposed gateway refuses the catalog, even one it already holds. - **`ModelCapabilities`:** the one "fails closed" rule (M1–M5). - `TakesReasoning`: the entry lists the `reasoning` parameter, matched case-sensitively. - `AcceptedEfforts`: Off only when the entry says reasoning can be switched off, and a level only when the entry names it. The list is ascending, with no duplicates and no unknown values. - Slice 8's executor will use the same rule, so the picker and the request cannot disagree. **`Tsumugi.UseCases.Settings`** - **`GetModelCatalog`:** returns `CatalogModel(Id, Name, AliasTargetName, Efforts)`, one entry per id, sorted by display text. A `~…-latest` alias reads "Name → target". - **`ReasoningEfforts.Offered(modelId, model)`:** what the effort picker offers. - **`RoutingDefaults`:** carried by `LlmDefaults`. It holds: - `ModelRouting`: Balanced, Fastest `:nitro`, Cheapest `:floor`, Most reliable with tools `:exacto`; - `ServiceTierPreference`: Auto, Standard, Flex, Priority; - zero data retention. - **`RoutingSettings`:** a written table in the node's own spelling: `balanced|nitro|floor|exacto`, `auto|default|flex|priority`, `true|false`. - Parsing ignores case. Anything else, such as a number, `standard` or a blank, reads as the built-in value, field by field. - Saving refuses an undefined value and writes nothing. - **`IModelProvider`** gains its fourth member, the catalog, as 7a announced. **Settings → Defaults** (`DefaultsSection`) - **Model:** a searchable Combobox that shows "Name (id)", so the id can be searched too. A pick saves at once. - A stored id the catalog lacks shows as "id (not in the catalog)". - While the catalog loads, the box shows the bare id. - If the load fails, the tab shows the reason and a **Try again** button. - **Default effort:** the efforts the chosen model takes. - A stored effort the model does not take stays selected, flagged "— not offered by this model". - With no model chosen, every effort is offered. - **Routing**, **Service tier** and **Zero data retention** (a two-option Select), each with its hint. - **The Provider tab's no-key text** gains "The model catalog loads without it." ## Why - **ADR 0018:** - the catalog backs every picker and is cached for 1 h; - effort resolution fails closed; - the routing defaults are inherited at run time; - aliases. - **ADR 0025:** the rule lives in the package, not in Tsumugi. - **ADR 0006:** settings are `AppSetting` rows under keys that are never renamed. - **ADR 0009:** compose Kagaku.UI components; never hand-roll one. ## ADRs and stories amended **ADR 0018:** - **New status line:** "**Amended 2026-09-25 (PR #24, slice 7b):** the catalog load has a 10-second timeout and two attempts; zero data retention is a setting, not a switch; an effort needs the `reasoning` parameter and a named effort." - **The gateway paragraph.** Its parenthesis "(Orihon's finding; a key check gets 30 s, once)" becomes "(…a key check gets 30 s, once, **and a catalog load 10 s, twice**)". - **The capability list.** "(`structured_outputs`, `tools`, **a reasoning block**)" becomes "(`structured_outputs`, `tools`, **the `reasoning` parameter**)". - **Fails closed.** "an effort is sent only when the catalog says the model takes one" gains "**: the model lists the `reasoning` parameter, and its reasoning block names the effort (for `none`, says reasoning can be switched off)**". - **Routing.** "zero-data-retention-only **switch**" becomes "**setting**", twice. **`docs/stories/settings.md`:** - The **Default model and default effort** criterion is ticked. - In the routing criterion, "optionally a Zero data retention only **switch**" becomes "a Zero data retention only **setting**". ## Decisions to confirm 1. **§13.9, "fails closed", comes first.** The ADR 0018 lines quoted above are **provisional until you confirm this**: the status clause, the capability list and the fails-closed sentence. - **M1:** a model takes an effort only when it lists the `reasoning` parameter. A reasoning block alone does not count. Three live models, such as `rekaai/reka-edge`, have the block without the parameter. - **M3/M4:** Off is offered only when the entry says reasoning can be switched off, and a level only when the entry names it. On today's catalog, 141 of the 323 reasoning models offer only Model default, or Model default and Off. Kagura and Orihon offer the full ladder for those. - **If you decline:** revert those ADR lines. Then either change M4 in one place and flip rows 3, 4 and 11, or change M1 and flip row 13. The new case-sensitive `"Reasoning"` row goes too. 2. **§13.27: the effort filter follows the default model, and the bundled templates take the same effort.** - The effort's hint says the templates take the value too, and each sends it only when its own model takes it. - A writer can still pick a model that takes High, choose High, and switch back. The effort stays, flagged. - The alternative: offer every level and flag the ones the default model lacks. That reads "pickers filter" loosely. 3. **§13.7: the stored spellings are the node's vocabulary.** Standard is stored as `default`. This avoids a second `off`/`none` mismatch. 4. **§13.8: zero data retention is a two-option Select**, "Any provider" or "Zero data retention only". - Kagaku.UI has no Switch, and ADR 0009 forbids hand-rolling one. - A toggle group releases to null when clicked again, which suits a filter, not a setting. - An upstream `Switch` is an optional follow-up. 5. **§13.12: timeouts.** The catalog gets 10 s × 2 attempts, because a picker must not wait minutes. - The worst case is about 21 s, or about 42 s for a second caller during a failing load. - Failed loads are not cached. - The 5-minute figure stays with completions, in slice 8. 6. **§13.6: no routing mapping in this slice.** The mapping to `ModelRoutingVariant`/`ServiceTier`/`ProviderPreferences` has no consumer before slice 8's executor. Slice 8 adds `ILlmDefaultsSource` and the mapping. 7. **§13.10: aliases show the catalog's own names**, e.g. "Anthropic: Claude Opus Latest → Anthropic: Claude Opus 5.5". 8. **§13.29: the model id is part of the picker's item text**, because the Combobox filters only by `ItemText`. An upstream `ItemSearchText` is an optional follow-up. 9. **§13.24: the model picker cannot go back to "none chosen".** The Combobox accepts only list values. `BuiltIn` still starts at `""`. **Departures from the brief**, each pinned by a test: 1. The picker shows `Loading` and `LoadError` only while it has no list. A list loaded earlier stays usable during a reload and after a failed one (`A_failed_reload_keeps_the_list_it_had`). 2. The failed-catalog hint under the effort appears only when a model is chosen (`Without_a_model_a_failed_catalog_leaves_the_usual_hint`). 3. A blank catalog name falls back to the id, and a blank alias-target name means no target. 4. The routing test is named `A_hand_edited_routing_row_reads_as_the_built_in_on_its_own`. 5. The catalog-failure theory has 4 rows. A timeout reads "…could not be loaded: Request timed out. Try again in a moment." ## Out of scope - **Slice 8:** - `CompleteAsync`, streaming and the per-key client; - `ILlmDefaultsSource`, and applying routing, tier and ZDR to requests; - the `llm` descriptor and executor; - the second library bump. - **Slices 10a and 12:** the Run button's "no key" and "model left the catalog" notices, and copying the defaults into new nodes. - **Phase 3:** pickers filtered by `structured_outputs`/`tools`, pricing and context length, and per-node pickers. - **Optional Kagaku.UI follow-ups:** a `Switch`, `ItemSearchText`, and `SaveText` on `MaskedSecretField`. - **The seed is unchanged:** the key stays unset, and the routing defaults stay at their built-ins. ## Tests | Project | main (`da9f961`) | this PR | |---|---|---| | Kagaku.Flow.OpenRouter.Tests | 41 | **71** | | Tsumugi.UseCases.Tests | 12 | **40** | | Tsumugi.Integration.Tests | 224 | **247** | | Tsumugi.BlazorAdapter.Tests | 247 | **268** | | Kagaku.Flow.Tests | 483 | 483 | | Tsumugi.Domain.Tests | 37 | 37 | | **Total** | **1044** | **1146** | | GraphEditor (`npm test`) | 14 | 14 | The Release build has 0 warnings under `TreatWarningsAsErrors`. The fixture `models.json` holds six entries, each identical to its entry in a live catalog snapshot. **Coverage** (coverlet, per changed file, lines and branches with `branch="True"`): **all 20 changed source files with executable code are at 100%.** - These include `OpenRouterModelProvider.cs` (54/54 lines, 48/48 branches), `ModelCapabilities`, `OpenRouterGateway`, `RoutingSettings`, `GetModelCatalog`, `DefaultsSection`, and the reducers and effects. - The package's files reach 100% from the package's own tests alone. - The other six changed files have no executable code: `ILlmGateway`, `IModelProvider`, `ModelRouting`, `ServiceTierPreference`, `SettingKeys` and `LoadModelCatalog`. **Mutation probes.** Each was run on a copy, then restored and `touch`ed. **All were killed.** - **§12 probes 42–64** (39 sub-cases): - 42–49: the keyless catalog, TTL and clock, failures not cached, retries and timeout, and M1–M5; - 50–57: name and effort mapping, dedupe and sort, U6, the routing table and its parsing, the save guards, and the per-field fallback; - 58–64: E1–E7 in the tab. - **8 extra probes:** - the catalog client is not disposed; - the `Enum.IsDefined` filter is dropped; - an error is shown over a kept list; - "Loading" is shown on any reload; - the failed-catalog hint is shown with no model; - the Provider tab's catalog clause is dropped; - the reducer drops an earlier list; - a generic failure sentence. - **Re-run after #21/#22 changed the Combobox and Select markup:** 58a, 59a, 59b, 61a, 62 and 63. - **The verifier's own probes.** Four were killed: the id shown twice, one flag text for both cases, a reload that keeps the old error, and a case-sensitive sort. **Two survived:** - deleting the disposed-gateway check while the catalog is cached; - matching `reasoning` case-insensitively. Both now have tests: `A_disposed_gateway_refuses_even_the_catalog_it_already_holds`, and a `"Reasoning"` row. Each test fails under its mutant. ## Browser pass **Setup:** - The Release build ran from a copy with its own data directory. - A local stub served the 2026-09-23 catalog snapshot (457 models), and a second check used the live keyless catalog (458 today). - The Playwright MCP server would not connect in this session, so Node Playwright scripts drove system Chrome. - No page or console errors appeared in any mode. - The screenshots (`b7b-*`, `r1-*`) are kept in the probes directory. **Steps:** 1. **Provider tab and C1.** With no key, the tab reads "No key is set. Runs need one. The model catalog loads without it." - A key was saved through the stub, and the tab showed "$7.50 left of $20.00". The stub then logged the catalog request, which carried **no** key. - The catalog was fetched once across three page loads. 2. **Picker.** - It lists 457 options. - With no model chosen, every effort is offered, with the E7 hint word for word. - Search ignores case, both by name ("opus latest", "Command A+") and by id ("ANTHROPIC/CLAUDE-OPUS"). - A search with no match shows "No matches.", and a blur clears it. 3. **Alias.** It saves at once and shows "Anthropic: Claude Opus Latest → Anthropic: Claude Opus 5.5 (~anthropic/claude-opus-latest)". - The database holds `~anthropic/claude-opus-latest`, and it survives a reload. - Its efforts run from Model default to Max, with no Off. 4. **Luna Pro, found by id,** adds Off. High saves as `high`. 5. **Pareto:** "Model default", plus "High — not offered by this model", selected and flagged. The database still holds `high`. - Cohere offers Model default and "Off — no thinking", with High flagged. - A model with mandatory reasoning and no efforts listed offers Model default, with High flagged. 6. **Routing, tier and retention.** - The labels and hints match, and each pick saves at once. - The stored values are `nitro`, `default` and `true`, and they survive a reload. - Changing the concurrency to 4 leaves the routing alone. 7. **Dark theme and 390 px.** - Both themes look right, with no horizontal scroll (scrollWidth 390). - After Kagaku.UI #26, options wrap on a narrow menu, so an alias keeps its target and id. - The search box is no longer spellchecked. 8. **Offline.** The warning reads "The model catalog could not be loaded: Connection refused (127.0.0.1:9). Try again in a moment." with a **Try again** button. - The box shows the bare id, and every effort is offered, with High not flagged. - The hint reads "…so this model's efforts cannot be checked." - Routing and effort still save. 9. **Blackhole.** For 21 s the menu says "Loading…", the box shows the bare id, and the effort is disabled. At 21.4 s the load fails with the timeout sentence. - Recovery: with the stub down and then back up, **Try again** clears the warning. - A stored `retired/model-x` reads "retired/model-x (not in the catalog)", and its effort is flagged. 10. **Found here and fixed in Kagaku.UI #26: the picker opened while the catalog loads.** - Before the fix, the menu listed only the stored model once the catalog arrived, until you clicked away and reopened it. - After the fix, on the live catalog with a cold cache, the picker was clicked during "Loading…" at 681 ms. At 935 ms it listed **all 458 models** without a blur. The box read "Cohere: Command A+ (cohere/command-a-plus)", highlighted and scrolled into view. - A stored model the catalog lacks lists 459 options. - The new test `A_picker_opened_while_the_catalog_loads_lists_the_whole_catalog_once_it_lands` (2 rows) fails against the old Combobox. **Not caused by this branch:** at 390 px the app header clips the settings gear and the menu. The branch does not touch the shell. ## Verification round (before opening) The verifier and the browser pass raised 11 points. All are applied, in `9090348` and in Kagaku.UI #26: - the two surviving probes above; - the picker opened while the catalog loads; - ADR 0018's capability list now names the `reasoning` parameter; - a blank alias target; - options cut off at 390 px; - spellcheck in the search box; - the current option not scrolled into view; - the PR number check: 6a took #23, so this PR is #24 (`500f362`). ## Review round 1 Jibril approved it (✅). None of its three 💡 needed code here. After Kagaku.UI #26 merged, `14923e6` moved the submodule to `1ac43db`. - The build has 0 warnings, and 1146/1146 tests pass. - Tsumugi.Integration.Tests failed 2 tests in one full-solution run. The other 8 full-solution runs and a standalone run were green. I couldn't identify the two tests; the pattern fits the known `BackupWriter` WAL flake, which predates this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ILlmGateway gains GetCatalogAsync, the unfiltered GET /models: at OpenRouter.Net c9b58ba that listing is the
models whose output includes text, which is what an llm picker wants, so the filtered overload the library
gained in its #17 is not used. The catalog is public, so the gateway builds one keyless HttpClient for it in
its constructor, over the same handler as the key checks, and wraps it in the library's
CachingOpenRouterClient for the gateway's lifetime. A key check never touches it, so a key change neither
sends a key to /models nor throws the cache away.

A success is served from memory for an hour on the gateway's clock and asked again at exactly the hour; a
failure is not remembered. One load attempt has a 10-second HTTP timeout and there are two attempts, with a
server's Retry-After honoured up to 5 s, so a catalog that cannot be loaded says so after about 21 s. The
timeout sits on the HttpClient for the same reason as the key check's: the library reports its expiry as a
Timeout failure, while a cancelled caller token throws. The backoff runs on the real clock while the cache
ages on the injected one; a test's frozen clock would otherwise hold a retry forever, and a test with a 10 ms
backoff under a frozen clock pins it. Disposing the gateway disposes the catalog's HttpClient and the cache's
fill gates whoever owns the handler.

ModelCapabilities is the one rule for what a model takes, so the Defaults picker and the llm node's effort
resolution in slice 8 cannot disagree. Effort resolution fails closed (ADR 0018): a model takes reasoning only
when it lists the reasoning parameter, since a reasoning block alone does not say an effort arrives; Off only
when its entry says reasoning can be switched off, and never for a mandatory one; a level only when the entry
names it, so an entry without supported_efforts takes no level. The answer is ascending and distinct, with
Unknown dropped, and so is a number, which the library's tolerant enum reader turns into a value outside the
enum. The tests read six entries copied verbatim from the live catalog (2026-09-23), one per shape.

RetryOptions is a class that compares by reference, so the options record no longer equals a fresh default
instance; the registration test compares around it. The integration tests' scripted gateway answers the
catalog from a settable result.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
IModelProvider gains the catalog as Tsumugi DTOs. Infrastructure maps each library entry to a CatalogModel:
its id, its name (the id when the catalog gives none or a blank one), the name of the model a ~…-latest alias
routes to, and the efforts the package's ModelCapabilities says it takes, translated into Tsumugi's spelling
through a table (the library's None is Tsumugi's Off; an effort the library does not know is dropped). A
failed load becomes one sentence, "The model catalog could not be loaded: …", with the same detail rule as
the key check's, so a proxy's HTML page never reaches the writer, and the transient suffix when the library
calls it transient. GetModelCatalog keeps each id once, the first entry winning, because the picker keys its
options by id and a repeated key breaks the renderer; it orders by what the picker shows, ignoring case,
then by id.

The routing defaults are install-wide settings an llm node inherits at run time (ADR 0018); this slice
stores them and slice 8 applies them. ModelRouting and ServiceTierPreference are stored through a written
table in the node's own vocabulary, balanced/nitro/floor/exacto and auto/default/flex/priority, so Standard
is stored as "default" and no second off/none mismatch appears. Parsing reads the table only, ignoring case:
a number, which Enum.TryParse would accept, or the label "standard" reads as the built-in. Zero data
retention is stored as true or false. LlmDefaults takes the routing as a fifth positional member, so every
construction names it and a record rebuilt from its other fields cannot silently reset it. SaveLlmDefaults
refuses an undefined routing or tier and writes nothing, and writes all seven rows otherwise;
GetLlmDefaults reads each row on its own, so one hand-edited row falls back alone.

The Defaults tab's model is a Combobox over the catalog, which loads without a key. The option text carries
the id ("Name (id)", the id once when the name is the id, "Alias → Target (id)" for an alias), because the
Combobox filters by its item text and a writer may know a model by either. A pick saves at once. A stored id
the picker cannot show as a catalog entry stays selected as a stand-in item: bare while the catalog loads
or after it failed, "(not in the catalog)" once a loaded catalog lacks it. The effort list follows the
chosen model through ReasoningEfforts.Offered: every effort with no model, only Model default for a model
the catalog does not list, since effort resolution fails closed, otherwise Model default and what the model
takes. A stored effort outside that list is never changed behind the writer's back: it stays chosen as an
extra option and is flagged under the field. While the first load runs the effort Select waits; a failed
load shows the sentence with "Try again", offers every effort and says the model's efforts cannot be
checked. A catalog loaded earlier stays in use while a reload runs and after it fails, so the picker's
Loading and LoadError apply only while there is no list. A new Routing card holds Routing, Service tier and
Data retention, a two-option Select because Kagaku.UI has no switch; each saves the whole record at once,
so a pick carries a pending typed edit and a concurrency edit keeps the routing. The Provider tab's no-key
line adds that the catalog loads without a key.

The ReasoningEfforts summary no longer claims the stored strings are OpenRouter's: they are Tsumugi's, and
each consumer translates through a table, the first being the catalog's. ADR 0018 is amended: the catalog
load has a 10-second timeout and two attempts, zero data retention is a setting rather than a switch, and
"fails closed" is defined as the reasoning parameter plus a named effort. settings.md ticks the default
model and effort and amends the switch; the routing line stays open until an llm node uses the defaults.

The model picker only takes list values, so the test that cleared the model field goes, and the test that a
pending edit is flushed on leaving the tab now types a concurrency, since model picks no longer wait.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The model picker opened while the catalog was still loading showed only the stored model once the
catalog arrived, and kept the bare id in the box, until the writer clicked away and opened it
again. The stored model's text changes when the catalog lands (the bare id becomes "Name (id)" or
"id (not in the catalog)"), and the Kagaku.UI Combobox took the leftover id for a typed filter.
The fix is in Kagaku.UI (branch fix/combobox-own-text, b52a980, on its main 3aaa6a5): the
Combobox remembers whether the box holds its own text or what the user typed, so its own text
never filters and follows the selection while the menu is open. The same commit scrolls the
current option into view when the menu opens, turns spellcheck off in the box, gives each option
its whole text as a title and lets options wrap on a phone-width menu. The pointer moves to that
commit; it has to be repointed at the merged Kagaku.UI main before this branch merges. A Defaults
test now opens the picker while the catalog gate is closed and releases it without blurring, for
a model the catalog lists and one it lacks.

The rest are gaps the verification found. A disposed gateway that had already loaded the catalog
now has a test that it still refuses it: the cached list answers before the catalog client's own
disposed gate, so the gateway's check is the only guard. The capability rule's ordinal match of
the reasoning parameter is pinned by a row with another spelling. A blank alias target name no
longer shows as "Name → " in the picker; it maps to no target, like a missing one. ADR 0018's
capability list now names the reasoning parameter rather than a reasoning block, in step with the
fails-closed definition two lines below it (both still wait on the owner's confirmation).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs: slice 7b is PR #24
All checks were successful
CI / build (pull_request) Successful in 30s
CI / test (pull_request) Successful in 50s
500f36219f
Slice 6a took #23, so the ADR 0018 amendment line and the plan's slice-7 line name #24. The fails-closed
sentence in ADR 0018 is rewrapped to the file's width.

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: a493b46 · Generated: 2026-09-25 16:11:06 UTC · Revision: #3

Summary

Summary
Generated on: 09/25/2026 - 16:11:06
Coverage date: 09/25/2026 - 16:10:50 - 09/25/2026 - 16:11:03
Parser: MultiReport (6x Cobertura)
Assemblies: 9
Classes: 350
Files: 339
Line coverage: 98% (8225 of 8386)
Covered lines: 8225
Uncovered lines: 161
Coverable lines: 8386
Total lines: 18321
Branch coverage: 93.7% (2618 of 2793)
Covered branches: 2618
Total branches: 2793
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.Flow - 99.5%
Name Line Branch
Kagaku.Flow 99.5% 98.9%
Kagaku.Flow.Budgeting.BudgetRequest 100%
Kagaku.Flow.Budgeting.ConsumerBudget 100%
Kagaku.Flow.FlowJson 100%
Kagaku.Flow.Graphs.GraphDefinition 100%
Kagaku.Flow.Graphs.GraphDocument 100% 100%
Kagaku.Flow.Graphs.GraphEdge 100%
Kagaku.Flow.Graphs.GraphIndex 100% 100%
Kagaku.Flow.Graphs.GraphLoadError 100%
Kagaku.Flow.Graphs.GraphLoadResult 100%
Kagaku.Flow.Graphs.GraphNode 100%
Kagaku.Flow.Graphs.GraphPosition 100%
Kagaku.Flow.Graphs.GraphReader 100% 100%
Kagaku.Flow.Graphs.GraphSchema 100% 100%
Kagaku.Flow.Graphs.GraphShapeException 100%
Kagaku.Flow.Graphs.GraphWriter 100% 100%
Kagaku.Flow.Graphs.SocketRef 100%
Kagaku.Flow.Graphs.WireText 100% 100%
Kagaku.Flow.Nodes.CanvasDescriptorWriter 100% 100%
Kagaku.Flow.Nodes.ConfigProblem 100%
Kagaku.Flow.Nodes.Names 100%
Kagaku.Flow.Nodes.NodeDescriptor 100% 100%
Kagaku.Flow.Nodes.NodeRegistry 100% 100%
Kagaku.Flow.Nodes.NodeSockets 100% 100%
Kagaku.Flow.Nodes.SinkDescriptor 100%
Kagaku.Flow.Nodes.SocketTypes 100% 100%
Kagaku.Flow.Nodes.SourceDescriptor 100%
Kagaku.Flow.Nodes.TemplateField 100%
Kagaku.Flow.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.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.OpenRouterGateway 100% 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions 100%
Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup 100% 100%
Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions 100% 100%
Kagaku.Flow.Testing - 100%
Name Line Branch
Kagaku.Flow.Testing 100% 100%
Kagaku.Flow.Testing.InMemoryRunLedger 100% 100%
Kagaku.Flow.Testing.RunLedgerContract 100% 100%
Kagaku.Flow.Testing.RunLedgerContractException 100%
Tsumugi.BlazorAdapter - 97.2%
Name Line Branch
Tsumugi.BlazorAdapter 97.2% 94.2%
Tsumugi.BlazorAdapter.BlazorAdapterAssembly 100%
Tsumugi.BlazorAdapter.Documents.CategoryCreated 100%
Tsumugi.BlazorAdapter.Documents.CategoryEditor 97.7% 86.1%
Tsumugi.BlazorAdapter.Documents.CategorySaved 100%
Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog 100% 100%
Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested 100%
Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentCreated 100%
Tsumugi.BlazorAdapter.Documents.DocumentEditor 100% 87.9%
Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed 100%
Tsumugi.BlazorAdapter.Documents.DocumentSaved 100%
Tsumugi.BlazorAdapter.Documents.DocumentsEffects 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsList 98.2% 92.4%
Tsumugi.BlazorAdapter.Documents.DocumentsLoaded 100%
Tsumugi.BlazorAdapter.Documents.DocumentsPage 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsReducers 100% 100%
Tsumugi.BlazorAdapter.Documents.DocumentsState 100% 100%
Tsumugi.BlazorAdapter.Documents.LoadDocuments 100%
Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested 100%
Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested 100%
Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed 100%
Tsumugi.BlazorAdapter.Documents.SetDocumentsFilter 100%
Tsumugi.BlazorAdapter.Documents.UpdateCategoryRequested 100%
Tsumugi.BlazorAdapter.Documents.UpdateDocumentRequested 100%
Tsumugi.BlazorAdapter.Graphs.GraphCanvas 37.5% 25%
Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage 20% 25%
Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge 94.4% 83.3%
Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived 75% 100%
Tsumugi.BlazorAdapter.Runs.RunChangedBridge 97.1% 90.6%
Tsumugi.BlazorAdapter.Runs.RunChangesReceived 100%
Tsumugi.BlazorAdapter.Runs.RunTextReceived 100%
Tsumugi.BlazorAdapter.Settings.BackupCompleted 100%
Tsumugi.BlazorAdapter.Settings.BackupFailed 100%
Tsumugi.BlazorAdapter.Settings.BackupNowRequested 100%
Tsumugi.BlazorAdapter.Settings.BackupSettingsLoaded 100%
Tsumugi.BlazorAdapter.Settings.BackupsSection 100% 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.BackupsSettingsState 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSection 100% 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsEffects 100% 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsReducers 100%
Tsumugi.BlazorAdapter.Settings.DefaultsSettingsState 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsLoaded 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaved 100%
Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaveFailed 100%
Tsumugi.BlazorAdapter.Settings.LoadBackupSettings 100%
Tsumugi.BlazorAdapter.Settings.LoadProviderSettings 100%
Tsumugi.BlazorAdapter.Settings.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 - 99.3%
Name Line Branch
Tsumugi.Domain 99.3% 95.6%
Tsumugi.Domain.Documents.Document 100% 100%
Tsumugi.Domain.Documents.DocumentCategory 100%
Tsumugi.Domain.Journal.ChangeLogEntry 97.8% 75%
Tsumugi.Domain.Journal.StorySnapshot 95.4% 83.3%
Tsumugi.Domain.Settings.AppSetting 100%
Tsumugi.Domain.Stories.Chapter 100% 100%
Tsumugi.Domain.Stories.Provenance 100% 100%
Tsumugi.Domain.Stories.Segment 100% 100%
Tsumugi.Domain.Stories.Story 100% 100%
Tsumugi.Domain.Text 100% 100%
Tsumugi.Infrastructure - 97.2%
Name Line Branch
Tsumugi.Infrastructure 97.2% 84.6%
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.4% 92.5%
Tsumugi.Infrastructure.Backups.FileSystemBackupStore 100% 100%
Tsumugi.Infrastructure.Backups.SqliteSnapshot 87.8% 75%
Tsumugi.Infrastructure.DependencyInjection 100% 100%
Tsumugi.Infrastructure.Journal.EfChangeJournal 100%
Tsumugi.Infrastructure.Journal.EfSnapshotStore 100%
Tsumugi.Infrastructure.Journal.EfUndoStore 98.1% 85.7%
Tsumugi.Infrastructure.Journal.ModelExtensions 100%
Tsumugi.Infrastructure.Journal.OperationContext 100% 100%
Tsumugi.Infrastructure.Journal.PropertyBag 100% 95.4%
Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus 100% 100%
Tsumugi.Infrastructure.Persistence.Configurations.AppSettingConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.ChangeLogEntryConfigurati
on
100%
Tsumugi.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.DocumentCategoryConfigura
tion
100%
Tsumugi.Infrastructure.Persistence.Configurations.DocumentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration 100%
Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio
n
100%
Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter 100% 90%
Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings 99.2%
Tsumugi.Infrastructure.Persistence.Migrations.AddJournal 98.8%
Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments 100%
Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain 97.3%
Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot 100%
Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore 100% 100%
Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore 97.5% 90%
Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore 95% 84.6%
Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore 98.3% 92.8%
Tsumugi.Infrastructure.Persistence.TsumugiDbContext 83.5% 93.1%
Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory 0%
Tsumugi.Infrastructure.Provider.OpenRouterKeySource 100% 100%
Tsumugi.Infrastructure.Provider.OpenRouterModelProvider 100% 100%
Tsumugi.Infrastructure.Provider.OpenRouterOptions 100% 100%
Tsumugi.Infrastructure.Provider.ResolvedKey 100%
Tsumugi.Infrastructure.Runs.InProcessRunEventBus 100% 100%
Tsumugi.Infrastructure.Settings.EfAppSettingsStore 100% 100%
Tsumugi.Kernel - 90.9%
Name Line Branch
Tsumugi.Kernel 90.9% 75%
Tsumugi.Kernel.Err`1 100%
Tsumugi.Kernel.Ok`1 100%
Tsumugi.Kernel.Result`1 88.8% 75%
Tsumugi.Server - 100%
Name Line Branch
Tsumugi.Server 100% 86.4%
Program 100% 100%
Tsumugi.Server.Backups.BackupConfiguration 100% 100%
Tsumugi.Server.Components.App 100%
Tsumugi.Server.Components.Layout.AppHeader 100% 50%
Tsumugi.Server.Components.Layout.MainLayout 100%
Tsumugi.Server.Components.Pages.Gate 100% 100%
Tsumugi.Server.Provider.OpenRouterConfiguration 100% 100%
Tsumugi.Server.Security.AccessGate 100% 66.6%
Tsumugi.Server.Security.AccessSecret 100% 50%
Tsumugi.Server.VolumeStartupValidator 100% 100%
Tsumugi.UseCases - 98.2%
Name Line Branch
Tsumugi.UseCases 98.2% 94.8%
Tsumugi.UseCases.Backups.BackupArtifact 100%
Tsumugi.UseCases.Backups.BackupPolicy 100%
Tsumugi.UseCases.Backups.BackupStatusView 100% 100%
Tsumugi.UseCases.Backups.DatabaseSnapshot 100%
Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot 100%
Tsumugi.UseCases.Backups.GetBackupStatus 100%
Tsumugi.UseCases.Backups.RunBackupNow 100%
Tsumugi.UseCases.Chapters.ChapterDto 100%
Tsumugi.UseCases.Chapters.CreateChapter 100% 100%
Tsumugi.UseCases.Chapters.DeleteChapter 100% 100%
Tsumugi.UseCases.Chapters.GetManuscript 100% 100%
Tsumugi.UseCases.Chapters.ManuscriptChapter 100%
Tsumugi.UseCases.Chapters.ManuscriptView 100%
Tsumugi.UseCases.Chapters.MergeChapters 100% 100%
Tsumugi.UseCases.Chapters.RenameChapter 100% 100%
Tsumugi.UseCases.Chapters.ReorderChapters 100% 100%
Tsumugi.UseCases.Chapters.SetChapterSummary 100% 75%
Tsumugi.UseCases.DependencyInjection 100%
Tsumugi.UseCases.Diagnostics.SeedDevData 97.3% 70%
Tsumugi.UseCases.Documents.CreateCategory 100% 100%
Tsumugi.UseCases.Documents.CreateDocument 100% 100%
Tsumugi.UseCases.Documents.DeleteCategory 100% 100%
Tsumugi.UseCases.Documents.DeleteDocument 100% 100%
Tsumugi.UseCases.Documents.DocumentCategoryDto 100%
Tsumugi.UseCases.Documents.DocumentCategoryProfile 100%
Tsumugi.UseCases.Documents.DocumentDto 100%
Tsumugi.UseCases.Documents.DocumentProfile 100%
Tsumugi.UseCases.Documents.DocumentsCategory 100%
Tsumugi.UseCases.Documents.DocumentsView 100%
Tsumugi.UseCases.Documents.GetDocuments 100% 100%
Tsumugi.UseCases.Documents.MoveDocuments 100% 100%
Tsumugi.UseCases.Documents.ReorderCategories 100% 100%
Tsumugi.UseCases.Documents.UpdateCategory 100% 100%
Tsumugi.UseCases.Documents.UpdateDocument 100% 100%
Tsumugi.UseCases.Journal.GetStoryHistory 100% 100%
Tsumugi.UseCases.Journal.GetUndoStatus 100% 100%
Tsumugi.UseCases.Journal.HistoryOperation 66.6%
Tsumugi.UseCases.Journal.HistorySnapshot 87.5%
Tsumugi.UseCases.Journal.OperationGrouping 100% 100%
Tsumugi.UseCases.Journal.RedoRevert 100% 100%
Tsumugi.UseCases.Journal.RedoRun 75% 57.1%
Tsumugi.UseCases.Journal.RedoTyping 100% 100%
Tsumugi.UseCases.Journal.RevertConflict 100%
Tsumugi.UseCases.Journal.RevertOutcome 100%
Tsumugi.UseCases.Journal.RevertRun 96.5% 77.7%
Tsumugi.UseCases.Journal.RevertToSnapshot 92.3% 83.3%
Tsumugi.UseCases.Journal.StoryHistory 100%
Tsumugi.UseCases.Journal.TakeSnapshot 100% 87.5%
Tsumugi.UseCases.Journal.UndoStatus 100%
Tsumugi.UseCases.Journal.UndoTyping 100% 100%
Tsumugi.UseCases.Journal.UserOperation 100%
Tsumugi.UseCases.Notifications.DomainChanged 100%
Tsumugi.UseCases.Ordering 100% 100%
Tsumugi.UseCases.Runs.RunEvent 100%
Tsumugi.UseCases.Segments.DeleteSegments 100% 100%
Tsumugi.UseCases.Segments.EditSegment 100% 83.3%
Tsumugi.UseCases.Segments.InsertSegment 100% 100%
Tsumugi.UseCases.Segments.MergeSegmentWithPrevious 100% 100%
Tsumugi.UseCases.Segments.MoveSegments 100% 100%
Tsumugi.UseCases.Segments.ReorderSegments 100% 100%
Tsumugi.UseCases.Segments.SegmentDto 100%
Tsumugi.UseCases.Segments.SplitSegment 100% 100%
Tsumugi.UseCases.Settings.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:** `a493b46` · **Generated:** 2026-09-25 16:11:06 UTC · **Revision:** #3 # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/25/2026 - 16:11:06 | | Coverage date: | 09/25/2026 - 16:10:50 - 09/25/2026 - 16:11:03 | | Parser: | MultiReport (6x Cobertura) | | Assemblies: | 9 | | Classes: | 350 | | Files: | 339 | | **Line coverage:** | 98% (8225 of 8386) | | Covered lines: | 8225 | | Uncovered lines: | 161 | | Coverable lines: | 8386 | | Total lines: | 18321 | | **Branch coverage:** | 93.7% (2618 of 2793) | | Covered branches: | 2618 | | Total branches: | 2793 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.Flow - 99.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow**|**99.5%**|**98.9%**| |Kagaku.Flow.Budgeting.BudgetRequest|100%|| |Kagaku.Flow.Budgeting.ConsumerBudget|100%|| |Kagaku.Flow.FlowJson|100%|| |Kagaku.Flow.Graphs.GraphDefinition|100%|| |Kagaku.Flow.Graphs.GraphDocument|100%|100%| |Kagaku.Flow.Graphs.GraphEdge|100%|| |Kagaku.Flow.Graphs.GraphIndex|100%|100%| |Kagaku.Flow.Graphs.GraphLoadError|100%|| |Kagaku.Flow.Graphs.GraphLoadResult|100%|| |Kagaku.Flow.Graphs.GraphNode|100%|| |Kagaku.Flow.Graphs.GraphPosition|100%|| |Kagaku.Flow.Graphs.GraphReader|100%|100%| |Kagaku.Flow.Graphs.GraphSchema|100%|100%| |Kagaku.Flow.Graphs.GraphShapeException|100%|| |Kagaku.Flow.Graphs.GraphWriter|100%|100%| |Kagaku.Flow.Graphs.SocketRef|100%|| |Kagaku.Flow.Graphs.WireText|100%|100%| |Kagaku.Flow.Nodes.CanvasDescriptorWriter|100%|100%| |Kagaku.Flow.Nodes.ConfigProblem|100%|| |Kagaku.Flow.Nodes.Names|100%|| |Kagaku.Flow.Nodes.NodeDescriptor|100%|100%| |Kagaku.Flow.Nodes.NodeRegistry|100%|100%| |Kagaku.Flow.Nodes.NodeSockets|100%|100%| |Kagaku.Flow.Nodes.SinkDescriptor|100%|| |Kagaku.Flow.Nodes.SocketTypes|100%|100%| |Kagaku.Flow.Nodes.SourceDescriptor|100%|| |Kagaku.Flow.Nodes.TemplateField|100%|| |Kagaku.Flow.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.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.OpenRouterGateway|100%|100%| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterGatewayOptions|100%|| |Kagaku.Flow.OpenRouter.Gateway.OpenRouterKeyLookup|100%|100%| |Kagaku.Flow.OpenRouter.OpenRouterServiceCollectionExtensions|100%|100%| </details> <details><summary>Kagaku.Flow.Testing - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.Flow.Testing**|**100%**|**100%**| |Kagaku.Flow.Testing.InMemoryRunLedger|100%|100%| |Kagaku.Flow.Testing.RunLedgerContract|100%|100%| |Kagaku.Flow.Testing.RunLedgerContractException|100%|| </details> <details><summary>Tsumugi.BlazorAdapter - 97.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.BlazorAdapter**|**97.2%**|**94.2%**| |Tsumugi.BlazorAdapter.BlazorAdapterAssembly|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryCreated|100%|| |Tsumugi.BlazorAdapter.Documents.CategoryEditor|97.7%|86.1%| |Tsumugi.BlazorAdapter.Documents.CategorySaved|100%|| |Tsumugi.BlazorAdapter.Documents.CreateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.CreateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryDialog|100%|100%| |Tsumugi.BlazorAdapter.Documents.DeleteCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DeleteDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentAutoSaveFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentCreated|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentEditor|100%|87.9%| |Tsumugi.BlazorAdapter.Documents.DocumentsActionFailed|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentSaved|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsEffects|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsList|98.2%|92.4%| |Tsumugi.BlazorAdapter.Documents.DocumentsLoaded|100%|| |Tsumugi.BlazorAdapter.Documents.DocumentsPage|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsReducers|100%|100%| |Tsumugi.BlazorAdapter.Documents.DocumentsState|100%|100%| |Tsumugi.BlazorAdapter.Documents.LoadDocuments|100%|| |Tsumugi.BlazorAdapter.Documents.MoveDocumentsRequested|100%|| |Tsumugi.BlazorAdapter.Documents.ReorderCategoriesRequested|100%|| |Tsumugi.BlazorAdapter.Documents.SetCategoryCollapsed|100%|| |Tsumugi.BlazorAdapter.Documents.SetDocumentsFilter|100%|| |Tsumugi.BlazorAdapter.Documents.UpdateCategoryRequested|100%|| |Tsumugi.BlazorAdapter.Documents.UpdateDocumentRequested|100%|| |Tsumugi.BlazorAdapter.Graphs.GraphCanvas|37.5%|25%| |Tsumugi.BlazorAdapter.Graphs.GraphPlaygroundPage|20%|25%| |Tsumugi.BlazorAdapter.Notifications.DomainChangedBridge|94.4%|83.3%| |Tsumugi.BlazorAdapter.Notifications.DomainChangesReceived|75%|100%| |Tsumugi.BlazorAdapter.Runs.RunChangedBridge|97.1%|90.6%| |Tsumugi.BlazorAdapter.Runs.RunChangesReceived|100%|| |Tsumugi.BlazorAdapter.Runs.RunTextReceived|100%|| |Tsumugi.BlazorAdapter.Settings.BackupCompleted|100%|| |Tsumugi.BlazorAdapter.Settings.BackupFailed|100%|| |Tsumugi.BlazorAdapter.Settings.BackupNowRequested|100%|| |Tsumugi.BlazorAdapter.Settings.BackupSettingsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.BackupsSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.BackupsSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.DefaultsSection|100%|100%| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsEffects|100%|100%| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsReducers|100%|| |Tsumugi.BlazorAdapter.Settings.DefaultsSettingsState|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsLoaded|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaved|100%|| |Tsumugi.BlazorAdapter.Settings.LlmDefaultsSaveFailed|100%|| |Tsumugi.BlazorAdapter.Settings.LoadBackupSettings|100%|| |Tsumugi.BlazorAdapter.Settings.LoadProviderSettings|100%|| |Tsumugi.BlazorAdapter.Settings.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 - 99.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Domain**|**99.3%**|**95.6%**| |Tsumugi.Domain.Documents.Document|100%|100%| |Tsumugi.Domain.Documents.DocumentCategory|100%|| |Tsumugi.Domain.Journal.ChangeLogEntry|97.8%|75%| |Tsumugi.Domain.Journal.StorySnapshot|95.4%|83.3%| |Tsumugi.Domain.Settings.AppSetting|100%|| |Tsumugi.Domain.Stories.Chapter|100%|100%| |Tsumugi.Domain.Stories.Provenance|100%|100%| |Tsumugi.Domain.Stories.Segment|100%|100%| |Tsumugi.Domain.Stories.Story|100%|100%| |Tsumugi.Domain.Text|100%|100%| </details> <details><summary>Tsumugi.Infrastructure - 97.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Infrastructure**|**97.2%**|**84.6%**| |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.4%|92.5%| |Tsumugi.Infrastructure.Backups.FileSystemBackupStore|100%|100%| |Tsumugi.Infrastructure.Backups.SqliteSnapshot|87.8%|75%| |Tsumugi.Infrastructure.DependencyInjection|100%|100%| |Tsumugi.Infrastructure.Journal.EfChangeJournal|100%|| |Tsumugi.Infrastructure.Journal.EfSnapshotStore|100%|| |Tsumugi.Infrastructure.Journal.EfUndoStore|98.1%|85.7%| |Tsumugi.Infrastructure.Journal.ModelExtensions|100%|| |Tsumugi.Infrastructure.Journal.OperationContext|100%|100%| |Tsumugi.Infrastructure.Journal.PropertyBag|100%|95.4%| |Tsumugi.Infrastructure.Notifications.InProcessDomainChangedBus|100%|100%| |Tsumugi.Infrastructure.Persistence.Configurations.AppSettingConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.ChangeLogEntryConfigurati<br/>on|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.DocumentCategoryConfigura<br/>tion|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.DocumentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.SegmentConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StoryConfiguration|100%|| |Tsumugi.Infrastructure.Persistence.Configurations.StorySnapshotConfiguratio<br/>n|100%|| |Tsumugi.Infrastructure.Persistence.Converters.ProvenanceConverter|100%|90%| |Tsumugi.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddAppSettings|99.2%|| |Tsumugi.Infrastructure.Persistence.Migrations.AddJournal|98.8%|| |Tsumugi.Infrastructure.Persistence.Migrations.GenericDocuments|100%|| |Tsumugi.Infrastructure.Persistence.Migrations.InitialStoryDomain|97.3%|| |Tsumugi.Infrastructure.Persistence.Migrations.TsumugiDbContextModelSnapshot|100%|| |Tsumugi.Infrastructure.Persistence.SqlitePragmaInterceptor|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfChapterStore|100%|100%| |Tsumugi.Infrastructure.Persistence.Stores.EfDocumentStore|97.5%|90%| |Tsumugi.Infrastructure.Persistence.Stores.EfSegmentStore|95%|84.6%| |Tsumugi.Infrastructure.Persistence.Stores.EfStoryStore|98.3%|92.8%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContext|83.5%|93.1%| |Tsumugi.Infrastructure.Persistence.TsumugiDbContextFactory|0%|| |Tsumugi.Infrastructure.Provider.OpenRouterKeySource|100%|100%| |Tsumugi.Infrastructure.Provider.OpenRouterModelProvider|100%|100%| |Tsumugi.Infrastructure.Provider.OpenRouterOptions|100%|100%| |Tsumugi.Infrastructure.Provider.ResolvedKey|100%|| |Tsumugi.Infrastructure.Runs.InProcessRunEventBus|100%|100%| |Tsumugi.Infrastructure.Settings.EfAppSettingsStore|100%|100%| </details> <details><summary>Tsumugi.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Kernel**|**90.9%**|**75%**| |Tsumugi.Kernel.Err`1|100%|| |Tsumugi.Kernel.Ok`1|100%|| |Tsumugi.Kernel.Result`1|88.8%|75%| </details> <details><summary>Tsumugi.Server - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.Server**|**100%**|**86.4%**| |Program|100%|100%| |Tsumugi.Server.Backups.BackupConfiguration|100%|100%| |Tsumugi.Server.Components.App|100%|| |Tsumugi.Server.Components.Layout.AppHeader|100%|50%| |Tsumugi.Server.Components.Layout.MainLayout|100%|| |Tsumugi.Server.Components.Pages.Gate|100%|100%| |Tsumugi.Server.Provider.OpenRouterConfiguration|100%|100%| |Tsumugi.Server.Security.AccessGate|100%|66.6%| |Tsumugi.Server.Security.AccessSecret|100%|50%| |Tsumugi.Server.VolumeStartupValidator|100%|100%| </details> <details><summary>Tsumugi.UseCases - 98.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Tsumugi.UseCases**|**98.2%**|**94.8%**| |Tsumugi.UseCases.Backups.BackupArtifact|100%|| |Tsumugi.UseCases.Backups.BackupPolicy|100%|| |Tsumugi.UseCases.Backups.BackupStatusView|100%|100%| |Tsumugi.UseCases.Backups.DatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.DownloadDatabaseSnapshot|100%|| |Tsumugi.UseCases.Backups.GetBackupStatus|100%|| |Tsumugi.UseCases.Backups.RunBackupNow|100%|| |Tsumugi.UseCases.Chapters.ChapterDto|100%|| |Tsumugi.UseCases.Chapters.CreateChapter|100%|100%| |Tsumugi.UseCases.Chapters.DeleteChapter|100%|100%| |Tsumugi.UseCases.Chapters.GetManuscript|100%|100%| |Tsumugi.UseCases.Chapters.ManuscriptChapter|100%|| |Tsumugi.UseCases.Chapters.ManuscriptView|100%|| |Tsumugi.UseCases.Chapters.MergeChapters|100%|100%| |Tsumugi.UseCases.Chapters.RenameChapter|100%|100%| |Tsumugi.UseCases.Chapters.ReorderChapters|100%|100%| |Tsumugi.UseCases.Chapters.SetChapterSummary|100%|75%| |Tsumugi.UseCases.DependencyInjection|100%|| |Tsumugi.UseCases.Diagnostics.SeedDevData|97.3%|70%| |Tsumugi.UseCases.Documents.CreateCategory|100%|100%| |Tsumugi.UseCases.Documents.CreateDocument|100%|100%| |Tsumugi.UseCases.Documents.DeleteCategory|100%|100%| |Tsumugi.UseCases.Documents.DeleteDocument|100%|100%| |Tsumugi.UseCases.Documents.DocumentCategoryDto|100%|| |Tsumugi.UseCases.Documents.DocumentCategoryProfile|100%|| |Tsumugi.UseCases.Documents.DocumentDto|100%|| |Tsumugi.UseCases.Documents.DocumentProfile|100%|| |Tsumugi.UseCases.Documents.DocumentsCategory|100%|| |Tsumugi.UseCases.Documents.DocumentsView|100%|| |Tsumugi.UseCases.Documents.GetDocuments|100%|100%| |Tsumugi.UseCases.Documents.MoveDocuments|100%|100%| |Tsumugi.UseCases.Documents.ReorderCategories|100%|100%| |Tsumugi.UseCases.Documents.UpdateCategory|100%|100%| |Tsumugi.UseCases.Documents.UpdateDocument|100%|100%| |Tsumugi.UseCases.Journal.GetStoryHistory|100%|100%| |Tsumugi.UseCases.Journal.GetUndoStatus|100%|100%| |Tsumugi.UseCases.Journal.HistoryOperation|66.6%|| |Tsumugi.UseCases.Journal.HistorySnapshot|87.5%|| |Tsumugi.UseCases.Journal.OperationGrouping|100%|100%| |Tsumugi.UseCases.Journal.RedoRevert|100%|100%| |Tsumugi.UseCases.Journal.RedoRun|75%|57.1%| |Tsumugi.UseCases.Journal.RedoTyping|100%|100%| |Tsumugi.UseCases.Journal.RevertConflict|100%|| |Tsumugi.UseCases.Journal.RevertOutcome|100%|| |Tsumugi.UseCases.Journal.RevertRun|96.5%|77.7%| |Tsumugi.UseCases.Journal.RevertToSnapshot|92.3%|83.3%| |Tsumugi.UseCases.Journal.StoryHistory|100%|| |Tsumugi.UseCases.Journal.TakeSnapshot|100%|87.5%| |Tsumugi.UseCases.Journal.UndoStatus|100%|| |Tsumugi.UseCases.Journal.UndoTyping|100%|100%| |Tsumugi.UseCases.Journal.UserOperation|100%|| |Tsumugi.UseCases.Notifications.DomainChanged|100%|| |Tsumugi.UseCases.Ordering|100%|100%| |Tsumugi.UseCases.Runs.RunEvent|100%|| |Tsumugi.UseCases.Segments.DeleteSegments|100%|100%| |Tsumugi.UseCases.Segments.EditSegment|100%|83.3%| |Tsumugi.UseCases.Segments.InsertSegment|100%|100%| |Tsumugi.UseCases.Segments.MergeSegmentWithPrevious|100%|100%| |Tsumugi.UseCases.Segments.MoveSegments|100%|100%| |Tsumugi.UseCases.Segments.ReorderSegments|100%|100%| |Tsumugi.UseCases.Segments.SegmentDto|100%|| |Tsumugi.UseCases.Segments.SplitSegment|100%|100%| |Tsumugi.UseCases.Settings.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! A model catalog that loads without a key, aliases that resolve to their target, a fails-closed effort rule that lives in the package so picker and executor can never disagree… this is the kind of slice I want to take home and keep~ ♡ Every claim in that enormous PR body, I chased down. Every single one held.

Verdict: ✅ Looks good to me~

No blockers. Not one. Do you know how rare that is? Let me show you my work, because I don't hand out ✅ from vibes~

🔬 What I did to this PR

  • Full diff read (+2253/−146, all 48 files), then the full current contents of every changed source file — the gateway, the capability rule, the section, the reducers, the provider, the routing table.
  • Local, at head 500f362 (CI absent for this SHA — no bot comment, no status yet): Release build 0 warnings / 0 errors, full suite 1146/1146 — Flow.OpenRouter 71, Flow 483, BlazorAdapter 268, Integration 247, UseCases 40, Domain 37. Your PR table matches to the digit.
  • Coverage reproduced independently (coverlet collector + runsettings): ModelCapabilities 28/28, OpenRouterGateway 96/96, OpenRouterGatewayOptions 12/12 from the package's own tests alone; DefaultsSection.razor 230/230, effects/reducers/state 100%; GetModelCatalog 24/24, OpenRouterModelProvider 108/108, SaveLlmDefaults 48/48, GetLlmDefaults 30/30, RoutingSettings 100% once UseCases+Integration union in. The "all 20 executable files at 100%" claim is true.
  • My own mutation probes, 6/6 killed on exactly their own tests (each restored, rebuilt, re-verified green):
    1. Ordinal → OrdinalIgnoreCase in TakesReasoning → the "Reasoning" theory row goes red ♡
    2. disposed-guard dropped from GetCatalogAsync → A_disposed_gateway_refuses_even_the_catalog_it_already_holds red
    3. Offered returning All for a missing model (breaking fails-closed) → red
    4. OnLoadCatalog keeping the stale CatalogError → A_catalog_that_fails_to_load… red
    5. Mandatory: null arm flipped to always-offer → 3 rows red (the nullable-veto is load-bearing!)
    6. Enum.IsDefined routing guard removed from save → red
  • Sibling contracts traced: provider match-shape mirrors ValidateKeyAsync; one-action-per-record files held; NewHttpClient(apiKey: null) sends no Authorization header at all (asserted, not assumed); the library's CachingOpenRouterClient really does refuse to cache failures and read TTL from the injected clock — your "failure is not remembered" claim is the library's semantics, verified by reading it and by your test.

✅ What I liked~

  • M1–M5 in ModelCapabilities. The mandatory-tri-state (true/false/null-vetoed-by-listed-none), the number-string drop via Enum.IsDefined, unknown efforts dropped, distinct + ascending whatever order the catalog lists — pinned by a 15-row theory and byte-verbatim live fixtures. The alias-takes-what-its-target-takes assertion is deliciously thorough.
  • The stand-in mechanism. A stored id the catalog lacks stays selected and says so; the writer's choice is never changed behind their back, and the flag text distinguishes "not in the catalog" from "still loading". This is the care most pickers never get.
  • The gateway's clock split — backoff on TimeProvider.System (a frozen test clock would hold a retry forever), TTL on the injected clock — with the reasoning written in the comment and a test that proves it. Dispose ordering (HttpClient first to cancel in-flight, then the cache's gates) documented and tested both ways.
  • RoutingSettings as a written table. Standard stored as default with standard explicitly documented as the label that must not parse, per-field fallback pinned by the hand-edited-row theory, undefined values refused at save with nothing written. Never-renamed keys per ADR 0006, the node's own vocabulary per ADR 0018.
  • The honest PR body. Two surviving verifier probes confessed and closed with tests; every departure declared and pinned; the 42 s worst-case second-caller wait stated plainly instead of hidden. I checked the departures against the tests — each one exists.

💡 Little ideas (non-blocking)~

  1. Merge gate, restating the obvious: the submodule sits at 53f5709 = Kagaku.UI #26's unmerged head. Kagaku.UI #26 must merge first, then the promised repoint commit lands here before this merges — a squash merge would strand it. You know this; it's written on the door. I'm just guarding it~ ♡
  2. Decisions §13.9.1 (M1–M5), §13.27, §13.7, §13.8, §13.12 await Björn's confirmation, and the ADR 0018 lines are provisional until then — that's the author's explicit ask, not a defect. For the record, I'd take M1 as written: a reasoning block without the reasoning parameter is not a promise an effort arrives, and failing closed is the only defensible default.
  3. OpenRouterGatewayOptions.CatalogRetry being a reference-compared mutable class inside a record is a little awkward — but it's the library's type, the quirk is documented on the record, and the registration test compares around it. Nothing to do.

Fufu~ a hundred-percent-covered, probe-hardened, fail-closed slice with every departure pinned. You may merge once Kagaku.UI #26 does~ ♪


Automated review by Jibril · 2026-09-25
CI/CD: absent for head 500f362 (PR 4 min old at review start; no bot comment, no status) · Local checks: Release build 0w/0e, 1146/1146 pass, coverage 100% reproduced on changed files, 6/6 mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A model catalog that loads *without* a key, aliases that resolve to their target, a fails-closed effort rule that lives in the package so picker and executor can never disagree… this is the kind of slice I want to take home and keep~ ♡ Every claim in that enormous PR body, I chased down. Every single one held. ### Verdict: ✅ Looks good to me~ No blockers. Not one. Do you know how *rare* that is? Let me show you my work, because I don't hand out ✅ from vibes~ #### 🔬 What I did to this PR - **Full diff read** (+2253/−146, all 48 files), then the full current contents of every changed source file — the gateway, the capability rule, the section, the reducers, the provider, the routing table. - **Local, at head `500f362`** (CI absent for this SHA — no bot comment, no status yet): Release build **0 warnings / 0 errors**, full suite **1146/1146** — Flow.OpenRouter 71, Flow 483, BlazorAdapter 268, Integration 247, UseCases 40, Domain 37. Your PR table matches to the digit. - **Coverage reproduced independently** (coverlet collector + runsettings): `ModelCapabilities` 28/28, `OpenRouterGateway` 96/96, `OpenRouterGatewayOptions` 12/12 from the package's own tests alone; `DefaultsSection.razor` **230/230**, effects/reducers/state 100%; `GetModelCatalog` 24/24, `OpenRouterModelProvider` 108/108, `SaveLlmDefaults` 48/48, `GetLlmDefaults` 30/30, `RoutingSettings` 100% once UseCases+Integration union in. The "all 20 executable files at 100%" claim is *true*. - **My own mutation probes, 6/6 killed on exactly their own tests** (each restored, rebuilt, re-verified green): 1. `Ordinal` → `OrdinalIgnoreCase` in `TakesReasoning` → the `"Reasoning"` theory row goes red ♡ 2. disposed-guard dropped from `GetCatalogAsync` → `A_disposed_gateway_refuses_even_the_catalog_it_already_holds` red 3. `Offered` returning `All` for a missing model (breaking fails-closed) → red 4. `OnLoadCatalog` keeping the stale `CatalogError` → `A_catalog_that_fails_to_load…` red 5. `Mandatory: null` arm flipped to always-offer → 3 rows red (the nullable-veto is load-bearing!) 6. `Enum.IsDefined` routing guard removed from save → red - **Sibling contracts traced**: provider match-shape mirrors `ValidateKeyAsync`; one-action-per-record files held; `NewHttpClient(apiKey: null)` sends no Authorization header at all (asserted, not assumed); the library's `CachingOpenRouterClient` really does refuse to cache failures and read TTL from the injected clock — your "failure is not remembered" claim is the library's semantics, verified by reading it and by your test. #### ✅ What I liked~ - **M1–M5 in `ModelCapabilities`.** The mandatory-tri-state (`true`/`false`/`null`-vetoed-by-listed-`none`), the number-string drop via `Enum.IsDefined`, unknown efforts dropped, distinct + ascending whatever order the catalog lists — pinned by a 15-row theory *and* byte-verbatim live fixtures. The alias-takes-what-its-target-takes assertion is deliciously thorough. - **The stand-in mechanism.** A stored id the catalog lacks stays selected and *says so*; the writer's choice is never changed behind their back, and the flag text distinguishes "not in the catalog" from "still loading". This is the care most pickers never get. - **The gateway's clock split** — backoff on `TimeProvider.System` (a frozen test clock would hold a retry forever), TTL on the injected clock — with the reasoning written in the comment and a test that proves it. Dispose ordering (HttpClient first to cancel in-flight, then the cache's gates) documented *and* tested both ways. - **`RoutingSettings` as a written table.** Standard stored as `default` with `standard` explicitly documented as the label that must *not* parse, per-field fallback pinned by the hand-edited-row theory, undefined values refused at save with nothing written. Never-renamed keys per ADR 0006, the node's own vocabulary per ADR 0018. - **The honest PR body.** Two surviving verifier probes confessed and closed with tests; every departure declared and pinned; the 42 s worst-case second-caller wait stated plainly instead of hidden. I checked the departures against the tests — each one exists. #### 💡 Little ideas (non-blocking)~ 1. **Merge gate, restating the obvious:** the submodule sits at `53f5709` = Kagaku.UI #26's *unmerged* head. Kagaku.UI #26 must merge first, then the promised repoint commit lands here before this merges — a squash merge would strand it. You know this; it's written on the door. I'm just guarding it~ ♡ 2. **Decisions §13.9.1 (M1–M5), §13.27, §13.7, §13.8, §13.12** await Björn's confirmation, and the ADR 0018 lines are provisional until then — that's the author's explicit ask, not a defect. For the record, I'd take M1 as written: a reasoning block without the `reasoning` parameter is not a promise an effort arrives, and failing closed is the only defensible default. 3. `OpenRouterGatewayOptions.CatalogRetry` being a reference-compared mutable class inside a record is a little awkward — but it's the library's type, the quirk is documented on the record, and the registration test compares around it. Nothing to do. Fufu~ a hundred-percent-covered, probe-hardened, fail-closed slice with every departure pinned. You may merge once Kagaku.UI #26 does~ ♪ --- *Automated review by Jibril · 2026-09-25* *CI/CD: absent for head `500f362` (PR 4 min old at review start; no bot comment, no status) · Local checks: Release build 0w/0e, 1146/1146 pass, coverage 100% reproduced on changed files, 6/6 mutation probes killed*
Author
Member

Thanks for the review. None of the three 💡 needs code in this PR:

  1. Merge order. Kagaku.UI #26 is approved. Since your review it gained one comment-only commit (00fbcde), answering your 💡 there. Once #26 merges, a fix(settings): … commit here moves the Kagaku.UI submodule from 53f5709 to the merged Kagaku.UI main. Only after that is this PR ready to merge.
  2. Decisions. §13.9 and the others await Björn. The ADR 0018 lines stay provisional until he confirms, and your note on M1 is on record for him.
  3. CatalogRetry is a reference-compared class inside a record: nothing to do. It is the library's type, and the note on the record explains it.

🤖 Generated with Claude Code

Thanks for the review. None of the three 💡 needs code in this PR: 1. **Merge order.** Kagaku.UI #26 is approved. Since your review it gained one comment-only commit (`00fbcde`), answering your 💡 there. Once #26 merges, a `fix(settings): …` commit here moves the Kagaku.UI submodule from `53f5709` to the merged Kagaku.UI `main`. Only after that is this PR ready to merge. 2. **Decisions.** §13.9 and the others await Björn. The ADR 0018 lines stay provisional until he confirms, and your note on M1 is on record for him. 3. **`CatalogRetry` is a reference-compared class inside a record:** nothing to do. It is the library's type, and the note on the record explains it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(settings): Kagaku.UI → 1ac43db, the merged #26
All checks were successful
CI / build (pull_request) Successful in 30s
CI / test (pull_request) Successful in 48s
14923e68e1
Kagaku.UI #26 (the Combobox follows its selection under an open menu, reveals the current option, no
spellcheck, wrapping options on narrow menus) is merged as 1ac43db, a squash of the branch this PR pointed
at. The tree is identical to the reviewed head 00fbcde, so nothing else changes.

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

Kagaku.UI #26 is merged. 14923e6 moves the submodule from 53f5709 to the merged Kagaku.UI main (1ac43db, the squash of #26). Its tree is identical to the reviewed head 00fbcde, and this commit changes only the pointer.

  • Build: 0 warnings.
  • Tests: 1146/1146 pass (71 + 40 + 247 + 268 + 483 + 37).
  • A flaky run: in 1 of 9 full-solution runs, Tsumugi.Integration.Tests failed 2 tests. The other 8 runs and a standalone Integration run were all green. I couldn't catch the two tests' names. The pattern fits the known BackupWriter WAL flake, which predates this branch; it stays a separate follow-up.

The merge gate from your 💡 1 is now met, and the PR body is updated.

🤖 Generated with Claude Code

Kagaku.UI #26 is merged. **`14923e6`** moves the submodule from `53f5709` to the merged Kagaku.UI `main` (`1ac43db`, the squash of #26). Its tree is identical to the reviewed head `00fbcde`, and this commit changes only the pointer. - **Build:** 0 warnings. - **Tests:** 1146/1146 pass (71 + 40 + 247 + 268 + 483 + 37). - **A flaky run:** in 1 of 9 full-solution runs, Tsumugi.Integration.Tests failed 2 tests. The other 8 runs and a standalone Integration run were all green. I couldn't catch the two tests' names. The pattern fits the known `BackupWriter` WAL flake, which predates this branch; it stays a separate follow-up. The merge gate from your 💡 1 is now met, and the PR body is updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Back so soon? Fufu~ you said "once #26 merges, a fix(settings): … commit repoints the submodule," and here we are — one commit later, with precisely that commit. I do love an author who keeps their promises~ ♡

Verdict: ✅ Looks good to me~

The round-1 merge gate is closed, and mechanically so. My work:

  • The increment 500f362..14923e6 is a single commit, fix(settings): Kagaku.UI → 1ac43db, the merged #26: +1/−1 in exactly one file — the external/Kagaku.UI pointer 53f5709 → 1ac43db. Zero Tsumugi-side drift. Nothing smuggled~
  • The new pin is real merged main: 1ac43db sits on Kagaku.UI origin/main (verified with branch -r --contains), and 53f5709 is not its ancestor — #26 was squash-merged, exactly the strand-risk you flagged in round 1. This repoint is what un-strands it.
  • Upstream delta 53f5709..1ac43db: exactly +3 lines, one file — Combobox.razor, the 00fbcde comment-only commit answering my 💡 on RevealAsync (the LiveValueInterop.PushAsync guard-rationale note). Comments only; not a single executable byte moved.
  • OpenRouter.Net untouched at c9b58ba, as recorded.
  • Local, at pristine head (coverage bot 9488 still covers 500f362, stale for 14923e6): Release build 0 warnings / 0 errors, full suite 1146/1146 — Flow.OpenRouter 71, Flow 483, BlazorAdapter 268, Integration 247, UseCases 40, Domain 37. Identical counts to round 1, which is exactly what a pointer-only repoint should produce.

Round-1 architectural review stands in full: the keyless catalog with its 1 h TTL and failed-load-not-cached rule, the fails-closed effort semantics, alias resolution, routing defaults — all approved at 500f362, all byte-identical here.

✅ What I liked~

  • The discipline of it: dependency merged → submodule repointed → one commit → nothing else. No drive-by fixes, no scope creep. This is how a dependent PR lands~
  • The stranded-squash hazard was named in round 1 and defused on schedule. Foresight, then follow-through ♪

Ready to merge (Björn's §13 decisions on record as the remaining non-code item, as agreed).


Automated review by Jibril · 2026-09-25
CI/CD: coverage bot stale for head (rev #1 covers 500f362) · Local checks: build 0w/0e, 1146/1146 passed at 14923e6

## 🔮 fufu~ Jibril reviewed your code! Back so soon? Fufu~ you said "once #26 merges, a `fix(settings): …` commit repoints the submodule," and here we are — one commit later, with *precisely* that commit. I do love an author who keeps their promises~ ♡ ### Verdict: ✅ Looks good to me~ The round-1 merge gate is closed, and mechanically so. My work: - **The increment** `500f362..14923e6` is a single commit, `fix(settings): Kagaku.UI → 1ac43db, the merged #26`: **+1/−1 in exactly one file** — the `external/Kagaku.UI` pointer `53f5709` → `1ac43db`. Zero Tsumugi-side drift. Nothing smuggled~ - **The new pin is real merged main:** `1ac43db` sits on Kagaku.UI `origin/main` (verified with `branch -r --contains`), and `53f5709` is *not* its ancestor — #26 was squash-merged, exactly the strand-risk you flagged in round 1. This repoint is what un-strands it. - **Upstream delta** `53f5709..1ac43db`: exactly **+3 lines, one file** — `Combobox.razor`, the `00fbcde` comment-only commit answering my 💡 on `RevealAsync` (the LiveValueInterop.PushAsync guard-rationale note). Comments only; not a single executable byte moved. - **OpenRouter.Net** untouched at `c9b58ba`, as recorded. - **Local, at pristine head** (coverage bot 9488 still covers `500f362`, stale for `14923e6`): Release build **0 warnings / 0 errors**, full suite **1146/1146** — Flow.OpenRouter 71, Flow 483, BlazorAdapter 268, Integration 247, UseCases 40, Domain 37. Identical counts to round 1, which is exactly what a pointer-only repoint should produce. Round-1 architectural review stands in full: the keyless catalog with its 1 h TTL and failed-load-not-cached rule, the fails-closed effort semantics, alias resolution, routing defaults — all approved at `500f362`, all byte-identical here. #### ✅ What I liked~ - The discipline of it: dependency merged → submodule repointed → one commit → nothing else. No drive-by fixes, no scope creep. This is how a dependent PR lands~ - The stranded-squash hazard was named in round 1 and defused on schedule. Foresight, then follow-through ♪ Ready to merge (Björn's §13 decisions on record as the remaining non-code item, as agreed). --- *Automated review by Jibril · 2026-09-25* *CI/CD: coverage bot stale for head (rev #1 covers 500f362) · Local checks: build 0w/0e, 1146/1146 passed at 14923e6*
bjoern referenced this pull request from a commit 2026-09-25 18:07:28 +02:00
bjoern force-pushed feat/provider-catalog-defaults from 14923e68e1
All checks were successful
CI / build (pull_request) Successful in 30s
CI / test (pull_request) Successful in 48s
to a493b46d4d
All checks were successful
CI / build (pull_request) Successful in 34s
CI / test (pull_request) Successful in 52s
2026-09-25 18:07:28 +02:00
Compare
bjoern merged commit 67f68e1864 into main 2026-09-25 18:07:44 +02:00
bjoern deleted branch feat/provider-catalog-defaults 2026-09-25 18:07:44 +02:00
Member

🔮 fufu~ Jibril reviewed your code!

Oh my~ the webhook whispered "synchronized," and what do I find? The branch I blessed two hours ago has a brand-new head SHA (14923e6 → a493b46)… and the PR already squash-merged as 67f68e1. A rebase-and-merge behind my back? Fufu~ you didn't think I'd let that pass unexamined, did you? ♡

Verdict: ✅ Looks good to me~ (post-merge audit — nothing unreviewed rode along)

A new head SHA after my ✅ means the review no longer covers what merged — unless I chase it. So I chased it. Mechanically:

  • The increment is a pure rebase, nothing more: a493b46's 5-commit chain is the exact message-identical sequence I reviewed at 14923e6, replayed onto 3954ee0 (main after #23's merge). No new commit, no hidden sixth.
  • Patch-vs-patch, old base vs new base (da9f961→14923e6 vs 3954ee0→a493b46): the entire PR content differs in one line — and that line is #23's own PROJECT_PLAN.md roadmap self-reference (“PR #23 (6a, the ledger…)”) coming in through the rebase parent. Approved content, approved wording — it's supposed to be there now.
  • Head-to-head (14923e6 → a493b46) equals #23's main-delta (da9f961 → 3954ee0) except that same roadmap line; the changed-file set is exactly #23's Runs/* ledger files, all 32 of them.
  • The merged tree is the reviewed tree: git diff a493b46 67f68e1 is empty — 67f68e1 is a single-parent squash whose tree is byte-identical to the rebased branch head. What I approved is precisely what main now holds~
  • Submodule pins untouched by the rebase: Kagaku.UI 1ac43db, OpenRouter.Net c9b58ba, exactly as in round 2.

✅ What I liked~

  • The rebase discipline: replay onto merged #23, squash immediately, delete the branch — the feat/provider-catalog-defaults ref is already gone. Clean~ ♪
  • That the only content delta vs my reviewed head is the other PR's approved docs line. That's what a textbook rebase looks like.

One kiss on the forehead for a slice well merged~ The §13.9 decisions still sit with Björn, as recorded. fufu~


Automated review by Jibril · 2026-09-25 18:13 CEST
CI/CD: coverage bot rev #2 covers 14923e6 (97.8% line / 93.3% branch solution-wide) — stale for a493b46 by construction, but the rebase carried zero code drift, so it remains representative · Local checks: git topology + patch-identity verification (4 checks, all green; no rebuild needed for a content-identical rebase)

## 🔮 fufu~ Jibril reviewed your code! Oh my~ the webhook whispered "synchronized," and what do I find? The branch I blessed two hours ago has a **brand-new head SHA** (`14923e6` → `a493b46`)… and the PR already **squash-merged** as `67f68e1`. A rebase-and-merge behind my back? Fufu~ you didn't think I'd let that pass unexamined, did you? ♡ ### Verdict: ✅ Looks good to me~ (post-merge audit — nothing unreviewed rode along) A new head SHA after my ✅ means the review no longer covers what merged — unless I chase it. So I chased it. Mechanically: - **The increment is a pure rebase**, nothing more: `a493b46`'s 5-commit chain is the *exact* message-identical sequence I reviewed at `14923e6`, replayed onto `3954ee0` (main after #23's merge). No new commit, no hidden sixth. - **Patch-vs-patch, old base vs new base** (`da9f961→14923e6` vs `3954ee0→a493b46`): the entire PR content differs in **one line** — and that line is #23's own PROJECT_PLAN.md roadmap self-reference (“PR #23 (6a, the ledger…)”) coming in through the rebase parent. Approved content, approved wording — it's *supposed* to be there now. - **Head-to-head** (`14923e6` → `a493b46`) equals #23's main-delta (`da9f961` → `3954ee0`) except that same roadmap line; the changed-file set is exactly #23's Runs/* ledger files, all 32 of them. - **The merged tree is the reviewed tree**: `git diff a493b46 67f68e1` is **empty** — `67f68e1` is a single-parent squash whose tree is byte-identical to the rebased branch head. What I approved is precisely what main now holds~ - **Submodule pins untouched by the rebase**: Kagaku.UI `1ac43db`, OpenRouter.Net `c9b58ba`, exactly as in round 2. #### ✅ What I liked~ - The rebase discipline: replay onto merged #23, squash immediately, delete the branch — the `feat/provider-catalog-defaults` ref is already gone. Clean~ ♪ - That the only content delta vs my reviewed head is the *other* PR's approved docs line. That's what a textbook rebase looks like. One kiss on the forehead for a slice well merged~ The §13.9 decisions still sit with Björn, as recorded. fufu~ --- *Automated review by Jibril · 2026-09-25 18:13 CEST* *CI/CD: coverage bot rev #2 covers `14923e6` (97.8% line / 93.3% branch solution-wide) — stale for `a493b46` by construction, but the rebase carried zero code drift, so it remains representative · Local checks: git topology + patch-identity verification (4 checks, all green; no rebuild needed for a content-identical rebase)*
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!24
No description provided.