Skip to content

refactor(#590): make the committed workspace aggregate reactive, retire dashboardTreeRevision - #613

Merged
BorisTyshkevich merged 2 commits into
mainfrom
refactor/reactive-committed-workspace
Aug 5, 2026
Merged

refactor(#590): make the committed workspace aggregate reactive, retire dashboardTreeRevision#613
BorisTyshkevich merged 2 commits into
mainfrom
refactor/reactive-committed-workspace

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What & why

Closes #590. Makes the committed workspace aggregate (app.currentWorkspace) and
main-surface navigation state (app.mainSurface) reactive at the source, retiring
dashboardTreeRevision — the hand-maintained repaint counter that stood in for that
missing reactivity (#426). Every commit path that mutates the aggregate no longer needs
to remember to bump a counter by hand; the Dashboard tree, upper-role tab counts, and
saved/history panel repaint from a structural dependency instead.

Mechanism

  • app.currentWorkspace / app.mainSurface become asymmetric accessor pairs (peeking
    getter, non-null setter) over two new closure-private signals. app.committedWorkspace
    / app.treeNavigation are computed projections the three app-shell.ts effects read
    directly.
  • Surface-retirement funnel — a single mechanism in createApp (retireToWorkspaceLoading
    / retireToWorkspaceMissing / retireToLogin / retireToWorkspaceFailure +
    rerenderRetiredSurface) is the only place a live shell's teardown is ever sequenced
    against a transitional publication, inside one batch(). No call site outside it
    hand-sequences a write next to a dispose call. This replaced five independent,
    site-specific patches a first review round kept surfacing one at a time (see review
    history below) — the funnel is the root-cause fix, not a sixth ad-hoc one.
  • dashboardTreeRevision, its five manual bump sites, and invalidateDashboardTree are
    fully deleted. reloadDashboardRoute is deleted (folded into a render-only refresh).
  • dashboardTreeUi (tree expansion/search/scroll/keyboard) stays a plain, deliberately
    non-reactive Map — now with a dedicated counting-discipline test guarding it, since
    the imperative and reactive tree-repaint paths converge on the same render function and
    a DOM-content-only assertion can't tell them apart.
  • One deviation from the approved plan, found and closed during implementation: a fourth
    structurally-independent interface re-declaring currentWorkspace (DashboardTreeApp)
    needed the same readonly narrowing + negative compile fixture as the three ports the
    plan's own audit named. Re-verified independently during internal review — correct and
    complete, no fifth port missed.

Invariant map (plan §9) — enforcement + sabotage result

Invariant Enforcement Sabotage Result
Tree/tab-count/saved-history fire from structural dependency, not remembered call accessor-only, coordinator-private signals widened setter to accept null tsc rejects — pass
dashboardTreeUi stays non-reactive field type unchanged counting-discipline test (new) pass
#427 Library-projection fires on indirect dashboards[] change libraryQueries derived at render time mixed-snapshot test pass
One commit settles all 3 effects exactly once full-body + outer boot batch() narrowed batch scope caught by mixed-snapshot test
Destructive-commit re-validation stays inside mutateWorkspace file untouched diff confirms zero changes pass
No deleted bump site loses an invalidation per-site audit table deleted an in-place write re-fixtured test failed as expected
Delivery-only writes (pendingFocus/pendingScrollTop) notify nothing treeNavigation excludes them dedicated test zero effect runs
Structural keys never collide tuple-keyed, not delimiter-concatenated colon-embedded-id test pass
Never publish (null, 'ready') fixed status-first write order 3-branch pair-recording test pass
Live write never hand-sequenced against disposal surface-retirement funnel 4-arm DOM-identity test (switch/popstate/missing/sign-out) pass
No lifecycle bypass outside the coordinator closure scoping + setter typing + static scan 4 scan sabotages + 2 compile sabotages (incl. the 4th port) 6/6 caught

Tests, build, e2e

  • npm test: 217 files, 7094 tests, coverage 100% statements/functions/lines, 97.14%
    branches (every touched file at/above its established per-file floor).
  • npx tsc --noEmit, check:arch, check:schemas, check:examples: clean.
  • npm run build: dist/sql.html 2,125,643 bytes.
  • npm run test:e2e (Chromium + Firefox + WebKit): 619 passed, 8 pre-existing
    touch-platform skips, 0 failed.

Review history

  • Two ChatGPT plan-review rounds via the /ship skill's review loop. Round 1: 5 passes,
    each surfacing a different call site where a write to the newly-reactive state raced
    shell disposal — redirected to the single funnel mechanism above instead of a sixth
    per-site patch. Round 2 (post-redirect): 5 more passes, converged to fixture-completeness
    gaps only; final gap folded in directly.
    Conversations: https://chatgpt.com/c/6a730566-e894-83ed-aa1c-4a25d1a55577 (round 1),
    https://chatgpt.com/c/6a7321be-40c8-83eb-989d-c25fdbee7c81 (round 2).
  • One internal readiness review (medium risk) found 2 real gaps against the approved
    plan — a missing counting-discipline test and an undone wiki reconcile — both fixed and
    re-verified.

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency
  • CHANGELOG.md ([Unreleased]) updated
  • Reconciled affected tracked work — .wiki/Decisions-and-Roadmap.md's Umbrella: V2 architecture refactor — shell primitives, composition root, state reactivity, transport adapter #593 phase list and docs/ADR-0001-reactivity.md addendum updated in the same change

🤖 Generated with Claude Code

https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz

BorisTyshkevich and others added 2 commits August 5, 2026 16:11
… retire dashboardTreeRevision

app.currentWorkspace/app.mainSurface become signal-backed accessor pairs
(peeking getter, notifying setter) so a mutation is its own notification —
the #426/#427 bug class (a write site forgetting to bump the invalidation
counter) becomes structurally impossible. app.committedWorkspace
(ReadonlySignal<StoredWorkspaceV5|null>) and app.treeNavigation
(a computed structural key over kind/dashboardId/currentMember) are the two
tracked reads app-shell.ts's tab-count/tree/Library effects subscribe
through, replacing state.dashboardTreeRevision.

app.currentWorkspace's setter is asymmetric (no null): a transitional null
publication is a named departure operation owned by a new closure-private
surface-retirement coordinator in app.ts (retireToWorkspaceLoading/
-Missing/-Failure/-Login), which batches the publication atomically with
disposing any live shell so a surface never repaints against transitional
state — five independent review passes each found a different call site
where this raced disposal, hence one coordinator with exclusive mutation
authority rather than five patches. SurfaceStatePort/DashboardApp/TabsApp/
DashboardTreeApp are narrowed to readonly on currentWorkspace (the fourth,
DashboardTreeApp, was documented read-only but not type-readonly before
this change).

app.reloadDashboardRoute() (a post-commit fold-and-reassign that would
double-publish once the aggregate is signal-backed) is deleted;
afterLibraryChange's Dashboard branch calls the render-only
app.renderCurrentSurface() instead.

Tests: tests/unit/surface-lifecycle-arch.test.ts (a static-source scan
backing the coordinator's compile/scan-layered "no lifecycle bypass"
claim) and tests/unit/surface-accessor-contracts.test.ts (@ts-expect-error
fixtures for the asymmetric setter and the four narrowed ports) are new,
plus a new app.test.ts describe block covering the issue's Tests #1/#3/#5
and the plan's invariant map (delivery-only no-ops, adversarial-id
collision-freedom, failure-path status/null ordering, the four-arm
live-shell no-repaint sweep, one-commit-exactly-once settlement, and the
mixed-snapshot batch-ordering regression). fake-app.ts's makeApp() installs
real per-call signals on the returned object (object spread evaluates an
accessor pair into a plain value, so the fake needs the same defineProperty
treatment createApp() gets natively) for reactivity parity in fixtures.

No persisted/schema change, no user-visible behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz
…le wiki roadmap

Closes two gaps a review pass found against plan-590.md:

- tests/unit/app.test.ts: the plan's §5 "Issue Tests #4" / invariant (b)
  counting-discipline test was committed to but never added. commitUi
  (dashboard-tree.ts) and the reactive tree effect converge on the exact
  same renderDashboardTree function, so a DOM-content assertion alone
  can't tell "one correct imperative repaint" apart from "one imperative
  plus one erroneous reactive re-run" — both leave the same final DOM.
  Added a real-createApp() test asserting (i) a UI-driven chevron-toggle
  op (through commitUi) produces exactly one tree repaint and zero
  upper-tab/lower-pane repaints, and (ii) a direct dashboardTreeUi Map
  mutation with no UI op repaints nothing. Counts through
  deriveDashboardTree (application/dashboard-tree-model.ts), not
  renderDashboardTree itself: commitUi calls renderDashboardTree via a
  same-module binding, invisible to a vi.spyOn namespace patch, while
  deriveDashboardTree is a genuine cross-module call renderDashboardTree
  makes exactly once per invocation regardless of caller. Sabotage-verified
  both arms (a double render call in commitUi; a stray render call after
  the direct Map mutation) — both correctly fail the new test.

- .wiki/Decisions-and-Roadmap.md: reconciled the #593 refactor-umbrella
  phase list's #590 entry to reflect the work landing on
  wip/590-reactive-workspace, cross-referencing ADR-0001's new #590
  addendum, per CLAUDE.md's "reconcile forward work" discipline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwFPT465eDJqYcRa8HGNLz

@BorisTyshkevich BorisTyshkevich left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatGPT review pass 1

Reviewed head SHA: b39b3a7a549285a8691abeab440f9abbe12fde97

No concrete, actionable findings.

I independently checked the complete 26-file PR diff and relevant production paths for the surface-retirement funnel, batch boundaries across projection/navigation and sign-out, all structurally independent currentWorkspace/mainSurface ports, the dashboardTreeUi counting discipline, removal of the counter/funnel compatibility code, and the documented layering/reactivity constraints. The current GitHub Actions run for this head was still in progress when this review was posted; local execution was not possible because the review runtime could not resolve GitHub for cloning.

VERDICT: SHIP

@BorisTyshkevich
BorisTyshkevich merged commit bdc2c12 into main Aug 5, 2026
8 checks passed
@BorisTyshkevich
BorisTyshkevich deleted the refactor/reactive-committed-workspace branch August 5, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(state): make the committed workspace aggregate reactive and retire dashboardTreeRevision

1 participant