Skip to content

refactor: delete the dead messaging island + add knip to CI (#92) - #95

Open
TortoiseWolfe wants to merge 4 commits into
mainfrom
chore/delete-messaging-island
Open

refactor: delete the dead messaging island + add knip to CI (#92)#95
TortoiseWolfe wants to merge 4 commits into
mainfrom
chore/delete-messaging-island

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Fixes #92. Resolves the messaging cluster of #90.

Four modules (719 LOC) plus three test files formed a closed island — importing each other, with nothing outside importing any of them. Verified beyond static imports: no dynamic import() with a matching specifier, no string-built paths.

useConversationRealtime.ts        ← no importers at all
  ├── useConversationRealtimeSync.ts  (:24)
  │     └── decrypt-message.ts        (:22)
  ├── decrypt-message.ts              (:23)
  └── decryption-cache.ts             (:19)

Born dead, not orphaned. useConversationRealtime was in the initial commit, and page.tsx in that same commit already had the full-refetch loadMessages. git log --all -S over src/app/ and src/components/ returns empty across all history (control query confirms the search works). There is nothing to roll back to.

It had already cost real time twice. On 2026-03-24, a5d4e3d added 55 lines of polling fallback into the dead hook to fix CI flake; it fixed nothing, and 812cead hand-copied the logic into page.tsx twelve hours later — its title says "was dead code". More recently it produced a false root cause on #69, which named clearDecryptionCaches() as the live cause of message loss. That function cannot execute.

Changes

  • Delete the four modules + three test files (c1c6217)
  • Remove onKeysChanged/notifyKeyChange + five call sites + its one test (30abb17) — kept a separate commit because it touches a live service, so it can be reverted independently
  • Retarget the four documentation sites that referenced the island (d0d1d3d) — including SECURITY-ARCHITECTURE.md, which described the never-wired cache as a live DoS mitigation
  • Add knip with a baseline and a CI gate (325d114)

No behaviour change

Both harvest items in the issue turned out moot, which makes this a pure deletion:

knip

Baselined at 37 findings so the gate is green on arrival and fails only on new unreferenced files. The baseline holds two deliberately-distinct categories, labelled in knip.jsonc: 16 one-line barrels re-exporting live components (an import-style choice, out of scope per #82), and 21 genuine orphans for #90 to burn down. It is a burn-down list, not an allowlist.

.storybook/mocks/** is ignored for a different reason — reached via path.resolve aliases knip cannot follow.

Mutation-checked: planting an unreferenced file under src/lib/ turns the gate red and names it; removing it returns exit 0.

Verification

Check Result
type-check clean
lint 0 errors (1 pre-existing warning outside this diff)
Unit suite 385 files, 4015 passed, 0 failed
Test-count delta exact match — 4047→4015 is 32; deleted files held 15 + 9 + 7 + 1 = 32
Coverage 68.34 / 60 / 70.79 / 69.16 vs 58 thresholds — passes
knip exit 0, mutation-proven

The coverage concern raised in the issue (deleting well-covered LOC might trip the 58% gate) was flagged there as unmeasured. Measured: it does not.

Note on ordering

pnpm build could not be verified locally on this branch — that is #93, fixed in #94, which should land first. CI verifies the build here.

🤖 Generated with Claude Code

TortoiseWolfe and others added 4 commits August 2, 2026 10:15
…r wired)

useConversationRealtime, useConversationRealtimeSync, decrypt-message and
decryption-cache formed a closed island: they imported each other and nothing
outside imported any of them. Verified beyond static imports — no dynamic
import() with a matching specifier, no string-built module paths.

Born dead, not orphaned. useConversationRealtime was present in the initial
commit (893f552) and page.tsx in that same commit already had the full-refetch
loadMessages. `git log --all -S` over src/app/ and src/components/ returns
empty across all history, with a control query confirming the search works.

It had already cost real time twice. On 2026-03-24 a5d4e3d added 55 lines of
polling fallback into the dead hook to fix CI flake; it fixed nothing, and
812cead hand-copied the logic into page.tsx twelve hours later ("was dead
code"). More recently it produced a false root cause on #69, whose diagnosis
named clearDecryptionCaches() — a function that cannot execute.

No harvest was needed after all. The two candidates in #92 both turned out
moot: upsertMessage's id-based dedupe cannot replace page.tsx's content
heuristic while optimistic rows carry `optimistic-<ts>` ids and server rows
carry UUIDs (blocked on #91), and the live path already has all three failure
strings (message-service.ts:767, :859, plus EncryptionLockedError).

Refs #92, #90, #69

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ibers)

keyManagementService.onKeysChanged() existed to let the shared-secret cache in
useConversationRealtime invalidate on key rotation. That cache is gone, and the
API had zero production subscribers even before it went — its own docstring
named the dead consumer.

Removes the type, the listener Set, onKeysChanged, notifyKeyChange, and its
five call sites, plus the one test that exercised it.

Also drops two comments in key-service.test.ts asserting a listener "in
decrypt-message.ts" that never existed in that file — one of the docstrings
that made #69's false root cause look well-sourced.

Kept separate from the island deletion so it can be reverted independently:
this touches a live service, whereas the island deletion touched nothing that
runs. Note a listener is the wrong mechanism for cache invalidation here
anyway — static-static ECDH means K(A,B) changes when *either* side rotates,
and nothing subscribes to peer key changes. Key-epoch memoization with
evict-and-retry is the shape that self-heals; see #92.

Refs #92

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four places documented or scheduled work against modules that never ran:

- SECURITY-ARCHITECTURE.md described the shared-secret cache as a live DoS
  mitigation, with a "~50ms to ~1ms" figure and a pointer to
  useConversationRealtime.ts. The mitigation was never in effect. Replaced with
  what the shipping path actually does: one ECDH derivation per
  getMessageHistory() batch, amortized across up to 50 messages, with no
  client-side cache. Records what that gives up (nothing security-relevant;
  cost is tracked in #91) and what it gains (rotation and revocation self-heal,
  no long-lived key material in module scope).

- real-time-delivery.spec.ts credited the dead sync hook with setting
  data-messages-subscribed; it is set by messages/page.tsx.

- specs/010-group-chats T038 and plan.md scheduled MODIFY against the deleted
  hook — retargeted at messages/page.tsx. T040's typing-indicator half now
  names useTypingIndicator, which is the hook that actually runs.

No references to the island remain anywhere in the repo.

Refs #92

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TypeScript's unused-symbol checks do not span modules, so a closed island — a
group of files importing each other with no entry point from the app —
type-checks and builds cleanly forever. Nothing in this repo could detect that
shape. One survived from the initial commit to 2026-08 (#92), and the same
class produced the 131 files deleted in #82 and the leads in #90.

knip.jsonc configures app-router entry points and disables knip's built-in
Next plugin, whose assumptions produce false entries against this static
export.

The gate is baselined: 37 pre-existing findings are listed under `ignore` so
CI is green on arrival and fails only on NEW unreferenced files. Two distinct
categories are in there and the config says so — one-line barrels re-exporting
live components (an import-style choice, explicitly out of scope per #82) and
genuine orphans awaiting individual verification under #90. It is a burn-down
list, not an allowlist.

.storybook/mocks/** is ignored for a different reason: those files are reached
through path.resolve aliases in .storybook/main.ts:43,47, which knip cannot
follow. Reachable, not orphaned.

Mutation-checked: planting a new unreferenced file under src/lib/ turns the
gate red and names the file; removing it returns exit 0.

Refs #92, #90, #82

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Delete the dead messaging island (4 modules, 719 LOC, never wired) + add knip to CI

1 participant