Skip to content

State Calculation Engine Reference

Overview

The calculation engine processes state machines in working versions, resolving references, executing calculations, and updating values. This document defines the engine's behavior, expression language, and error handling.

INFO

Work In Progress I have only started thinking about this and have changed the model a few times since putting this initial draft together. There is not much point in spending a lot of time on this unless the overall vision of this state machine makes sense.

Key Principles:

  • Calculations run only in working versions (submitted versions are immutable)
  • Override status (O) prevents value updates from calculations
  • Missing inputs do not null out user-provided values
  • Errors are non-blocking (reported but don't stop processing)
  • References are resolved at calculation time (not stored)

Part 1: Calculation Trigger & Flow

1.1 When Calculations Run

TriggerAction
User modifies input valueRecalculate all dependent states and financial clauses
User modifies state value (when status = O)No recalculation (override respected)
User changes state status from O → otherRecalculate that state
Working version created from submittedFull calculation on all elements
User adds new state/input/financial clauseRecalculate affected elements

1.2 Calculation Flow

┌──────────────────────────────────────────────────────────────┐
│              USER MODIFIES INPUT VALUE                        │
│              (e.g., TICKET_DETAIL_INPUT.quantity = 950)      │
└──────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│ STEP 1: UPDATE INPUT VALUE                                    │
│                                                               │
│ - Set input.value.quantity = 950                             │
│ - Auto-save to working version                               │
│ - If value was null and now has value:                       │
│   - If status != O, set status = C                           │
└──────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│ STEP 2: BUILD DEPENDENCY GRAPH                                │
│                                                               │
│ Scan all states and financial clauses in version:            │
│                                                               │
│ - Which states reference TICKET_DETAIL_INPUT?                │
│   → SHOW_SETTLED (references TICKET_DETAIL_INPUT.total_amount)│
│                                                               │
│ - Which states reference SHOW_SETTLED?                       │
│   → None in this block                                       │
│                                                               │
│ - Which financial clauses reference SHOW_SETTLED?            │
│   → clause_guarantee_vs_nbor                                 │
│                                                               │
│ Dependency Graph:                                            │
│   TICKET_DETAIL_INPUT                                        │
│        ↓                                                     │
│   SHOW_SETTLED                                               │
│        ↓                                                     │
│   clause_guarantee_vs_nbor                                   │
│        ↓                                                     │
│   clause_guarantee_vs_nbor.payment_terms                     │
└──────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│ STEP 3: TOPOLOGICAL SORT                                      │
│                                                               │
│ Order: [SHOW_SETTLED, clause_guarantee_vs_nbor]              │
│                                                               │
│ If cycle detected:                                           │
│   - Report error: "Circular dependency: A → B → A"           │
│   - Skip cyclic nodes                                        │
│   - Continue with non-cyclic nodes                           │
└──────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│ STEP 4: PROCESS EACH NODE IN ORDER                            │
│                                                               │
│ For SHOW_SETTLED:                                            │
│   - Check status: F (Forecast)                               │
│   - Status != O, so proceed with calculation                 │
│   - Resolve references in calculation                        │
│   - Execute calculation                                      │
│   - If success: Update value                                 │
│   - If error: Keep old value, store error                    │
│                                                               │
│ For clause_guarantee_vs_nbor:                                │
│   - Evaluate trigger condition                               │
│   - If trigger = true:                                       │
│     - Execute calculation                                    │
│     - Update amount                                          │
│   - If trigger = false:                                      │
│     - Set amount = 0                                         │
│                                                               │
│   - Process payment_terms:                                   │
│     - Calculate each term's amount based on clause total     │
│     - Evaluate due_date triggers                             │
│     - Update term status (e.g. pending -> ready)             │
└──────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│ STEP 5: AUTO-SAVE RESULTS                                     │
│                                                               │
│ - All updated values saved to working version                │
│ - Calculation errors stored on affected elements             │
│ - UI notified of changes                                     │
└──────────────────────────────────────────────────────────────┘

Part 2: Override Behavior (Status = O)

2.1 Override Rules

When a state has status = O:

  1. Calculation still runs (for transparency/comparison)
  2. Value is NOT updated (user override preserved)
  3. Calculated value stored separately (for "what would it be?" display)
javascript
// State with Override
{
  state_key: "SHOW_SETTLED",
  status: "O",                         // Override
  value: {                             // User-provided value (preserved)
    net_box_office_receipts: 20000
  },
  calculated_value: {                  // What calculation would produce
    net_box_office_receipts: 18393.75
  },
  override_active: true,
  override_difference: 1606.25         // User value - calculated value
}

2.2 Reverting Override

When user changes status from O → C (or D, F):

javascript
// Before: Override active
{
  status: "O",
  value: { net_box_office_receipts: 20000 },
  calculated_value: { net_box_office_receipts: 18393.75 }
}

// User changes status to C

// After: Calculated value becomes actual value
{
  status: "C",
  value: { net_box_office_receipts: 18393.75 },
  calculated_value: null,
  override_active: false
}

Part 3: Expression Language

3.1 Supported Operations

Arithmetic:

OperatorDescriptionExample
+Additiona + b
-Subtractiona - b
*Multiplicationa * b
/Divisiona / b
%Moduloa % b

Comparison:

OperatorDescriptionExample
==Equalsa == b
!=Not equalsa != b
>Greater thana > b
>=Greater or equala >= b
<Less thana < b
<=Less or equala <= b

Logical:

OperatorDescriptionExample
&&ANDa && b
||ORa || b
!NOT!a

Functions:

FunctionDescriptionExample
MAX(a, b, ...)Maximum valueMAX(guarantee, percentage)
MIN(a, b, ...)Minimum valueMIN(a, b)
SUM(array)Sum of arraySUM(amounts)
AVG(array)Average of arrayAVG(amounts)
ABS(a)Absolute valueABS(difference)
ROUND(a, decimals)Round to decimalsROUND(amount, 2)
EXISTS(ref)Check if reference exists and has valueEXISTS(MERCH_STATE)
COALESCE(a, b)Return first non-nullCOALESCE(input, 0)
IF(cond, then, else)ConditionalIF(sold > 500, bonus, 0)

Date Functions:

FunctionDescriptionExample
today()Current datedate < today()
DAYS_BETWEEN(a, b)Days between datesDAYS_BETWEEN(start, end)
DATE_ADD(date, days)Add days to dateDATE_ADD(start, 30)

3.2 Reference Syntax

Local References (within same clause_block):

javascript
// Reference to input value
TICKET_DETAIL_INPUT.value.quantity

// Reference to input status
TICKET_DETAIL_INPUT.status

// Reference to state value
SHOW_SETTLED.value.net_box_office_receipts

// Reference to state status
SHOW_SETTLED.status

// Reference to static variable
v_guarantee

// Reference to financial clause amount
financial_clauses.clause_guarantee_vs_nbor.amount

Cross-Block References (within same version):

javascript
// Reference another clause_block's state
CB_SHOW_1.states.SHOW_SETTLED.value.net_box_office_receipts

// Reference another clause_block's input
CB_MERCH.inputs.MERCH_DATA_INPUT.value.gross_sales

// Reference another clause_block's financial clause amount
CB_SHOW_2.financial_clauses.clause_main.amount

// Reference another clause_block's static variable
CB_SHOW_1.static_variables.v_guarantee

Aggregation References:

javascript
// Sum all financial clause amounts where trigger is true
SUM(cb.financial_clauses[0].amount for cb in [CB_SHOW_1, CB_SHOW_2, CB_SHOW_3] where cb.financial_clauses[0].trigger == true)

// Alternative: Sum by clause_block type
SUM(cb.financial_clauses.clause_guarantee.amount for cb in clause_blocks where cb.type == 'show_performance')

Cross-Deal References (must specify version):

javascript
// Reference specific version of another deal
DEAL:deal_show_001:VERSION:v_abc123.clause_blocks.CB_GUARANTEE.financial_clauses.clause_main.amount

3.3 Expression Examples

Flat Guarantee:

javascript
// Trigger
SHOW_STATE.value == true

// Calculation
v_guarantee

Guarantee vs NBOR:

javascript
// Trigger
SHOW_SETTLED.status == 'C' || SHOW_SETTLED.status == 'F'

// Calculation
MAX(
  (SHOW_SETTLED.value.net_box_office_receipts * v_artist_percentage / 100),
  v_guarantee
)

Split Point:

javascript
// Split point calculation
v_guarantee + EXPENSE_INPUT.value.total + (EXPENSE_INPUT.value.total * v_promoter_profit_percentage / 100)

// Artist payment
v_guarantee + MAX(0, (SHOW_SETTLED.value.net_box_office - split_point) * v_artist_overage_percentage / 100)

Tour Summary:

javascript
// Total across shows
SUM(cb.financial_clauses[0].amount for cb in [CB_SHOW_1, CB_SHOW_2, CB_SHOW_3] where cb.financial_clauses[0].trigger == true)

Conditional Bonus:

javascript
// Bonus if tickets sold > 500
IF(TICKET_DETAIL_INPUT.value.quantity > 500, v_bonus_amount, 0)

Part 4: Error Handling

4.1 Error Types

Error TypeCauseBehavior
undefined_referenceReference to non-existent elementKeep old value, store error
type_mismatchArithmetic on non-numericKeep old value, store error
division_by_zeroDivision by zeroKeep old value, store error
circular_dependencyA → B → ASkip node, store error
syntax_errorInvalid expression syntaxKeep old value, store error
null_referenceReference resolves to nullKeep old value if exists, else null

4.2 Error Storage

javascript
{
  state_key: "SHOW_SETTLED",
  value: { net_box_office_receipts: 18000 },  // Previous value kept
  status: "F",
  calculation_error: {
    type: "type_mismatch",
    message: "Cannot perform arithmetic: TICKET_DETAIL_INPUT.value.quantity is string '$900'",
    field: "TICKET_DETAIL_INPUT.value.quantity",
    expected_type: "number",
    actual_value: "$900",
    timestamp: "2025-12-04T15:30:00Z"
  }
}

4.3 Error Display

UI should:

  • Show error indicator on affected state/clause
  • Display error message on hover/click
  • Allow user to fix input and retry
  • NOT block saving working version
  • NOT block other calculations

4.4 Null Handling

When input has no value:

javascript
// Input with null value
{
  input_key: "TICKET_DETAIL_INPUT",
  value: null,           // No value provided yet
  status: "P"            // Pending
}

// State that references this input
{
  state_key: "SHOW_SETTLED",
  value: { net_box_office_receipts: 15000 },  // User-provided estimate
  status: "F",
  calculation: "TICKET_DETAIL_INPUT.value.total_amount - ..."
}

// Calculation behavior:
// 1. Resolve TICKET_DETAIL_INPUT.value.total_amount → null
// 2. Calculation fails (cannot subtract from null)
// 3. SHOW_SETTLED.value is PRESERVED (not set to null)
// 4. Error stored: "Reference TICKET_DETAIL_INPUT.value.total_amount is null"

Key Rule: User-provided values are NEVER overwritten with null due to missing inputs.


Part 5: Dependency Graph

5.1 Building the Graph

javascript
function buildDependencyGraph(version) {
  const graph = new DirectedGraph();
  
  for (const clauseBlock of version.clause_blocks) {
    // Add states as nodes
    for (const state of clauseBlock.states) {
      graph.addNode(state.state_key, { type: 'state', block: clauseBlock.clause_block_ref });
      
      // Parse calculation for references
      const refs = parseReferences(state.calculation);
      for (const ref of refs) {
        graph.addEdge(ref, state.state_key);  // ref → state (ref must calc before state)
      }
    }
    
    // Add financial clauses as nodes
    for (const clause of clauseBlock.financial_clauses) {
      graph.addNode(clause.clause_id, { type: 'financial_clause', block: clauseBlock.clause_block_ref });

      // Parse trigger and calculation for references
      const triggerRefs = parseReferences(clause.trigger);
      const calcRefs = parseReferences(clause.calculation);

      for (const ref of [...triggerRefs, ...calcRefs]) {
        graph.addEdge(ref, clause.clause_id);
      }
    }
    
    // Add payment terms as dependents of the clause
    for (const term of clause.payment_terms || []) {
        // Payment terms depend on the clause amount
        graph.addEdge(clause.clause_id, term.payment_term_id);
        // Also add edges for any triggers in the payment term due_date
    }
  }
  
  return graph;
}

5.2 Detecting Cycles

javascript
function detectCycles(graph) {
  const visited = new Set();
  const recursionStack = new Set();
  const cycles = [];
  
  function dfs(node, path) {
    visited.add(node);
    recursionStack.add(node);
    
    for (const neighbor of graph.getNeighbors(node)) {
      if (!visited.has(neighbor)) {
        dfs(neighbor, [...path, neighbor]);
      } else if (recursionStack.has(neighbor)) {
        // Cycle detected
        const cycleStart = path.indexOf(neighbor);
        cycles.push(path.slice(cycleStart));
      }
    }
    
    recursionStack.delete(node);
  }
  
  for (const node of graph.getNodes()) {
    if (!visited.has(node)) {
      dfs(node, [node]);
    }
  }
  
  return cycles;
}

5.3 Topological Sort

javascript
function topologicalSort(graph, cycles) {
  // Remove cyclic nodes from consideration
  const cyclicNodes = new Set(cycles.flat());
  
  const inDegree = new Map();
  const queue = [];
  const result = [];
  
  // Calculate in-degrees
  for (const node of graph.getNodes()) {
    if (cyclicNodes.has(node)) continue;
    
    let degree = 0;
    for (const neighbor of graph.getIncomingNeighbors(node)) {
      if (!cyclicNodes.has(neighbor)) {
        degree++;
      }
    }
    inDegree.set(node, degree);
    
    if (degree === 0) {
      queue.push(node);
    }
  }
  
  // Process queue
  while (queue.length > 0) {
    const node = queue.shift();
    result.push(node);
    
    for (const neighbor of graph.getOutgoingNeighbors(node)) {
      if (cyclicNodes.has(neighbor)) continue;
      
      inDegree.set(neighbor, inDegree.get(neighbor) - 1);
      if (inDegree.get(neighbor) === 0) {
        queue.push(neighbor);
      }
    }
  }
  
  return result;
}

Part 6: Auto-Status Updates

6.1 Status Transition Rules

Automatic transitions:

ConditionCurrent StatusNew Status
Input was null, now has valueD, F, CC
Input was null, now has valueOO (unchanged)
State calculated successfullyD, F(unchanged)
State calculated successfullyCC
State calculated successfullyOO (unchanged)

Manual transitions (user action):

  • User can set any status at any time
  • No restrictions on status → status transitions

6.2 Implementation

javascript
function updateStatusAfterCalculation(element, previousValue, newValue) {
  // Only auto-update if status is not Override
  if (element.status === 'O') {
    return; // No change
  }
  
  // If value went from null/undefined to having a value
  if (previousValue == null && newValue != null) {
    element.status = 'C';  // Auto-confirm
  }
  
  // Otherwise, keep existing status
}

Part 7: Not Required (X) Detection

7.1 Detection Methods

Method 1: Pre-condition Evaluation

javascript
// Clause has pre_condition that evaluates to false
clause: {
  pre_condition: "SHOW_STATE.value == true",
  // ...
}

// If SHOW_STATE.value is false:
// → All inputs referenced ONLY by this clause can be marked X

Method 2: Post-calculation Branch Detection

javascript
// MAX(A, B) calculation
// If B > A, then inputs for A were not needed

// After calculation:
calculation_metadata: {
  winning_branch: "percentage",  // Not "guarantee"
  unused_inputs: ["v_guarantee"]  // Could suggest marking related inputs as X
}

Method 3: User Manual

javascript
// User knows input won't be needed
// User sets status = X directly

7.2 Suggestion Algorithm

javascript
function suggestNotRequiredInputs(version) {
  const suggestions = [];

  for (const clauseBlock of version.clause_blocks) {
    for (const clause of clauseBlock.financial_clauses) {
      // Check if financial clause trigger is definitely false
      const triggerResult = evaluateTrigger(clause.trigger, clauseBlock);

      if (triggerResult === false) {
        // Find inputs ONLY used by this financial clause
        const inputsUsedOnlyByThisClause = findExclusiveInputs(clause, clauseBlock);

        for (const input of inputsUsedOnlyByThisClause) {
          if (input.status !== 'X') {
            suggestions.push({
              input_key: input.input_key,
              reason: `Financial clause '${clause.name}' trigger is false`,
              suggested_status: 'X'
            });
          }
        }
      }
    }
  }

  return suggestions;
}

Part 8: Cross-Block Calculation

8.1 Reference Resolution Order

When processing a version with cross-block references:

  1. Build global dependency graph (all clause_blocks)
  2. Detect cycles (may span blocks)
  3. Topological sort (global order)
  4. Process nodes in order, regardless of which block they belong to
javascript
// Example: CB_TOUR_SUMMARY references CB_SHOW_1, CB_SHOW_2, CB_SHOW_3

// Global dependency graph:
CB_SHOW_1.SHOW_SETTLED
CB_SHOW_2.SHOW_SETTLED
CB_SHOW_3.SHOW_SETTLED

CB_SHOW_1.clause_guarantee
CB_SHOW_2.clause_guarantee
CB_SHOW_3.clause_guarantee

CB_TOUR_SUMMARY.TOTAL_ARTIST_PAYMENT

CB_TOUR_SUMMARY.clause_tour_total

// Processing order:
// 1. CB_SHOW_1.SHOW_SETTLED
// 2. CB_SHOW_2.SHOW_SETTLED
// 3. CB_SHOW_3.SHOW_SETTLED
// 4. CB_SHOW_1.clause_guarantee
// 5. CB_SHOW_2.clause_guarantee
// 6. CB_SHOW_3.clause_guarantee
// 7. CB_TOUR_SUMMARY.TOTAL_ARTIST_PAYMENT
// 8. CB_TOUR_SUMMARY.clause_tour_total

8.2 Cross-Deal References

Cross-deal references are resolved once when working version is created:

javascript
// Working version references another deal's submitted version
{
  input_key: "EXTERNAL_SHOW_AMOUNT",
  source: {
    type: "cross_deal",
    deal_id: "deal_other_001",
    version_id: "v_submitted_002",
    path: "clause_blocks.CB_MAIN.financial_clauses.clause_guarantee.amount"
  },
  value: 15000,          // Resolved at creation time
  status: "C",           // Confirmed (from external source)
  resolved_at: "2025-12-04T15:00:00Z"
}

Key Rule: Cross-deal references do NOT auto-update. To get new values from source deal, create a new working version.


Part 9: Performance Considerations

9.1 Incremental Calculation

When user changes one input, only recalculate affected nodes:

javascript
function onInputChange(inputKey, newValue) {
  // 1. Find affected nodes
  const affected = graph.getDescendants(inputKey);
  
  // 2. Filter to only this input's dependents
  const toRecalculate = topologicalSort(subgraph(affected));
  
  // 3. Recalculate only affected nodes
  for (const node of toRecalculate) {
    recalculateNode(node);
  }
}

9.2 Caching

For complex calculations, cache intermediate results:

javascript
{
  state_key: "SPLIT_POINT_CALC",
  value: { ... },
  _cache: {
    total_expenses: 8000,
    promoter_profit: 1200,
    split_point: 11200
  },
  _cache_valid_until: "next_input_change"
}

9.3 Async Processing

For large versions (50+ clause_blocks), consider async processing:

javascript
async function recalculateVersion(version) {
  // Show "Recalculating..." indicator
  ui.showRecalculatingIndicator();
  
  try {
    // Process in batches
    const batches = chunkArray(sortedNodes, 10);
    
    for (const batch of batches) {
      await Promise.all(batch.map(node => recalculateNode(node)));
      
      // Update UI incrementally
      ui.updateResults(batch);
    }
  } finally {
    ui.hideRecalculatingIndicator();
  }
}

Part 10: Validation on Submit

10.1 Validation Checks

Before a working version can be submitted:

javascript
function validateForSubmit(version) {
  const errors = [];
  
  // 1. All references resolve
  for (const ref of getAllReferences(version)) {
    if (!resolveReference(ref, version)) {
      errors.push({
        type: 'unresolved_reference',
        reference: ref,
        message: `Reference '${ref}' does not resolve to a valid element`
      });
    }
  }
  
  // 2. No circular dependencies
  const cycles = detectCycles(buildDependencyGraph(version));
  if (cycles.length > 0) {
    errors.push({
      type: 'circular_dependency',
      cycles: cycles,
      message: `Circular dependencies detected: ${cycles.map(c => c.join(' → ')).join('; ')}`
    });
  }
  
  // 3. Required inputs have values (unless status = X)
  for (const clauseBlock of version.clause_blocks) {
    for (const input of clauseBlock.inputs) {
      if (input.status !== 'X' && input.value == null) {
        errors.push({
          type: 'missing_required_input',
          input_key: input.input_key,
          clause_block: clauseBlock.name,
          message: `Input '${input.name}' is required but has no value`
        });
      }
    }
  }
  
  // 4. No unresolved calculation errors on required paths
  for (const clauseBlock of version.clause_blocks) {
    for (const state of clauseBlock.states) {
      if (state.calculation_error && isOnRequiredPath(state, clauseBlock)) {
        errors.push({
          type: 'calculation_error',
          state_key: state.state_key,
          error: state.calculation_error,
          message: `State '${state.name}' has unresolved calculation error`
        });
      }
    }
  }
  
  // 5. Cross-deal references point to submitted versions
  for (const ref of getCrossDealReferences(version)) {
    const targetVersion = resolveCrossDealVersion(ref);
    if (targetVersion.status !== 'submitted') {
      errors.push({
        type: 'invalid_cross_deal_reference',
        reference: ref,
        message: `Cross-deal reference points to non-submitted version`
      });
    }
  }
  
  return {
    valid: errors.length === 0,
    errors: errors
  };
}

10.2 Validation Response

javascript
// Invalid submission
{
  success: false,
  errors: [
    {
      type: 'missing_required_input',
      input_key: 'TICKET_DETAIL_INPUT',
      clause_block: 'Show 1 - The Fonda',
      message: "Input 'Ticket Sales Data' is required but has no value"
    },
    {
      type: 'circular_dependency',
      cycles: [['STATE_A', 'STATE_B', 'STATE_A']],
      message: 'Circular dependencies detected: STATE_A → STATE_B → STATE_A'
    }
  ]
}

// Valid submission
{
  success: true,
  version_id: 'v_submitted_004',
  submitted_at: '2025-12-04T16:00:00Z'
}

Conclusion

The State Calculation Engine provides:

  • Reactive Calculations: Changes propagate through dependency graph
  • Override Support: Users can override any calculated value
  • Error Tolerance: Errors don't block other calculations
  • Null Safety: User values protected from null overwrites
  • Cross-Block Support: References work across clause_blocks
  • Validation: Comprehensive checks before submission

The engine enables real-time what-if planning while maintaining data integrity and user control.

Confidential. For internal use only.