When Uncertainty Must Still Produce an Answer; Designing Total Decision Functions for Fragmented Evi

4
calendar_today agoschedule8 min read

Most software systems are comfortable returning incomplete answers.

An API returns null.
A model returns a low-confidence score.
A workflow enters an undefined status.
A human operator is left to infer what the system failed to decide.

That may be acceptable in a search interface or recommendation engine. It is not acceptable when software controls whether an operational process may advance, must stop, or requires intervention.

In those systems, uncertainty cannot remain an implementation accident. It must become a first-class decision state.

The architectural objective is therefore not merely to produce accurate positive matches. It is to construct a total decision function: a function that maps every valid input into exactly one documented terminal outcome.

Formally:

D(record, evidence, policy) → decision

Where:

decision ∈ {
  PROCEED,
  REVIEW,
  BLOCK,
  NO_RESULT
}

The defining property is completeness:

∀ valid record r:
D(r) returns exactly one terminal decision

This changes the entire system design. Data retrieval is no longer the product. The controlled commitment made from that data is the product.


Partial Functions Are Hidden Operational Risk

Many data systems are effectively partial functions.

They work when:

  • the input is clean;
  • the upstream provider responds;
  • one candidate dominates;
  • no fields contradict each other;
  • and the expected entity appears in the expected schema.

The difficult records fall outside that happy path. They contain stale attributes, competing identities, incomplete timelines, duplicated upstream data, ambiguous relationships, or explicit restrictions.

A partial implementation often handles these cases with one of three shortcuts:

return null
return bestCandidate
return confidenceScore

All three defer the real decision.

null does not explain what failed.

A best candidate may conceal unresolved contradictions.

A confidence score does not specify what action is permitted.

A decision system must instead answer a more precise question:

Given the evidence currently available, what is the most defensible operational state for this record?

That answer may be “do not advance.” It may be “human review required.” It may be “no usable evidence exists.” Those are still successful resolutions of the decision problem.


Separate Observations From Decisions

The first architectural rule is strict separation between raw observations and final decisions.

External systems return incompatible structures:

{
  "telephone": "..."
}
{
  "phones": [
    {
      "number": "...",
      "status": "current"
    }
  ]
}
{
  "subjects": [
    {
      "contactPoints": []
    }
  ]
}

Allowing downstream decision logic to consume these payloads directly creates several problems:

  • provider-specific conditionals spread throughout the codebase;
  • provenance becomes difficult to preserve;
  • schema changes affect policy behavior;
  • duplicate upstream data may be counted as independent evidence;
  • raw private data can leak into logs or interfaces;
  • and decision rules become inseparable from integration code.

The safer pattern is:

Raw Provider Response
        ↓
Provider Adapter
        ↓
Normalized Evidence Units
        ↓
Evidence Ledger
        ↓
Context and Policy Evaluation
        ↓
Terminal Decision

A normalized evidence unit can be represented generically:

type EvidenceUnit = {
  id: string;
  sourceFamily: string;
  evidenceType: string;
  subject: string;
  value: unknown;

  supports: string[];
  contradicts: string[];

  confidenceBand: "high" | "medium" | "low" | "unknown";
  recencyBand: "current" | "recent" | "historical" | "unknown";

  restrictionFlag: boolean;
  rawReference?: string;
  publicSummary: string;
};

The important feature is not the exact schema. It is the boundary.

The decision engine receives evidence with canonical semantics. It does not receive arbitrary provider JSON.


Use an Append-Only Evidence Ledger

Normalized evidence should enter an append-only ledger.

Once an observation has been recorded, downstream modules may query it or derive new findings from it, but they should not silently rewrite the original unit.

function addEvidence(
  ledger: EvidenceLedger,
  observation: EvidenceUnit
): Readonly<EvidenceUnit> {
  const immutableUnit = Object.freeze({
    ...observation,
    id: nextEvidenceId(ledger),
    createdAt: new Date().toISOString()
  });

  ledger.units.push(immutableUnit);
  return immutableUnit;
}

This produces several useful properties.

Provenance

Every conclusion can be traced back to the observations that supported or contradicted it.

Reproducibility

A decision can be rerun against the same ledger and policy version.

Safe projection

Operator-facing output can expose a redacted evidence view without returning raw source material.

Conflict preservation

Contradictory observations remain visible. A later module cannot erase inconvenient evidence simply because another candidate appears stronger.

Testability

Tests can assert not only the final decision but also the exact evidence path that produced it.

The evidence ledger becomes the stable interface between retrieval and judgment.


Provider Count Is Not Evidence Independence

A common failure in multi-provider systems is treating the number of returned records as the number of independent confirmations.

Suppose three vendors return the same telephone number. That may look like three corroborating observations.

But all three vendors may derive the number from the same upstream dataset.

Counting them independently produces false confidence.

The correct unit of corroboration is not the vendor. It is the source family or underlying evidence structure.

