DEV Community

Arnold Holm
Arnold Holm

Posted on

A Missing Risk Check Is Not a "No"

A decision trace says WAIT. Did the risk check actually return false, or did its service time out?

Both paths may submit no order, but they need different fixes. An observed rejection means inspect the rule and its inputs. A timeout means restore the source of evidence. If the trace calls both WAIT, the second problem can sit there unnoticed.

My earlier example kept false separate from an absent field. Raju's follow-up question points to the next boundary: an observation can be present in the record but unavailable or invalid. The decision itself must respect that status.

Give each input an evidence state

This small example has two required checks: routeConnected and riskAllowed. It is a decision-trace example, not an order executor.

Recorded input Meaning Decision state
observed, value: true The check ran and passed Continue to the other checks
observed, value: false The check ran and failed WAIT, if all required observations are valid
unavailable, with reason and attempt ID The source did not provide a result BLOCKED
invalid, with reason and source ID A result arrived but failed validation BLOCKED
No input record The trace is incomplete BLOCKED

The important order is: inspect all required inputs for missing or unusable evidence, then evaluate the observed booleans. Otherwise an early false can hide a later timeout.

A small decision boundary

const gateNames = ['routeConnected', 'riskAllowed'];

function decide(trace) {
  if (!trace || typeof trace.decisionId !== 'string' || !trace.decisionId) {
    return { state: 'BLOCKED', reason: 'missing_decision_id' };
  }

  const inspected = [];
  for (const gate of gateNames) {
    if (!trace.observations || !Object.hasOwn(trace.observations, gate)) {
      return { state: 'BLOCKED', gate, reason: 'missing_observation' };
    }

    const observation = trace.observations[gate];
    if (!observation || typeof observation !== 'object') {
      return { state: 'BLOCKED', gate, reason: 'invalid_envelope' };
    }

    if (observation.status === 'observed') {
      if (typeof observation.value !== 'boolean' || !observation.sourceEventId) {
        return { state: 'BLOCKED', gate, reason: 'invalid_observed_value' };
      }
      inspected.push({ gate, value: observation.value, evidenceId: observation.sourceEventId });
      continue;
    }

    if (observation.status === 'unavailable' || observation.status === 'invalid') {
      if (!observation.reason || !observation.evidenceId) {
        return { state: 'BLOCKED', gate, reason: 'missing_failure_evidence' };
      }
      return {
        state: 'BLOCKED', gate,
        reason: `${observation.status}:${observation.reason}`,
        evidenceId: observation.evidenceId
      };
    }

    return { state: 'BLOCKED', gate, reason: 'unknown_status' };
  }

  const failed = inspected.find(observation => observation.value === false);
  if (failed) {
    return { state: 'WAIT', gate: failed.gate, reason: 'observed_false', evidenceId: failed.evidenceId };
  }
  return { state: 'READY', reason: 'all_observed_true' };
}
Enter fullscreen mode Exit fullscreen mode

For example, riskAllowed: { status: 'unavailable', reason: 'risk_service_timeout', evidenceId: 'poll-21' } produces BLOCKED. The poll-21 reference names the failed attempt. It does not pretend there was a risk observation. By contrast, { status: 'observed', value: false, sourceEventId: 'risk-20' } produces WAIT and points to the actual result.

Tests that make the distinction visible

I ran this boundary with Node's built-in test runner. All six local cases passed:

  1. Two observed true inputs produce READY.
  2. An observed false produces WAIT with its source event ID.
  3. An unavailable risk service produces BLOCKED, never WAIT.
  4. An invalid risk value produces BLOCKED with a reason.
  5. A missing required observation produces BLOCKED.
  6. A false route check cannot hide an unavailable risk check. The result remains BLOCKED.

The sixth case matters most. A system can decline to place an order in both cases while still telling its operator the wrong reason. The trace should preserve whether it saw a negative result or lacked evidence to decide.

This example only tests the decision mapping. It does not prove that a source event ID exists, that an observation is fresh, or that a broker accepted or filled anything. A production trace should retain observation time and source identity, reject stale records, and join the decision to any later order request and fill as separate events.

To check your own boundary, remove one required input, replace another with false, then make its source unavailable. If all three cases show the same state and reason, the trace still loses a failure mode.

Reference: Node.js test runner.

Top comments (0)