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
| Property | Type | Description |
|---|---|---|
timestamp | number | Date.now() when the rule fired |
type | "revert" | "apply" | "combo" | "dropdown" | "order" | "init" | Which phase produced this event |
triggerField | string | The field whose value change triggered evaluation |
triggerValue | unknown | The value that triggered the rule |
affectedField | string | The field whose state was changed |
previousState | Partial<IRuntimeFieldState> | State before rule applied |
newState | Partial<IRuntimeFieldState> | State after rule applied |
FormDevTools
The built-in DevTools panel provides a visual inspector with 7 tabs:
| Tab | Content |
|---|---|
| Rules | Per-field runtime state: type, required, hidden, readOnly, active rules |
| Values | JSON dump of all current form values |
| Errors | JSON dump of current form errors |
| Graph | Text-based dependency graph |
| Perf | Per-field render counts, hot field detection |
| Deps | Sortable dependency table with effect types, cycle detection |
| Timeline | Chronological 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:
truemust match"true", not"True" - Numbers:
42must 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
| Warning | Cause |
|---|---|
Lookup table "X" referenced in template but not found | A template expression uses $lookup.X but the table is not in config.lookups or the global lookup registry |
Connection "X" has mismatched port fields | Source 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 to | The 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:
| Field | Source | Effect |
|---|---|---|
shipping.street | address via shipping | -- |
shipping.state | address via shipping | dropdown (rule from address template) |
billing.street | address via billing | copyValues (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
- Enable tracing --
enableRuleTracing()to capture all rule events - Open DevTools -- Add
<FormDevTools>to visualize rules, values, errors, and the dependency graph - Validate config -- Run
validateFieldConfigs()to catch structural issues - Check value types -- Dependency keys use string comparison
- Check the trace log -- Look for
"revert"events that may be clearing state - Check combo conditions -- All combo rule conditions must match simultaneously
- Check dropdown keys -- Option values must match actual
IOption.valuevalues