| ... | ... | @@ -1,185 +0,0 @@ |
| 1 | | # Plan: Frontend Contract Schema Validation Alignment |
| 2 | | |
| 3 | | ## Summary |
| 4 | | Align frontend input/output validation with `@t3tools/contracts` schemas so the web client stops relying on ad-hoc checks and unsafe casts at critical boundaries. |
| 5 | | |
| 6 | | ## Goals |
| 7 | | 1. Decode all WebSocket envelopes with shared contract schemas. |
| 8 | | 2. Decode push payloads before handing them to app logic. |
| 9 | | 3. Replace duplicated frontend range/input checks with contract schema decoding. |
| 10 | | 4. Validate project search params with shared constraints before dispatch. |
| 11 | | 5. Validate script keybinding updates with `KeybindingRule` schema before RPC. |
| 12 | | 6. Propagate branded IDs (`ThreadId`, `ProjectId`, etc.) through typed app flow after single ingress conversion. |
| 13 | | |
| 14 | | ## Non-Goals |
| 15 | | - No protocol shape changes. |
| 16 | | - No server behavior changes unless uncovered by failing tests. |
| 17 | | - No UI redesign; only validation/transport boundary hardening. |
| 18 | | |
| 19 | | ## Phase 1: WebSocket Envelope Decoding |
| 20 | | |
| 21 | | ### Issue |
| 22 | | `apps/web/src/wsTransport.ts` parses JSON and performs manual object/cast checks (`as WsPush`) instead of decoding with contracts. |
| 23 | | |
| 24 | | ### Changes |
| 25 | | 1. Decode inbound messages with contract schema union (`WsResponse`). |
| 26 | | 2. Remove manual push/response shape checks that duplicate schema logic. |
| 27 | | 3. Convert decode failures into safe no-op handling with structured debug logging. |
| 28 | | |
| 29 | | ### Target Files |
| 30 | | - `apps/web/src/wsTransport.ts` |
| 31 | | - `apps/web/src/wsTransport.test.ts` |
| 32 | | |
| 33 | | ### Tests |
| 34 | | 1. Accept valid push envelope and route to channel listener. |
| 35 | | 2. Accept valid response envelope and resolve pending request. |
| 36 | | 3. Reject malformed envelope without crashing transport. |
| 37 | | |
| 38 | | ## Phase 2: Push Payload Decoding at API Boundary |
| 39 | | |
| 40 | | ### Issue |
| 41 | | `apps/web/src/wsNativeApi.ts` forwards unknown push payloads and uses direct casts (`data as WsWelcomePayload`). |
| 42 | | |
| 43 | | ### Changes |
| 44 | | 1. Decode `server.welcome` payload with `WsWelcomePayload`. |
| 45 | | 2. Decode `terminal.event` payload with `TerminalEvent`. |
| 46 | | 3. Decode `orchestration.domainEvent` payload with `OrchestrationEvent`. |
| 47 | | 4. Skip invalid payloads safely and keep subscriptions alive. |
| 48 | | |
| 49 | | ### Target Files |
| 50 | | - `apps/web/src/wsNativeApi.ts` |
| 51 | | - `apps/web/src/wsNativeApi.test.ts` |
| 52 | | |
| 53 | | ### Tests |
| 54 | | 1. Valid welcome payload is delivered and cached. |
| 55 | | 2. Invalid welcome payload is ignored. |
| 56 | | 3. Valid terminal/orchestration events are forwarded. |
| 57 | | 4. Invalid push data does not invoke consumers. |
| 58 | | |
| 59 | | ## Phase 3: Replace Duplicated Checkpoint Range Validation |
| 60 | | |
| 61 | | ### Issue |
| 62 | | `apps/web/src/lib/providerReactQuery.ts` duplicates `fromTurnCount`/`toTurnCount` validation rules already represented in contracts. |
| 63 | | |
| 64 | | ### Changes |
| 65 | | 1. Use `OrchestrationGetTurnDiffInput` / `OrchestrationGetFullThreadDiffInput` decoding before RPC calls. |
| 66 | | 2. Remove bespoke integer/range checks where schema decoding can be authoritative. |
| 67 | | 3. Preserve current UX behavior (`enabled` gating + retry policy). |
| 68 | | |
| 69 | | ### Target Files |
| 70 | | - `apps/web/src/lib/providerReactQuery.ts` |
| 71 | | - `apps/web/src/lib/providerReactQuery.test.ts` |
| 72 | | |
| 73 | | ### Tests |
| 74 | | 1. Valid range decodes and calls expected RPC. |
| 75 | | 2. Invalid range fails fast and avoids RPC call. |
| 76 | | 3. `fromTurnCount === 0` path still routes to `getFullThreadDiff`. |
| 77 | | |
| 78 | | ## Phase 4: Project Search Input Validation from Contracts |
| 79 | | |
| 80 | | ### Issue |
| 81 | | `apps/web/src/lib/projectReactQuery.ts` forwards search inputs without enforcing `ProjectSearchEntriesInput` constraints, and `ChatView` can pass empty query strings. |
| 82 | | |
| 83 | | ### Changes |
| 84 | | 1. Decode project search input with `ProjectSearchEntriesInput` before request dispatch. |
| 85 | | 2. Gate query execution when query is empty after trim. |
| 86 | | 3. Replace magic search limit usage with shared constant (`PROJECT_SEARCH_ENTRIES_MAX_LIMIT`) or schema-safe value. |
| 87 | | |
| 88 | | ### Target Files |
| 89 | | - `apps/web/src/lib/projectReactQuery.ts` |
| 90 | | - `apps/web/src/components/ChatView.tsx` |
| 91 | | - `apps/web/src/lib/projectReactQuery.test.ts` |
| 92 | | |
| 93 | | ### Tests |
| 94 | | 1. Empty query does not dispatch request. |
| 95 | | 2. Over-limit values are rejected or clamped via schema-safe path. |
| 96 | | 3. Valid queries still return prior placeholder behavior. |
| 97 | | |
| 98 | | ## Phase 5: Script/Keybinding Validation Consistency |
| 99 | | |
| 100 | | ### Status |
| 101 | | Complete |
| 102 | | |
| 103 | | ### Issue |
| 104 | | `ProjectScriptsControl` and `ChatView` rely mostly on local non-empty checks while sending keybinding data into a contract-validated endpoint (`server.upsertKeybinding`). |
| 105 | | |
| 106 | | ### Changes |
| 107 | | 1. Add pre-submit decode for keybinding payload using `KeybindingRule`. |
| 108 | | 2. Normalize and validate keybinding values via schema before calling `upsertKeybinding`. |
| 109 | | 3. Keep existing script ID command generation logic, but ensure command + key are schema-valid before mutation. |
| 110 | | |
| 111 | | ### Target Files |
| 112 | | - `apps/web/src/components/ProjectScriptsControl.tsx` |
| 113 | | - `apps/web/src/components/ChatView.tsx` |
| 114 | | - (optional helper) `apps/web/src/projectScripts.ts` |
| 115 | | - `apps/web/src/components/ProjectScriptsControl.test.tsx` (or logic test file) |
| 116 | | |
| 117 | | ### Tests |
| 118 | | 1. Invalid keybinding string surfaces UI validation error and blocks submission. |
| 119 | | 2. Valid keybinding updates still persist and refresh cached server config. |
| 120 | | 3. Editing existing script preserves valid keybinding mapping behavior. |
| 121 | | |
| 122 | | ## Phase 6: Branded ID Flow (Single Ingress Branding) |
| 123 | | |
| 124 | | ### Issue |
| 125 | | Frontend currently converts plain strings to branded IDs repeatedly (`asThreadId`, `asProjectId`, etc.) at many callsites, which spreads unsafe conversions across app logic. |
| 126 | | |
| 127 | | ### Desired Model |
| 128 | | 1. Convert/decode to branded IDs once at ingress boundaries. |
| 129 | | 2. Keep IDs branded through store state, selectors, and command/query builders. |
| 130 | | 3. Avoid repeated `*.makeUnsafe` wrappers in feature logic. |
| 131 | | |
| 132 | | ### Boundary Rules |
| 133 | | 1. Router/search params (string) -> decode to branded IDs at route boundary. |
| 134 | | 2. Storage/network payloads (untyped JSON) -> decode with contract schemas at transport/query boundary. |
| 135 | | 3. ID generation (`newThreadId`, `newProjectId`, etc.) remains branded at source. |
| 136 | | 4. UI components should consume typed IDs from store/hooks, not raw strings. |
| 137 | | |
| 138 | | ### Changes |
| 139 | | 1. Introduce a thin typed ID layer in web app (or reuse contracts decoders directly) for safe `unknown -> branded` conversion at boundaries. |
| 140 | | 2. Refactor web store and thread/project domain types to carry branded IDs where feasible. |
| 141 | | 3. Replace repeated `asThreadId`/`asProjectId` callsites in: |
| 142 | | - `apps/web/src/components/ChatView.tsx` |
| 143 | | - `apps/web/src/components/Sidebar.tsx` |
| 144 | | - `apps/web/src/components/BranchToolbar.tsx` |
| 145 | | - `apps/web/src/lib/providerReactQuery.ts` |
| 146 | | 4. Keep unavoidable casts isolated in one boundary module (not feature components). |
| 147 | | 5. Minimize/remove broad `asXId` helpers from `apps/web/src/lib/orchestrationIds.ts` once flow is typed. |
| 148 | | |
| 149 | | ### Target Files |
| 150 | | - `apps/web/src/lib/orchestrationIds.ts` |
| 151 | | - `apps/web/src/types.ts` |
| 152 | | - `apps/web/src/store.ts` |
| 153 | | - `apps/web/src/components/ChatView.tsx` |
| 154 | | - `apps/web/src/components/Sidebar.tsx` |
| 155 | | - `apps/web/src/components/BranchToolbar.tsx` |
| 156 | | - `apps/web/src/lib/providerReactQuery.ts` |
| 157 | | - route boundary files (`apps/web/src/routes/*`) as needed |
| 158 | | |
| 159 | | ### Tests |
| 160 | | 1. Type-level regression checks (compile-time) proving command builders accept branded IDs directly. |
| 161 | | 2. Route/load path tests verifying invalid ID ingress is rejected/fails safely. |
| 162 | | 3. Existing behavior tests still pass for thread/project operations after refactor. |
| 163 | | |
| 164 | | ## Cross-Cutting Execution Notes |
| 165 | | 1. Prefer a small shared frontend utility for Effect schema decoding to avoid repeated `decodeUnknownSync` boilerplate. |
| 166 | | 2. Use `Schema.decodeUnknownEither` or equivalent non-throwing path where event stream robustness matters. |
| 167 | | 3. Keep failure behavior fail-soft at stream boundaries (drop invalid events, keep transport running). |
| 168 | | 4. Brands are TypeScript-level constraints; JSON boundaries still require decode/re-brand at ingress. |
| 169 | | |
| 170 | | ## Validation Checklist |
| 171 | | 1. `bun run lint` |
| 172 | | 2. `bun run test --filter=@t3tools/web` (or equivalent web test command in repo) |
| 173 | | 3. Manual smoke checks: |
| 174 | | - App boot + welcome sync |
| 175 | | - Terminal stream still renders |
| 176 | | - Diff panel loads checkpoint ranges |
| 177 | | - Path autocomplete search remains responsive |
| 178 | | - Script add/edit with keybinding works in Electron flow |
| 179 | | |
| 180 | | ## Done Criteria |
| 181 | | 1. No unsafe push/event casts remain at core WebSocket boundaries. |
| 182 | | 2. Frontend no longer duplicates contract input validation for checkpoint range and project search. |
| 183 | | 3. Keybinding submission path is schema-validated before RPC. |
| 184 | | 4. Branded IDs flow through frontend feature code without repeated `asThreadId`/`asProjectId` wrappers. |
| 185 | | 5. Updated tests cover malformed payload and invalid input regressions. |