# Oversized Handler Cohesion Split Plan

> Canonical path: `specs/api/oversized-handler-cohesion-plan.md`
> Status: living architecture spec — plan-only, no implementation in this document
> Related specs: [`internal-api-clean-code-design.md`](./internal-api-clean-code-design.md) §9, [`raw-gin-surface-classification.md`](./raw-gin-surface-classification.md)
> Scope: proposed split boundaries for three oversized handlers identified in the naming audit.
> Non-goal: implementing any handler split in this document.

## 1. Purpose

Three handler structs exceed the spec §9 cohesion threshold (>20 methods or >1 feature cluster):

| Handler | Package | Methods | Concerns | Typing |
|---------|---------|---------|----------|--------|
| `ChannelHandler` | `channel` | 28 | 9 distinct domain concerns | All typed (WorkspaceScoped) |
| `CollaboratorHandler` | `task` | 18 | 3 scopes × 6 operations (scope-polymorphic) | All typed (WorkspaceScoped) |
| `TaskHandler` | `agent` | 17 | 7 concern groups | All raw Gin (keep-raw) |

This document proposes split boundaries, priority ordering, independent implementation slices, and verification strategies for each.

## 2. Split Principles

1. **One concern = one handler struct = one file.** Split by domain concern, not by HTTP method type.
2. **Preserve route parity.** Every split must result in the same set of route method+path registrations. The `routes.go` registration function becomes the composition root that wires sub-handlers.
3. **Preserve wire/status/message/auth behavior.** Splitting handlers into separate structs must not change HTTP responses.
4. **Preserve keep-raw classification.** `agent.TaskHandler` is keep-raw; the split only reorganizes file ownership, not handler typing.
5. **Minimize struct field duplication.** Sub-handlers share dependencies through a common base struct or receive only the narrow ports they need.
6. **File moves only.** Moving a method from one file to another within the same package is safe. Moving across packages changes import paths and is a separate task.

## 3. ChannelHandler Split Plan

### 3.1 Current State

- **File:** `internal/api/channel/api_channels.go` (651 lines) + `api_notifications.go` (52 lines)
- **Struct:** `ChannelHandler` with 5 dependencies: channel scopes, `appStore`, `privateAgents`, `realtime`, `logger`
- **Methods:** 28 public + 2 private helpers
- **Route registration:** flat in `registerChannelRoutes` (routes.go:59-95)

### 3.2 Proposed Sub-Handlers

| Sub-Handler | File | Methods | Dependencies | Route Prefix |
|-------------|------|---------|-------------|--------------|
| `ChannelCRUDHandler` | `api_channels.go` | `List`, `Create`, `Update`, `Delete` | channel scopes, `appStore`, `realtime` | `/channels` |
| `ChannelDiscoveryHandler` | `api_channel_discovery.go` | `DiscoverList`, `DiscoverGet` | channel scopes | `/channels/discover` |
| `ChannelLifecycleHandler` | `api_channel_lifecycle.go` | `Archive`, `Unarchive`, `TransferOwner`, `DismissOwnerContext` | `appStore`, `realtime` | `/channels/:channelId/{archive,unarchive,owner,...}` |
| `ChannelMembershipHandler` | `api_channel_membership.go` | `Join`, `Leave`, `GetMembers`, `AddMember`, `RemoveAgentMember`, `RemoveServiceMember`, `RemoveUserMember` + `leaveChannelRealtimeRoomsForUser` helper | channel scopes, `appStore`, `privateAgents`, `realtime`, `logger` | `/channels/:channelId/{join,leave,members,...}` |
| `ChannelDMHandler` | `api_channel_dm.go` | `ListDM`, `OpenDM` | `appStore`, `realtime` | `/channels/dm` |
| `ChannelNotificationHandler` | `api_notifications.go` (already separate) | `NotificationsGet`, `NotificationsSelfSet`, `NotificationsAgentSet` | `privateAgents` | `/channels/:channelId/notifications` |

### 3.3 Dependency Matrix After Split

