Evidence: floors, populations, portable runs¶
A finished run and a passed gate are not yet a claim. Between them sit the questions a reviewer asks: where did the floor come from, on which rows, at what threshold, against which baseline, on which machine, and can the same answer be produced again from the records alone. maatml answers each of them as a lifecycle step with a record, so a model folder does not have to carry its own scripts for any of it.
Every number below is read from or written to three places: the eval report
(output/eval/<run>.json, versioned), the run record (output/runs.jsonl)
and model.yml. Nothing is inferred at read time.
Floors are derived, never typed¶
maatml gates derive <model-dir> --run RUN # print the floors a run's report supports
maatml gates derive <model-dir> --run RUN --write # rewrite evaluation.gates in model.yml
maatml gates derive <model-dir> --seed-study FILE # the per-metric minimum across seeds
A floor is the Wilson 95 % lower bound of the observed rate at that metric's own denominator, floored to two places, written with its derivation beside it:
evaluation:
gates:
routing_refusal_recall_rate: 0.84 # 66/71 = 0.930, w95 0.846
all_layers_pass_rate: 0.94 # 401/414 = 0.969, w95 0.947
gates_benchmark: 3f9c⦠# the split these floors were derived on
The denominators come from the report's counts ({metric: {k, n}}), which
the harness records for every rate it computes and a metrics plugin adds for
its own through __counts__. A metric below --min-n rows is refused rather
than floored on thin evidence. When the run carries a prediction cache
(evaluate --cache), the floor is instead a cluster bootstrap over the group
key (--cluster-by, default family), because rows from one scenario or one
camera are not independent trials and the row-level bound is optimistic.
--write stamps evaluation.gates_benchmark with the split's content hash.
A later evaluate --gate on a different split warns (population_mismatch
in the gates payload), and --strict-population refuses: floors describe the
population they came from.
Gates carry a tier: {min: 0.80, tier: advisory} is reported and listed
under advisory_failed but never fails the step. A "slice:<field>=<value>"
gate floors one slice of the split (declared under evaluation.slices),
which is how a pooled rate is kept from hiding the family it averages away.
Whether a run ships¶
maatml ship-check <model-dir> CANDIDATE BASELINE
maatml ship-check <model-dir> CANDIDATE BASELINE --replay # benchmark changed since BASELINE
One verdict in three parts, in order: absolute (every blocking floor met
at production tier), delta (no gated metric drops more than one row at
n ā„ 30, or --max-regression), population (both reports on the same
split; else --replay re-evaluates both checkpoints over the current test
split under output/eval/replay/, touching neither run's own evidence). Exit
1 on DO NOT SHIP; --json for machines.
The operating point comes from val¶
maatml evaluate <model-dir> --checkpoint RUN --split val --cache --set evaluation.score_thresh=0.05
maatml operating-point derive <model-dir> --run RUN --split val
maatml operating-point derive <model-dir> --run RUN --write --confirm-on-test
evaluation:
operating_point:
threshold_key: score_threshold # the evaluation key the sweep writes
objective: recall
budget: {metric: false_alarms_per_hour, max: 2.0}
The sweep re-scores a val prediction cache through the predictor's
rescore(rows, threshold), with no inference re-run, skips cuts below the one
the cache was decoded at, and picks the best objective under the budget. The
val evaluate that writes the cache runs under --set, which overrides
model.yml for that one evaluate, is recorded on the report as
extras.overrides, and is refused together with --gate or --blind.
--write sets the threshold in model.yml with the sweep as provenance.
--confirm-on-test evaluates once on test at that cut and records a test
spend on the run; a second spend on the same benchmark version warns and
lists both. Deriving on the test split is refused.
Populations are named, not implied¶
dataset:
isolation:
fields: [clip, camera, site] # row fields, fine -> coarse
policy: {val: camera, benchmark: camera, blind: site}
pins:
val: ["camera:G339"]
benchmark: ["camera:G341", "camera:G421"]
blind_samples: datasets/samples/blind_v001.jsonl
group_by keeps correlated rows on one side of a split; it cannot say which
side a camera lands on or that a site is absent from training. isolation
declares the hierarchy and the level each held-out population is disjoint at;
pins move whole groups after the hash split, and a pinned population is
exactly its pins. prepare refuses to write splits that violate the policy,
and audit re-checks the ones on disk.
Every prepare records a benchmark version (output/prepared/benchmark.json,
an order-insensitive hash of the test rows plus the pins) that reports carry
as extras.benchmark_version. Editing benchmark_samples in place is
refused; a new file is a new version, so floors keep naming the population
they were derived on.
maatml evaluate --blind spends the blind manifest once per frozen
candidate: the run must hold a production gate pass whose fingerprint
(evaluation config + weights) is unchanged, the gates are enforced on the
blind rows, and the spend is recorded on the run. A repeat needs --force
and is recorded as forced.
maatml train --seeds N trains the same recipe at N seeds and records the
spread (output/seeds/<first-run>-xN.json), which gates derive
--seed-study floors on.
Sources carry their licence¶
A sidecar Markdown table, one row per source (licence, commercial-use,
sign-off required; consent and attribution read when present), keyed on
another row field when dataset.attribution_field names one. prepare
refuses a row without a source or without a table row, a blocked or unsigned
sign-off, and a no / unknown commercial-use unless the sign-off itself
reads accepted-risk ā <name> <date>. Acceptance is signed in the table, not
passed as a flag, so it carries a name and a date.
Every prepare writes output/prepared/corpus.lock.json (input files by
sha256 and row count, the attribution rows used, the risk accepted), and
export copies it into manifest.json as corpus_lock, with the rows
rendered as the bundle's ATTRIBUTION.md. Declared metadata only; nothing is
looked up.
Training tells the truth earlier¶
training:
select_by: all_layers_pass_rate # choose the checkpoint on val, by the gated metric
keep_checkpoints: 4
After training, every saved checkpoint-<step> and the final weights are
evaluated on val with the evaluate harness; the best is recorded on the
run and the run id resolves to it from then on, with evidence still recorded
against the run. Reports under output/eval/select/<run>/.
Every evaluate reports pathologies (never_fires, identical_output,
one_class, plus a plugin's __pathologies__), and at the smoke tier each
one is a failing gate, so a rehearsal cannot pass on a model that does not
work. distill refuses a prompt pool that overlaps a held-out population
before any teacher call.
Runs travel¶
maatml runs <model-dir> --pack RUN # output/bundles/<run>.maatml-run.tar.gz
maatml runs <model-dir> --adopt BUNDLE # on the receiving machine
maatml report <model-dir> # output/REPORT.md (--format csv)
A bundle holds the run directory (without checkpoint-* unless
--with-checkpoints), the run's eval reports and caches, the registry record,
the environment and the model.yml fingerprint, every file hashed. --adopt
verifies each file, refuses another identity or recipe and an existing run
without --force, and appends the record with its paths rewritten. Records
move; jobs do not.
Every train record and every evaluate carries an environment manifest: git SHA, Python, OS, package versions, CUDA / cuDNN, GPUs and driver, determinism settings.
maatml report renders runs, every report's metrics with k / n and the
Wilson bound beside the floor that judged them, slice and pathology gates,
seed statistics and spends, from runs.jsonl, output/eval/*.json and
output/seeds/*.json alone, so it regenerates byte-identically and never
reads model.yml.
The configuration surface for all of this is in the skill reference,
docs/skill/references/model-yml.md.