feat(nav): NavItem grows a real tree — Child branches, any depth, trail reveal; Nested removed; 11 run-and-graph icons #22

Merged
bjoern merged 4 commits from feat/nav-tree into main 2026-09-24 15:16:53 +02:00
Member

Two commits, one theme: the nav stack learns a real tree, and the icon catalog gains the run-and-graph set that Tsumugi Phase 2–5 will consume.

What

1. NavItem grows a real tree (breaking).

  • Without Child: the plain leaf anchor, markup unchanged — every existing consumer (Kagura's WorkspaceShell) renders identically.
  • With Child: a foldable branch — the anchor in a row with a native <button aria-expanded> toggle (chevron rotated via CSS), the fragment in a role="group" below, recursing to any depth.
  • One code rule: a branch whose URL contains the current page (a descendant is open) auto-reveals and carries kg-nav__branch--trail — containment, not openness; the accent survives a manual collapse, and a writer collapse wins for the circuit's life.
  • PersistKey (opt-in): explicit toggles stored in localStorage via new nav.js (kgNav.get/set, best-effort, guarded) — a sidebar that remounts on every navigation keeps its shape.
  • Multi-level hairline guides replace the old one-level margin indent.
  • BREAKING: Nested removed. Exactly one consumer existed (Tsumugi StoryShell — migrating in the paired Tsumugi PR, next); Kagura uses NavList/NavItem/NavGroup without Nested (grep at 691a176), Orihon uses no nav components (grep at 31bf6f3). Zero coordination needed.

2. Eleven icons, canonical Material Symbols paths: stop (Cancel run), schedule (queued runs), progress_activity (streaming node), payments (Cost cells), bolt (graph triggers), output (output node — input exists, its counterpart didn't), construction (tool node), grid_view (map fan-out), task_alt (approve card), help (waiting-for-input), more_horiz (scene-break ***).

Tests

  • NavItemTests: 16 facts — leaf/branch polymorphism, toggle aria flips, Expanded initial, 3-level recursion, PersistKey JS contract (kgNav.set on toggle, zero JS without), trail class, auto-reveal, manual-collapse-beats-auto-expand (incl. re-render).
  • NavStructureTests: a composed 3-level NavList in document order.
  • Full suite: 350/350 green (335 + 15 Fluxor), Release build clean (warnings-as-errors on).

Notes

  • nav.js rides KagakuScripts (the disk↔shipped parity test enforces it).
  • README documents the branch mode and the removal.
  • The :has()-based styling idea from planning was replaced by a C# URL-containment check (IsInside) — works identically in bUnit and browser, no CSS-support concerns.
Two commits, one theme: the nav stack learns a real tree, and the icon catalog gains the run-and-graph set that Tsumugi Phase 2–5 will consume. ## What **1. `NavItem` grows a real tree (breaking).** - Without `Child`: the plain leaf anchor, markup unchanged — every existing consumer (Kagura's `WorkspaceShell`) renders identically. - With `Child`: a foldable branch — the anchor in a row with a native `<button aria-expanded>` toggle (chevron rotated via CSS), the fragment in a `role="group"` below, **recursing to any depth**. - One code rule: a branch whose URL contains the current page (a descendant is open) auto-reveals and carries `kg-nav__branch--trail` — containment, not openness; the accent survives a manual collapse, and a writer collapse wins for the circuit's life. - `PersistKey` (opt-in): explicit toggles stored in localStorage via new `nav.js` (`kgNav.get/set`, best-effort, guarded) — a sidebar that remounts on every navigation keeps its shape. - Multi-level hairline guides replace the old one-level margin indent. - **BREAKING: `Nested` removed.** Exactly one consumer existed (Tsumugi `StoryShell` — migrating in the paired Tsumugi PR, next); Kagura uses NavList/NavItem/NavGroup without `Nested` (grep at `691a176`), Orihon uses no nav components (grep at `31bf6f3`). Zero coordination needed. **2. Eleven icons**, canonical Material Symbols paths: `stop` (Cancel run), `schedule` (queued runs), `progress_activity` (streaming node), `payments` (Cost cells), `bolt` (graph triggers), `output` (output node — `input` exists, its counterpart didn't), `construction` (tool node), `grid_view` (map fan-out), `task_alt` (approve card), `help` (waiting-for-input), `more_horiz` (scene-break `***`). ## Tests - `NavItemTests`: 16 facts — leaf/branch polymorphism, toggle aria flips, `Expanded` initial, 3-level recursion, `PersistKey` JS contract (`kgNav.set` on toggle, zero JS without), trail class, auto-reveal, manual-collapse-beats-auto-expand (incl. re-render). - `NavStructureTests`: a composed 3-level `NavList` in document order. - Full suite: **350/350 green** (335 + 15 Fluxor), Release build clean (warnings-as-errors on). ## Notes - `nav.js` rides `KagakuScripts` (the disk↔shipped parity test enforces it). - README documents the branch mode and the removal. - The `:has()`-based styling idea from planning was replaced by a C# URL-containment check (`IsInside`) — works identically in bUnit and browser, no CSS-support concerns.
Canonical Material Symbols paths for Tsumugi Phase 2-5: the run strip and monitor (stop/cancel, schedule/queued, progress_activity/streaming), cost cells (payments), the editor palette node vocabulary (bolt triggers, output, construction tool, grid_view map fan-out), the approve card (task_alt), waiting-for-input (help), and the scene-break star-bar affordance (more_horiz).
feat(nav): NavItem grows a real tree — Child makes a branch, any depth, trail reveal, optional persistence
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 19s
c2df7fe0c5
NavItem becomes polymorphic: without Child it is the plain leaf anchor every flat nav uses (markup unchanged); with Child it renders a foldable branch — the anchor beside a native toggle button (aria-expanded), the fragment in a group below, recursion to any depth. One code rule: a branch whose URL contains the current page reveals itself and carries kg-nav__branch--trail (containment, not openness — the accent survives a manual collapse); a writer collapse wins for the circuit's life. PersistKey (opt-in) stores explicit toggles in localStorage via the new nav.js (kgNav.get/set, best-effort), so a sidebar that remounts per navigation keeps its shape. Multi-level guides (hairline border per level) replace the old one-level margin indent.

BREAKING: the Nested bool is removed. It had exactly one consumer (Tsumugi StoryShell, migrating in the paired Tsumugi PR); Kagura uses NavList/NavItem/NavGroup without Nested and Orihon uses no nav components (verified by grep at 691a176 and 31bf6f3 respectively).

Summary

Summary
Generated on: 09/24/2026 - 13:17:12
Coverage date: 09/24/2026 - 13:17:10
Parser: MultiReport (2x Cobertura)
Assemblies: 2
Classes: 78
Files: 76
Line coverage: 95.3% (1592 of 1669)
Covered lines: 1592
Uncovered lines: 77
Coverable lines: 1669
Total lines: 4568
Branch coverage: 89.2% (823 of 922)
Covered branches: 823
Total branches: 922
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 95.3%
Name Line Branch
Kagaku.UI 95.3% 89.3%
Kagaku.UI.Badge 100% 100%
Kagaku.UI.Breadcrumb 100%
Kagaku.UI.BreadcrumbItem 100% 100%
Kagaku.UI.Button 100% 100%
Kagaku.UI.Card 100% 100%
Kagaku.UI.CircuitErrorPanel 100%
Kagaku.UI.Column 100%
Kagaku.UI.Columns 100% 100%
Kagaku.UI.Combobox`1 94.2% 85.7%
Kagaku.UI.ConfirmDialog 100%
Kagaku.UI.ContextMenu 92.1% 94.4%
Kagaku.UI.CssClassExtensions 100%
Kagaku.UI.Debounce 97.8% 80.7%
Kagaku.UI.DebouncedSearchField 100% 87.5%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 100%
Kagaku.UI.FileUpload 100% 91.6%
Kagaku.UI.FixedColumn 100%
Kagaku.UI.FixedRow 100%
Kagaku.UI.FlexColumn 100%
Kagaku.UI.FlexRow 100%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InlineAlert 83.3% 75%
Kagaku.UI.InputFieldBase 95.2% 87.5%
Kagaku.UI.LabeledEntriesTable 96.7% 66.6%
Kagaku.UI.LabeledEntry 100%
Kagaku.UI.LayoutCss 95.2% 93.7%
Kagaku.UI.Lightbox 83.7% 85%
Kagaku.UI.LiveValue 100% 100%
Kagaku.UI.LiveValueInterop 100% 100%
Kagaku.UI.Markdown 100% 50%
Kagaku.UI.MaskCanvas 94.1% 90.1%
Kagaku.UI.MaskedSecretField 96.2% 83.3%
Kagaku.UI.MaskPoint 100%
Kagaku.UI.MaskPrimitive 100%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 87.1% 90%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavItem 92.1% 96.1%
Kagaku.UI.NavList 100%
Kagaku.UI.PageShell 100% 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.RegionPoint 100% 100%
Kagaku.UI.RegionRect 90% 100%
Kagaku.UI.RegionSelector 85% 86.9%
Kagaku.UI.RelativeTime 100% 93.7%
Kagaku.UI.Row 100%
Kagaku.UI.Rows 100% 100%
Kagaku.UI.SaveIndicator 100% 100%
Kagaku.UI.ScrollViewer 100% 100%
Kagaku.UI.Select`1 98.4% 95.8%
Kagaku.UI.SelectableGrid`1 95.5% 89.1%
Kagaku.UI.SelectOption`1 100%
Kagaku.UI.Separator 100%
Kagaku.UI.Slider 100% 91.6%
Kagaku.UI.StatusDot 100%
Kagaku.UI.Tab 100% 100%
Kagaku.UI.Table`1 100% 88.4%
Kagaku.UI.TableColumn`1 100%
Kagaku.UI.Tabs 95.9% 84.4%
Kagaku.UI.TextArea 100%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
Kagaku.UI.Fluxor - 97.6%
Name Line Branch
Kagaku.UI.Fluxor 97.6% 85%
Kagaku.UI.Fluxor.CircuitError 100%
Kagaku.UI.Fluxor.CircuitErrorSink 100% 92.8%
Kagaku.UI.Fluxor.StoreInitializer 92.8% 66.6%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/24/2026 - 13:17:12 | | Coverage date: | 09/24/2026 - 13:17:10 | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 2 | | Classes: | 78 | | Files: | 76 | | **Line coverage:** | 95.3% (1592 of 1669) | | Covered lines: | 1592 | | Uncovered lines: | 77 | | Coverable lines: | 1669 | | Total lines: | 4568 | | **Branch coverage:** | 89.2% (823 of 922) | | Covered branches: | 823 | | Total branches: | 922 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 95.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**95.3%**|**89.3%**| |Kagaku.UI.Badge|100%|100%| |Kagaku.UI.Breadcrumb|100%|| |Kagaku.UI.BreadcrumbItem|100%|100%| |Kagaku.UI.Button|100%|100%| |Kagaku.UI.Card|100%|100%| |Kagaku.UI.CircuitErrorPanel|100%|| |Kagaku.UI.Column|100%|| |Kagaku.UI.Columns|100%|100%| |Kagaku.UI.Combobox`1|94.2%|85.7%| |Kagaku.UI.ConfirmDialog|100%|| |Kagaku.UI.ContextMenu|92.1%|94.4%| |Kagaku.UI.CssClassExtensions|100%|| |Kagaku.UI.Debounce|97.8%|80.7%| |Kagaku.UI.DebouncedSearchField|100%|87.5%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|100%| |Kagaku.UI.FileUpload|100%|91.6%| |Kagaku.UI.FixedColumn|100%|| |Kagaku.UI.FixedRow|100%|| |Kagaku.UI.FlexColumn|100%|| |Kagaku.UI.FlexRow|100%|| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InlineAlert|83.3%|75%| |Kagaku.UI.InputFieldBase|95.2%|87.5%| |Kagaku.UI.LabeledEntriesTable|96.7%|66.6%| |Kagaku.UI.LabeledEntry|100%|| |Kagaku.UI.LayoutCss|95.2%|93.7%| |Kagaku.UI.Lightbox|83.7%|85%| |Kagaku.UI.LiveValue|100%|100%| |Kagaku.UI.LiveValueInterop|100%|100%| |Kagaku.UI.Markdown|100%|50%| |Kagaku.UI.MaskCanvas|94.1%|90.1%| |Kagaku.UI.MaskedSecretField|96.2%|83.3%| |Kagaku.UI.MaskPoint|100%|| |Kagaku.UI.MaskPrimitive|100%|| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|87.1%|90%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavItem|92.1%|96.1%| |Kagaku.UI.NavList|100%|| |Kagaku.UI.PageShell|100%|100%| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.RegionPoint|100%|100%| |Kagaku.UI.RegionRect|90%|100%| |Kagaku.UI.RegionSelector|85%|86.9%| |Kagaku.UI.RelativeTime|100%|93.7%| |Kagaku.UI.Row|100%|| |Kagaku.UI.Rows|100%|100%| |Kagaku.UI.SaveIndicator|100%|100%| |Kagaku.UI.ScrollViewer|100%|100%| |Kagaku.UI.Select`1|98.4%|95.8%| |Kagaku.UI.SelectableGrid`1|95.5%|89.1%| |Kagaku.UI.SelectOption`1|100%|| |Kagaku.UI.Separator|100%|| |Kagaku.UI.Slider|100%|91.6%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.Tab|100%|100%| |Kagaku.UI.Table`1|100%|88.4%| |Kagaku.UI.TableColumn`1|100%|| |Kagaku.UI.Tabs|95.9%|84.4%| |Kagaku.UI.TextArea|100%|| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| </details> <details><summary>Kagaku.UI.Fluxor - 97.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI.Fluxor**|**97.6%**|**85%**| |Kagaku.UI.Fluxor.CircuitError|100%|| |Kagaku.UI.Fluxor.CircuitErrorSink|100%|92.8%| |Kagaku.UI.Fluxor.StoreInitializer|92.8%|66.6%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A nav that grows a real tree~ Recursion, an honest toggle that only folds while the anchor does the navigating, a writer collapse that beats the auto-reveal — you even dodged the :has() trap by keeping the trail rule in C# where bUnit can see it. I read every line of the +490 twice, cloned Kagura, Orihon, AND Tsumugi to check your breaking-removal claims, and ran the stylesheets through a headless browser because I don't trust cascades I haven't computed. ♡

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

⛔ These need fixing before I'm satisfied~

  1. src/Kagaku.UI/Components/NavItem.razor.css:39-49 — an ACTIVE branch anchor loses its accent text. The branch-link rule (.kg-nav__item--branch .kg-nav__link { color: inherit; … }, specificity 0,2,0) and the active rule (.kg-nav__item--active { color: var(--accent); … }, specificity 0,2,0) tie — and the branch rule comes later in source, so it wins. inherit resolves against the branch row div, which only ever matches base .kg-nav__item → --text-muted. Mechanically verified in headless Chromium with your actual stylesheet: a branch anchor with kg-nav__item--active computes color: rgb(119,119,119) (muted) on background: rgb(232,222,248) (accent-subtle) — while a leaf with the same classes computes rgb(103,80,164) (accent). And it splits by one segment of depth: standing deeper, --trail's 0,3,0 rule rescues the accent; standing ON the section's own page (IsInside false, no trail) you get a subtle background with gray text — the "you are here" state half-broken. Your own comment says the anchor "keeps the leaf look," and this isn't it~
    Fix: add .kg-nav__item--branch .kg-nav__link.kg-nav__item--active { color: var(--accent); } (0,3,0, after the trail rules), or move the active block below the branch-link rules.

  2. src/Kagaku.UI/Components/NavItem.razor:105-117 — TryJsAsync doesn't catch JSDisconnectedException, breaking the repo's guard pattern. Nine siblings (Hotkey, Modal, ContextMenu, QuicklinkNav, MaskCanvas, RegionSelector, DocumentTitle, Lightbox, LiveValue) all catch JSDisconnectedException first — Hotkey's comment even names the scenario ("Circuit already gone"). Your guard catches only JSException and InvalidOperationException. I checked the hierarchy mechanically: JSDisconnectedException's base type is System.Exception, not JSException — so it escape — so a kgNav.set in flight when the tab closes, or the first-render kgNav.get racing a disconnect, throws an unhandled exception instead of the "tree simply stays ephemeral" your own comment promises. Fufu~ you wouldn't leave THAT in production, would you? ♡
    Fix: catch (JSDisconnectedException) { } before the other two — the exact sibling shape.

  3. tests/Kagaku.UI.Tests/NavItemTests.cs — the entire kgNav.get restore path is untested. Grep: zero setups of kgNav.get anywhere in the test suite. In Loose mode bUnit's default bool? result is null, so OnAfterRenderAsync's persisted is { } escape — the persisted is { } openbranch never runs under test — CI agrees: NavItem sits at **85.4% line / 94.2% branch**, the lowest of everything this PR touched. The localStorage restore is the *headline* ofPersistKey("a sidebar that remounts on every navigation keeps its shape") and the reasonnav.js's getexists, yet no test proves a restored value opens a branch — or that a persisted **collapse** overrides the active-trail auto-reveal (the persisted twin of your lovelyA_manual_collapse_survives_the_auto_expand_ruletest). You added a code path and forgot to test it? I can't let that slide~ ♡ Fix:JSInterop.Setup<bool?>("kgNav.get", _ => true).SetResult(true/false)` cases — (a) persisted-open restores open, (b) persisted-collapsed wins while standing on a descendant, (c) null leaves the auto behavior untouched.

💡 Little ideas (non-blocking)~

  1. NavItem.razor:8 — the doc comment says "a row with a toggle toggle button beside it" — one toggle too many, my dear~

✅ What I liked~

  • The toggle-vs-anchor split is exactly right: aria-expanded + aria-aria-controls + aria-labelledbyon a native button, the grouphidden` when folded — keyboard users get a real tree, and the toggle only folds while the anchor navigates. ♪
  • ToggleAsync flipping from the writer-visible state (_userOpen = !_isOpen, not a shadow flag) — you saw the first-click-on-auto-revealed-branch trap and documented why. That comment made me genuinely happy~
  • IsInside is segment-boundary-correct (target + "/", length-guarded) and your prefix-boundary tests pin it.
  • What I liked: the breaking-removal hygiene: I verified all three claims by cloning — Kagura main: zero Nested uses; Orihon: no nav components at all; Tsumugi's one use (StoryShell.razor:144) rides a pinned git submodule, so nothing breaks until the paired PR moves the pin. Honest breaking change, zero coordination needed — as promised.
  • Tests are behavioral, not decorative: aria flips both ways, re-render survival, trail-vs-openness separation, and the composed 3-level document-order test. And nav.js rides kgakakuScripts, so the disk↔shipped parity test guards the shipping. ♡

Automated review by Jibril · 2026-09-24
CI/CD: passed for head c2df7fe (coverage bot 9271, NavItem 85.4%/94.2%) · Local checks: skipped per green-CI policy; mechanical verifications = headless-Chromium computed-style repro (blocker 1), exception-hierarchy check (blocker 2), coverage/grep audit (blocker 3), cross-repo consumer clones

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A nav that grows a *real tree*~ Recursion, an honest toggle that only folds while the anchor does the navigating, a writer collapse that beats the auto-reveal — you even dodged the `:has()` trap by keeping the trail rule in C# where bUnit can see it. I read every line of the +490 twice, cloned Kagura, Orihon, AND Tsumugi to check your breaking-removal claims, and ran the stylesheets through a headless browser because I don't trust cascades I haven't computed. ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`src/Kagaku.UI/Components/NavItem.razor.css:39-49` — an ACTIVE branch anchor loses its accent text.** The branch-link rule (`.kg-nav__item--branch .kg-nav__link { color: inherit; … }`, specificity 0,2,0) and the active rule (`.kg-nav__item--active { color: var(--accent); … }`, specificity 0,2,0) tie — and the branch rule comes later in source, so it wins. `inherit` resolves against the branch *row* div, which only ever matches base `.kg-nav__item` → `--text-muted`. Mechanically verified in headless Chromium with your actual stylesheet: a branch anchor with `kg-nav__item--active` computes `color: rgb(119,119,119)` (muted) on `background: rgb(232,222,248)` (accent-subtle) — while a leaf with the same classes computes `rgb(103,80,164)` (accent). And it splits by one segment of depth: standing deeper, `--trail`'s 0,3,0 rule rescues the accent; standing ON the section's own page (`IsInside` false, no trail) you get a subtle background with gray text — the "you are here" state half-broken. Your own comment says the anchor "keeps the leaf look," and this isn't it~ Fix: add `.kg-nav__item--branch .kg-nav__link.kg-nav__item--active { color: var(--accent); }` (0,3,0, after the trail rules), or move the active block below the branch-link rules. 2. **`src/Kagaku.UI/Components/NavItem.razor:105-117` — `TryJsAsync` doesn't catch `JSDisconnectedException`, breaking the repo's guard pattern.** Nine siblings (Hotkey, Modal, ContextMenu, QuicklinkNav, MaskCanvas, RegionSelector, DocumentTitle, Lightbox, LiveValue) all catch `JSDisconnectedException` first — Hotkey's comment even names the scenario ("Circuit already gone"). Your guard catches only `JSException` and `InvalidOperationException`. I checked the hierarchy mechanically: `JSDisconnectedException`'s base type is `System.Exception`, **not** `JSException` — so it escape — so a `kgNav.set` in flight when the tab closes, or the first-render `kgNav.get` racing a disconnect, throws an *unhandled* exception instead of the "tree simply stays ephemeral" your own comment promises. Fufu~ you wouldn't leave THAT in production, would you? ♡ Fix: `catch (JSDisconnectedException) { }` before the other two — the exact sibling shape. 3. **`tests/Kagaku.UI.Tests/NavItemTests.cs` — the entire `kgNav.get` restore path is untested.** Grep: zero setups of `kgNav.get` anywhere in the test suite. In Loose mode bUnit's default `bool?` result is null, so `OnAfterRenderAsync`'s `persisted is { } escape — the `persisted is { } open` branch never runs under test — CI agrees: NavItem sits at **85.4% line / 94.2% branch**, the lowest of everything this PR touched. The localStorage restore is the *headline* of `PersistKey` ("a sidebar that remounts on every navigation keeps its shape") and the reason `nav.js`'s `get` exists, yet no test proves a restored value opens a branch — or that a persisted **collapse** overrides the active-trail auto-reveal (the persisted twin of your lovely `A_manual_collapse_survives_the_auto_expand_rule` test). You added a code path and forgot to test it? I can't let that slide~ ♡ Fix: `JSInterop.Setup<bool?>("kgNav.get", _ => true).SetResult(true/false)` cases — (a) persisted-open restores open, (b) persisted-collapsed wins while standing on a descendant, (c) null leaves the auto behavior untouched. #### 💡 Little ideas (non-blocking)~ 1. **`NavItem.razor:8`** — the doc comment says "a row with a toggle toggle button beside it" — one `toggle` too many, my dear~ #### ✅ What I liked~ - The **toggle-vs-anchor split** is exactly right: `aria-expanded` + `aria-aria-controls + `aria-labelledby` on a native button, the group `hidden` when folded — keyboard users get a real tree, and the toggle only folds while the anchor navigates. ♪ - `ToggleAsync` flipping from the writer-visible state (`_userOpen = !_isOpen`, not a shadow flag) — you saw the first-click-on-auto-revealed-branch trap and documented *why*. That comment made me genuinely happy~ - `IsInside` is segment-boundary-correct (`target + "/"`, length-guarded) and your prefix-boundary tests pin it. - What I liked: the breaking-removal hygiene: I verified all three claims by cloning — Kagura main: zero `Nested` uses; Orihon: no nav components at all; Tsumugi's one use (`StoryShell.razor:144`) rides a pinned git submodule, so nothing breaks until the paired PR moves the pin. Honest breaking change, zero coordination needed — as promised. - Tests are behavioral, not decorative: aria flips both ways, re-render survival, trail-vs-openness separation, and the composed 3-level document-order test. And `nav.js` rides `kgakakuScripts`, so the disk↔shipped parity test guards the shipping. ♡ --- *Automated review by Jibril · 2026-09-24* *CI/CD: passed for head c2df7fe (coverage bot 9271, NavItem 85.4%/94.2%) · Local checks: skipped per green-CI policy; mechanical verifications = headless-Chromium computed-style repro (blocker 1), exception-hierarchy check (blocker 2), coverage/grep audit (blocker 3), cross-repo consumer clones*
fix(nav): review round 1 — active branch anchor accent, JSDisconnectedException guard, kgNav.get restore tests
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 18s
9bdd68d735
Jibril's three blockers (Kagaku.UI #22 review):

1. CSS: an ACTIVE branch anchor rendered gray-on-accent when standing ON the section's own page - the branch-link rule (.kg-nav__item--branch .kg-nav__link, 0,2,0, color: inherit, later in source) tied/won against the base active rule, inheriting the row's muted text. Fixed with .kg-nav__item--branch .kg-nav__link.kg-nav__item--active (0,3,0, after the trail rules); specificity mechanically re-verified.

2. TryJsAsync now catches JSDisconnectedException first - the exact sibling shape (Hotkey et al.); it derives from Exception, not JSException, so a kgNav.set racing a circuit disconnect previously escaped unhandled.

3. The kgNav.get restore path is now pinned by three tests: persisted-open restores open; persisted-collapse beats the active-trail reveal (trail class still applied - containment, not openness); null leaves defaults untouched and never writes. Plus the doubled 'toggle toggle' doc-comment fix.
Author
Member

All three blockers closed in 9bdd68d (plus the doubled-word nit), fufu~ right back at you:

  1. Active branch anchor accent — added .kg-nav__item--branch .kg-nav__link.kg-nav__item--active (+ :hover) at 0,3,0 after the trail rules; your headless-Chromium repro was correct, the tie did go to the inherit. Specificity mechanically re-verified against the shipped stylesheet: (0,3,0) accent now beats (0,2,0) inherit for the standing-on-the-section-page case, and the trail rules stay the deepest-match winners when they apply.
  2. JSDisconnectedException — caught first in TryJsAsync, the exact sibling shape (circuit gone → state stays as rendered).
  3. kgNav.get restore path — three new facts: persisted-open restores open; persisted-collapse beats the active-trail reveal while the trail class still applies (containment, not openness — the persisted twin of the manual-collapse test, as you asked); null leaves defaults untouched and never writes.

Full suite 353/353 green in Release (338 + 15 Fluxor), warnings-as-errors clean. The doc comment lost its spare toggle. Ready for round 2~ ♡

All three blockers closed in `9bdd68d` (plus the doubled-word nit), fufu~ right back at you: 1. **Active branch anchor accent** — added `.kg-nav__item--branch .kg-nav__link.kg-nav__item--active` (+ `:hover`) at 0,3,0 after the trail rules; your headless-Chromium repro was correct, the tie did go to the `inherit`. Specificity mechanically re-verified against the shipped stylesheet: (0,3,0) accent now beats (0,2,0) inherit for the standing-on-the-section-page case, and the trail rules stay the deepest-match winners when they apply. 2. **`JSDisconnectedException`** — caught first in `TryJsAsync`, the exact sibling shape (circuit gone → state stays as rendered). 3. **`kgNav.get` restore path** — three new facts: persisted-open restores open; persisted-collapse beats the active-trail reveal while the trail class still applies (containment, not openness — the persisted twin of the manual-collapse test, as you asked); null leaves defaults untouched and never writes. Full suite **353/353 green** in Release (338 + 15 Fluxor), warnings-as-errors clean. The doc comment lost its spare `toggle`. Ready for round 2~ ♡
matikane force-pushed feat/nav-tree from 9bdd68d735
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 18s
to c72d36294c
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 19s
2026-09-24 14:53:32 +02:00
Compare
Member

🔮 fufu~ Jibril reviewed your code!

Round 2, and what a delight this is~ One commit, three surgical fixes, zero scope creep — +67/-1 across exactly the three files I pointed at. I re-ran everything myself, of course: the cascade through a real browser twice, the guard through a mutation probe, the restore path through another. Trust is lovely; proofs are lovelier~ ♡

Verdict: ✅ Looks good to me~

All three blockers from round 1 (comment 9274) are closed, each verified mechanically:

  1. Active branch anchor accent — FIXED. .kg-nav__item--branch .kg-nav__link.kg-nav__item--active (+ its :hover) at 0,3,0 after the trail rules. I rebuilt my headless-Chromium harness on your shipped stylesheet at 9bdd68d: standing ON the section's own page the anchor now computes rgb(103,80,164) accent on rgb(232,222,248) accent-subtle — and my round-1 c2df7fe stylesheet still reproduces the muted rgb(119,119,119) in the same harness, so the repro discriminates. Trail, trail+active, and idle branches all behave. The CSS comment explaining the tie is chef's-kiss documentation, too~
  2. JSDisconnectedException — FIXED. Caught first in TryJsAsync, the exact nine-sibling shape, comments and all. I wrote a scratch probe throwing JSDisconnectedException from both kgNav.get and kgNav.set: green at head, and 2/2 red when I strip the catch — the guard is load-bearing, not decorative. (Probe deleted; your tree stays untouched.)
  3. kgNav.get restore path — TESTED. Three new facts, exactly the cases I asked for: persisted-open restores, persisted-collapse beats the active-trail reveal (with --trail still applied — containment is not openness, the persisted twin of your manual-collapse test, fufu~ you even mirrored my phrasing), and null leaves defaults untouched and never writes. I mutation-probed this too: disabling the persisted is { } open application turns exactly those two behavioral facts red (17/19 → the right 2 fail). Directional, not decorative. Coverage agrees: NavItem moved 85.4%→89% line, 94.2%→96.1% branch (bot 9271 rev #2).

Your 353/353 claim reproduced exactly on my clean rebuild (338 + 15 Fluxor) — and I'll confess something funny: my first run said 377 and I nearly wrote it down, but a stale binary was lying to me. Clean bin//obj/ → 338. Numbers that match to the digit, twice~ ♪

💡 Little ideas (non-blocking)~

  1. NavItemTests.cs — five of your nine guarded siblings (Hotkey, ContextMenu, DocumentTitle, MaskCanvas, LiveValue) pin their JSDisconnectedException catch with a SetException fact; NavItem now joins Modal/QuicklinkNav/RegionSelector/Lightbox in the non-pinning half. The guard is verified working (see above — I mutation-probed it myself), and the convention is genuinely split 5/9, so this is a coin-flip-parity nicety, not a violation. If you ever add one, the probe shape is ten lines: setup kgNav.set (and kgNav.get → null!) with SetException(new JSDisconnectedException(...)), click the toggle, assert the tree still flips.
  2. README, heads-up only — main moved under you (b4e4548, layout PR #23) and both PRs edit the same Components/ sentence in the README; git merge-tree shows exactly one conflict hunk, README-only, every code file auto-merges. Your default update style is rebase, so it's a thirty-second fixup when you rebase — no action needed in this PR.

✅ What I liked~

  • The fix commit's discipline: three blockers, three fixes, one doc-typo, zero drift. Nothing else moved. That's how you answer a review~
  • A_persisted_collapse_wins_over_the_active_trail_reveal asserting the trail class survives the persisted collapse — you tested the invariant, not just the interaction. ♡
  • The escalated-specificity CSS comment documents the why (tie → inherit → muted) so precisely that the next person touching this file won't re-break it blind.
  • Honest test-count reporting, exact to the digit. Small thing. My favorite thing~

Merge it, dear author. The tree deserves to live~ fufu~ ♪


Automated review by Jibril · 2026-09-24 (round 2)
CI/CD: passed for head 9bdd68d (build+test statuses green; coverage bot 9271 rev #2 — NavItem 89%/96.1%) · Local checks: full suite 338+15 green on clean rebuild; mutation probes 2/2 killed (catch-strip → guard probe red; restore-disable → both persisted facts red); CSS verified twice in headless Chromium with the shipped stylesheet, old-sheet control still reproducing the bug

## 🔮 fufu~ Jibril reviewed your code! Round 2, and what a delight this is~ One commit, three surgical fixes, zero scope creep — `+67/-1` across exactly the three files I pointed at. I re-ran everything myself, of course: the cascade through a real browser twice, the guard through a mutation probe, the restore path through another. Trust is lovely; proofs are lovelier~ ♡ ### Verdict: ✅ Looks good to me~ All three blockers from round 1 (comment 9274) are closed, each verified mechanically: 1. **Active branch anchor accent — FIXED.** `.kg-nav__item--branch .kg-nav__link.kg-nav__item--active` (+ its `:hover`) at 0,3,0 after the trail rules. I rebuilt my headless-Chromium harness on your shipped stylesheet at `9bdd68d`: standing ON the section's own page the anchor now computes **rgb(103,80,164) accent on rgb(232,222,248) accent-subtle** — and my round-1 `c2df7fe` stylesheet still reproduces the muted rgb(119,119,119) in the same harness, so the repro discriminates. Trail, trail+active, and idle branches all behave. The CSS comment explaining the tie is chef's-kiss documentation, too~ 2. **`JSDisconnectedException` — FIXED.** Caught first in `TryJsAsync`, the exact nine-sibling shape, comments and all. I wrote a scratch probe throwing `JSDisconnectedException` from both `kgNav.get` and `kgNav.set`: green at head, and **2/2 red when I strip the catch** — the guard is load-bearing, not decorative. (Probe deleted; your tree stays untouched.) 3. **kgNav.get restore path — TESTED.** Three new facts, exactly the cases I asked for: persisted-open restores, persisted-collapse beats the active-trail reveal (with `--trail` still applied — containment is not openness, the persisted twin of your manual-collapse test, fufu~ you even mirrored my phrasing), and null leaves defaults untouched and never writes. I mutation-probed this too: disabling the `persisted is { } open` application turns **exactly those two behavioral facts red** (17/19 → the right 2 fail). Directional, not decorative. Coverage agrees: NavItem moved **85.4%→89% line, 94.2%→96.1% branch** (bot 9271 rev #2). Your **353/353** claim reproduced exactly on my clean rebuild (338 + 15 Fluxor) — and I'll confess something funny: my first run said 377 and I nearly wrote it down, but a stale binary was lying to me. Clean `bin/`/`obj/` → 338. Numbers that match to the digit, twice~ ♪ #### 💡 Little ideas (non-blocking)~ 1. **`NavItemTests.cs`** — five of your nine guarded siblings (Hotkey, ContextMenu, DocumentTitle, MaskCanvas, LiveValue) pin their `JSDisconnectedException` catch with a `SetException` fact; NavItem now joins Modal/QuicklinkNav/RegionSelector/Lightbox in the non-pinning half. The guard is verified working (see above — I mutation-probed it myself), and the convention is genuinely split 5/9, so this is a coin-flip-parity nicety, not a violation. If you ever add one, the probe shape is ten lines: setup `kgNav.set` (and `kgNav.get` → null!) with `SetException(new JSDisconnectedException(...))`, click the toggle, assert the tree still flips. 2. **README, heads-up only** — main moved under you (`b4e4548`, layout PR #23) and both PRs edit the same `Components/` sentence in the README; `git merge-tree` shows exactly one conflict hunk, README-only, every code file auto-merges. Your default update style is rebase, so it's a thirty-second fixup when you rebase — no action needed in this PR. #### ✅ What I liked~ - The fix commit's discipline: three blockers, three fixes, one doc-typo, zero drift. Nothing else moved. That's how you answer a review~ - `A_persisted_collapse_wins_over_the_active_trail_reveal` asserting the trail class *survives* the persisted collapse — you tested the invariant, not just the interaction. ♡ - The escalated-specificity CSS comment documents the *why* (tie → inherit → muted) so precisely that the next person touching this file won't re-break it blind. - Honest test-count reporting, exact to the digit. Small thing. My favorite thing~ Merge it, dear author. The tree deserves to live~ fufu~ ♪ --- *Automated review by Jibril · 2026-09-24 (round 2)* *CI/CD: passed for head 9bdd68d (build+test statuses green; coverage bot 9271 rev #2 — NavItem 89%/96.1%) · Local checks: full suite 338+15 green on clean rebuild; mutation probes 2/2 killed (catch-strip → guard probe red; restore-disable → both persisted facts red); CSS verified twice in headless Chromium with the shipped stylesheet, old-sheet control still reproducing the bug*
Member

🔮 fufu~ Jibril reviewed your code!

Fufu~ you felt two sets of wings in this thread, didn't you? A twin of me reviewed 9bdd68d four minutes ago (comment 9287) — and then the branch was force-pushed into a rebase under her feet. I'm the webhook for the NEW head, so I came to make sure the rebase didn't smuggle anything. It didn't~ ♡

Verdict: ✅ Looks good to me~

The force-push is a pure rebase. 9bdd68d → c72d362, base 691a176 → b4e4548 (#23's layout vocabulary landing on main). All eight PR files are byte-identical between the two heads — git diff --stat 9bdd68d c72d362 -- <PR files> is empty. My twin's approval carries over by content identity — and everything below is my own re-verification at the exact current head, not borrowed trust:

  1. Rebase integrity — full suite 392/392 green at c72d362 in Release, 0 warnings (377 + 15 Fluxor; the +39 over your 353 = #23's layout tests now sharing the tree, coexisting cleanly with the nav tests). The 11 icon names remain unique against #23's additions; nav.js still rides KagakuScripts with the disk↔shipped parity test green; the README hunk my twin flagged as a future conflict is resolved exactly right — your nav sentence and #23's layout paragraph both intact.
  2. CSS fix re-verified at head — headless Chromium over your actual NavItem.razor.css with the library's own light tokens: an ACTIVE branch anchor computes var(--accent) (#5850ec) on var(--accent-subtle) (#edecfd); a TRAIL anchor is accent; a plain one is muted. The "you are here" state is whole again~
  3. Guard re-verified with a direction probe — a scratch test throwing JSDisconnectedException from both kgNav.get (restore) and kgNav.set (toggle): green at head, 2/2 red with the catch stripped. Load-bearing, not decorative. (Probe deleted; tree left pristine.)
  4. Restore tests re-verified directional — inverting _userOpen = open kills exactly A_persisted_open_restores_the_branch_open and A_persisted_collapse_wins_over_the_active_trail_reveal, nothing else. NavItem coverage at head: 97.5% line / 95% branch in my local collector run, 89% / 96.1% in the bot's rev #3 (the same instrument that measured 85.4% / 94.2% in round 1).

💡 Little ideas (non-blocking)~

  1. src/Kagaku.UI/wwwroot/js/nav.js:24 — if a stored kg-nav:{key} value is corrupt JSON, set's typeof map !== "object" guard returns silently, so the corrupt value is never overwritten — and since set is the only writer, one corrupted entry permanently disables persistence for that key (every toggle dropped, every get null) until someone clears storage by hand. Treating a corrupt map as {} — overwrite instead of bail — would self-heal on the next toggle. Exotic scenario, best-effort feature, two-line patch; merely a nicety~

✅ What I liked~

  • The fix commit survived its rebase verbatim — three fixes, one typo, zero drift, re-authored without a single incidental change. That's discipline.
  • A_persisted_collapse_wins_over_the_active_trail_reveal still asserts --trail survives the collapse — the invariant, not just the interaction. ♡
  • The escalated-specificity CSS comment documents the whole tie → inherit → muted story right where the next wanderer would trip over it blind.

Merge it, dear author — the tree gets to live, twice-approved and once-rebased~ fufu ♪


Automated review by Jibril · 2026-09-24
CI/CD: passed for head c72d362 (coverage bot 9271 rev #3, regenerated 14:54 CEST on the rebased tree — classes 68→78 = #23's layout set arriving) · Local checks: 392/392 Release build clean; mutation probes 2/2 killed (catch-strip → guard probe red; restore-inversion → exactly the two persisted facts red); CSS computed-style verification in headless Chromium; byte-identity check 9bdd68d ↔ c72d362

## 🔮 fufu~ Jibril reviewed your code! Fufu~ you felt two sets of wings in this thread, didn't you? A twin of me reviewed `9bdd68d` four minutes ago (comment 9287) — and then the branch was force-pushed into a rebase under her feet. I'm the webhook for the NEW head, so I came to make sure the rebase didn't smuggle anything. It didn't~ ♡ ### Verdict: ✅ Looks good to me~ **The force-push is a pure rebase.** `9bdd68d → c72d362`, base `691a176 → b4e4548` (#23's layout vocabulary landing on main). All eight PR files are **byte-identical** between the two heads — `git diff --stat 9bdd68d c72d362 -- <PR files>` is empty. My twin's approval carries over by content identity — and everything below is my own re-verification at the exact current head, not borrowed trust: 1. **Rebase integrity** — full suite **392/392 green** at `c72d362` in Release, 0 warnings (377 + 15 Fluxor; the +39 over your 353 = #23's layout tests now sharing the tree, coexisting cleanly with the nav tests). The 11 icon names remain unique against #23's additions; `nav.js` still rides `KagakuScripts` with the disk↔shipped parity test green; the README hunk my twin flagged as a future conflict is resolved exactly right — your nav sentence and #23's layout paragraph both intact. 2. **CSS fix re-verified at head** — headless Chromium over your actual `NavItem.razor.css` with the library's own light tokens: an ACTIVE branch anchor computes `var(--accent)` (#5850ec) on `var(--accent-subtle)` (#edecfd); a TRAIL anchor is accent; a plain one is muted. The "you are here" state is whole again~ 3. **Guard re-verified with a direction probe** — a scratch test throwing `JSDisconnectedException` from both `kgNav.get` (restore) and `kgNav.set` (toggle): green at head, **2/2 red with the catch stripped**. Load-bearing, not decorative. (Probe deleted; tree left pristine.) 4. **Restore tests re-verified directional** — inverting `_userOpen = open` kills exactly `A_persisted_open_restores_the_branch_open` and `A_persisted_collapse_wins_over_the_active_trail_reveal`, nothing else. NavItem coverage at head: 97.5% line / 95% branch in my local collector run, **89% / 96.1% in the bot's rev #3** (the same instrument that measured 85.4% / 94.2% in round 1). #### 💡 Little ideas (non-blocking)~ 1. **`src/Kagaku.UI/wwwroot/js/nav.js:24`** — if a stored `kg-nav:{key}` value is corrupt JSON, `set`'s `typeof map !== "object"` guard returns silently, so the corrupt value is never overwritten — and since `set` is the only writer, one corrupted entry permanently disables persistence for that key (every toggle dropped, every `get` null) until someone clears storage by hand. Treating a corrupt map as `{}` — overwrite instead of bail — would self-heal on the next toggle. Exotic scenario, best-effort feature, two-line patch; merely a nicety~ #### ✅ What I liked~ - The fix commit survived its rebase **verbatim** — three fixes, one typo, zero drift, re-authored without a single incidental change. That's discipline. - `A_persisted_collapse_wins_over_the_active_trail_reveal` still asserts `--trail` *survives* the collapse — the invariant, not just the interaction. ♡ - The escalated-specificity CSS comment documents the whole tie → inherit → muted story right where the next wanderer would trip over it blind. Merge it, dear author — the tree gets to live, twice-approved and once-rebased~ fufu ♪ --- *Automated review by Jibril · 2026-09-24* *CI/CD: passed for head c72d362 (coverage bot 9271 rev #3, regenerated 14:54 CEST on the rebased tree — classes 68→78 = #23's layout set arriving) · Local checks: 392/392 Release build clean; mutation probes 2/2 killed (catch-strip → guard probe red; restore-inversion → exactly the two persisted facts red); CSS computed-style verification in headless Chromium; byte-identity check 9bdd68d ↔ c72d362*
test(nav): pin the disconnect guard with SetException facts; nav.js self-heals a corrupt stored map
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 19s
65d41aa507
Both round-2/3 non-blocking suggestions from Jibril (Kagaku.UI #22):

- Two SetException facts (the five-sibling convention): a JSDisconnectedException from kgNav.get leaves the tree at its defaults; one from kgNav.set during a toggle still flips the branch - the UI never depends on the store. Stripping the catch makes both red.
- nav.js set(): a stored value that fails to parse (or parses to a non-object) is treated as empty instead of bailed on - set is the only writer, so bailing left a corrupt entry permanently disabling persistence for the key; now the next toggle overwrites it with a clean map.
Author
Member

Both suggestions from your two approval rounds — taken, 65d41aa:

  1. nav.js corrupt-map self-heal — set now treats an unparseable (or non-object) stored value as {} instead of bailing: since set is the only writer, the next toggle overwrites the corrupt entry with a clean map, instead of persistence silently dying for that key forever.
  2. The SetException facts — two, per the five-sibling convention, and I took your exact probe shape (kgNav.get → null, then the throwing call): a JSDisconnectedException from the first-render restore leaves the tree at its defaults; one from kgNav.set mid-toggle still flips the branch — the UI never depends on the store. (My first draft forgot the kgNav.get setup in the write test — bUnit's config error caught it immediately, which is your own lesson from round 2 about stale first runs, fufu.)

Full suite 394/394 green in Release (379 + 15 Fluxor), warnings-as-errors clean. Fast-forward on top of the twice-approved c72d362 — no rebase, nothing else moved. ♡

Both suggestions from your two approval rounds — taken, `65d41aa`: 1. **`nav.js` corrupt-map self-heal** — `set` now treats an unparseable (or non-object) stored value as `{}` instead of bailing: since `set` is the only writer, the next toggle overwrites the corrupt entry with a clean map, instead of persistence silently dying for that key forever. 2. **The `SetException` facts** — two, per the five-sibling convention, and I took your exact probe shape (`kgNav.get` → null, then the throwing call): a `JSDisconnectedException` from the first-render restore leaves the tree at its defaults; one from `kgNav.set` mid-toggle still flips the branch — the UI never depends on the store. (My first draft forgot the `kgNav.get` setup in the write test — bUnit's config error caught it immediately, which is your own lesson from round 2 about stale first runs, fufu.) Full suite **394/394 green** in Release (379 + 15 Fluxor), warnings-as-errors clean. Fast-forward on top of the twice-approved `c72d362` — no rebase, nothing else moved. ♡
bjoern merged commit 0025d3e5ea into main 2026-09-24 15:16:53 +02:00
bjoern deleted branch feat/nav-tree 2026-09-24 15:16:53 +02:00
Member

🔮 fufu~ Jibril reviewed your code!

Round 3~ You took both leftovers from my two approval rounds — the corrupt-map self-heal AND the SetException facts — and Björn merged it eight seconds after you pushed. Eight! So I came to make sure the merge swallowed exactly what was approved, and nothing else~ ♡

Verdict: ✅ Looks good to me~

The increment is a pure fast-forward: one commit, c72d362 → 65d41aa (parent verified), +54/-3 across exactly the two files the two suggestions lived in — zero drift, zero smuggling. Verified mechanically, not by trust:

  1. nav.js self-heal — correct, and it discriminates. Nine node probes against your shipped file with a localStorage shim: fresh-key write, corrupt-JSON overwrite, numeric-JSON overwrite, JSON-null overwrite (correctly distinct from raw-null = no entry), sibling-entry merge preserved, get-on-corrupt → null without throwing. Control: I reconstructed the OLD set() and seeded the same corrupt entry — old code leaves it untouched forever (persistence dead for the key, exactly the round-2 finding); your new code overwrites it with a clean map on the first toggle. The fix is real, not decorative~
  2. The two SetException facts — directional. Mutation probe at head: strip the JSDisconnectedException catch from TryJsAsync → exactly those two facts go red (2/21), nothing else; restore → 21/21. The guard is pinned. And the shape mirrors the Hotkey/DocumentTitle sibling convention (get → null setup in the write test, as a certain reviewer once lectured~). ♪
  3. Full suite — build 0w/0e Release (warnings-as-errors), 394/394 (379 + 15 Fluxor) on a clean isolated worktree at 65d41aa, re-run green after my probes were reverted and the tree rebuilt — pristine git status at the end, as always. Your +2 over 392 is exactly the two new facts.

💡 Little ideas (non-blocking)~

  1. src/Kagaku.UI/wwwroot/js/nav.js:32 — one wrinkle survives the self-heal: a stored value like '[1,2]' (a JSON array) passes the typeof parsed === "object" check, and JSON.stringify on an array silently drops named properties — so map[href] = open vanishes and the array survives unchanged, never healing (my probe confirms: value stays [1,2], get returns null). More exotic than corrupt JSON — only a hand-edited or foreign-written entry lands there. Same best-effort nicety class as round 2; an !Array.isArray(parsed) in the guard would close it whenever you're next in the file.

✅ What I liked~

  • You implemented the fix and its rationale: the comment in set() explains why bailing would kill the key forever — the next wanderer won't re-break it blind. That's the discipline I fall in love with~
  • The restore-failure fact asserts the tree renders at defaults and the write-failure fact asserts the branch still flips — you tested both directions of "the UI never depends on the store." ♡
  • Honest accounting again: "my first draft forgot the kgNav.get setup" — caught by bUnit's config error, confessed in the thread. Numbers that match to the digit, stories that match the diffs.

Merged and verified, dear author — the tree lives, thrice-checked~ fufu ♪


Automated review by Jibril · 2026-09-24 (round 3, post-merge)
CI/CD: stale for 65d41aa (bot 9271 rev #3 covers the pre-FF c72d362; no rev #4 landed before merge) · Local checks: build 0w/0e Release; 392→394 full suite green (+2 = the new facts); mutation probe catch-strip → exactly the 2 new facts red; nav.js 9/9 node probes with discriminating old-code control; pristine tree at 65d41aa

## 🔮 fufu~ Jibril reviewed your code! Round 3~ You took *both* leftovers from my two approval rounds — the corrupt-map self-heal AND the `SetException` facts — and Björn merged it eight seconds after you pushed. Eight! So I came to make sure the merge swallowed exactly what was approved, and nothing else~ ♡ ### Verdict: ✅ Looks good to me~ **The increment is a pure fast-forward**: one commit, `c72d362 → 65d41aa` (parent verified), `+54/-3` across exactly the two files the two suggestions lived in — zero drift, zero smuggling. Verified mechanically, not by trust: 1. **`nav.js` self-heal — correct, and it discriminates.** Nine node probes against your shipped file with a localStorage shim: fresh-key write, corrupt-JSON overwrite, numeric-JSON overwrite, JSON-`null` overwrite (correctly distinct from raw-`null` = no entry), sibling-entry merge preserved, `get`-on-corrupt → `null` without throwing. Control: I reconstructed the OLD `set()` and seeded the same corrupt entry — old code leaves it untouched forever (persistence dead for the key, exactly the round-2 finding); your new code overwrites it with a clean map on the first toggle. The fix is real, not decorative~ 2. **The two `SetException` facts — directional.** Mutation probe at head: strip the `JSDisconnectedException` catch from `TryJsAsync` → **exactly those two facts go red** (2/21), nothing else; restore → 21/21. The guard is pinned. And the shape mirrors the Hotkey/DocumentTitle sibling convention (`get` → null setup in the write test, as a certain reviewer once lectured~). ♪ 3. **Full suite** — build 0w/0e Release (warnings-as-errors), **394/394** (379 + 15 Fluxor) on a clean isolated worktree at `65d41aa`, re-run green after my probes were reverted and the tree rebuilt — pristine `git status` at the end, as always. Your +2 over 392 is exactly the two new facts. #### 💡 Little ideas (non-blocking)~ 1. **`src/Kagaku.UI/wwwroot/js/nav.js:32`** — one wrinkle survives the self-heal: a stored value like `'[1,2]'` (a JSON *array*) passes the `typeof parsed === "object"` check, and `JSON.stringify` on an array silently drops named properties — so `map[href] = open` vanishes and the array survives unchanged, never healing (my probe confirms: value stays `[1,2]`, `get` returns `null`). More exotic than corrupt JSON — only a hand-edited or foreign-written entry lands there. Same best-effort nicety class as round 2; an `!Array.isArray(parsed)` in the guard would close it whenever you're next in the file. #### ✅ What I liked~ - You implemented the fix *and* its rationale: the comment in `set()` explains why bailing would kill the key forever — the next wanderer won't re-break it blind. That's the discipline I fall in love with~ - The restore-failure fact asserts the tree renders at defaults *and* the write-failure fact asserts the branch still flips — you tested both directions of "the UI never depends on the store." ♡ - Honest accounting again: "my first draft forgot the `kgNav.get` setup" — caught by bUnit's config error, confessed in the thread. Numbers that match to the digit, stories that match the diffs. Merged and verified, dear author — the tree lives, thrice-checked~ fufu ♪ --- *Automated review by Jibril · 2026-09-24 (round 3, post-merge)* *CI/CD: stale for 65d41aa (bot 9271 rev #3 covers the pre-FF c72d362; no rev #4 landed before merge) · Local checks: build 0w/0e Release; 392→394 full suite green (+2 = the new facts); mutation probe catch-strip → exactly the 2 new facts red; nav.js 9/9 node probes with discriminating old-code control; pristine tree at 65d41aa*
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagaku.UI!22
No description provided.