function independentSupport(
  ledger: EvidenceLedger,
  claim: string
): number {
  return new Set(
    ledger.units
      .filter(unit => unit.supports.includes(claim))
      .map(unit => unit.sourceFamily)
  ).size;
}

This distinction matters because confidence must represent evidentiary diversity, not response volume.

Ten repetitions of the same observation are not equivalent to two structurally independent anchors.

The system should preserve both facts:

provider_return_count = 10
independent_source_family_count = 1

Only the second should influence corroboration logic.


Context Must Be Computed, Not Assumed

An attribute has little meaning without context.

An address can be:

  • current;
  • historical;
  • institutional;
  • associated with a relative;
  • associated with an employer;
  • temporarily occupied;
  • or shared by multiple people with similar names.

A telephone can be:

  • directly held;
  • household-held;
  • business-owned;
  • recycled;
  • masked;
  • or associated with a third party.

A name difference can represent:

  • a collision;
  • a prior surname;
  • a life event;
  • an alias;
  • or a simple mismatch.

A decision engine should therefore avoid treating evidence as a flat collection of fields.

Instead, it should construct intermediate models:

Evidence Ledger
 ├── Identity Clusters
 ├── Contact Reversals
 ├── Name-Collision Analysis
 ├── Address Timeline
 ├── Household Relationships
 ├── Life-Event Continuity
 ├── Restrictions
 └── Available Operational Paths

These modules do not make the final decision. They transform observations into structured findings.

For example:

type AddressTimelineResult = {
  entries: AddressTimelineEntry[];
  timelineExplained: boolean;
  unresolvedTransitions: string[];
};
type CollisionResult = {
  candidateClusters: IdentityCluster[];
  targetCluster?: IdentityCluster;
  collisionRisk: "low" | "medium" | "high";
  unresolvedReason?: string;
};

The final arbiter consumes these findings together. No single module is allowed to promote a record independently.


Safety Constraints Should Be Monotonic

In a controlled decision system, restrictions and safety sentinels should be monotonic.

A restriction may make a result more conservative. It must never make the result less conservative.

Conceptually:

PROCEED → REVIEW → BLOCK

A newly discovered contradiction may downgrade PROCEED to REVIEW.

A confirmed restriction may downgrade PROCEED or REVIEW to BLOCK.

But a suppressor should never increase confidence or promote a record.

This can be expressed as an invariant:

assert(
  applyRestriction(existingDecision, restriction)
  <= existingDecision
);

Where <= represents the permitted ordering from less restrictive to more restrictive.

The precise state ordering may vary by application, but the principle is stable:

Safety evidence can constrain action. It cannot manufacture authorization.

This is especially important when probabilistic components are present. A highly confident model output must not outrank an explicit restriction.


Keep Probabilistic Planning Outside the Final Arbiter

Machine learning and language models can be useful for:

  • suggesting additional investigations;
  • organizing unresolved questions;
  • drafting operator notes;
  • identifying potentially useful evidence gaps;
  • and prioritizing expensive API calls.

They should not be the sole source of the final operational state.

The safe separation is:

Evidence State ──→ Advisory Planner
       │
       └─────────→ Deterministic Policy ──→ Final Decision

The advisory planner may recommend:

{
  "suggestedActions": [
    "investigate competing address cluster",
    "request an independent contact-path source"
  ]
}

But the final decision policy must not read a model-generated instruction as evidence.

function decide(
  state: InvestigationState,
  policy: DecisionPolicy
): Decision {
  // Deliberately does not consume state.aiPlan.
}

This creates a strong architectural boundary:

  • probabilistic systems help determine what to investigate;
  • deterministic systems determine what may happen next.

Build the Final Decision as an Ordered Policy

The final arbiter should be the only module authorized to emit a terminal state.

An ordered policy is easier to audit than a collection of distributed booleans.

Generic pseudocode might look like this:

function decide(
  state: InvestigationState,
  policy: DecisionPolicy
): Decision {
  const inputs = deriveDecisionInputs(state);

  if (inputs.hasExplicitRestriction) {
    return block({
      reasonCode: "EXPLICIT_RESTRICTION",
      nextAllowedStep: "STOP"
    });
  }

  if (inputs.hasRestrictedAlternatePath) {
    return block({
      reasonCode: "DIRECT_PATH_RESTRICTED",
      nextAllowedStep: "USE_AUTHORIZED_ALTERNATE_PATH"
    });
  }

  if (inputs.hasMaterialCollision) {
    return review({
      reasonCode: "UNRESOLVED_IDENTITY_COLLISION",
      nextAllowedStep: "HUMAN_REVIEW"
    });
  }

  if (
    inputs.identitySupported &&
    inputs.contactPathSupported &&
    inputs.timelineExplained &&
    policy.allowAction
  ) {
    return proceed({
      reasonCode: "ALL_REQUIRED_CHECKS_SUPPORTED",
      nextAllowedStep: "ENTER_APPROVED_WORKFLOW"
    });
  }

  if (inputs.hasUsefulEvidence) {
    return review({
      reasonCode: "EVIDENCE_REQUIRES_INTERPRETATION",
      nextAllowedStep: "HUMAN_REVIEW"
    });
  }

  return noResult({
    reasonCode: "NO_USABLE_EVIDENCE",
    nextAllowedStep: "RETURN_TO_SOURCE_WORKFLOW"
  });
}