| Dependency | Used By |
|------------|---------|
| channel scopes (*channel.Store) | `ChannelCRUDHandler`, `ChannelDiscoveryHandler`, `ChannelMembershipHandler` |
| `appStore` (*app.Store) | `ChannelCRUDHandler`, `ChannelLifecycleHandler`, `ChannelMembershipHandler`, `ChannelDMHandler` |
| `privateAgents` (ChannelPrivateAgentSurfaceStore) | `ChannelMembershipHandler`, `ChannelNotificationHandler` |
| `realtime` (ChannelRealtime) | `ChannelCRUDHandler`, `ChannelLifecycleHandler`, `ChannelMembershipHandler`, `ChannelDMHandler` |
| `logger` (*slog.Logger) | `ChannelMembershipHandler` |

### 3.4 Route Registration Change

`registerChannelRoutes` constructs all sub-handlers and registers routes. Route method+path unchanged. Only the handler receiver changes from one struct to multiple.

### 3.5 Implementation Slices

These slices can run **in parallel** because they touch disjoint files:

| Slice | Files Changed | Risk |
|-------|-------------|------|
| **C1:** Extract DM handler | `api_channel_dm.go` (new), `api_channels.go`, `routes.go` | Low — 2 methods, clean dependency |
| **C4:** Extract notification handler | `api_notifications.go`, `routes.go` | Lowest — already in separate file |
| **C5:** Extract discovery handler | `api_channel_discovery.go` (new), `api_channels.go`, `routes.go` | Low — 2 methods, only channel scopes |
| **C6:** Extract membership handler | `api_channel_membership.go` (new), `api_channels.go`, `routes.go` | Medium — 7 methods, helper, 5 dependencies |
| **C7:** Extract lifecycle handler | `api_channel_lifecycle.go` (new), `api_channels.go`, `routes.go` | Low — 4 methods |
| **C8:** Rename residual `ChannelHandler` to `ChannelCRUDHandler` | All channel files | Low — mechanical rename |

**Recommended order:** C1+C4+C5 first (low risk, parallel), then C6 (medium), then C7+C8. 旧 channel read-state 兼容路由已由 Web unread stream-store 收敛方案下线，不再作为拆分目标。

### 3.6 Result

After the split:
- `api_channels.go` drops from 651 lines to ~80 lines (CRUD only).
- Each new sub-handler file is under 150 lines.
- No file in `channel/` exceeds the 500-line or 20-method threshold.
- The 9 concerns become independently navigable.

## 4. CollaboratorHandler Split Plan

### 4.1 Current State

- **File:** `internal/api/task/api_collaborators.go`
- **Struct:** `CollaboratorHandler` with 2 dependencies: `taskHandlerBase` (embeds unused `taskBoard`), `collaborators TaskCollaboratorBoard`
- **Methods:** 18 public + 7 private helpers
- **Pattern:** scope-polymorphic — each of 6 operations is triplicated for task/thread/channel scope

### 4.2 Assessment

The CollaboratorHandler's "oversized" nature is **structural, not cohesion-broken**. The 18 methods are thin scope-dispatching wrappers that delegate to 7 private helpers. The actual cohesion is tight — all methods operate on the `TaskCollaboratorBoard` interface.

**Splitting by concern is not appropriate here** because there is only one concern (task collaborators). Splitting by scope would create three nearly-identical handler structs with no meaningful independence.

### 4.3 Recommended Approach

Rather than splitting the handler struct, improve the structure:

1. **Remove dead weight:** Drop the embedded `taskHandlerBase.taskBoard` field — it is never used by any collaborator method.
2. **Reduce method count by generalizing scope dispatch (optional, future):** Replace the 3-scope wrapper pattern with a single set of methods that accept a scope parameter via route-level middleware or adapter.
3. **File organization is already clean** — all collaborator logic is in one file with clear helper structure.

### 4.4 Implementation Slices

| Slice | Description | Risk |
|-------|-------------|------|
| **T1:** Remove unused `taskHandlerBase.taskBoard` from `CollaboratorHandler` | Clean dead weight | Low |
| **T2 (future):** Evaluate scope generalization | Requires adapter or middleware change | Medium-high |

**Recommendation:** Execute T1. Defer T2 until a specific maintainability pain emerges.

## 5. TaskHandler Split Plan

### 5.1 Current State

- **Files:** `internal/api/agent/api_tasks.go` (415 lines), `task_operations.go` (248 lines), `task_dispatch.go` (204 lines), `task_fanout.go` (36 lines), `task_create_source.go` (96 lines), `task_contract.go` (576 lines), plus adapter/DTO files
- **Struct:** `TaskHandler` with 7 dependencies: `auth`, `taskBoard`, `store`, `waiter`, `notifier`, `realtime`, `logger`
- **Methods:** 17 public raw Gin handlers
- **Classification:** keep-raw (agent/daemon protocol)

