Architecture deepening: extract six deep modules #20

Merged
pecheny merged 6 commits from architecture-deepening into main 2026-07-23 00:14:50 +00:00
Owner

Why

Six behaviour-preserving refactors. Each carves a testable deep module out of the frontend page monoliths or the match-edit path, following patterns the codebase already uses (sync.js, DopeTable, stage-cache.js, domain/resolver).

  • xy carddraft.js: the card-draft dirty kernel
  • dope stats-sync.js: the live stats loop shared by host and viewer
  • dope domain/matchedit: pure EK match-edit validation
  • dope entry-model.js: the od.js paste/coerce/invert kernel
  • xy handoutsession.js: the handout staging session
  • xy boardmembers.js: the members seam split out

Every extracted module has unit tests. Both apps' full suites pass.

🤖 Generated with Claude Code

# Why Six behaviour-preserving refactors. Each carves a testable deep module out of the frontend page monoliths or the match-edit path, following patterns the codebase already uses (sync.js, DopeTable, stage-cache.js, domain/resolver). - xy carddraft.js: the card-draft dirty kernel - dope stats-sync.js: the live stats loop shared by host and viewer - dope domain/matchedit: pure EK match-edit validation - dope entry-model.js: the od.js paste/coerce/invert kernel - xy handoutsession.js: the handout staging session - xy boardmembers.js: the members seam split out Every extracted module has unit tests. Both apps' full suites pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Why
board.js held the card editor's draft/baseline as six module globals and
computed the Save-button dirty check inline, reachable only through the DOM —
so its trickiest, most bug-prone rule had no test. Move the draft state and the
dirty/normalize logic into carddraft.js behind a small interface; board.js keeps
the DOM and drives it. The kernel now has jstest coverage; behaviour is
unchanged (verified in-browser: the save/commit/edit/alias cycle is identical).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Why
host.js and viewer.js each held a byte-identical copy of the stats-page resync
loop (applyStatsMatchEvent + the throttle/debounce schedulers) over their own
timer globals, untested. Move it into stats-sync.js as a create(deps) factory —
the same injected-dependency shape DopeStageCache uses — so the
throttle/coalesce/seq-gap logic is unit-tested once and the duplicate is deleted.
Both pages now call statsSync.applyMatchEvent. Behaviour unchanged; verified by
the loop unit tests and the page-compile/selector-contract Go test (no seeded EK
fest available locally for a live-SSE browser pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Why
applyMatchEditTx mixed the pure match-edit rules (team bounds, action shape,
place/theme/answer/mark checks) with tx SQL in the server trunk, so the rules
were reachable only through a booted server + SQLite. Move the validation into a
pure domain/matchedit.Validate that returns a Plan; the server executes the plan
as SQL (which needs the tx, so it stays here — per ARCHITECTURE.md's domain/server
split, and mirroring domain/resolver). The rules now have direct unit tests;
behaviour is unchanged (the EK batch integration tests pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Why
od.js held its paste-parse, cell-coercion and column-invert logic as loose
functions tangled with the grid DOM, untestable (od.js runs getElementById at
load, so the harness can't import it). Move the three pure pieces —
parseClipboard, coerceValue, invertColumn — into entry-model.js and unit-test
them; od.js keeps the DOM/selection/persistence and calls in. Behaviour
unchanged (verified by unit tests + the page selector-contract Go test; no
seeded OD game available for a live browser pass). Wrapping od.js itself as a
loadable factory stays a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Why
board.js held the handout image-staging session (session id, staged names,
in-flight guard, heartbeat timer) as loose globals and functions scattered
across the file, untested. Move the lifecycle — stage-once/dedup, single
in-flight, heartbeat, reap→clear, cleanup on close — into handoutsession.js as a
create(deps) factory with injected network + timer primitives; board.js keeps
the image gather/upload and DOM. The state machine now has unit tests;
behaviour unchanged (verified in-browser: the modal opens and closes cleanly).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Why
First seam-by-seam step of breaking up board.js: the members/sharing feature
(roster fetch/cache, the sharing overlay, add/remove) moves into boardmembers.js
as createBoardMembers(state, boardId) → { load, open }, owning its own DOM +
listeners and caching the roster onto the shared state. board.js drops ~90 lines
and the feature is now one file. Behaviour unchanged (verified in-browser: the
overlay opens, the roster renders, add-member errors surface). The remaining
seams (notifications, import, export, labels, timeline) are follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
pecheny/dopesuite!20
No description provided.