Part of the coding standards. The client AI boundary is src/lib/ai/client.ts, and response validation lives in src/lib/ai/request.ts, src/lib/ai/streaming.ts, src/lib/ai/sources.ts, and each src/lib/ai/features/*.ts; the server boundaries are the Edge Function handlers described in supabase/README.md.
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.
- Validate the whole output with its schema before saving sources. A count check plus a cast let malformed output through (#9).
- Normalize optional fields to their primitive types. An object in
qualityLabelreached React as a child (#5). - Use
validateUrlfor every URL you render as a link, and store the value it returns, not the input (#17, #17 review). - Validate raw types before coercing.
Boolean("false")istrue(#29). - Invalid client input returns a 4xx, never a 500 from a property access on
null(#13 review). - 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).
- Send only values the server contract accepts. Local-only source IDs made every AI request fail with 400 (#66).
- Prefer a typed result over
ascasts at call sites. Unchecked casts hide shape drift (#18, single finding).
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). 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).
The Edge limit is on serialized bytes, so budget in bytes, across the whole serialized payload, before transport (#66, #12).
- 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).
- A value exactly at the limit is allowed. Check the boundary with a test on each side (#12).
- Check
response.okbefore reading a body, and read bounded bodies inside the timeout (#11).
- A failed entitlement read is indeterminate. Treating it as "not Pro" persisted a downgrade of the user's model choice (#47).
- Check the result of every write before reporting success (#63). Lint rejects unhandled promises; a
voidin front of one is correct only when the promise cannot reject or handles its own errors and tells the user (#63 outside diff). - Catch the specific error you expect (a missing file, a missing column) and rethrow the rest (#5).
- Read provider per-item status before falling back to a generic message (#11).
- A fix for a permission change covers every write path, not only the one that failed first (#44).
A visible toggle or setting must change the request or deterministic post-processing. This is an existing team rule that reviewers enforced:
useActiveSourcesdid not change the opponent request (#11).- Research instructions were attached when research returned nothing, inviting unsupported citations (#5).
- A privacy disclosure stays only while the code enforces it (#22).
- Tier defaults must reach every fallback path, not only a migration (#47).
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.
- Cache-only usage returned before pricing (#5). Repair attempts dropped the failed call's tokens (#5 review, #9).
- A stream that errors after its finish frame is incomplete (#46).
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, single finding). Configuration that exists in two places (such as two CSP lists) changes together, with a test that compares them (#15).