PR B: VALIDATING phase + unified ExceptionManager (arch steps 3, 7) #6

Merged
nsatoshi merged 2 commits from devin/1776875351-validating-exception-manager into main 2026-04-22 17:15:59 +00:00
Owner

Implements steps 3 and 7 from the architecture gap-analysis. Stacks on PR A.

What lands

services/exceptionManager.ts (new)

One file, one taxonomy. Architecture note §12 collapsed into ExceptionClass = 'timing' | 'data' | 'control' | 'business' | 'system' + the 18 fine-grained ExceptionCodes. Factory helpers (Timing.dispatch, Data.valueMismatch, Control.unauthorized, Business.manualStop, …) keep call sites short and self-documenting. classify(err) normalises anything into a SettlementException, and route() is a deterministic table:

class decision
timing retry (exponential backoff via errorRecovery)
system/network_error retry
system/other dead_letter
data abort_transaction
control/duplicate_event dead_letter
control/other escalate
business/manual_stop abort_transaction
business/other escalate

handle() is the single entry point callers should use — it does classify → route → retry-or-DLQ and returns the decision so the coordinator can still decide to abort.

services/execution.ts (refactored, 68% rewrite)

ExecutionCoordinator.executePlan now drives the full 12-state machine through stateMachine.transition():

DRAFT -> INITIATED -> PRECONDITIONS_PENDING -> READY_FOR_PREPARE
      -> PREPARED (approver) -> EXECUTING (releaser)
      -> VALIDATING -> COMMITTED (approver) -> CLOSED
failure path: VALIDATING|* -> ABORTED -> CLOSED

New validatePhase() (arch §9.2) reconciles before COMMIT:

  • DLT tx hash format (0x + 64 hex)
  • bank message id non-empty
  • every non-issueInstrument step amount > 0

Mismatches raise Data.valueMismatch(mismatches) and the machine transitions to ABORTED. SoD-gated edges use distinct default actors (system-approver, system-releaser, system-validator) so the matrix in PR A stays satisfied; production callers pass real identities via the new actors param.

api/plans.ts + index.ts

New GET /api/plans/:planId/state returns { transaction_state, legacy_status, transitions: [...] } — the full audit chain from the transaction_state_transitions table (arch §14).

Tests

tests/unit/exceptionManager.test.ts — 14 cases covering the four-class taxonomy, classify() idempotence, and every routing-matrix edge.

Verification

$ npx tsc --noEmit              # clean
$ npx jest                      # 45 passed, 3 suites

Not in this PR

  • Events are still emitted via in-memory EventEmitter — PR D adds the signed, persisted, SSE-exposed event bus.
  • NotaryRegistry.finalizePlan() still calls the mock — PR C wires the real contract.
  • errorHandler.ts, errorRecovery.ts, deadLetterQueue.ts, gracefulDegradation.ts left in place; ExceptionManager consumes them rather than replacing them, so HTTP error middleware is unchanged.

Series order

A → B → C → D → E → F → G → H.

