Debugging Business Rules

Debugging Business Rules

This guide covers the business rules evaluation lifecycle, tracing API, config validation, the DevTools panel, and solutions to common issues.


Rule Tracing API

The rule tracing system records every rule evaluation event, making it possible to see exactly what happened and why.

Enabling Tracing

import {
  enableRuleTracing,
  disableRuleTracing,
  getRuleTraceLog,
  clearRuleTraceLog,
} from "@formosaic/core/devtools";

enableRuleTracing();

// ... user interacts with form ...

const log = getRuleTraceLog();
log.forEach(event => {
  console.log(
    `[${event.type}] ${event.triggerField}=${JSON.stringify(event.triggerValue)} -> ${event.affectedField}`
  );
});

disableRuleTracing();

Real-Time Callback

enableRuleTracing((event) => {
  console.log(`[${event.type}] ${event.triggerField} -> ${event.affectedField}`, event.newState);
});

IRuleTraceEvent Shape

PropertyTypeDescription
timestampnumberDate.now() when the rule fired
type"revert" | "apply" | "combo" | "dropdown" | "order" | "init"Which phase produced this event
triggerFieldstringThe field whose value change triggered evaluation
triggerValueunknownThe value that triggered the rule
affectedFieldstringThe field whose state was changed
previousStatePartial<IRuntimeFieldState>State before rule applied
newStatePartial<IRuntimeFieldState>State after rule applied

FormDevTools

The built-in DevTools panel provides a visual inspector with 7 tabs:

TabContent
RulesPer-field runtime state: type, required, hidden, readOnly, active rules
ValuesJSON dump of all current form values
ErrorsJSON dump of current form errors
GraphText-based dependency graph
PerfPer-field render counts, hot field detection
DepsSortable dependency table with effect types, cycle detection
TimelineChronological event log with filtering
import { FormDevTools } from "@formosaic/core/devtools";

<FormDevTools
  configName="myForm"
  formState={runtimeFormState}
  formValues={getValues()}
  formErrors={formState.errors}
  dirtyFields={formState.dirtyFields}
  enabled={process.env.NODE_ENV === "development"}
/>

Config Validation

import { validateFieldConfigs } from "@formosaic/core";

const errors = validateFieldConfigs(fieldConfigs, registeredComponents);
if (errors.length > 0) {
  errors.forEach(err => console.error(`[${err.type}] ${err.fieldName}: ${err.message}`));
}

Checks for: missing dependency targets, self-dependencies, unregistered components, unregistered validators, missing dropdown options, and circular dependencies.


Common Issues and Solutions

"Rule not applying"

The most common cause is a value mismatch. Dependency keys are matched using string comparison: String(fieldValue) === ruleKey.

  • Boolean values: true must match "true", not "True"
  • Numbers: 42 must match "42"
  • null/undefined: Match against the empty string ""

"Circular dependency" warning

const errors = validateFieldConfigs(fieldConfigs);
const cycles = errors.filter(e => e.type === "circular_dependency");

Refactor the config so dependencies form a DAG (directed acyclic graph).

"Field reverted unexpectedly"

When a field's value changes, ALL dependents from the previous value are reverted to their default config state before new rules are applied. Use the trace log to see the full revert-then-apply sequence.


Console Warnings

Beyond validateFieldConfigs(), the runtime emits console.warn for misconfigurations that would otherwise fail silently. Two were added in v1.5.0:

Unregistered validator in a validate rule

A typo'd validator name used to silently disable the rule. In development (NODE_ENV !== "production"), referencing a validator that is not in the registry now warns once per unknown name:

[formosaic] Validation rule references unregistered validator "requierd";
the rule is skipped. Register it via registerValidators({ requierd: ... })
or fix the rule's "name".

Fix: correct the name, or register the custom validator with registerValidators() before the form mounts.

Port-field suffix collision in form connections

Connection port fields are matched by their local suffix (the field name with the fragment prefix stripped). If two port fields in the same fragment reduce to the same suffix — e.g. shipping.address.street and a non-prefixed address.street — the later one wins and the earlier one used to silently drop out of the connection. This now warns at compose time:

[Formosaic] Port fields "shipping.address.street" and "address.street" in
fragment "shipping" both reduce to suffix "address.street"; "address.street"
wins and "shipping.address.street" is ignored.

Fix: rename one of the fields or adjust the port list so each port field has a unique suffix within its fragment.

Other warnings to watch for

WarningCause
Lookup table "X" referenced in template but not foundA template expression uses $lookup.X but the table is not in config.lookups or the global lookup registry
Connection "X" has mismatched port fieldsSource and target ports do not pair up; unmatched fields are skipped
composeForm(): connection rules were defined but there is no owner field to attach them toThe composed form has no standalone or fragment field to carry the connection rules
[Formosaic] <dependency-graph message>Circular or self dependencies detected at init — see Config Validation

Template Provenance in DevTools

When using form templates, the Deps tab in FormDevTools includes a Source column that shows the template and fragment each field originated from. This makes it straightforward to trace expanded fields back to their template definition.

For example, a field named shipping.street will show:

FieldSourceEffect
shipping.streetaddress via shipping--
shipping.stateaddress via shippingdropdown (rule from address template)
billing.streetaddress via billingcopyValues (from shipping.street)

This provenance tracking helps you understand:

  • Which template defined a field's original configuration
  • Which fragment prefix was applied during expansion
  • Whether a rule came from the template itself or from a composeForm() connection

Template resolution errors (unregistered templates, circular references, max depth exceeded) are surfaced in the Timeline tab as TemplateResolutionError events.


Debugging Checklist

  1. Enable tracing -- enableRuleTracing() to capture all rule events
  2. Open DevTools -- Add <FormDevTools> to visualize rules, values, errors, and the dependency graph
  3. Validate config -- Run validateFieldConfigs() to catch structural issues
  4. Check value types -- Dependency keys use string comparison
  5. Check the trace log -- Look for "revert" events that may be clearing state
  6. Check combo conditions -- All combo rule conditions must match simultaneously
  7. Check dropdown keys -- Option values must match actual IOption.value values