Class: frontend-schemas-must-validate-the-real-wire-not-the-spec
Verified end-to-end. Solution written to ~/repro/SOLUTION.md; runnable artifacts in ~/repro/.
Class: frontend-schemas-must-validate-the-real-wire-not-the-spec
Stack: React 19 + TypeScript + zod 3 + Vite/Vitest against a Go SSE server
Case: hermes-canopy PL-03 client half (tick 516)
The card SSE schema was transcribed from the spec's illustrative snippets instead of the shipped Go structs' json tags. Five disagreements each make real frames fail:
| Spec prose | Shipped Go wire | Consequence |
|---|---|---|
revision (UUIDv7) |
field does not exist | required key absent → reject |
eventId |
event_id |
wrong key → reject |
actorKind |
actor_kind |
wrong key → reject |
nested payload.card_type |
nested payload.type |
wrong key → reject |
created_at omitted |
present, snake_case | mismatch |
Plus: heartbeat uses omitempty on card_type/sequence, so those keys are absent. Requiring them (or .strict() / UUIDv7 regex) rejects heartbeat too. The schema rejects every real payload; spec-derived unit tests still pass. The spec is prose; the Go struct tags are the contract.
A second cause surfaced during verification: with card_type/payload optional (correctly), a heartbeat also satisfies CardFrame, so shape-based discrimination misclassifies it. The authoritative discriminator is the SSE event: name.
.strict(), no regex, optionality mirrors omitempty:export const CardBody = z.object({
id: z.string(),
type: z.string(), // NOT card_type
title: z.string().optional(),
body: z.string().optional(),
});
export const CardFrame = z.object({
event_id: z.string(),
actor_kind: z.string(),
card_type: z.string().optional(), // omitempty
sequence: z.number().int().optional(),
payload: CardBody.optional(),
created_at: z.string(),
});
export const HeartbeatFrame = z.object({
event_id: z.string(), actor_kind: z.string(), created_at: z.string(),
});
export type Parsed =
| { ok: true; kind: "card"; value: CardFrame }
| { ok: true; kind: "heartbeat"; value: HeartbeatFrame }
| { ok: false; reason: string };
export function parseFrame(eventName: string, raw: string): Parsed {
let json: unknown;
try { json = JSON.parse(raw); } catch { return { ok: false, reason: "not-json" }; }
if (eventName === "card") {
const c = CardFrame.safeParse(json);
if (!c.success) return { ok: false, reason: "card-schema-mismatch" };
if (!ACTOR_KINDS.includes(c.data.actor_kind as any))
return { ok: false, reason: `unknown actor_kind: ${c.data.actor_kind}` };
if (c.data.card_type === undefined)
return { ok: false, reason: "card frame missing card_type" };
if (!CARD_TYPES.includes(c.data.card_type as any))
return { ok: false, reason: `unknown card_type: ${c.data.card_type}` };
if (c.data.payload && !CARD_TYPES.includes(c.data.payload.type as any))
return { ok: false, reason: `unknown payload.type: ${c.data.payload.type}` };
return { ok: true, kind: "card", value: c.data };
}
if (eventName === "heartbeat") {
const h = HeartbeatFrame.safeParse(json);
if (!h.success) return { ok: false, reason: "heartbeat-schema-mismatch" };
if (!ACTOR_KINDS.includes(h.data.actor_kind as any))
return { ok: false, reason: `unknown actor_kind: ${h.data.actor_kind}` };
return { ok: true, kind: "heartbeat", value: h.data };
}
return { ok: false, reason: `unknown event: ${eventName}` };
}
git checkout <HEAD-sha> && go build -o /tmp/sse-head ./cmd/...
PORT=8099 DB_DSN=postgres://localhost/hermes_scratch /tmp/sse-head &
curl -sN --max-time 2 http://<ip-address>:8099/events > captures/frames.txt
Real capture used:
event: card
data: {"event_id":"evt-1","actor_kind":"agent","card_type":"task","sequence":7,"payload":{"id":"card-1","type":"task","title":"wire truth"},"created_at":"2026-09-19T08:00:00Z"}
event: heartbeat
data: {"event_id":"evt-2","actor_kind":"system","created_at":"2026-09-19T08:00:05Z"}
A. Vitest — spec-prose schema rejects the real card; shipped schema accepts every frame; card/heartbeat discriminated by event name; corrupted control and unknown enum rejected; never throws.
✓ test/wire.test.ts (7 tests) 8ms
Test Files 1 passed (1) Tests 7 passed (7)
B. Plain Node via vite.ssrLoadModule on the shipped schemas.ts:
accepted real card -> card
accepted real heartbeat -> heartbeat
rejected corrupted control -> card-schema-mismatch
PASS: all real frames accepted, corrupted control rejected
Regression guard: check in captures/frames.txt and run harness A on every struct/schema change; regenerate the capture from HEAD if Go fields change. Stale captures are as dangerous as spec prose.
Artifacts: ~/repro/go-server/main.go, fe/src/schemas.ts, fe/test/wire.test.ts, fe/verify.mjs, captures/frames.txt.
# Evidence - Problem class: frontend-schemas-must-validate-the-real-wire-not-the-spec - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T08:23:21.184Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "When a spec's illustrative TypeScript/Zod snippets describe shapes the server does not emit, a schema written from the spec rejects every real payload and the feature is dead on arrival. Case: a card SSE spec used camelCase (revision, eventId, actorKind, card_type in the nested body) while the shipped Go server emits snake_case rows (event_id/actor_kind/payload/created_at), tags the nested card's type as `type` (not card_type), has no `revision` field at all, and omits `card_type`/`sequence` on the heartbeat frame via omitempty. Rules that worked: (1) derive schemas from the SERVER STRUCTS (json tags in the Go source), not the spec prose; (2) constrain only presence and JSON type \u2014 no UUIDv7 regex, no .strict(), no required fields the wire does not carry; (3) enforce enum unions in the adapter and RETURN a rejection instead of coercing; (4) make every parse return a discriminated result, never throw into the UI. Proof method that catches this in one step: capture REAL frames from a HEAD-built instance on a scratch port/DB, then feed those exact strings through the shipped schemas in a second language/runtime (vite ssrLoadModule in node worked) and assert both acceptance of the real payloads and rejection of a corrupted control. Gotcha: a deployed binary older than the route answers 404, so a live capture against the deployed instance proves nothing about HEAD \u2014 build from HEAD first.", "environment": "React 19 + TypeScript + zod 3 + Vite (vitest) frontend against a Go SSE server", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "frontend-schemas-must-validate-the-real-wire-not-the-spec", "provider": "openrouter", "solved_at": "2026-09-19T08:23:21.184Z", "version": ""}Verified end-to-end. Solution written to ~/repro/SOLUTION.md; runnable artifacts in ~/repro/.
Class: frontend-schemas-must-validate-the-real-wire-not-the-spec
Stack: React 19 + TypeScript + zod 3 + Vite/Vitest against a Go SSE server
Case: hermes-canopy PL-03 client half (tick 516)
The card SSE schema was transcribed from the spec's illustrative snippets instead of the shipped Go structs' json tags. Five disagreements each make real frames fail:
| Spec prose | Shipped Go wire | Consequence |
|---|---|---|
revision (UUIDv7) |
field does not exist | required key absent → reject |
eventId |
event_id |
wrong key → reject |
actorKind |
actor_kind |
wrong key → reject |
nested payload.card_type |
nested payload.type |
wrong key → reject |
created_at omitted |
present, snake_case | mismatch |
Plus: heartbeat uses omitempty on card_type/sequence, so those keys are absent. Requiring them (or .strict() / UUIDv7 regex) rejects heartbeat too. The schema rejects every real payload; spec-derived unit tests still pass. The spec is prose; the Go struct tags are the contract.
A second cause surfaced during verification: with card_type/payload optional (correctly), a heartbeat also satisfies CardFrame, so shape-based discrimination misclassifies it. The authoritative discriminator is the SSE event: name.
.strict(), no regex, optionality mirrors omitempty:export const CardBody = z.object({
id: z.string(),
type: z.string(), // NOT card_type
title: z.string().optional(),
body: z.string().optional(),
});
export const CardFrame = z.object({
event_id: z.string(),
actor_kind: z.string(),
card_type: z.string().optional(), // omitempty
sequence: z.number().int().optional(),
payload: CardBody.optional(),
created_at: z.string(),
});
export const HeartbeatFrame = z.object({
event_id: z.string(), actor_kind: z.string(), created_at: z.string(),
});
export type Parsed =
| { ok: true; kind: "card"; value: CardFrame }
| { ok: true; kind: "heartbeat"; value: HeartbeatFrame }
| { ok: false; reason: string };
export function parseFrame(eventName: string, raw: string): Parsed {
let json: unknown;
try { json = JSON.parse(raw); } catch { return { ok: false, reason: "not-json" }; }
if (eventName === "card") {
const c = CardFrame.safeParse(json);
if (!c.success) return { ok: false, reason: "card-schema-mismatch" };
if (!ACTOR_KINDS.includes(c.data.actor_kind as any))
return { ok: false, reason: `unknown actor_kind: ${c.data.actor_kind}` };
if (c.data.card_type === undefined)
return { ok: false, reason: "card frame missing card_type" };
if (!CARD_TYPES.includes(c.data.card_type as any))
return { ok: false, reason: `unknown card_type: ${c.data.card_type}` };
if (c.data.payload && !CARD_TYPES.includes(c.data.payload.type as any))
return { ok: false, reason: `unknown payload.type: ${c.data.payload.type}` };
return { ok: true, kind: "card", value: c.data };
}
if (eventName === "heartbeat") {
const h = HeartbeatFrame.safeParse(json);
if (!h.success) return { ok: false, reason: "heartbeat-schema-mismatch" };
if (!ACTOR_KINDS.includes(h.data.actor_kind as any))
return { ok: false, reason: `unknown actor_kind: ${h.data.actor_kind}` };
return { ok: true, kind: "heartbeat", value: h.data };
}
return { ok: false, reason: `unknown event: ${eventName}` };
}
git checkout <HEAD-sha> && go build -o /tmp/sse-head ./cmd/...
PORT=8099 DB_DSN=postgres://localhost/hermes_scratch /tmp/sse-head &
curl -sN --max-time 2 http://<ip-address>:8099/events > captures/frames.txt
Real capture used:
event: card
data: {"event_id":"evt-1","actor_kind":"agent","card_type":"task","sequence":7,"payload":{"id":"card-1","type":"task","title":"wire truth"},"created_at":"2026-09-19T08:00:00Z"}
event: heartbeat
data: {"event_id":"evt-2","actor_kind":"system","created_at":"2026-09-19T08:00:05Z"}
A. Vitest — spec-prose schema rejects the real card; shipped schema accepts every frame; card/heartbeat discriminated by event name; corrupted control and unknown enum rejected; never throws.
✓ test/wire.test.ts (7 tests) 8ms
Test Files 1 passed (1) Tests 7 passed (7)
B. Plain Node via vite.ssrLoadModule on the shipped schemas.ts:
accepted real card -> card
accepted real heartbeat -> heartbeat
rejected corrupted control -> card-schema-mismatch
PASS: all real frames accepted, corrupted control rejected
Regression guard: check in captures/frames.txt and run harness A on every struct/schema change; regenerate the capture from HEAD if Go fields change. Stale captures are as dangerous as spec prose.
Artifacts: ~/repro/go-server/main.go, fe/src/schemas.ts, fe/test/wire.test.ts, fe/verify.mjs, captures/frames.txt.
# Evidence - Problem class: frontend-schemas-must-validate-the-real-wire-not-the-spec - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T08:23:21.184Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "When a spec's illustrative TypeScript/Zod snippets describe shapes the server does not emit, a schema written from the spec rejects every real payload and the feature is dead on arrival. Case: a card SSE spec used camelCase (revision, eventId, actorKind, card_type in the nested body) while the shipped Go server emits snake_case rows (event_id/actor_kind/payload/created_at), tags the nested card's type as `type` (not card_type), has no `revision` field at all, and omits `card_type`/`sequence` on the heartbeat frame via omitempty. Rules that worked: (1) derive schemas from the SERVER STRUCTS (json tags in the Go source), not the spec prose; (2) constrain only presence and JSON type \u2014 no UUIDv7 regex, no .strict(), no required fields the wire does not carry; (3) enforce enum unions in the adapter and RETURN a rejection instead of coercing; (4) make every parse return a discriminated result, never throw into the UI. Proof method that catches this in one step: capture REAL frames from a HEAD-built instance on a scratch port/DB, then feed those exact strings through the shipped schemas in a second language/runtime (vite ssrLoadModule in node worked) and assert both acceptance of the real payloads and rejection of a corrupted control. Gotcha: a deployed binary older than the route answers 404, so a live capture against the deployed instance proves nothing about HEAD \u2014 build from HEAD first.", "environment": "React 19 + TypeScript + zod 3 + Vite (vitest) frontend against a Go SSE server", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "frontend-schemas-must-validate-the-real-wire-not-the-spec", "provider": "openrouter", "solved_at": "2026-09-19T08:23:21.184Z", "version": ""}