The final fallback is essential.

Without it, the function remains partial.


“No Result” Is a Successful Decision

Engineers often treat “no result” as failure.

That is correct for a lookup function:

lookup(identifier) → entity | null

It is not correct for a decision function:

decide(record, evidence, policy) → terminal state

When the available evidence cannot justify action, NO_RESULT is the correct result.

It establishes that:

  • the system completed its investigation;
  • no supported path passed the policy gates;
  • no unresolved exception was hidden;
  • the record must not be promoted;
  • and the next step is explicitly defined.

This is operationally different from a system error.

type RunStatus =
  | "COMPLETED"
  | "FAILED";

type DecisionState =
  | "PROCEED"
  | "REVIEW"
  | "BLOCK"
  | "NO_RESULT";

A completed run can return any of the four decision states.

A failed run means the system could not execute the decision function because of an infrastructure, validation, or integrity failure.

Conflating these concepts creates bad metrics and dangerous retry behavior.


A Decision Packet Is More Useful Than a Score

A score gives an operator a number.

A decision packet gives the organization an accountable state transition.

type DecisionPacket = {
  recordId: string;
  decision: DecisionState;

  reasonCode: string;
  operatorReason: string;
  nextAllowedStep: string;

  supportingEvidence: PublicEvidenceUnit[];
  contradictingEvidence: PublicEvidenceUnit[];
  unresolvedQuestions: string[];

  sourceFamiliesUsed: string[];
  policyVersion: string;
  createdAt: string;
};

The reason code serves machines.

The operator reason serves humans.

The next allowed step connects the decision to workflow.

The evidence projection explains why the state exists.

The policy version makes historical decisions reproducible after rules evolve.

This is the difference between returning information and controlling a process.


Test Invariants, Not Only Examples

Fixture tests are necessary, but decision systems also need invariant-based tests.

Completeness

Every valid record produces one terminal decision.

assert(terminalStates.includes(result.decision));

Exclusivity

A record cannot be both PROCEED and BLOCK.

assert.equal(result.terminalStateCount, 1);

Determinism

The same normalized evidence and policy version produce the same decision.

assert.deepEqual(
  decide(state, policy),
  decide(state, policy)
);

Provenance

Every supporting conclusion references existing evidence.

for (const reference of result.evidenceReferences) {
  assert(ledger.has(reference));
}

Monotonic restrictions

Adding a restriction cannot upgrade a decision.

assert(
  restrictiveness(withRestriction) >=
  restrictiveness(withoutRestriction)
);

Source independence

Repeated returns from the same family do not inflate independent support.

assert.equal(
  independentSupport(duplicatedLedger, claim),
  independentSupport(originalLedger, claim)
);

Public-output safety

Raw payload references and credentials never appear in operator output.

assert.equal(publicPacket.rawPayload, undefined);
assert.equal(publicPacket.credentials, undefined);

These invariants protect the architecture even as providers, schemas, and policy configurations change.


The Hard Problem Is Controlled Commitment

It is relatively easy to retrieve more data.

It is harder to determine:

  • which observations are independent;
  • which observations contradict each other;
  • what context changes their meaning;
  • what restrictions dominate;
  • what uncertainty remains;
  • and what action is justified.

That is why the core abstraction should not be the provider call, the confidence score, or the candidate record.

The core abstraction should be the transition from evidence to controlled commitment.

A mature decision system does not claim certainty where none exists. It does something more useful:

  • it preserves evidence;
  • separates observation from interpretation;
  • models conflict explicitly;
  • applies restrictions conservatively;
  • isolates probabilistic advice;
  • produces one exhaustive terminal state;
  • and explains the next permitted action.

The quality of such a system is not measured only by how often it says “yes.”

It is measured by whether every “yes,” “no,” “review,” and “not enough evidence” is deliberate, reproducible, and defensible.

That is the point at which decisioning stops being a feature layered on top of data.

The decision becomes the product.


Suggested tags: #architecture #systemdesign #backend #decisioning #dataengineering #softwareengineering

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Local-First: The Browser as the Vault

Pocket Portfolio - Apr 20

The Sovereign Vault — A Comprehensive Guide to Protocol-Driven AI

Ken W. Algerverified - Jun 4

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelskiverified - Apr 23

The Interface of Uncertainty: Designing Human-in-the-Loop

Pocket Portfolio - Mar 10

The Trust Gap: Why Your Product Fails Even When the Math is Right

Karol Modelskiverified - Jul 16
chevron_left
160 Points4 Badges
2Posts
0Comments
1Connections
Creator, Destroyer, Lover, Builder

Related Jobs

View all jobs →

Commenters (This Week)

5 comments
3 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!