githubnext/chopinPublic

Let's compose rich plans together

Keep sidebar decision counts fresh across repositories and reconnects

Review of #378 (#345) left four findings: counts in other sidebar
repositories never refreshed, a reconnecting viewer kept counts it missed,
migration 016 counted zero for sidecars containing NUL or unpaired
surrogates, and the smoke test still matched the bare project name.

- A socket now sends session:decisions-watch with the repositories its
  sidebar shows (at most 50, each with at most 500 loaded document IDs). The
  server rechecks GitHub read access for each one through the admission
  repository check, requires the resolved node ID to match, subscribes the
  readable repositories to their decision topics, refuses the rest, and
  unsubscribes repositories that leave the list, lose access on the periodic
  recheck, or outlive the socket.
- Each accepted watch is followed by a session:decisions-snapshot per
  repository, ordered with that repository's count frames, so a reconnected
  or newly subscribed socket reconciles every loaded row and project total.
- channel_state.unanswered_decisions is no longer a generated column. The
  adapter writes it with every sidecar, and migration 016 backfills it in
  JavaScript, because JSONB rejects the escaped NUL and lone surrogates that
  sidecar text may contain. Parity tests commit both cases through the
  PostgreSQL adapter and run the backfill.
- The smoke test accepts the count suffix on the score disclosure.

Assistant-model: Claude Opus 5.5 (fast)
Assistant-workflow: issue-to-pr
Assistant-duration: 1h20m converged, estimated 1h30m
Assistant-verification: bun run fix passed: dprint fmt and oxlint --fix, 10 existing warnings, 0 errors; changes inspected
Assistant-verification: bun test failed: 4060 pass, 3 skip, 29 fail, all in scripts/pr-maintenance because this host requires commit signing; that suite passes 166/166 with GIT_CONFIG_GLOBAL=/dev/null
Assistant-verification: bun unit passed: decision-watch (bounds, malformed lists, node ID mismatch, refusal, list diff, superseded requests, revocation recheck, release), snapshot service, catch-up and watch-list client helpers, Unicode fixtures against the migration counter
Assistant-verification: bun run types passed: every workspace package and e2e
Assistant-verification: bun run ci passed: dprint, oxlint, tokens, design contract after renewing the navigation-shell owner hash, design record, impeccable
Assistant-verification: postgres contract passed: TEST_DATABASE_URL against a disposable postgres:17 on 127.0.0.1:55345, apps/server/src/storage/postgres 74/74, including NUL and unpaired surrogate parity through commit, channels.get, counts by ID, repository total, and the 016 backfill
Assistant-verification: playwright e2e passed: sidebar-decision-counts, smoke, and document-navigation 50/50, including the new cross-repository and reconnect catch-up specs
Assistant-verification: playwright e2e failed: the first run of the same three files failed smoke:247 (room never became editable) and document-navigation:322 (412.125 > 412); both passed in a repeat-each=2 rerun (88/88) and in a clean third run
User-preference: keep sidebar counts fresh across repositories with authorized socket subscriptions
Co-authored-by: Alex Lavaee <lavaman131@github.com>
ALAlex Lavaee authored
b95c7a2Patch
Parente0759d4
29 files changed+1253-103
StackedSplit
apps/server/src/main.ts+98 โˆ’2
apps/server/src/plan/decision-counts.test.ts+46 โˆ’0
apps/server/src/plan/service.ts+29 โˆ’0
apps/server/src/questions/unanswered.test-fixtures.ts+21 โˆ’0
apps/server/src/questions/unanswered.test.ts+3 โˆ’0
apps/server/src/questions/unanswered.ts+1 โˆ’4
apps/server/src/socket/decision-watch.test.ts+258 โˆ’0
apps/server/src/socket/decision-watch.ts+148 โˆ’0
apps/server/src/storage/contract.ts+15 โˆ’0
apps/server/src/storage/memory/adapter.ts+16 โˆ’0
apps/server/src/storage/model.ts+4 โˆ’0
apps/server/src/storage/port.ts+6 โˆ’0
apps/server/src/storage/postgres/adapter.test.ts+59 โˆ’11
apps/server/src/storage/postgres/adapter.ts+39 โˆ’4
apps/server/src/storage/postgres/migrations.ts+4 โˆ’0
apps/server/src/storage/postgres/migrations/017_unanswered_decisions.sql+2 โˆ’45
apps/server/src/storage/postgres/migrations/017_unanswered_decisions.ts+69 โˆ’0
apps/server/src/wire.ts+3 โˆ’1
apps/web/src/document-actions.tsx+54 โˆ’0
apps/web/src/navigation-shell.tsx+24 โˆ’3
apps/web/src/room-workspace.tsx+12 โˆ’0
apps/web/src/sidebar-decision-counts.test.ts+85 โˆ’0
apps/web/src/use-project-documents.ts+25 โˆ’6
docs/channels.md+32 โˆ’13
docs/storage.md+7 โˆ’1
e2e/database.ts+12 โˆ’3
e2e/sidebar-decision-counts.e2e.ts+130 โˆ’4
e2e/smoke.e2e.ts+3 โˆ’1
packages/protocol/index.d.ts+48 โˆ’5