|
| 1 | +# Trust boundaries and validation |
| 2 | + |
| 3 | +Part of the [coding standards](../../CODING_STANDARDS.md). The client AI boundary is [`src/lib/ai/client.ts`](../../src/lib/ai/client.ts); the server boundaries are the Edge Function handlers described in [supabase/README.md](../../supabase/README.md). |
| 4 | + |
| 5 | +## Validate at the boundary, then trust the type |
| 6 | + |
| 7 | +Model output, Edge responses, URLs, and request bodies are untrusted until a validator accepts them. Validate the complete shape where the data enters, before rendering, autosave, or a cast. |
| 8 | + |
| 9 | +- Validate the whole output with its schema before saving sources. A count check plus a cast let malformed output through ([#9](https://github.com/Programcode/DebateLab/pull/9#discussion_r3861289852)). |
| 10 | +- Normalize optional fields to their primitive types. An object in `qualityLabel` reached React as a child ([#5](https://github.com/Programcode/DebateLab/pull/5#discussion_r3835805775)). |
| 11 | +- Use `validateUrl` for every URL you render as a link, and store the value it returns, not the input ([#17](https://github.com/Programcode/DebateLab/pull/17#discussion_r3896309984), [#17 review](https://github.com/Programcode/DebateLab/pull/17#issuecomment-5481176881)). |
| 12 | +- Validate raw types before coercing. `Boolean("false")` is `true` ([#29](https://github.com/Programcode/DebateLab/pull/29#discussion_r3937934126)). |
| 13 | +- Invalid client input returns a 4xx, never a 500 from a property access on `null` ([#13 review](https://github.com/Programcode/DebateLab/pull/13#issuecomment-5477866033)). |
| 14 | +- When the client validates locally, apply the same rules as the server, so a request the client accepted does not fail as a generic error ([#12](https://github.com/Programcode/DebateLab/pull/12#discussion_r3893641258)). |
| 15 | +- Send only values the server contract accepts. Local-only source IDs made every AI request fail with 400 ([#66](https://github.com/Programcode/DebateLab/pull/66#discussion_r4085167559)). |
| 16 | +- Prefer a typed result over `as` casts at call sites. Unchecked casts hide shape drift ([#18](https://github.com/Programcode/DebateLab/pull/18), single finding). |
| 17 | + |
| 18 | +## Model output can omit what the prompt asked for |
| 19 | + |
| 20 | +The shared JSON rules tell the model to omit a part it cannot fill. Map an omitted field to its documented empty value instead of failing the request ([#12](https://github.com/Programcode/DebateLab/pull/12#discussion_r3893641334)). A response variant that does not match the operation is an error the UI can recover from, not a silent return that leaves the page pending ([#12](https://github.com/Programcode/DebateLab/pull/12#discussion_r3893641293)). |
| 21 | + |
| 22 | +## Size limits are UTF-8 bytes |
| 23 | + |
| 24 | +The Edge limit is on serialized bytes, so budget in bytes, across the whole serialized payload, before transport ([#66](https://github.com/Programcode/DebateLab/pull/66#discussion_r4085167588), [#12](https://github.com/Programcode/DebateLab/pull/12#discussion_r3893641325)). |
| 25 | + |
| 26 | +- Do not silently shorten what the user typed. Reject it with a visible message, or keep the truncation marker so the server can report it ([#66](https://github.com/Programcode/DebateLab/pull/66#discussion_r4085302451)). |
| 27 | +- A value exactly at the limit is allowed. Check the boundary with a test on each side ([#12](https://github.com/Programcode/DebateLab/pull/12#discussion_r3893641317)). |
| 28 | +- Check `response.ok` before reading a body, and read bounded bodies inside the timeout ([#11](https://github.com/Programcode/DebateLab/pull/11#discussion_r3872004292)). |
| 29 | + |
| 30 | +## Fail closed, and say "unknown" when you do not know |
| 31 | + |
| 32 | +- A failed entitlement read is indeterminate. Treating it as "not Pro" persisted a downgrade of the user's model choice ([#47](https://github.com/Programcode/DebateLab/pull/47#discussion_r4054031767)). |
| 33 | +- Check the result of every write before reporting success ([#63](https://github.com/Programcode/DebateLab/pull/63#discussion_r4081818967)). Lint rejects unhandled promises; a `void` in front of one is correct only when the promise cannot reject or handles its own errors and tells the user ([#63 outside diff](https://github.com/Programcode/DebateLab/pull/63)). |
| 34 | +- Catch the specific error you expect (a missing file, a missing column) and rethrow the rest ([#5](https://github.com/Programcode/DebateLab/pull/5#discussion_r3835805773)). |
| 35 | +- Read provider per-item status before falling back to a generic message ([#11](https://github.com/Programcode/DebateLab/pull/11#discussion_r3872004284)). |
| 36 | +- A fix for a permission change covers every write path, not only the one that failed first ([#44](https://github.com/Programcode/DebateLab/pull/44#discussion_r4049229043)). |
| 37 | + |
| 38 | +## Controls must be honest |
| 39 | + |
| 40 | +A visible toggle or setting must change the request or deterministic post-processing. This is an existing team rule that reviewers enforced: |
| 41 | + |
| 42 | +- `useActiveSources` did not change the opponent request ([#11](https://github.com/Programcode/DebateLab/pull/11#discussion_r3872004224)). |
| 43 | +- Research instructions were attached when research returned nothing, inviting unsupported citations ([#5](https://github.com/Programcode/DebateLab/pull/5#discussion_r3835805784)). |
| 44 | +- A privacy disclosure stays only while the code enforces it ([#22](https://github.com/Programcode/DebateLab/pull/22#discussion_r3896703404)). |
| 45 | +- Tier defaults must reach every fallback path, not only a migration ([#47](https://github.com/Programcode/DebateLab/pull/47#discussion_r4054031797)). |
| 46 | + |
| 47 | +## Usage and cost records |
| 48 | + |
| 49 | +Every provider call that can bill gets recorded, including retries, repairs, research sub-calls, and cache-only usage. A request that did not complete records its observed usage with a null cost. |
| 50 | + |
| 51 | +- Cache-only usage returned before pricing ([#5](https://github.com/Programcode/DebateLab/pull/5#discussion_r3835805777)). Repair attempts dropped the failed call's tokens ([#5 review](https://github.com/Programcode/DebateLab/pull/5#issuecomment-5379671525), [#9](https://github.com/Programcode/DebateLab/pull/9#discussion_r3861289886)). |
| 52 | +- A stream that errors after its finish frame is incomplete ([#46](https://github.com/Programcode/DebateLab/pull/46#discussion_r4053914209)). |
| 53 | + |
| 54 | +## Keep one copy of a contract |
| 55 | + |
| 56 | +When two code paths implement the same privacy-sensitive payload or validation, move it to one helper. The two copies of the AI failure report had already drifted ([#18](https://github.com/Programcode/DebateLab/pull/18), single finding). Configuration that exists in two places (such as two CSP lists) changes together, with a test that compares them ([#15](https://github.com/Programcode/DebateLab/pull/15#discussion_r3894972424)). |
0 commit comments