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."
}
}
}