MD.OFFICE
FAL

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

ConstraintRationale
One traversal on saveThe existing analyzeDuplicateLabelFormulaAndButtonSignals() does a single iterative stack walk. The new check piggybacks on this — no second walk.
No component-level logicAll save-time validation goes through BuilderValidationOrchestratorService. No reaching into RightPanelStaticComponent or FormulaBuilderComponent.
StatelessRecomputed fresh on every save. No dirty-tracking or session-level state needed — save is a user-initiated action, not a hot path.
Separation of concernsThe 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:

  1. Unconfigured formulasallFormulaElements.filter(isUnconfiguredFormulaField) (existing, refactored from inline to post-traversal)
  2. Stale cross-formula refsbuildStaleFormulaRefIssuesSummary(allFormulaElements, pathNodeMap) (NEW)
  3. Duplicate labels — handled separately via duplicateLabelGroupsByScope (existing, unchanged)

4.3 Core Detection Algorithm — buildStaleFormulaRefIssuesSummary

Source: L454–L534

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:

  1. Collect all outputType values from library entries into a Set
  2. If the set has more than one entry → formula is mixed-type
  3. Store jsonKey → displayLabel in mixedTypeKeyToLabel map
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

  1. Pre-compile a single combined regex with word-boundary look-arounds:
    const mixedTypeKeyPattern = new RegExp(
      `(?<![\\w-])(${escapedKeys.join('|')})(?![\\w-])`
    );
    
  2. For each formula element, collect all expression strings via collectFormulaExpressions() (handles both legacy formula and formulaLibrary[*].formula)
  3. Test each expression against the combined regex
  4. If match found → push a stale-ref issue with both formula names

4.4 Helper Functions

FunctionPurpose
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 enrichIssuesForDisplaystaleFormulaRef 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: staleFormulaRef uses the same orange styling as formula (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."

In center-panel.component.ts:

  • validationErrors type was expanded with 'staleFormulaRef' in its errorType union to stay in sync with the service.

5. Complexity Analysis

PhaseComplexityNotes
Tree traversalO(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 compilationO(m)m = mixed-type key count. One-time cost.
Total added costO(f² · b) worst casef is typically very small (< 10 formula fields). Negligible.

Fast-exit guards ensure zero added cost in the common case:

  • formulaElements.length < 2 → return immediately
  • mixedTypeKeyToLabel.size === 0 → return immediately (no mixed-type providers)

6. Files Modified

FileWhat Changed
builder-validation-orchestrator.service.tsType expansions, allFormulaElements collector in traversal, buildStaleFormulaRefIssuesSummary(), collectFormulaExpressions(), escapeRegex(), wiring into validateTemplateForBuilder()
center-panel.component.tserrorType union expanded with 'staleFormulaRef'
center-panel.component.htmlValidation modal: badge color, icon, and label rendering for staleFormulaRef

7. Verification

Manual Test Steps

  1. Create Formula A with "Use Rules" enabled. Add two library entries — one returning decimal (e.g., 10 + 5), one returning date (e.g., @SomeDate + 1). Formula A is now mixed-type.
  2. Create Formula B referencing @FormulaA + 10. Validate — builder correctly blocks this at design-time.
  3. Now make Formula A uniform (all branches same type). Create Formula B again with @FormulaA + 10. Validate — succeeds. Save.
  4. Go back and edit Formula A to be mixed-type again.
  5. Save TemplateExpected: Save blocked. Validation modal shows "Invalid Formula Reference" pointing to Formula B.
  6. Fix Formula A (make all branches same type). Save TemplateExpected: 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 total doesn't match totalTax)
  • Both legacy (formula) and rules (formulaLibrary[*].formula) expression strings are scanned
  • Error message names both the referencing and referenced formula for actionable UX