fix(workflows): canonicalize FINAL_DECISION audit details to camelCase (PR1/3)#35
Merged
Merged
Conversation
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>
7 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR1 of 3 from
~/.gstack/projects/consensus-tools-toolkit/ceo-plans/dashboard-zod-trust-boundary.md.Aligns
node-executor.ts:596-598with the canonical camelCase emit shape already used byGuardEngine.persistAuditEvents(core/guard-engine.ts:111). Three audit-event producers now emit consistent FINAL_DECISION details: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
/reviewon the previousfix/dashboard-error-handlingbranch (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 readsrisk_score(snake), some producers emitriskScore(camel), others emitrisk_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 forGuardEngine.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:
guardResultsruntime type andGuardResultreturn 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/72pnpm --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 packagespnpm dep-check— clean (225 modules, 423 deps)Known intermediate state
After PR1 lands,
RunDetailPage.tsx:107andBoardDetailPage.tsx:189,196will renderNaN%for FINAL_DECISION events emitted bynode-executor.ts(they readrisk_score/guard_typesnake_case). Pre-PR, those same render paths were already broken for FINAL_DECISION events emitted byguard-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:256already handles both shapes (payload.guard_type || payload.guardType) so it survives the intermediate state.Test plan
finalDecisionPayloadSchemain@consensus-tools/schemas+parseTypedPayloadAPI🤖 Generated with Claude Code