CFDL

This is a specification page.

It defines CFDL normatively — complete, exact, and written for people implementing against it. If you are building a model, Reference covers the same ground at the altitude of the work.

Results schema

The shape of a cfdl run results document. This is the published contract, also served at cfdl.dev/schemas; every committed results golden is validated against it by make results-schema.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://cfdl.dev/schemas/CFDL_v0_1_Results.schema.json",
  "title": "CFDL v0.1 Results",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "results_version",
    "model_hash",
    "engine",
    "warnings",
    "deterministic",
    "scenarios",
    "monte_carlo",
    "ledger_hash"
  ],
  "properties": {
    "results_version": {
      "type": "string",
      "const": "0.3",
      "description": "Schema version of this document. 0.3 added `ledger_hash` and the optional `inputs` section, and `category` on IR streams upstream of it."
    },
    "model_hash": {
      "type": "string",
      "description": "Hash of canonical IR for traceability",
      "minLength": 8
    },
    "ledger_hash": {
      "type": "string",
      "description": "SHA-256 over the canonical form of the deterministic ledger — `deterministic.series` and `deterministic.annual_rollup`. Together with `model_hash` and `engine` this closes the chain: identical inputs on an identical engine must reproduce an identical ledger_hash. It covers the LEDGER, not the metrics: NPV and IRR are derived FROM the ledger, so including them would make the hash move for a reason the ledger did not. It is therefore invariant to the discount rate, which is correct — the ledger is cash before discounting."
    },
    "engine": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "version"
      ],
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1
        },
        "version": {
          "type": "string",
          "minLength": 1
        },
        "build": {
          "type": "string"
        }
      }
    },
    "warnings": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "inputs": {
      "$ref": "#/$defs/InputsSection"
    },
    "deterministic": {
      "$ref": "#/$defs/DeterministicSection"
    },
    "scenarios": {
      "$ref": "#/$defs/ScenariosSection"
    },
    "monte_carlo": {
      "$ref": "#/$defs/MonteCarloSection"
    },
    "domain_metrics": {
      "$ref": "#/$defs/DomainMetrics"
    },
    "statements": {
      "$ref": "#/$defs/StatementsSection"
    }
  },
  "$defs": {
    "Currency": {
      "type": "string",
      "pattern": "^[A-Z]{3}$"
    },
    "Decimal": {
      "type": "number"
    },
    "Money": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "amount",
        "currency"
      ],
      "properties": {
        "amount": {
          "$ref": "#/$defs/Decimal"
        },
        "currency": {
          "$ref": "#/$defs/Currency"
        }
      }
    },
    "Date": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
    },
    "SeriesIndex": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "calendar",
        "start",
        "periods"
      ],
      "properties": {
        "calendar": {
          "type": "string",
          "enum": [
            "daily",
            "monthly",
            "quarterly",
            "annual"
          ]
        },
        "start": {
          "$ref": "#/$defs/Date"
        },
        "periods": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "Scalar": {
      "description": "Scalar metric output",
      "oneOf": [
        {
          "type": "number"
        },
        {
          "$ref": "#/$defs/Money"
        },
        {
          "type": "string"
        },
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ]
    },
    "Series": {
      "description": "Time series aligned to the model timeline",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "index",
        "values"
      ],
      "properties": {
        "index": {
          "$ref": "#/$defs/SeriesIndex"
        },
        "values": {
          "type": "array",
          "minItems": 1,
          "items": {
            "oneOf": [
              {
                "$ref": "#/$defs/Decimal"
              },
              {
                "$ref": "#/$defs/Money"
              },
              {
                "type": "null"
              }
            ]
          },
          "description": "One entry per period. Money for a cash series; a bare number for a dimensionless one such as a declared `state`, which has no denomination."
        },
        "offset": {
          "description": "Where in each period this series' cash falls: 0.0 at the period's open (an annuity due, or a one-shot on its date), 1.0 at its close (an ordinary annuity, the default), 0.5 for the mid-period convention. The same offset used to discount the series, and the axis `model.wal_years` and `model.payback_years` are measured on — so an ordinary annuity's first monthly collection is at 1/12 of a year, not 0. Absent on aggregates (`model.net_cash_flow`, the annual rollup), which sum streams whose placements differ. See 12_payment_timing.md. Absent on `state.` series, which are not paid and so sit nowhere in their period.",
          "type": "number"
        }
      }
    },
    "MetricMap": {
      "type": "object",
      "description": "Named metric scalars",
      "additionalProperties": {
        "$ref": "#/$defs/Scalar"
      }
    },
    "SeriesMap": {
      "type": "object",
      "description": "Named time series outputs. Keys are prefixed by what they are: `stream.<name>` and `option.<name>` are cash and carry a currency; `model.net_cash_flow` is their aggregate; `state.<name>` is a declared `state` and is NOT cash — it is a bare number with no currency and no offset, published so a recurrence can be inspected, and it never enters model.total, model.npv, the annual rollup or any domain metric. `entity.<symbol>.net_cash_flow` is an entity's cash AGGREGATED BY RELATION — its own streams plus every descendant's, following `part_of` rather than a name prefix, so a building's cash is its units' cash because they ARE its units. An entity with no children carries its own streams only, which is the pool that models collective behaviour directly; the grain is the modeller's choice. Like a subtotal it is a fold OF the cash and never counts AS cash — excluded from model.total, model.npv, model.net_cash_flow and the annual rollup, because counting a parent and its children would double what it touches. `domain.<pack>.<name>` is a per-period SUBTOTAL — a declared aggregation of the classified streams. Money for a sum, a bare number or `null` for a ratio whose denominator vanishes. Like `state.`, it never enters model.total, model.npv, model.net_cash_flow or the per-stream annual rollup: it is an aggregation OF the cash, so counting it as cash would double what it touches. It carries no `offset`, because a subtotal spans streams that may settle at different points in a period and so has no single placement to claim.",
      "additionalProperties": {
        "$ref": "#/$defs/Series"
      }
    },
    "DeterministicSection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "status",
        "metrics",
        "series"
      ],
      "properties": {
        "status": {
          "type": "string",
          "enum": [
            "not_run",
            "ok",
            "error"
          ]
        },
        "metrics": {
          "$ref": "#/$defs/MetricMap"
        },
        "series": {
          "$ref": "#/$defs/SeriesMap"
        },
        "errors": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/RuntimeError"
          }
        },
        "annual_rollup": {
          "$ref": "#/$defs/AnnualRollupSection"
        },
        "transitions": {
          "type": "array",
          "description": "Every state change an event made, in the order it happened — the audit trail for whether and when something occurred. Entity state is otherwise unobservable: nothing else distinguishes an event that fired against a misspelled target from an event that never fired, and without this a case cannot assert a transition. Recorded even when the value does not change, because the question the log answers is whether the event fired. Omitted when a model has no events. Visibility is two rules, not one: an event or option guard reads the state as the period OPENED, so declaration order cannot change an answer; a stream reads it as the period CLOSED, so a transition takes effect in the period it fires.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "period",
              "date",
              "entity",
              "field",
              "to",
              "event"
            ],
            "properties": {
              "period": {
                "type": "integer",
                "minimum": 0
              },
              "date": {
                "type": "string"
              },
              "entity": {
                "type": "string"
              },
              "field": {
                "type": "string"
              },
              "from": {
                "type": "string",
                "description": "The value before. Absent when the field had none — which, for a typed entity with a lifecycle, should not happen, because it opens in its declared initial state."
              },
              "to": {
                "type": "string"
              },
              "event": {
                "type": "string",
                "description": "The event that fired. A transition always has a cause."
              }
            }
          }
        }
      }
    },
    "ScenariosSection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "status",
        "summaries"
      ],
      "properties": {
        "status": {
          "type": "string",
          "enum": [
            "not_run",
            "ok",
            "error"
          ]
        },
        "summaries": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/ScenarioSummary"
          }
        },
        "errors": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/RuntimeError"
          }
        }
      }
    },
    "ScenarioSummary": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "metrics"
      ],
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1
        },
        "metrics": {
          "$ref": "#/$defs/MetricMap"
        }
      }
    },
    "MonteCarloSection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "status",
        "trials",
        "seed",
        "metrics",
        "trial_summaries"
      ],
      "properties": {
        "status": {
          "type": "string",
          "enum": [
            "not_run",
            "ok",
            "error"
          ]
        },
        "trials": {
          "type": "integer",
          "minimum": 1
        },
        "seed": {
          "type": "integer",
          "minimum": 0
        },
        "metrics": {
          "type": "object",
          "description": "Named Monte Carlo metric summaries",
          "additionalProperties": {
            "$ref": "#/$defs/MetricSummary"
          }
        },
        "trial_summaries": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/TrialSummary"
          }
        },
        "aggregates": {
          "$ref": "#/$defs/MonteCarloAggregates"
        },
        "errors": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/RuntimeError"
          }
        }
      }
    },
    "TrialSummary": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "trial",
        "metrics"
      ],
      "properties": {
        "trial": {
          "type": "integer",
          "minimum": 0
        },
        "metrics": {
          "$ref": "#/$defs/MetricMap"
        }
      }
    },
    "MonteCarloAggregates": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "npv"
      ],
      "properties": {
        "npv": {
          "$ref": "#/$defs/NpvAggregate"
        }
      }
    },
    "NpvAggregate": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "mean",
        "median",
        "stddev",
        "p_negative"
      ],
      "properties": {
        "mean": {
          "$ref": "#/$defs/Decimal"
        },
        "median": {
          "$ref": "#/$defs/Decimal"
        },
        "stddev": {
          "$ref": "#/$defs/Decimal"
        },
        "p_negative": {
          "$ref": "#/$defs/Decimal"
        }
      }
    },
    "MetricSummary": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "type",
        "mean",
        "p50"
      ],
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "number",
            "money"
          ]
        },
        "mean": {
          "$ref": "#/$defs/Scalar"
        },
        "stdev": {
          "$ref": "#/$defs/Scalar"
        },
        "min": {
          "$ref": "#/$defs/Scalar"
        },
        "max": {
          "$ref": "#/$defs/Scalar"
        },
        "p01": {
          "$ref": "#/$defs/Scalar"
        },
        "p05": {
          "$ref": "#/$defs/Scalar"
        },
        "p10": {
          "$ref": "#/$defs/Scalar"
        },
        "p25": {
          "$ref": "#/$defs/Scalar"
        },
        "p50": {
          "$ref": "#/$defs/Scalar"
        },
        "p75": {
          "$ref": "#/$defs/Scalar"
        },
        "p90": {
          "$ref": "#/$defs/Scalar"
        },
        "p95": {
          "$ref": "#/$defs/Scalar"
        },
        "p99": {
          "$ref": "#/$defs/Scalar"
        }
      }
    },
    "RuntimeError": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "code",
        "message"
      ],
      "properties": {
        "code": {
          "type": "string",
          "minLength": 1
        },
        "message": {
          "type": "string",
          "minLength": 1
        },
        "path": {
          "type": "string"
        },
        "hint": {
          "type": "string"
        }
      }
    },
    "MetricLineage": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "numerator_streams",
        "denominator_streams",
        "formula"
      ],
      "description": "Where a domain metric's value came from: the stream selectors it summed and the human-readable formula the pack declared. Emitted so a metric can be audited without reading the pack.",
      "properties": {
        "numerator_streams": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "denominator_streams": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "formula": {
          "type": "string"
        }
      }
    },
    "DomainMetrics": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "pack",
        "metrics",
        "lineage"
      ],
      "description": "Pack-defined metrics, present only when the run named a pack (`--pack <name>`). Engine-universal metrics live in `deterministic.metrics`; these are the domain's own, declared in the pack's metrics.toml.",
      "properties": {
        "pack": {
          "type": "string"
        },
        "metrics": {
          "$ref": "#/$defs/MetricMap"
        },
        "lineage": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/$defs/MetricLineage"
          }
        }
      }
    },
    "AnnualRollupSection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "series"
      ],
      "description": "The deterministic series aggregated to annual buckets, for reporting a sub-annual model on a yearly grid. Present whenever the deterministic run succeeded. Carries no `offset`: an annual bucket sums periods whose placements differ.",
      "properties": {
        "series": {
          "$ref": "#/$defs/SeriesMap"
        }
      }
    },
    "InputsSection": {
      "type": "object",
      "additionalProperties": false,
      "description": "What went in, above the line items — the top of the audit chain. Absent when the model declares neither assumptions nor pack-lowered streams.",
      "properties": {
        "resolved": {
          "type": "object",
          "additionalProperties": {
            "type": "number"
          },
          "description": "Evaluated `assume` values, as `inputs.<name>` resolves them. In a deterministic run a random assumption resolves to its clipped CENTRAL value rather than to a draw; publishing it here is what stops that being invisible."
        },
        "streams": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "Per-stream record of the contract terms a pack rule consumed to strike it, passed through from the IR's `stream_inputs` verbatim. See the IR schema's StreamInputs. Hand-written streams have no entry, because no rule struck them."
        }
      }
    },
    "StatementsSection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "pack",
        "statements"
      ],
      "description": "Statements the active pack declares, rendered against this run. Rows carry order, labels, depth and a display sign; they compute nothing the engine has not already aggregated. Absent when the pack declares no statement.",
      "properties": {
        "pack": {
          "type": "string"
        },
        "statements": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Statement"
          }
        }
      }
    },
    "Statement": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "label",
        "default",
        "grain",
        "rows",
        "reconciliation"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "label": {
          "type": "string"
        },
        "default": {
          "type": "boolean"
        },
        "grain": {
          "$ref": "#/$defs/StatementGrain"
        },
        "rows": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/StatementRow"
          }
        },
        "reconciliation": {
          "$ref": "#/$defs/StatementReconciliation"
        },
        "diagnostics": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/StatementDiagnostic"
          },
          "description": "Completeness findings. Empty is the healthy case."
        }
      }
    },
    "StatementGrain": {
      "type": "object",
      "additionalProperties": false,
      "description": "The grain this statement reports at, and one ready-to-render label per column. Published because a consumer cannot derive it: an annual statement over a monthly model has ten values where the model has 120, and nothing else in the document says which ten periods those are.",
      "required": [
        "calendar",
        "start",
        "labels"
      ],
      "properties": {
        "calendar": {
          "type": "string",
          "description": "monthly | quarterly | annual | daily — the bucketing, not the model grid."
        },
        "start": {
          "type": "string"
        },
        "labels": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "One per column, aligned with every row's `values`."
        }
      }
    },
    "StatementRow": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "kind",
        "depth",
        "display_sign"
      ],
      "description": "One row. `residual` is emitted for cash no row claimed and cannot be authored; a `spacer` carries no values.",
      "properties": {
        "kind": {
          "enum": [
            "line",
            "subtotal",
            "ratio",
            "spacer",
            "residual"
          ]
        },
        "label": {
          "type": "string"
        },
        "depth": {
          "type": "integer",
          "minimum": 0
        },
        "display_sign": {
          "type": "number",
          "enum": [
            1,
            -1
          ],
          "description": "How to RENDER the sign. `values` is always the signed arithmetic quantity, so a consumer that ignores this still adds up correctly. -1 is how a deduction prints as a positive number in a 'less:' row while still being counted negatively — a line can be shown AND counted."
        },
        "values": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/SeriesValue"
          }
        },
        "total": {
          "type": "number",
          "description": "Lifetime total. Absent for a ratio, where summing a column of ratios answers nothing, and for a spacer."
        },
        "streams": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The streams this row drew from — what makes a published figure traceable without a flow ledger."
        }
      }
    },
    "StatementReconciliation": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "bottom_line",
        "model_total",
        "residual"
      ],
      "description": "Does the statement account for the model's cash? Published always and asserted rather than corrected: a bottom line that quietly differs from model.total is the failure this exists to make visible.",
      "properties": {
        "bottom_line": {
          "type": "number"
        },
        "model_total": {
          "type": "number"
        },
        "residual": {
          "type": "number"
        }
      }
    },
    "StatementDiagnostic": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "code",
        "message"
      ],
      "properties": {
        "code": {
          "type": "string"
        },
        "message": {
          "type": "string"
        }
      }
    },
    "SeriesValue": {
      "description": "A series point: money, a bare number, or null where undefined."
    }
  }
}