### 5.2 Assessment

The file organization is **already well-structured** despite raw Gin. The handler file is 415 lines (under the 500-line threshold), and supporting logic is properly decomposed into operation, dispatch, fanout, and source files.

The cohesion issue is **dependency-driven**, not method-count-driven:
- 15 of 17 methods use only `auth` + `taskBoard` (thin wrappers)
- `Create` adds `store` + `realtime`
- `Discuss` uses all 7 dependencies

### 5.3 Recommended Approach

Since the handler is keep-raw and already under the file-size threshold, the split priority is **low**. The recommended actions are:

1. **Extract `Discuss` dispatch logic** into a dedicated `task_discuss.go` file. `Discuss` is the only method that crosses into the dispatch concern. Extracting its post-mutation logic (resolve recipients → build dispatch → fan out) would reduce its complexity without changing the handler struct.
2. **Further delegate `Create` source-thread logic** into `task_create_source.go` (already partially done).
3. **Do not split the handler struct.** The 17 methods share `auth` + `taskBoard` as universal dependencies. Splitting would add wiring complexity without reducing the dependency footprint.
4. **Consider typed adapter migration** as a future Phase 2 item for the 15 simple methods, but this is outside cohesion split scope.

### 5.4 Implementation Slices

| Slice | Description | Risk |
|-------|-------------|------|
| **A1:** Extract `Discuss` dispatch pipeline into `task_discuss.go` | Move inline dispatch logic, handler calls it | Low |
| **A2:** Further delegate `Create` source-thread logic | Already partially in `task_create_source.go` | Low |

**Recommendation:** Execute A1+A2 as opportunistic cleanup. No handler struct split needed.

## 6. Priority Summary

| Priority | Handler | Action | Slices | Risk |
|----------|---------|--------|--------|------|
| **P1 (high)** | `channel.ChannelHandler` | Split into 8 sub-handlers by concern | C1–C8 | Low–Medium |
| **P2 (low)** | `task.CollaboratorHandler` | Remove dead weight only | T1 | Low |
| **P3 (low)** | `agent.TaskHandler` | Extract dispatch complexity, keep struct intact | A1–A2 | Low |

**Rationale:** ChannelHandler has the most acute cohesion problem (28 methods, 9 concerns, scattered file layout). CollaboratorHandler is verbose but structurally sound. TaskHandler is keep-raw and already well-organized.

## 7. Verification Strategy

For every split slice:

1. **Route parity:** Before and after route method+path snapshot must be identical (383/383 total, 0 missing/extra for touched domain).
2. **Build/vet:** `GOWORK=off go build ./...` and `go vet ./internal/api/{domain}` pass.
3. **Targeted tests:** `go test ./internal/api/{domain} -count=1 -short` pass.
4. **Guardrails:** `bash -n guardrails/api/internal-api-clean-code-design.sh` syntax check + guardrail script run.
5. **Old-name grep:** After extracting a sub-handler, grep for the old receiver/method pattern to confirm zero residual references.
6. **`git diff --check`:** No whitespace errors, conflict markers, or trailing whitespace.
7. **Long-lived spec check:** If this spec is updated during a slice, apply `skills/reviewing-long-lived-specs` — ensure no one-time PR/task/commit references are introduced.

## 8. Cross-Slice Dependencies

- **C1–C5** (channel low-risk slices) are fully independent of each other and can run in parallel.
- **C6** (membership extraction) should follow C1–C5 to reduce `api_channels.go` to a manageable size first.
- **C7–C8** depend on C6 completing.
- **T1** is independent of all channel slices.
- **A1–A2** are independent of all other slices.
- **Port-narrowing work** that changes ChannelHandler membership fields or CollaboratorHandler ports should be sequenced after the relevant extraction/dead-field cleanup slices (C6 and T1).

## 9. Relationship to Spec §9 Thresholds

After the P1 channel split completes:
- `api_channels.go` drops from 651 lines to ~80 lines (CRUD only).
- Each new sub-handler file is under 150 lines.
- No file in `channel/` exceeds the 500-line or 20-method threshold.
- The 9 concerns become independently navigable.
