Skip to content

feat(schemas): add Tier-0 finalDecisionPayloadSchema + parseTypedPayload helper (PR2/3)#36

Merged
kaicianflone merged 1 commit into
mainfrom
feat/audit-events-schema
May 6, 2026
Merged

feat(schemas): add Tier-0 finalDecisionPayloadSchema + parseTypedPayload helper (PR2/3)#36
kaicianflone merged 1 commit into
mainfrom
feat/audit-events-schema

Conversation

@kaicianflone
Copy link
Copy Markdown
Collaborator

Summary

PR2 of 3 from ~/.gstack/projects/consensus-tools-toolkit/ceo-plans/dashboard-zod-trust-boundary.md. Builds on PR #35.

Changes

Schemas package — Tier 0 (zero deps preserved)

packages/schemas/src/audit-events.ts (new):

Export Purpose
finalDecisionPayloadSchema Runtime-validated FINAL_DECISION audit payload. Permissive z.object accepts canonical camelCase + legacy snake_case + chat-approval-limited shape. .transform() normalizes to canonical output. .passthrough() preserves unknown fields (forward-compat).
participantMetadataSchema Stricter than participantSchema.metadata (which stays z.record(z.unknown()) for backwards-compat per /plan-eng-review decision). Rejects Date/Map/Set/class instances (closes round-3 finding C); transform returns fresh object (closes finding E).
consensusMetaSchema Extracted shape for the consensus-meta sub-object emitted by node-executor.ts and historically present in DB rows.

15 unit tests cover the matrix in plan T02/T03 acceptance: canonical/legacy/limited shapes, forward-compat passthrough, mutation-safety, primitive rejection.

Dashboard helper

apps/dashboard/src/lib/safeJson.ts extended with parseTypedPayload:

export type TypedParseResult<T> =
  | { status: "empty" }
  | { status: "invalid"; reason: string }
  | { status: "ok"; value: T };

export function parseTypedPayload<S extends ZodTypeAny>(
  input: string | null | undefined,
  schema: S,
  context?: string,
): TypedParseResult<z.output<S>>;

Generic primitive that takes any zod schema. PR3 will add thin wrappers (parseFinalDecision, parseParticipantMetadata) and use this directly from EventTimeline with a discriminated event-payload schema. The discriminated tagged result forces callers to handle 'invalid' explicitly — TypeScript's exhaustive switch closes the silent-fallback class of bug.

12 unit tests: empty/invalid/ok branches, schema transforms, dev-mode logging, no-leak guarantee.

Producer contract test promotion

PR1's producer contract tests (in core, workflows, sdk-node) now also validate emit shapes through finalDecisionPayloadSchema. Catches drift at the source if any future change reintroduces snake_case or otherwise breaks the canonical shape.

Dashboard runtime deps

Adds @consensus-tools/schemas: workspace:* and zod: ^3.23.0 to apps/dashboard/package.json. Required for PR3 to import the schema. Dashboard is private: true and excluded from changesets/publish.

Changeset

.changeset/audit-events-schema.md — minor bump for @consensus-tools/schemas (new public exports). Documents the 2026-09-01 sunset for legacy shapes.

Test Coverage

  • pnpm build — 23/23 successful
  • pnpm test — 47/47 turbo tasks pass
    • schemas: 38 (15 new audit-events + 23 inputs)
    • core: 73 (+1 schema-validation test)
    • workflows: 35 (existing producer test now also validates via schema)
    • sdk-node: 33 (existing producer test now also validates via schema)
    • dashboard: 48 (was 36; +12 parseTypedPayload)
  • pnpm typecheck — 42/42 successful
  • pnpm dep-check — clean (226 modules, 424 deps)

Plan reconciliation

