Save-Time Stale Cross-Formula Mixed-Type Reference Validation
Project: ifile-teapot-web-dynaforms
Date: 2026-03-21
Scope:builder-validation-orchestrator.service.ts,center-panel.component.ts/.html
1. Overview & Motivation
Formula fields can reference other formula fields. At design-time (in the Formula Builder), the system validates that a referenced formula has consistent output types across all its rule branches. If Formula A's branches produce different output types (e.g., one returns decimal, another returns date), we call it "mixed-type" and block the reference.
However, there was a gap: once Formula B had been validated and saved with a valid reference to Formula A, a user could later edit Formula A's library to introduce mixed-type branches. Formula B's saved JSON was never re-checked — leaving a stale, type-inconsistent reference that the renderer couldn't resolve at runtime.
The Problem in Steps
The Solution
Block the template save if any formula field's expression references another formula that has become mixed-type. This validation runs inside the existing single-pass tree traversal on save — no extra DOM walk needed.
2. Architecture & Key Constraints
| Constraint | Rationale |
|---|---|
| One traversal on save | The existing analyzeDuplicateLabelFormulaAndButtonSignals() does a single iterative stack walk. The new check piggybacks on this — no second walk. |
| No component-level logic | All save-time validation goes through BuilderValidationOrchestratorService. No reaching into RightPanelStaticComponent or FormulaBuilderComponent. |
| Stateless | Recomputed fresh on every save. No dirty-tracking or session-level state needed — save is a user-initiated action, not a hot path. |
| Separation of concerns | The traversal only collects data. Semantic analysis (mixed-type detection, reference scanning) happens post-traversal via dedicated functions. |
3. Data Flow
4. Implementation Details
4.1 Type System Changes
BuilderValidationIssue.errorType — expanded union:
errorType:
| 'duplicate'
| 'formula'
| 'schema'
| 'typeOfData'
| 'staleFormulaRef'; // ← NEW
NonSchemaValidationSummary — new field:
staleFormulaRefIssues: Pick<BuilderValidationIssue, 'label' | 'type' | 'path'>[];
4.2 Traversal Integration
In analyzeDuplicateLabelFormulaAndButtonSignals, during the existing stack walk, one new line was added:
if (current.type === 'formula') {
allFormulaElements.push(current); // Passive collection — no logic
}
After the traversal completes, three post-traversal analyses run on the flat allFormulaElements array:
- Unconfigured formulas —
allFormulaElements.filter(isUnconfiguredFormulaField)(existing, refactored from inline to post-traversal) - Stale cross-formula refs —
buildStaleFormulaRefIssuesSummary(allFormulaElements, pathNodeMap)(NEW) - Duplicate labels — handled separately via
duplicateLabelGroupsByScope(existing, unchanged)
4.3 Core Detection Algorithm — buildStaleFormulaRefIssuesSummary
Two compact loops over formula elements only (not the full tree):
Loop 1 — Identify Mixed-Type Providers
For each formula element with useRules enabled and formulaLibrary.length >= 2:
- Collect all
outputTypevalues from library entries into aSet - If the set has more than one entry → formula is mixed-type
- Store
jsonKey → displayLabelinmixedTypeKeyToLabelmap
const uniqueOutputTypes = new Set<string>(
el.formulaLibrary.map((entry: any) => entry.outputType || 'decimal')
);
if (uniqueOutputTypes.size > 1 && el.jsonKey) {
mixedTypeKeyToLabel.set(el.jsonKey, el.defaultFieldName || el.jsonKey);
}
Fast-exit: If mixedTypeKeyToLabel.size === 0, return empty — no stale refs are possible (most common case).
Loop 2 — Scan Referencing Expressions
- Pre-compile a single combined regex with word-boundary look-arounds:
const mixedTypeKeyPattern = new RegExp( `(?<![\\w-])(${escapedKeys.join('|')})(?![\\w-])` ); - For each formula element, collect all expression strings via
collectFormulaExpressions()(handles both legacyformulaandformulaLibrary[*].formula) - Test each expression against the combined regex
- If match found → push a stale-ref issue with both formula names
4.4 Helper Functions
| Function | Purpose |
|---|---|
| collectFormulaExpressions(el) | Returns all formula expression strings from an element (legacy formula + all formulaLibrary[*].formula). Handles both modes. |
| escapeRegex(text) | Escapes regex metacharacters in jsonKeys so they can be safely embedded in a RegExp constructor. |
4.5 Error Wiring
The stale-ref issues flow through the existing pipeline:
buildStaleFormulaRefIssuesSummary()
→ NonSchemaValidationSummary.staleFormulaRefIssues
→ mapped with errorType: 'staleFormulaRef'
→ merged into nonSchemaIssues[]
→ resolveDecision() sees nonSchemaIssues.length > 0
→ shouldBlock: true
No special case is needed in enrichIssuesForDisplay — staleFormulaRef falls through the existing non-schema display path (errorType !== 'schema' && errorType !== 'typeOfData').
4.6 Validation Modal Display
In center-panel.component.html:
- Icon/badge color:
staleFormulaRefuses the same orange styling asformula(unconfigured) errors - Badge text: Displays as "Invalid Formula Reference" (distinct from "Unconfigured Formula")
- Display title: Shows the full descriptive message, e.g.:
"FormulaB references FormulaA which has inconsistent output types across its rules. Fix FormulaA's formula library first."
validationErrorstype was expanded with'staleFormulaRef'in itserrorTypeunion to stay in sync with the service.
5. Complexity Analysis
| Phase | Complexity | Notes |
|---|---|---|
| Tree traversal | O(n) | Single pass, n = all canvas elements. Unchanged. |
| Loop 1 (mixed-type detection) | O(f · b) | f = formula elements, b = avg library entries per formula |
| Loop 2 (reference scanning) | O(f · e) | f = formula elements, e = avg expression strings per formula |
| Regex compilation | O(m) | m = mixed-type key count. One-time cost. |
| Total added cost | O(f² · b) worst case | f is typically very small (< 10 formula fields). Negligible. |
Fast-exit guards ensure zero added cost in the common case:
formulaElements.length < 2→ return immediatelymixedTypeKeyToLabel.size === 0→ return immediately (no mixed-type providers)
6. Files Modified
| File | What Changed |
|---|---|
| builder-validation-orchestrator.service.ts | Type expansions, allFormulaElements collector in traversal, buildStaleFormulaRefIssuesSummary(), collectFormulaExpressions(), escapeRegex(), wiring into validateTemplateForBuilder() |
| center-panel.component.ts | errorType union expanded with 'staleFormulaRef' |
| center-panel.component.html | Validation modal: badge color, icon, and label rendering for staleFormulaRef |
7. Verification
Manual Test Steps
- Create Formula A with "Use Rules" enabled. Add two library entries — one returning
decimal(e.g.,10 + 5), one returningdate(e.g.,@SomeDate + 1). Formula A is now mixed-type. - Create Formula B referencing
@FormulaA + 10. Validate — builder correctly blocks this at design-time. - Now make Formula A uniform (all branches same type). Create Formula B again with
@FormulaA + 10. Validate — succeeds. Save. - Go back and edit Formula A to be mixed-type again.
- Save Template → Expected: Save blocked. Validation modal shows "Invalid Formula Reference" pointing to Formula B.
- Fix Formula A (make all branches same type). Save Template → Expected: Save proceeds normally.
Verified Behaviors
- Fast-exit works: forms with 0 or 1 formula fields incur no overhead
- Word-boundary regex prevents false positives (e.g., key
totaldoesn't matchtotalTax) - Both legacy (
formula) and rules (formulaLibrary[*].formula) expression strings are scanned - Error message names both the referencing and referenced formula for actionable UX