docs · format & toolchain

The format, precisely

A working guide to the open-lec 0.2 format and its reference toolchain. The normative text is SPEC.md; where this page and the spec disagree, the spec wins.


Install

The reference implementation is a single zero-dependency package: validator, curve math, SVG renderer, React wrapper, headless export, adapters, and CLI.

$npm install open-lec
◆ note

Publishing to the npm registry is deferred while 0.x settles (tracked in sh4t/open-lec#5). Until then, install straight from GitHub: npm install github:sh4t/open-lec.

Quick start

Fetch a document, validate it, render it. The renderer draws into any container and returns a handle with update() and destroy().

import { render, validate } from "open-lec";

const doc = await fetch("/my-lec.json").then((r) => r.json());
const result = validate(doc);

if (result.valid) {
  render(document.querySelector("#chart"), doc, { logX: true });
} else {
  console.error(result.errors);   // [{ path, message }, …]
}

Your first document

Four points and a currency make a conforming curve. Points sample the exceedance function P(annual loss ≥ loss).


    

Paste it into the playground and break it on purpose — reorder the losses, push a probability above an earlier one — to watch the validator name the exact path and rule.

Document structure

An open-lec document is a single UTF-8 JSON object with three required members and one optional one.

fieldtypereq.description
openlecstringSpec version, "0.2". 0.1 documents remain valid.
metadataobjectDocument-level metadata; currency is required.
scenariosScenario[]One or more curves. Array order is presentation order.
referenceLinesRefLine[]Document-level guide lines on either axis.
✓ tip

Readers must ignore unrecognized keys, and writers should namespace extensions under x- prefixed keys at any level ("x-acme": {…}). The validator warns — never errors — on unknown keys outside that namespace.

Metadata

fieldtypereq.description
currencystringISO 4217 code. Every loss in the document is in this currency's major unit.
titlestringHuman-readable title; renderers use it for the chart's accessible name.
createdstringRFC 3339 timestamp of generation.
horizonobject{ value > 0, unit: year | quarter | month }. Default 1 year. No conversion semantics exist — a 1-quarter curve is not a 1-year curve ÷ 4.
generatorobject{ name, version? } of the emitting tool.
methodologystringFree text or tag, e.g. "FAIR / Monte Carlo".
iterationsintegerSimulation iteration count (≥ 1), if applicable.

Scenarios

A scenario is one curve — the current state, a projected state after a treatment plan, or one member of a portfolio comparison. Comparing a baseline against one or more treatment scenarios is the canonical board visual.

fieldtypereq.description
idstringUnique in the document; matches [A-Za-z0-9_-]+.
labelstringDisplay name; renderers fall back to id.
rolestringbaseline | treatment | comparison — a presentation hint only.
curveCurveThe exceedance curve (below).
bandsBand[]Uncertainty bands (below).
statisticsobjectPrecomputed summaries — informative only.
annotationsobject[]{ loss, label } markers pinned to the curve.
colorstring#rrggbb rendering hint; renderers may override.

Curves & interpolation

A curve is at least two samples of the exceedance function, under four semantic rules the validator enforces:

  • loss is finite and ≥ 0, in the metadata currency;
  • p is finite, in [0, 1];
  • losses are sorted strictly ascending — no duplicates;
  • p is non-increasing — an exceedance function cannot rise.

Curves may declare how to interpolate between knots:

profilemeaninguse when
linearp linear in loss (default)evenly sampled curves, linear axes
log-lossp linear in ln(loss); every loss must be > 0heavy-tailed curves on log-spaced grids — segments render straight on a log axis, so the drawn chord, the interpolation math, and the hover crosshair all agree exactly

Below the first point the curve extends horizontally at p₀; above the last, at p_N. From the curve alone, compliant tooling derives exceedance probability at any loss, loss at any probability (VaR-style), return periods, and the expected annual loss — the exact per-segment integral under the declared profile, a lower bound when the final p is still > 0.

Uncertainty bands

Bands express an interval around the curve at a stated level, and 0.2 lets them say what kind of uncertainty they carry — the two kinds make very differently sized bands:

kindexpressesbehaves
samplingMonte Carlo estimation error (e.g. a Wilson interval on each p̂)shrinks as iterations grow
parameterepistemic uncertainty about model inputsdoes not shrink with iterations
mixedboth at once
! warning

Quantiles of the raw 0/1 exceedance indicator are not a band — they collapse to [0, 1] almost everywhere. The validator warns when every band point spans the full interval, because the band carries no information.

Reference lines

Document-level guide lines put governance context in the picture: risk tolerance, insurance attachment and limit, a 1-in-N probability line.

"referenceLines": [
  { "axis": "loss",        "value": 5000000, "label": "Risk tolerance" },
  { "axis": "probability", "value": 0.05,    "label": "1-in-20" }
]

Statistics

Optional precomputed summaries — mean, median, percentiles, sampleCount, observed extremes — so simple consumers can print headline numbers without math. Readers must never trust them over the curve; the reference tooling always re-derives.

Conformance & versioning

  • A conforming document validates against the JSON Schema and satisfies the semantic rules — the reference validator checks both, and an agreement test suite keeps the two layers from drifting apart.
  • A conforming reader ignores unknown keys, interpolates per the declared profile, and treats statistics as informative.
  • Versions are major.minor; 0.x minors are additive, so readers of 0.x accept any 0.y ≥ x by ignoring unknown fields.

JavaScript API

Everything is importable from the package root; adapters and headless export live in subpath exports.

exportfromdescription
validate(doc)open-lecFull structural + semantic validation → { valid, errors, warnings }, each with a JSONPath-style path.
render(el, doc, opts?)open-lecMount the interactive SVG chart → { svg, update(doc), destroy() }.
probabilityAt(points, loss, interp?)open-lecP(annual loss ≥ loss), interpolated per the profile.
lossAt(points, p, interp?)open-lecLoss at an exceedance probability — the VaR-style inverse.
returnPeriod(p)open-lec1 / p horizons.
expectedAnnualLoss(points, interp?)open-lec∫ P(X ≥ x) dx — exact under the declared profile.
summarize(points, interp?)open-lecEAL plus 1-in-2 / 1-in-10 / 1-in-100 losses in one call.
renderToSvgString(doc, opts?)open-lec/headlessStandalone SVG string, no browser — same drawing code as render().
scenarioFromSamples(samples, opts?)open-lec/adaptersRaw per-iteration losses → curve + Wilson band + statistics.
makeDocument(scenarios, opts?)open-lec/adaptersWrap adapter output in a complete document.

Renderer options

optiondefaultdescription
width · height720 · 420ViewBox size; the SVG scales responsively to its container.
logXtrueLog₁₀ loss axis with 1–2–5 ticks; linear otherwise.
showBandstrueDraw uncertainty bands at 14% opacity.
showLegendtrueLegend when the document has 2+ scenarios.
interactivetrueHover crosshair with per-scenario probabilities and return periods.
densifyfalseSince 0.2.2 — subsample segments to draw the declared interpolation profile exactly when the axis scale doesn't match it. Off by default: curves draw as chords in axis space, which is exact whenever profile and axis agree (log-loss ↔ log axis). This site passes true so pasted mismatched documents keep the crosshair on the line.
palette6 colorsCategorical colors by scenario index; per-scenario color hints win.
themeneutraltext, grid, axis, reference, tooltip colors, font — this site feeds it Hearth tokens and re-renders on theme switch.
margin24/24/52/64Plot margins, merged over the defaults.

React

import OpenLEC from "open-lec/react";

<OpenLEC
  doc={doc}
  options={{ logX: true }}
  onValidation={(result) => setErrors(result.errors)}
/>

The component validates on every doc change, renders only valid documents, updates in place, and cleans up listeners on unmount.

CLI

commanddescription
openlec validate <files…>Validate one or more documents; exit 0 only if all conform. Errors carry exact paths.
openlec stats <file> [--json]Derived EAL and 1-in-2/10/100 losses per scenario; --json for pipelines.
openlec from riskquant <csv>Netflix riskquant scenario CSV → document, re-running its Poisson × lognormal model with a seeded Monte Carlo. --aggregate adds a portfolio sum.
openlec from csv <file>loss,p rows → curve; or a samples column → curve + band.
openlec from samples <file>Raw per-iteration losses (JSON array or one-column CSV) → curve + Wilson band + statistics.
openlec render <file> [-o out.svg]Standalone SVG, no browser. --width, --height, --linear, --no-bands.

Every command reads - as stdin, so adapters, stats, and rendering compose:

$openlec from riskquant scenarios.csv | openlec stats -

JSON Schema

For toolchains that speak draft 2020-12, the schema captures the structural half of conformance (the semantic rules — monotonicity, ordering, band coherence — need the validator).

$curl -O /schema/openlec.schema.json

Served from this site at /schema/openlec.schema.json, canonical in the repository at schema/openlec.schema.json.