Skip to content

feat(runid,fit): run.json output_schema — column roles for tabular outputs#436

Merged
vsbuffalo merged 4 commits into
mainfrom
hardening/sampler-output-schema
Jul 15, 2026
Merged

feat(runid,fit): run.json output_schema — column roles for tabular outputs#436
vsbuffalo merged 4 commits into
mainfrom
hardening/sampler-output-schema

Conversation

@vsbuffalo

Copy link
Copy Markdown
Owner

run.json output_schema — column roles for tabular outputs (fit)

Every camdl run writes tabular TSVs (draws.tsv, per-chain trace.tsv, …), and
a consumer must reverse-engineer each file's columns to display it — which is the
x-axis, which are grouping keys, which are model quantities vs diagnostics. This
produced a real bug: the fit watcher plots MCMC traces against draw-position
instead of the true sweep numbers, because nothing declares which column is the
iteration axis (the index is spelled sweep/step/draw/iteration by method).

This adds a machine-readable output_schema to each run's run.json, declaring
every tabular file's columns and their roles from a closed vocabulary (time /
iteration / chain / dimension / state / flow / param_estimated /
param_fixed / observable / quantile / diagnostic). A consumer reads
role: iteration for the x-axis, role: chain for grouping — no per-method
knowledge, no hardcoded names.

Design

  • Drift-proof by construction — the schema is built by classifying each
    written file's real header post-run, not a per-method recipe. The file is
    its own source of truth; classification needs only set membership, never
    column order.
  • Identity-inertRunRecord is never hashed, and the schema attaches
    after the run_id is fixed at claim time (verified against the store). It is
    additive and skip-serialized when empty, so existing cached runs round-trip
    unchanged.
  • Fit-first — wires the fit stage (draws.tsv, chain_{n}/trace.tsv,
    parameter_traces.tsv), the surface behind the bug. Sim (traj.tsv/obs.tsv)
    and predict/survey/profile follow the same seam (proposal §6, v1.5).

Consumer contract

docs/camdl-run-spec.md §2.5 documents the shape and vocabulary — camdl-watch
reads it to fix the trace x-axis (forward the iteration column, plot against it).

Proposal: docs/dev/proposals/2026-07-15-run-output-column-schema.md

Every camdl command writes tabular outputs (simulate: traj.tsv/obs.tsv;
fit: draws.tsv/trace.tsv; predict: predictive/observed; survey/profile:
grids), and a consumer must reverse-engineer each file's columns — which is
the x-axis, which are grouping keys, which are model quantities vs sampler
diagnostics. The run store already declares machine-readable schemas for its
observation and predictive surfaces; the tabular outputs are the surface
that principle doesn't yet cover, and that gap produced a real display bug
(the fit-watcher x-axis).

Propose a uniform column schema, expressed with a small closed vocabulary of
column roles (time / iteration / chain / dimension / state / parameter /
observable / quantile / diagnostic / …), surfaced centrally in the one
manifest every command writes: run.json (a new identity-inert output_schema
field). Declare, don't rename.

Chosen over the fit-only fit.meta.json home because run.json is universal —
a simulate or profile leaf has no fit.meta.json, so a fit-only home would
fork the mechanism per command. Includes the run-store background, the
role vocabulary, the per-kind column map, the watcher reproduction, the
identity-safety verdict, a one-source-of-truth drift test, and a v1
(fit+sim) / v2 (predict/survey/profile/quantities) staging.
Add the column-schema vocabulary to the run.json RunRecord: a closed
ColumnRole enum (time / iteration / chain / replicate / scenario /
dimension / state / flow / incidence / param_estimated / param_fixed /
observable / quantile / diagnostic), plus TableRole, ColumnSpec,
TableSchema, and an `output_schema: BTreeMap<String, TableSchema>` field
on RunRecord keyed by leaf-relative path.

RunRecord is never hashed and the field is skip_serializing_if empty, so
this is identity-inert and golden-neutral until a writer populates it: no
run re-keys, and existing manifests round-trip unchanged (pinned by the
empty-omitted test). The snake_case wire tags a consumer reads are pinned
by the round-trip test.

The point of the vocabulary is to let a consumer render any camdl tabular
output mechanically — x-axis = time|iteration, group by chain|replicate|
scenario, facet by dimension, series = state|param|observable, ribbons =
quantile, overlays = diagnostic — instead of reverse-engineering a header.
This is the first step of that arc; the fit (draws.tsv / trace) and sim
(traj.tsv / obs) writers populate it next.

Proposal: docs/dev/proposals/2026-07-15-run-output-column-schema.md
Wire the fit stage to declare its tabular outputs' column schema in
run.json: after the stage runs, classify each written file's real header
(draws.tsv, chain_{n}/trace.tsv, parameter_traces.tsv) by role against the
model's parameter set, and attach it to the record before finalize via a
new ResolvedClaim::set_output_schema setter.

Reading the actual header rather than reconstructing a per-method column
recipe makes the declaration drift-proof by construction: the file is its
own source of truth, and classification needs only set membership, never
column order. A consumer reads role=iteration for the x-axis, role=chain
for grouping, param_estimated for the mixing series — no per-method
knowledge, no hardcoded column names.

Identity-inert: RunRecord is never hashed, and the schema attaches after
the run_id is fixed at claim time. classify() and a real-header tempdir
test pin the role assignment.

Proposal: docs/dev/proposals/2026-07-15-run-output-column-schema.md
Update the proposal to the implemented design: the schema attaches at
finalize time via a claim setter (no store-signature change), and it is
built by classifying each written file's real header (drift-proof by
construction) rather than a per-method recipe plus a drift test. Scope v1
to fit; sim and the rest follow the same seam.

Add run-spec §2.5 declaring the run.json.output_schema shape and the
column-role vocabulary — the consumer contract for camdl-watch and other
tooling.
@vsbuffalo
vsbuffalo force-pushed the hardening/sampler-output-schema branch from 4397ba6 to 9bd225c Compare July 15, 2026 18:01
@vsbuffalo
vsbuffalo merged commit 5d041e7 into main Jul 15, 2026
3 checks passed
@vsbuffalo
vsbuffalo deleted the hardening/sampler-output-schema branch July 15, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant