{"id":219,"date":"2026-05-12T15:52:04","date_gmt":"2026-05-12T15:52:04","guid":{"rendered":"https:\/\/www.fabricioruch.ch\/?p=219"},"modified":"2026-05-12T15:52:04","modified_gmt":"2026-05-12T15:52:04","slug":"the-most-valuable-state-in-my-rule-engine-is-not-true-or-false","status":"publish","type":"post","link":"https:\/\/www.fabricioruch.ch\/?p=219","title":{"rendered":"The Most Valuable State in My Rule Engine Is Not true or false"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Designing Tri-State Logic for Safer and Cheaper AI Automation<\/h2>\n\n\n\n<p>Most rule engines start with a simple assumption: every condition evaluates to either <code>true<\/code> or <code>false<\/code>.<\/p>\n\n\n\n<p>That sounds clean.<\/p>\n\n\n\n<p>A rule matches, or it does not. An action runs, or it does not. The system remains deterministic, easy to reason about, and simple to implement.<\/p>\n\n\n\n<p>Then real data arrives.<\/p>\n\n\n\n<p>Some conditions cannot be decided from the information currently available. A rule may require data that has not been loaded yet. A later enrichment step may be needed. A semantic classifier may be required. A document may need to be parsed before a condition can be evaluated honestly.<\/p>\n\n\n\n<p>A binary engine still demands an answer.<\/p>\n\n\n\n<p>That is the problem.<\/p>\n\n\n\n<p>When a system only supports <code>true<\/code> and <code>false<\/code>, it often collapses two very different situations into the same result:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>false = this condition is definitively false\nfalse = this condition cannot be decided yet<\/code><\/pre>\n\n\n\n<p>Those are not the same thing.<\/p>\n\n\n\n<p>The first is a real decision. The second is missing information.<\/p>\n\n\n\n<p>A production rule engine should not pretend to know something it does not know. That is especially important in AI-assisted automation, where expensive or risky steps should only run when the engine has enough evidence to justify them.<\/p>\n\n\n\n<p>The architectural fix is simple but powerful: add a third state.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>true\nfalse\nunknown<\/code><\/pre>\n\n\n\n<p><code>unknown<\/code> is not an error. It is a valid evaluation result.<\/p>\n\n\n\n<p>It means: the engine does not yet have enough information to make a final decision.<\/p>\n\n\n\n<p>That small change makes the system safer, cheaper, easier to debug, and much more honest.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why Binary Rule Engines Fail in Real Systems<\/h2>\n\n\n\n<p>A binary-only rule engine works well when all required data is available upfront and every condition is deterministic.<\/p>\n\n\n\n<p>That is rarely true in production automation.<\/p>\n\n\n\n<p>Real systems often evaluate rules in stages. Early stages may only have metadata. Later stages may load additional data. Expensive stages may call external services or AI models. Some conditions can be answered cheaply, while others require more context.<\/p>\n\n\n\n<p>If the engine only supports <code>true<\/code> and <code>false<\/code>, uncertainty gets lost.<\/p>\n\n\n\n<p>That creates four common problems.<\/p>\n\n\n\n<p>First, false negatives appear. A rule may return <code>false<\/code> too early because the data needed to evaluate it has not been loaded yet.<\/p>\n\n\n\n<p>Second, false positives appear. Weak early signals may trigger actions that should have required deeper validation.<\/p>\n\n\n\n<p>Third, cost increases. Teams often compensate by calling expensive semantic stages too often because the engine cannot clearly distinguish \u201cnot matched\u201d from \u201cnot enough context.\u201d<\/p>\n\n\n\n<p>Fourth, observability becomes poor. Logs may say a condition evaluated to <code>false<\/code>, but they do not explain whether the system was confidently negative or simply unable to decide.<\/p>\n\n\n\n<p>The root problem is not bad prompts, bad regex, or bad if-statements.<\/p>\n\n\n\n<p>The root problem is missing state semantics.<\/p>\n\n\n\n<p>A rule engine needs a way to represent uncertainty explicitly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Tri-State Logic as a Decision Protocol<\/h2>\n\n\n\n<p>Tri-state logic gives the engine a more accurate decision model:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>true     \u2192 enough evidence exists and the condition matched\nfalse    \u2192 enough evidence exists and the condition did not match\nunknown  \u2192 not enough evidence exists at the current stage<\/code><\/pre>\n\n\n\n<p>The important part is that <code>unknown<\/code> is not treated as failure.<\/p>\n\n\n\n<p>It is an orchestration signal.<\/p>\n\n\n\n<p>It tells the system:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Do not run the action yet.\nDo not reject the rule yet.\nAcquire more context if allowed.\nThen evaluate again.<\/code><\/pre>\n\n\n\n<p>That makes rule evaluation safer because side effects only run after a final decision.<\/p>\n\n\n\n<p>It also makes the system cheaper because expensive enrichment steps are only executed for unresolved decisions.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">A Generic Rule Engine Model<\/h2>\n\n\n\n<p>The rule engine itself should not know about invoices, support tickets, contracts, routing, fraud, compliance, or any other business-specific domain.<\/p>\n\n\n\n<p>Those concepts belong to user-defined rules and user-defined conditions.<\/p>\n\n\n\n<p>The engine should only know how to evaluate a rule tree.<\/p>\n\n\n\n<p>A clean generic model can look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export type EvalResult = 'true' | 'false' | 'unknown';\n\nexport type RuleNode =\n  | ConditionNode\n  | AndNode\n  | OrNode\n  | NotNode;\n\nexport type ConditionNode = {\n  type: 'condition';\n  conditionId: string;\n};\n\nexport type AndNode = {\n  type: 'and';\n  children: RuleNode&#91;];\n};\n\nexport type OrNode = {\n  type: 'or';\n  children: RuleNode&#91;];\n};\n\nexport type NotNode = {\n  type: 'not';\n  child: RuleNode;\n};<\/code><\/pre>\n\n\n\n<p>This structure is intentionally generic.<\/p>\n\n\n\n<p>A condition is identified by <code>conditionId<\/code>. The engine does not care what that condition means. It may represent a metadata check, a text pattern, a classification result, a user-defined predicate, or a semantic decision.<\/p>\n\n\n\n<p>The condition implementation is plugged in separately.<\/p>\n\n\n\n<p>That separation matters because the rule engine remains reusable.<\/p>\n\n\n\n<p>The user defines what conditions mean.<\/p>\n\n\n\n<p>The engine defines how results combine.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Truth Tables for Tri-State Logic<\/h2>\n\n\n\n<p>Tri-state logic needs explicit operator semantics.<\/p>\n\n\n\n<p>For <code>AND<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>any false     \u2192 false\nall true      \u2192 true\notherwise     \u2192 unknown<\/code><\/pre>\n\n\n\n<p>For <code>OR<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>any true      \u2192 true\nall false     \u2192 false\notherwise     \u2192 unknown<\/code><\/pre>\n\n\n\n<p>For <code>NOT<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>not true      \u2192 false\nnot false     \u2192 true\nnot unknown   \u2192 unknown<\/code><\/pre>\n\n\n\n<p>These rules preserve uncertainty without fabricating certainty.<\/p>\n\n\n\n<p>A minimal implementation looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export function evalAnd(children: EvalResult&#91;]): EvalResult {\n  if (children.includes('false')) {\n    return 'false';\n  }\n\n  if (children.every((x) => x === 'true')) {\n    return 'true';\n  }\n\n  return 'unknown';\n}\n\nexport function evalOr(children: EvalResult&#91;]): EvalResult {\n  if (children.includes('true')) {\n    return 'true';\n  }\n\n  if (children.every((x) => x === 'false')) {\n    return 'false';\n  }\n\n  return 'unknown';\n}\n\nexport function evalNot(child: EvalResult): EvalResult {\n  if (child === 'true') {\n    return 'false';\n  }\n\n  if (child === 'false') {\n    return 'true';\n  }\n\n  return 'unknown';\n}<\/code><\/pre>\n\n\n\n<p>This is small code, but it changes the behavior of the entire system.<\/p>\n\n\n\n<p>The engine no longer forces an artificial answer when the available context is insufficient.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Generic Evaluation Context<\/h2>\n\n\n\n<p>The engine should evaluate against a generic context object.<\/p>\n\n\n\n<p>The context represents currently available information. It may grow over multiple stages.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export type EvaluationContext = {\n  facts: Record&lt;string, unknown>;\n};<\/code><\/pre>\n\n\n\n<p>This allows user-defined conditions to read whatever facts they require.<\/p>\n\n\n\n<p>For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export type ConditionEvaluator = (\n  ctx: EvaluationContext\n) => EvalResult;<\/code><\/pre>\n\n\n\n<p>The engine receives a registry of condition evaluators:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export type ConditionRegistry = Record&lt;string, ConditionEvaluator>;<\/code><\/pre>\n\n\n\n<p>This keeps the core engine independent from business logic.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Generic Rule Tree Evaluation<\/h2>\n\n\n\n<p>The evaluator can now process any rule tree:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export function evaluateRule(\n  node: RuleNode,\n  ctx: EvaluationContext,\n  conditions: ConditionRegistry\n): EvalResult {\n  switch (node.type) {\n    case 'condition': {\n      const evaluator = conditions&#91;node.conditionId];\n\n      if (!evaluator) {\n        return 'unknown';\n      }\n\n      return evaluator(ctx);\n    }\n\n    case 'and': {\n      const results = node.children.map((child) =>\n        evaluateRule(child, ctx, conditions)\n      );\n\n      return evalAnd(results);\n    }\n\n    case 'or': {\n      const results = node.children.map((child) =>\n        evaluateRule(child, ctx, conditions)\n      );\n\n      return evalOr(results);\n    }\n\n    case 'not': {\n      const result = evaluateRule(node.child, ctx, conditions);\n\n      return evalNot(result);\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p>This implementation has a deliberate behavior: if a condition is not registered, the result becomes <code>unknown<\/code>, not <code>false<\/code>.<\/p>\n\n\n\n<p>That is safer.<\/p>\n\n\n\n<p>A missing evaluator means the engine cannot decide. It does not mean the condition failed.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Example: User-Defined Conditions Without Domain Coupling<\/h2>\n\n\n\n<p>The engine remains generic. Users define the actual condition behavior.<\/p>\n\n\n\n<p>A condition may check whether a fact exists:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const hasRequiredField: ConditionEvaluator = (ctx) => {\n  return typeof ctx.facts.requiredField === 'string'\n    ? 'true'\n    : 'unknown';\n};<\/code><\/pre>\n\n\n\n<p>Another condition may check a deterministic value:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const isPriorityHigh: ConditionEvaluator = (ctx) => {\n  if (ctx.facts.priority === undefined) {\n    return 'unknown';\n  }\n\n  return ctx.facts.priority === 'high'\n    ? 'true'\n    : 'false';\n};<\/code><\/pre>\n\n\n\n<p>Another condition may depend on a semantic enrichment result:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const semanticMatch: ConditionEvaluator = (ctx) => {\n  if (ctx.facts.semanticMatch === undefined) {\n    return 'unknown';\n  }\n\n  return ctx.facts.semanticMatch === true\n    ? 'true'\n    : 'false';\n};<\/code><\/pre>\n\n\n\n<p>The engine does not know what these facts mean. It only knows how to combine their results.<\/p>\n\n\n\n<p>That is the correct separation.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Stage-Based Evaluation<\/h2>\n\n\n\n<p>Tri-state logic becomes most valuable when paired with staged evaluation.<\/p>\n\n\n\n<p>A staged system starts with cheap context and only loads expensive context when needed.<\/p>\n\n\n\n<p>The pattern looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Stage A: evaluate cheap facts\nIf result is true or false \u2192 stop\nIf result is unknown \u2192 enrich context\n\nStage B: evaluate with additional deterministic data\nIf result is true or false \u2192 stop\nIf result is unknown \u2192 call expensive semantic stage\n\nStage C: evaluate with semantic facts\nReturn final result<\/code><\/pre>\n\n\n\n<p>The engine itself stays pure.<\/p>\n\n\n\n<p>The orchestrator handles enrichment.<\/p>\n\n\n\n<p>That boundary is critical.<\/p>\n\n\n\n<p>Evaluation should be deterministic and testable. Loading data, calling services, parsing documents, or invoking AI should happen outside the evaluator.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Generic Orchestrator<\/h2>\n\n\n\n<p>A generic staged orchestrator can look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export type ContextEnricher = (\n  ctx: EvaluationContext\n) => Promise&lt;EvaluationContext>;\n\nexport async function decideWithStages(\n  rule: RuleNode,\n  initialContext: EvaluationContext,\n  conditions: ConditionRegistry,\n  enrichers: ContextEnricher&#91;]\n): Promise&lt;EvalResult> {\n  let ctx = initialContext;\n\n  let result = evaluateRule(rule, ctx, conditions);\n\n  if (result !== 'unknown') {\n    return result;\n  }\n\n  for (const enrich of enrichers) {\n    ctx = await enrich(ctx);\n\n    result = evaluateRule(rule, ctx, conditions);\n\n    if (result !== 'unknown') {\n      return result;\n    }\n  }\n\n  return 'unknown';\n}<\/code><\/pre>\n\n\n\n<p>This keeps the design clean.<\/p>\n\n\n\n<p>The rule engine evaluates.<\/p>\n\n\n\n<p>The orchestrator enriches.<\/p>\n\n\n\n<p>The conditions interpret facts.<\/p>\n\n\n\n<p>No layer does too much.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why <code>unknown<\/code> Reduces Cost<\/h2>\n\n\n\n<p>The cost benefit comes from short-circuiting.<\/p>\n\n\n\n<p>If a rule definitively evaluates to <code>false<\/code> early, there is no reason to call an expensive semantic stage.<\/p>\n\n\n\n<p>If a rule definitively evaluates to <code>true<\/code> early, there may also be no reason to call an LLM.<\/p>\n\n\n\n<p>Only unresolved decisions move forward.<\/p>\n\n\n\n<p>That creates a cost-aware funnel:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cheap deterministic certainty first\nexpensive semantic evaluation last<\/code><\/pre>\n\n\n\n<p>This is especially useful in AI automation because many inputs can be classified or rejected using deterministic signals before any model call is needed.<\/p>\n\n\n\n<p>The engine does not need domain-specific assumptions to support that.<\/p>\n\n\n\n<p>It only needs to preserve uncertainty correctly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Defensive Semantic Parsing<\/h2>\n\n\n\n<p>If an expensive AI or external semantic stage is used, its output should be treated as untrusted input.<\/p>\n\n\n\n<p>The rule engine should not blindly trust free-form model text.<\/p>\n\n\n\n<p>A generic semantic parser can use a strict contract:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export type SemanticGateResult = {\n  match: boolean;\n  reason: string;\n};<\/code><\/pre>\n\n\n\n<p>A defensive parser may look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export function parseSemanticGateOutput(\n  text: string\n): SemanticGateResult | null {\n  const cleaned = stripMarkdownFence(text);\n  const candidate = extractFirstBalancedJson(cleaned);\n\n  if (!candidate) {\n    return null;\n  }\n\n  try {\n    const parsed = JSON.parse(candidate) as unknown;\n\n    if (\n      parsed &amp;&amp;\n      typeof parsed === 'object' &amp;&amp;\n      typeof (parsed as any).match === 'boolean' &amp;&amp;\n      typeof (parsed as any).reason === 'string'\n    ) {\n      return {\n        match: (parsed as any).match,\n        reason: (parsed as any).reason.trim()\n      };\n    }\n  } catch {\n    return null;\n  }\n\n  return null;\n}<\/code><\/pre>\n\n\n\n<p>A simple fence stripper:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export function stripMarkdownFence(text: string): string {\n  return text\n    .replace(\/^```(?:json)?\/i, '')\n    .replace(\/```$\/i, '')\n    .trim();\n}\n<\/code><\/pre>\n\n\n\n<p>A balanced JSON extractor can be implemented like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export function extractFirstBalancedJson(text: string): string | null {\n  const start = text.indexOf('{');\n\n  if (start === -1) {\n    return null;\n  }\n\n  let depth = 0;\n  let inString = false;\n  let escaped = false;\n\n  for (let i = start; i &lt; text.length; i++) {\n    const ch = text&#91;i];\n\n    if (escaped) {\n      escaped = false;\n      continue;\n    }\n\n    if (ch === '\\\\') {\n      escaped = true;\n      continue;\n    }\n\n    if (ch === '\"') {\n      inString = !inString;\n      continue;\n    }\n\n    if (inString) {\n      continue;\n    }\n\n    if (ch === '{') {\n      depth++;\n    }\n\n    if (ch === '}') {\n      depth--;\n\n      if (depth === 0) {\n        return text.slice(start, i + 1);\n      }\n    }\n  }\n\n  return null;\n}<\/code><\/pre>\n\n\n\n<p>If parsing fails, the orchestrator should not guess.<\/p>\n\n\n\n<p>It should return a conservative result, log the failure, or keep the decision unresolved depending on the safety requirements.<\/p>\n\n\n\n<p>For many systems, invalid semantic output should not trigger side effects.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Safe Finalization Policy<\/h2>\n\n\n\n<p>A tri-state engine must define what happens if the final result remains <code>unknown<\/code>.<\/p>\n\n\n\n<p>There are three common policies.<\/p>\n\n\n\n<p>The first is block:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>unknown \u2192 do not execute action<\/code><\/pre>\n\n\n\n<p>This is safest for destructive or high-risk workflows.<\/p>\n\n\n\n<p>The second is route to human review:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>unknown \u2192 manual decision required<\/code><\/pre>\n\n\n\n<p>This is useful when missed opportunities matter.<\/p>\n\n\n\n<p>The third is fallback:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>unknown \u2192 apply configured default<\/code><\/pre>\n\n\n\n<p>This should be used carefully and explicitly.<\/p>\n\n\n\n<p>The worst design is implicit coercion:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const shouldRun = result === 'true';<\/code><\/pre>\n\n\n\n<p>That silently treats <code>unknown<\/code> as <code>false<\/code>.<\/p>\n\n\n\n<p>Sometimes that is acceptable as a final safety policy, but it should be named explicitly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export function toActionDecision(result: EvalResult): boolean {\n  return result === 'true';\n}<\/code><\/pre>\n\n\n\n<p>Do not hide uncertainty accidentally.<\/p>\n\n\n\n<p>Make the conversion visible.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Observability: Make Unknown Measurable<\/h2>\n\n\n\n<p>Once <code>unknown<\/code> exists, it becomes a powerful optimization signal.<\/p>\n\n\n\n<p>You can measure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>unknown rate per rule\nunknown rate per condition\nstage where unknown was resolved\nunknown \u2192 true ratio\nunknown \u2192 false ratio\ncost per resolved decision\nlatency per stage\nsemantic parser failure rate<\/code><\/pre>\n\n\n\n<p>This gives the system a feedback loop.<\/p>\n\n\n\n<p>If a rule produces many unknowns at Stage A but almost all resolve at Stage B, deterministic enrichment is working.<\/p>\n\n\n\n<p>If many rules reach Stage C, the earlier stages may need better conditions or better available facts.<\/p>\n\n\n\n<p>If semantic parsing fails often, the output contract or prompt needs improvement.<\/p>\n\n\n\n<p>Without <code>unknown<\/code>, all of this gets hidden inside misleading <code>false<\/code> results.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Strategy<\/h2>\n\n\n\n<p>Tri-state engines are easy to test if the evaluator is pure.<\/p>\n\n\n\n<p>Leaf condition tests should cover:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>true\nfalse\nunknown<\/code><\/pre>\n\n\n\n<p>Group operator tests should cover mixed combinations:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AND(true, true)       \u2192 true\nAND(true, unknown)    \u2192 unknown\nAND(false, unknown)   \u2192 false\n\nOR(false, false)      \u2192 false\nOR(false, unknown)    \u2192 unknown\nOR(true, unknown)     \u2192 true\n\nNOT(true)             \u2192 false\nNOT(false)            \u2192 true\nNOT(unknown)          \u2192 unknown<\/code><\/pre>\n\n\n\n<p>Orchestrator tests should verify stage behavior:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Stage A resolves true \u2192 no enrichers called\nStage A resolves false \u2192 no enrichers called\nStage A unknown \u2192 Stage B called\nStage B resolves \u2192 Stage C not called\nAll stages unknown \u2192 final unknown policy applies\nMalformed semantic output \u2192 no unsafe action<\/code><\/pre>\n\n\n\n<p>The most important safety test is this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>No side effect runs while result is unknown.<\/code><\/pre>\n\n\n\n<p>That is the behavioral contract.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Common Implementation Mistakes<\/h2>\n\n\n\n<p>The first common mistake is collapsing <code>unknown<\/code> into <code>false<\/code> too early. This destroys the diagnostic value of the third state and reintroduces premature decisions.<\/p>\n\n\n\n<p>The second mistake is allowing actions to run before the rule reaches a terminal decision. <code>unknown<\/code> must not trigger side effects.<\/p>\n\n\n\n<p>The third mistake is mixing IO inside condition evaluators. If a condition calls a network service or model directly, the evaluator becomes slow, expensive, and hard to test.<\/p>\n\n\n\n<p>The fourth mistake is treating AI output as trusted structured data. It is not. It must be parsed, validated, and handled defensively.<\/p>\n\n\n\n<p>The fifth mistake is making the engine domain-specific. A good rule engine should not contain business concepts. Those belong in user-defined conditions and context enrichers.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Final Takeaway<\/h2>\n\n\n\n<p>The most valuable state in a modern rule engine is often not <code>true<\/code> or <code>false<\/code>.<\/p>\n\n\n\n<p>It is <code>unknown<\/code>.<\/p>\n\n\n\n<p>That one state allows the system to acknowledge uncertainty honestly, defer expensive work intelligently, avoid premature side effects, and expose ambiguity as a measurable signal.<\/p>\n\n\n\n<p>In AI-assisted automation, robustness does not come from pretending the system knows everything at the first evaluation step.<\/p>\n\n\n\n<p>It comes from knowing when the system knows enough.<\/p>\n\n\n\n<p>A binary rule engine can decide.<\/p>\n\n\n\n<p>A tri-state rule engine can decide responsibly.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Designing Tri-State Logic for Safer and Cheaper AI Automation Most rule engines start with a simple assumption: every condition evaluates to either true or false. That sounds clean. A rule matches, or it does not. An action runs, or it does not. The system remains deterministic, easy to reason about, and simple to implement. Then [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[38,37,41,39,40,27,35,8,36],"tags":[],"class_list":["post-219","post","type-post","status-publish","format-standard","hentry","category-ai-automation","category-ai-engineering","category-cost-aware-ai-systems","category-decision-engines","category-llm-orchestration","category-programming-principles","category-rule-engines","category-software-architecture","category-workflow-systems"],"_links":{"self":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/219","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=219"}],"version-history":[{"count":1,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/219\/revisions"}],"predecessor-version":[{"id":220,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/219\/revisions\/220"}],"wp:attachment":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=219"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=219"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=219"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}