Reports, runtime and extensions
Two connected layers live here: a typed report model that every backend renders, and a runtime layer that turns operations into catalog entries and execution results. Both favour explicit contracts over convenience.
One document, many backends
report_document is the single value a report is built from:
fincore::report_document document;
document.domain = "portfolio";
document.title = "Monthly review";
document.metadata = {{"source", "fixture"}};
A document holds sections; a section holds metrics, tables, series,
notes, units and legends. Values are typed cells — text, number, boolean or
missing — so a backend never has to guess whether a column is numeric.
Backend |
Function |
Notes |
|---|---|---|
HTML |
|
|
XLSX |
|
One sheet per section; cells use spreadsheet names and inline strings |
|
|
|
Plotly |
|
Returns a bundle with a |
Bokeh |
|
Same bundle shape as Plotly |
Static axes |
|
Caller-supplied axes are marked not owned |
Rendering the same document twice produces byte-identical output for the deterministic backends. That is what makes reports safe to hash and diff, and it is why the workbook writer fixes the ZIP timestamps instead of stamping the clock.
Artifact bundles
The plotting and PDF renderers return a report_artifact_bundle:
metadataalways carriesbackendandreport_digest;report_digest()is the document’s semantic digest, so two backends of one document agree.artifactscarries named outputs (figure,file,axis:<section>.<series>) withname,path,ownedandcontent.find(name)returnsnullptrwhen the name is absent, so an unchecked dereference is a programming error rather than a silent default.A caller-supplied axis, and the file produced by
write_pdf, are not owned: the caller keeps the lifetime.
Escaping and non-finite values
Two escaping schemes coexist because two output channels do. Markup text goes
through HTML entities; the series payload embedded in a script element is
JSON-escaped, so <, > and & become \u003c, \u003e and \u0026 and no
non-ASCII code point reaches the script source unescaped.
Non-finite numbers are not printed as NaN text. Where a channel can carry a gap
the value becomes a null; where the geometry cannot, the point is dropped or
collapsed to zero. The renderer documents which one it does, because a chart that
silently plots zero for infinity is worse than one that leaves a gap.
Runtime catalog
Operations are described rather than discovered by name at run time. Each module
exposes its own descriptor list — metrics_operations(), risk_operations(),
portfolio_operations(), and so on — and all_operations() concatenates them in
a fixed order. The order is part of the digest, so it is not alphabetical and not
incidental.
operation_catalog registers specs and resolves requests; an unknown or ambiguous
name raises operation_resolution_error. A request produces an
execution_plan, and executing it yields a runtime_result whose payload is a
typed runtime_value rather than a stringly-typed map.
Sessions and caching
analysis_context caches deterministic computations. summary() returns the same
map on a second call and computation_count(key) stays at one — the count, not
the equality, is what proves the cache was used. Because the cache is mutable,
one context is single-threaded by construction; frames can still be shared.
analysis_session owns its inputs and outputs and rejects calls after close().
Closing twice is safe. An extension merge happens once even when every callable
is complete, so a session never half-applies a registration set.
Extensions
Extension discovery uses the native snapshot and hook ABI documented in the extension headers. Two properties matter when reading a snapshot:
Hooks carry an event name and a priority: lower priorities run first, and hooks that share both an event and a priority are ordered by fingerprint. Sorting the whole registry by priority alone would therefore reorder hooks within an event.
The snapshot digest covers the ASCII-escaped payload, not host-encoded text, so a non-ASCII identifier has one canonical byte form.
Plotting adapters (ImPlot, GLFW-backed demos) are not part of the core CMake consumer contract; they are built only when their options are enabled.
A minimal report
#include <fincore/report/html.hpp>
#include <fincore/report/models.hpp>
#include <fincore/report/xlsx.hpp>
fincore::report_document document;
document.domain = "demo";
document.title = "Demo";
fincore::report_section section;
section.key = "summary";
fincore::report_metric metric;
metric.key = "sharpe";
metric.label = "Sharpe";
metric.number = 1.25;
section.metrics.push_back(metric);
document.sections.push_back(section);
const std::string html = fincore::render_html(document, {.language = "en"});
const std::string workbook = fincore::render_xlsx(document);
Both strings are complete files; nothing else needs to be generated or templated.