Skip to content

fix(workflows): canonicalize FINAL_DECISION audit details to camelCase (PR1/3)#35

Merged
kaicianflone merged 1 commit into
mainfrom
fix/audit-events-camelcase
May 6, 2026
Merged

fix(workflows): canonicalize FINAL_DECISION audit details to camelCase (PR1/3)#35
kaicianflone merged 1 commit into
mainfrom
fix/audit-events-camelcase

Conversation

@kaicianflone
Copy link
Copy Markdown
Collaborator

Summary

PR1 of 3 from ~/.gstack/projects/consensus-tools-toolkit/ceo-plans/dashboard-zod-trust-boundary.md.

Aligns node-executor.ts:596-598 with the canonical camelCase emit shape already used by GuardEngine.persistAuditEvents (core/guard-engine.ts:111). Three audit-event producers now emit consistent FINAL_DECISION details:

Producer Emit shape (after PR1)
core/guard-engine.ts:111 {auditId, decision, reason, riskScore, guardType} (already canonical, no change)
workflows/node-executor.ts:596 {runId, boardId, decision, reason, riskScore, guardType, consensusMeta} (was: risk_score, guard_type, consensus_meta)
sdk-node/chat-approval.ts:61 {runId, decision, approver, votesReceived, votesRequired, idempotencyKey?} (no risk fields by design — human approval, no change)

Why

Three rounds of /review on the previous fix/dashboard-error-handling branch (PR #34, now merged) kept finding the same trust-boundary class of bug. Round-3 finding D — "NaN% rendered when valid JSON has unexpected shape" — traces to the producer/consumer shape mismatch: dashboard reads risk_score (snake), some producers emit riskScore (camel), others emit risk_score (snake). Aligning producers first lets a single Tier-0 schema in PR2 validate canonically, then PR3 wires the dashboard through that schema.

Changes

  • packages/workflows/src/node-executor.ts:596-598 — snake → camel for the FINAL_DECISION audit details (3-line change).
  • packages/workflows/tests/node-executor.test.ts — new producer contract test that runs a guard node end-to-end and asserts the FINAL_DECISION audit details match the camelCase shape (with regression guards against legacy keys).
  • packages/core/tests/guard-engine.test.ts — new producer contract test for GuardEngine.evaluate (was already camelCase; now locked in).
  • packages/sdk-node/tests/chat-approval.test.ts — new file, 3 producer contract tests covering FINAL_DECISION on quorum, idempotencyKey propagation, and the VOTE_RECEIVED branch.

Note: guardResults runtime type and GuardResult return shape keep snake_case (risk_score, guard_type) per the plan-eng-review decision 7 — those are the runtime layer, distinct from the wire/audit shape this PR canonicalizes.

Test Coverage

  • pnpm --filter @consensus-tools/core test — 72/72
  • pnpm --filter @consensus-tools/workflows test — 35/35 (+1 producer contract)
  • pnpm --filter @consensus-tools/sdk-node test — 33/33 (+3 producer contracts)
  • pnpm --filter @consensus-tools/dashboard test — 36/36 (unchanged; dashboard tests don't cover the affected render paths)
  • pnpm --filter ... typecheck — clean across all 3 affected packages
  • pnpm dep-check — clean (225 modules, 423 deps)

Known intermediate state

After PR1 lands, RunDetailPage.tsx:107 and BoardDetailPage.tsx:189,196 will render NaN% for FINAL_DECISION events emitted by node-executor.ts (they read risk_score/guard_type snake_case). Pre-PR, those same render paths were already broken for FINAL_DECISION events emitted by guard-engine.ts (which is already camelCase). After PR1 they're uniformly broken; PR3 will land the consumer fix that closes both. PR2 (Tier-0 schema + parser) bridges them.

EventTimeline.tsx:256 already handles both shapes (payload.guard_type || payload.guardType) so it survives the intermediate state.

Test plan

  • Build passes for core, workflows, sdk-node
  • All affected packages' test suites pass
  • Dashboard tests still pass
  • dep-check clean
  • PR2 follow-up: Tier-0 finalDecisionPayloadSchema in @consensus-tools/schemas + parseTypedPayload API
  • PR3 follow-up: dashboard wiring + DriftBanner + component tests

🤖 Generated with Claude Code

Aligns node-executor.ts:596-598 with the canonical camelCase emit shape
already used by GuardEngine.persistAuditEvents (core/guard-engine.ts:111).
Three audit-event producers now emit consistent FINAL_DECISION details:

- core/guard-engine.ts: {auditId, decision, reason, riskScore, guardType}
- workflows/node-executor.ts: {runId, boardId, decision, reason, riskScore,
  guardType, consensusMeta} (was: risk_score, guard_type, consensus_meta)
- sdk-node/chat-approval.ts: {runId, decision, approver, votesReceived,
  votesRequired, idempotencyKey?} (no risk fields by design — human approval)

Adds producer contract tests in each emit-site package that assert the
exact camelCase shape and that legacy snake_case keys are absent.
Tests catch drift at the source if any future change reintroduces
snake_case at this trust boundary.

Part 1 of 3 from dashboard-zod-trust-boundary plan. Subsequent PRs add
the Tier-0 finalDecisionPayloadSchema in @consensus-tools/schemas and
wire the dashboard through the schema parser. Until those land, dashboard
RunDetailPage and BoardDetailPage will render NaN% for these events
(known intermediate state — was already broken for guard-engine emits
pre-PR; now uniformly broken until PR3 lands the consumer fix).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kaicianflone kaicianflone merged commit b37bdd2 into main May 6, 2026
3 checks passed
@kaicianflone kaicianflone deleted the fix/audit-events-camelcase branch May 6, 2026 18:54
kaicianflone added a commit that referenced this pull request May 6, 2026
…oad helper (#36)

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>
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