/plan-eng-review decision Status
Schema location: Tier 0 (decision 1) packages/schemas/src/audit-events.ts
Server contract tightened first (decision 2 / PR1) ✅ landed in PR #35
Schema accepts old snake + canonical camel via .transform() (decision 4)
Helper return shape: tagged {status, value} (decision 8)
Generic parseTypedPayload<T> primitive (decision 9)
Single permissive z.object + .transform(), NOT discriminated union (decision 10)
Producer contract tests in each emit-site package (decision 13) ✅ promoted from PR1
Sunset legacy shapes by 2026-09-01 (decision 17) ✅ documented in changeset
Changeset for Tier-0 export (decision 19)
DB replay test verifies "no migration needed" with real shapes (decision 14) ✅ via representative shapes (local DB has zero events; representative test hits all 3 historical emit shapes + drift case)

Out of scope for PR2 (lands in PR3): dashboard wiring, DriftBanner, @testing-library/react infra, 4 regression tests for findings B/D/E/F.

Test plan

  • Schema accepts all 3 historical emit shapes
  • Schema accepts drift payload (camel + snake both present) — canonical wins
  • Schema rejects missing decision, wrong types, primitives, dates
  • parseTypedPayload discriminates empty / invalid / ok
  • Dashboard build + tests still green
  • dep-check clean (Tier-0 schemas package gained no internal deps)
  • PR3 follow-up: dashboard wiring through schema parser

🤖 Generated with Claude Code

…oad helper

PR2 of 3 from dashboard-zod-trust-boundary plan.

## Schemas package

Add packages/schemas/src/audit-events.ts (Tier 0, zero internal deps):

- finalDecisionPayloadSchema — runtime-validated FINAL_DECISION audit
  payload. Single permissive z.object accepts canonical camelCase
  (post-PR1), legacy snake_case (pre-PR1 historical DB rows), and the
  chat-approval-limited shape. Transform normalizes to canonical
  camelCase output. Passthrough preserves unknown fields for
  forward-compat.
- participantMetadataSchema — stricter than participantSchema.metadata
  (z.record(z.unknown()) at Tier 0 stays loose for backwards compat).
  Rejects non-plain objects (Date, Map, Set, class instances) — closes
  round-3 finding C. Transform returns fresh object — closes finding E.
- consensusMetaSchema — extracted shape for consensusMeta sub-object.

Includes 15 unit tests covering canonical/legacy/limited shapes,
forward-compat passthrough, mutation-safety, and rejection of primitives.

## Dashboard helper

Add parseTypedPayload to apps/dashboard/src/lib/safeJson.ts:

- Generic primitive: parseTypedPayload<S>(input, schema, context).
- Returns discriminated tagged result: {status:'empty'|'invalid'|'ok'}.
  TypeScript exhaustive-switch catches missed cases at compile time —
  closes the silent-fallback bug class.
- Empty input (null/undefined/""/whitespace) → 'empty' (legitimate
  pending state). JSON.parse failure or schema rejection → 'invalid'
  (data corruption / drift). Validated and transformed → 'ok'.
- Dev-mode warns log input length only; never raw content (no
  token/PII leak).

Includes 12 unit tests covering empty/invalid/ok branches, schema
transforms, dev-mode logging, no-leak guarantee.

## Producer contract test promotion

Producer contract tests added in PR1 (#35) now also validate emit shapes
through the Tier-0 schema. Catches drift at the source if any future
change reintroduces snake_case OR shapes the schema rejects.

## Dashboard deps

Add @consensus-tools/schemas (workspace:*) and zod ^3.23.0 as runtime
dependencies of apps/dashboard. PR3 wires the schema into the dashboard
render paths and DriftBanner component.

## Changeset

Minor bump for @consensus-tools/schemas (new public exports).

## Verification

- pnpm build — 23/23 successful
- pnpm test — 47/47 turbo tasks pass (schemas 38, core 73, workflows
  35, sdk-node 33, dashboard 48, all others unchanged)
- pnpm typecheck — 42/42 successful
- pnpm dep-check — clean (226 modules, 424 deps)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kaicianflone kaicianflone merged commit dba99a7 into main May 6, 2026
3 checks passed
@kaicianflone kaicianflone deleted the feat/audit-events-schema branch May 6, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant