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
| Trigger | Action |
|---|---|
| User modifies input value | Recalculate all dependent states and financial clauses |
| User modifies state value (when status = O) | No recalculation (override respected) |
| User changes state status from O → other | Recalculate that state |
| Working version created from submitted | Full calculation on all elements |
| User adds new state/input/financial clause | Recalculate 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:
- Calculation still runs (for transparency/comparison)
- Value is NOT updated (user override preserved)
- Calculated value stored separately (for "what would it be?" display)
// 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):
// 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:
| Operator | Description | Example |
|---|---|---|
| + | Addition | a + b |
| - | Subtraction | a - b |
| * | Multiplication | a * b |
| / | Division | a / b |
| % | Modulo | a % b |
Comparison:
| Operator | Description | Example |
|---|---|---|
| == | Equals | a == b |
| != | Not equals | a != b |
| > | Greater than | a > b |
| >= | Greater or equal | a >= b |
| < | Less than | a < b |
| <= | Less or equal | a <= b |
Logical:
| Operator | Description | Example |
|---|---|---|
| && | AND | a && b |
| || | OR | a || b |
| ! | NOT | !a |
Functions:
| Function | Description | Example |
|---|---|---|
| MAX(a, b, ...) | Maximum value | MAX(guarantee, percentage) |
| MIN(a, b, ...) | Minimum value | MIN(a, b) |
| SUM(array) | Sum of array | SUM(amounts) |
| AVG(array) | Average of array | AVG(amounts) |
| ABS(a) | Absolute value | ABS(difference) |
| ROUND(a, decimals) | Round to decimals | ROUND(amount, 2) |
| EXISTS(ref) | Check if reference exists and has value | EXISTS(MERCH_STATE) |
| COALESCE(a, b) | Return first non-null | COALESCE(input, 0) |
| IF(cond, then, else) | Conditional | IF(sold > 500, bonus, 0) |
Date Functions:
| Function | Description | Example |
|---|---|---|
| today() | Current date | date < today() |
| DAYS_BETWEEN(a, b) | Days between dates | DAYS_BETWEEN(start, end) |
| DATE_ADD(date, days) | Add days to date | DATE_ADD(start, 30) |
3.2 Reference Syntax
Local References (within same clause_block):
// 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.amountCross-Block References (within same version):
// 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_guaranteeAggregation References:
// 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):
// Reference specific version of another deal
DEAL:deal_show_001:VERSION:v_abc123.clause_blocks.CB_GUARANTEE.financial_clauses.clause_main.amount3.3 Expression Examples
Flat Guarantee:
// Trigger
SHOW_STATE.value == true
// Calculation
v_guaranteeGuarantee vs NBOR:
// 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:
// 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:
// 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:
// 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 Type | Cause | Behavior |
|---|---|---|
| undefined_reference | Reference to non-existent element | Keep old value, store error |
| type_mismatch | Arithmetic on non-numeric | Keep old value, store error |
| division_by_zero | Division by zero | Keep old value, store error |
| circular_dependency | A → B → A | Skip node, store error |
| syntax_error | Invalid expression syntax | Keep old value, store error |
| null_reference | Reference resolves to null | Keep old value if exists, else null |
4.2 Error Storage
{
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:
// 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
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
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
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:
| Condition | Current Status | New Status |
|---|---|---|
| Input was null, now has value | D, F, C | C |
| Input was null, now has value | O | O (unchanged) |
| State calculated successfully | D, F | (unchanged) |
| State calculated successfully | C | C |
| State calculated successfully | O | O (unchanged) |
Manual transitions (user action):
- User can set any status at any time
- No restrictions on status → status transitions
6.2 Implementation
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
// 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 XMethod 2: Post-calculation Branch Detection
// 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
// User knows input won't be needed
// User sets status = X directly7.2 Suggestion Algorithm
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:
- Build global dependency graph (all clause_blocks)
- Detect cycles (may span blocks)
- Topological sort (global order)
- Process nodes in order, regardless of which block they belong to
// 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_total8.2 Cross-Deal References
Cross-deal references are resolved once when working version is created:
// 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:
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:
{
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:
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:
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
// 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.