Implements **steps 3 and 7** from the architecture gap-analysis. Stacks on PR A. ## What lands ### `services/exceptionManager.ts` (new) One file, one taxonomy. Architecture note §12 collapsed into `ExceptionClass = 'timing' | 'data' | 'control' | 'business' | 'system'` + the 18 fine-grained `ExceptionCode`s. Factory helpers (`Timing.dispatch`, `Data.valueMismatch`, `Control.unauthorized`, `Business.manualStop`, …) keep call sites short and self-documenting. `classify(err)` normalises anything into a `SettlementException`, and `route()` is a deterministic table: | class | decision | | --- | --- | | timing | retry (exponential backoff via `errorRecovery`) | | system/network_error | retry | | system/other | dead_letter | | data | abort_transaction | | control/duplicate_event | dead_letter | | control/other | escalate | | business/manual_stop | abort_transaction | | business/other | escalate | `handle()` is the single entry point callers should use — it does classify → route → retry-or-DLQ and returns the decision so the coordinator can still decide to abort. ### `services/execution.ts` (refactored, 68% rewrite) `ExecutionCoordinator.executePlan` now drives the full 12-state machine through `stateMachine.transition()`: ``` DRAFT -> INITIATED -> PRECONDITIONS_PENDING -> READY_FOR_PREPARE -> PREPARED (approver) -> EXECUTING (releaser) -> VALIDATING -> COMMITTED (approver) -> CLOSED failure path: VALIDATING|* -> ABORTED -> CLOSED ``` New `validatePhase()` (arch §9.2) reconciles before COMMIT: - DLT tx hash format (`0x` + 64 hex) - bank message id non-empty - every non-`issueInstrument` step amount > 0 Mismatches raise `Data.valueMismatch(mismatches)` and the machine transitions to `ABORTED`. SoD-gated edges use distinct default actors (`system-approver`, `system-releaser`, `system-validator`) so the matrix in PR A stays satisfied; production callers pass real identities via the new `actors` param. ### `api/plans.ts` + `index.ts` New `GET /api/plans/:planId/state` returns `{ transaction_state, legacy_status, transitions: [...] }` — the full audit chain from the `transaction_state_transitions` table (arch §14). ### Tests `tests/unit/exceptionManager.test.ts` — 14 cases covering the four-class taxonomy, `classify()` idempotence, and every routing-matrix edge. ## Verification ``` $ npx tsc --noEmit # clean $ npx jest # 45 passed, 3 suites ``` ## Not in this PR - Events are still emitted via in-memory EventEmitter — PR D adds the signed, persisted, SSE-exposed event bus. - `NotaryRegistry.finalizePlan()` still calls the mock — PR C wires the real contract. - `errorHandler.ts`, `errorRecovery.ts`, `deadLetterQueue.ts`, `gracefulDegradation.ts` left in place; ExceptionManager consumes them rather than replacing them, so HTTP error middleware is unchanged. ## Series order A → **B** → C → D → E → F → G → H.
nsatoshi changed target branch from devin/1776874611-instrument-leg-state-machine to main 2026-04-22 17:15:53 +00:00
nsatoshi added 2 commits 2026-04-22 17:15:53 +00:00
PR A: 12-state transaction machine + issueInstrument step + SoD matrix
Some checks failed
Code Quality / SonarQube Analysis (pull_request) Failing after 23s
Code Quality / Code Quality Checks (pull_request) Failing after 11s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 4s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 5s
b24a4df983
Architecture note steps 1, 2, 10 (data model).

- types/transactionState.ts: 12 states, allowed-transition table, SoD matrix
- types/plan.ts: add InstrumentTerms + 'issueInstrument' PlanStep type
- services/planValidation.ts: validate SBLC step (BIC, ISO-4217, sha256,
  YYYY-MM-DD expiry, >0 amount)
- services/stateMachine.ts: transition() enforces legality + SoD + appends
  to transaction_state_transitions
- db/migrations/002: plans.transaction_state (CHECK) +
  transaction_state_transitions append-only table
- tests/unit: 13 + 8 unit tests (31 total, all pass)

No behaviour change yet: coordinator still uses legacy status field.
PRs B-G will migrate execution paths onto the new machine.
- services/exceptionManager.ts: single taxonomy (timing/data/control/
  business/system) with §12 codes, deterministic route() table, and
  handle() dispatch to retry/DLQ/escalate
- services/execution.ts: refactor executePlan to drive the full 12-state
  machine (DRAFT -> INITIATED -> ... -> VALIDATING -> COMMITTED -> CLOSED)
  via stateMachine.transition(), with a new validatePhase() that
  reconciles DLT tx hash + bank message id + per-step amounts before
  COMMIT; SoD-gated edges use distinct synthetic actors by default
- api/plans.ts + index.ts: GET /api/plans/:planId/state returning
  current transaction_state + full audit trail of transitions
- tests/unit/exceptionManager.test.ts: 14 tests for classification +
  routing matrix

59 tests pass. tsc clean.
nsatoshi merged commit b4d28c77d8 into main 2026-04-22 17:15:59 +00:00
nsatoshi deleted branch devin/1776875351-validating-exception-manager 2026-04-22 17:16:02 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: d-bis/CurrenciCombo#6