Data Exhaust
An experiment's exhaust is everything worth publishing once it reaches a
terminal verdict: the aggregate artifacts already committed under
analysis-committed/<cell_id>/ (dose-response tables, direction fits, gate
AUROCs, manifests) and, separately and only where the license permits it, the
per-row generation text. This skill turns that exhaust into HF-ready dataset
directories, gates them, and uploads them. It never decides WHAT an experiment
found; it only packages what the experiment already produced.
When to use
An experiment has reached a terminal status (resolved, null-result, or
falsified in experiment.yaml) and its analysis-committed/ artifacts, or
locally staged row-level JSONL, are ready to become a public HF dataset. Do
not run this on a draft, signed, or running experiment: there is nothing
terminal to package yet, and publishing mid-run invites goalpost confusion
about what the released data actually represents.
Build-time requirement (binding on every harness, not just packaging)
Exhaust can only be packaged if the run persisted it. Every generation
harness MUST write, per row or per sample, into its gitignored row-level run
log: the raw generation text, the FULL sub-grade dict its grader computes
(not just the final booleans), and the termination/readback inputs the gate
math consumes. Booleans-only run logs are a build defect: they make failure
anatomy unrecoverable without a paid re-run, and they leave response-quality
problems (degenerate output, format-checker artifacts, batched-decode
misgrading) undetectable after the fact. The cautionary case is
experiments/snap-seed-sampled-decode-replication (H3, 2026-07-13): its
grader computed the complete sub-grade dict and the pipeline discarded it,
so the registered falsifier fired with the failure mechanism undiagnosable
from committed artifacts and the resolve PR had to be held for a
text-persisting re-run. Containment is unchanged by this rule: text stays
under gitignored analysis/; only ID-manifests and aggregates go to
analysis-committed/; publication still runs through the license gate
below. Harness builders and pre-sign reviewers both check this: a CPU smoke
that asserts the persistence schema (text present, sub-grade dict intact)
is part of the standard suite.
This rule is now structurally enforced, not just procedural. Generation
harnesses open their row-level run log through
experiments/common/runlog_contract.py's open_generation_runlog, which
refuses to accept any record missing non-empty text unless the harness
declares a textless_reason (durably recorded in the log's own meta
fingerprint). bin/exp validate separately requires every experiment's
experiment.yaml to carry a top-level text_capture field
(enabled, not-applicable, or textless: <reason>); a missing or invalid
value is a hard error. textless: is the only sanctioned opt-out from
either layer -- there is no way to omit text capture silently.
Start here
| Task |
Do |
| Understand the two dataset shapes and their fields |
read reference/dataset-schema.md |
| Check or update a source's redistribution verdict |
read reference/license-gates.md |
| Build a dataset dir |
run scripts/build_exhaust_dataset.py (see below) |
| Gate a built dataset dir before upload |
run scripts/verify_exhaust.py |
| Actually upload |
run scripts/upload_exhaust.py |
The five-move workflow
- License-gate. Before building anything with row text, confirm every
source that will appear has a verdict in
reference/license-gates.md. Each source
resolves to one of three row dispositions: full text (
permitted /
permitted-with-conditions), text-free (text-free-only, row kept but
its text-bearing fields stripped), or excluded entirely (forbidden /
pending-audit). A source with no table entry is pending-audit, the
most conservative disposition, not the text-free one. Aggregate-only
builds do not need this step to have resolved verdicts, since they carry
no source text, but they still run the same structural hard-exclusion
scan.
- Build. Run
build_exhaust_dataset.py against the experiment directory.
Aggregate shape (default) recursively copies EVERY file under
analysis-committed/ -- any depth, flat or celled layout -- byte-for-byte
into <out-dir>/, preserving relative paths. There is no filename
allowlist: analysis-committed/ is already the repo's containment
boundary, so the only filter applied is the hard-exclusion deny-list (a
file whose relative PATH matches OpenMOSS/Cheng IDK or
bridge_llama2_7b_chat is skipped and recorded with a reason; a file
whose CONTENT matches one aborts the whole build). Row-level shape
(--rows-dir <dir>) reads locally staged <cell_id>.jsonl files and
gives every row one of the three dispositions above, per row, based on
its own source field -- a single rows.jsonl can mix full-text and
text-free rows from different sources. Excluded rows are counted, not
written in any form. The build never writes inside the experiment's own
directory; only --out-dir is touched.
- Verify. Run
verify_exhaust.py --dataset-dir <out-dir> --experiment-dir experiments/<slug> against the build output. It checks schema/sha256
consistency, row counts against PROVENANCE.json, a containment lint over
every file (including an independent PER-ROW re-check of each row's
license-gate disposition, so a file mixing sources only passes if each row
matches its own source's rule), that any permitted-with-conditions
source's disclosure text actually landed in the README, that
license-gates.md's machine-readable table is well-formed and still
carries both hard exclusions, and (aggregate shape) a completeness check
that independently re-walks the source analysis-committed/ tree right
now and requires the staged files plus the recorded exclusions to equal it
exactly -- omitting --experiment-dir FAILS this check rather than
skipping it silently, so always pass it. A single failure fails the whole
gate loudly; it does not report a partial pass.
- Upload. Run
upload_exhaust.py --dataset-dir <out-dir>. Default is a
dry run: it prints the resolved repo id (professorsynapse/eh-<slug> for
aggregate, professorsynapse/eh-<slug>-rows for row-level), the file list,
and the provenance summary, and touches no network. Pass --live only
after the publication gate below is satisfied, and only with HF_TOKEN set
in the environment (never printed, never logged).
- Record. After a real upload, copy the printed HF revision SHA into the
experiment's
NOTEBOOK.md and into docs/public-artifacts.md (see that
file's own row format). An upload without a recorded revision is not
finished.
Publication gate
Do not run upload_exhaust.py --live until:
- The experiment's
experiment.yaml status is terminal (resolved,
null-result, or falsified), not draft/signed/running.
verify_exhaust.py passed with zero errors on the exact dir about to be
uploaded.
- For a row-level dataset, every source appearing has an explicit
permitted, permitted-with-conditions, or text-free-only verdict in
reference/license-gates.md -- not pending-audit. For any
permitted-with-conditions source, its disclosure text must actually be
in the built README (verified, not just built).
- The user has explicitly approved this specific upload. A prior approval
for a different experiment or a different dataset shape does not carry
over.
This mirrors the model-artifact publication gate in
.skills/experiment-runner/reference/hf-publication.md; that reference stays
the source of truth for model checkpoints, this skill is the analogous gate
for exhaust data.
Invariants
- The public git repo never receives question text, answer aliases, row-level
generation text, or token IDs. HF is the sanctioned text channel, and only
for license-permitted sources. Do not commit a built dataset dir; the
--out-dir you point the builder at should be a scratch location outside
version control.
- Two hard exclusions apply regardless of anything else: OpenMOSS/Cheng IDK
data and
bridge_llama2_7b_chat. These are enforced structurally in code
(scripts/build_exhaust_dataset.py and scripts/verify_exhaust.py), not
only by the license-gates table, so an accidental table edit cannot reopen
them.
- A source with no entry in
reference/license-gates.md is pending-audit
(excluded entirely), never text-free-only and never permitted. Never
infer a permissive verdict from a different dataset's card or from memory
of a prior release.
HF_TOKEN comes from the environment only. Never print, log, or echo it.
- Quick commands:
python3 .skills/data-exhaust/scripts/build_exhaust_dataset.py \
--experiment-dir experiments/<slug> \
--out-dir /tmp/<slug>-exhaust-aggregate
python3 .skills/data-exhaust/scripts/verify_exhaust.py \
--dataset-dir /tmp/<slug>-exhaust-aggregate \
--experiment-dir experiments/<slug> # required for the aggregate completeness check
python3 .skills/data-exhaust/scripts/upload_exhaust.py \
--dataset-dir /tmp/<slug>-exhaust-aggregate # dry run by default; add --live once approved
Skill maintenance
Edit the canonical tree under .skills/data-exhaust/ only. .agents/ and
.claude/ are generated mirrors; never hand-edit them. After canonical edits:
python3 bin/sync_skills.py --write --skill data-exhaust
python3 bin/sync_skills.py --check --skill data-exhaust
1---2name: data-exhaust3description: Package a terminal experiment's data exhaust (aggregate stats and, where license-permitted, row-level generation text) into reproducible Hugging Face datasets, with a license gate, containment lint, and a thin uploader. Use when an experiment under experiments/<slug>/ reaches a terminal verdict (resolved, null-result, or falsified) and its analysis-committed/ artifacts should be published.4---56# Data Exhaust78An experiment's exhaust is everything worth publishing once it reaches a9terminal verdict: the aggregate artifacts already committed under10`analysis-committed/<cell_id>/` (dose-response tables, direction fits, gate11AUROCs, manifests) and, separately and only where the license permits it, the12per-row generation text. This skill turns that exhaust into HF-ready dataset13directories, gates them, and uploads them. It never decides WHAT an experiment14found; it only packages what the experiment already produced.1516## When to use1718An experiment has reached a terminal status (`resolved`, `null-result`, or19`falsified` in `experiment.yaml`) and its `analysis-committed/` artifacts, or20locally staged row-level JSONL, are ready to become a public HF dataset. Do21not run this on a `draft`, `signed`, or `running` experiment: there is nothing22terminal to package yet, and publishing mid-run invites goalpost confusion23about what the released data actually represents.2425## Build-time requirement (binding on every harness, not just packaging)2627Exhaust can only be packaged if the run persisted it. Every generation28harness MUST write, per row or per sample, into its gitignored row-level run29log: the raw generation text, the FULL sub-grade dict its grader computes30(not just the final booleans), and the termination/readback inputs the gate31math consumes. Booleans-only run logs are a build defect: they make failure32anatomy unrecoverable without a paid re-run, and they leave response-quality33problems (degenerate output, format-checker artifacts, batched-decode34misgrading) undetectable after the fact. The cautionary case is35`experiments/snap-seed-sampled-decode-replication` (H3, 2026-07-13): its36grader computed the complete sub-grade dict and the pipeline discarded it,37so the registered falsifier fired with the failure mechanism undiagnosable38from committed artifacts and the resolve PR had to be held for a39text-persisting re-run. Containment is unchanged by this rule: text stays40under gitignored `analysis/`; only ID-manifests and aggregates go to41`analysis-committed/`; publication still runs through the license gate42below. Harness builders and pre-sign reviewers both check this: a CPU smoke43that asserts the persistence schema (text present, sub-grade dict intact)44is part of the standard suite.4546This rule is now structurally enforced, not just procedural. Generation47harnesses open their row-level run log through48`experiments/common/runlog_contract.py`'s `open_generation_runlog`, which49refuses to accept any record missing non-empty text unless the harness50declares a `textless_reason` (durably recorded in the log's own meta51fingerprint). `bin/exp validate` separately requires every experiment's52`experiment.yaml` to carry a top-level `text_capture` field53(`enabled`, `not-applicable`, or `textless: <reason>`); a missing or invalid54value is a hard error. `textless:` is the only sanctioned opt-out from55either layer -- there is no way to omit text capture silently.5657## Start here5859| Task | Do |60|------|----|61| Understand the two dataset shapes and their fields | read [reference/dataset-schema.md](reference/dataset-schema.md) |62| Check or update a source's redistribution verdict | read [reference/license-gates.md](reference/license-gates.md) |63| Build a dataset dir | run `scripts/build_exhaust_dataset.py` (see below) |64| Gate a built dataset dir before upload | run `scripts/verify_exhaust.py` |65| Actually upload | run `scripts/upload_exhaust.py` |6667## The five-move workflow68691. **License-gate.** Before building anything with row text, confirm every70 source that will appear has a verdict in71 [reference/license-gates.md](reference/license-gates.md). Each source72 resolves to one of three row dispositions: full text (`permitted` /73 `permitted-with-conditions`), text-free (`text-free-only`, row kept but74 its text-bearing fields stripped), or excluded entirely (`forbidden` /75 `pending-audit`). A source with no table entry is `pending-audit`, the76 most conservative disposition, not the text-free one. Aggregate-only77 builds do not need this step to have resolved verdicts, since they carry78 no source text, but they still run the same structural hard-exclusion79 scan.802. **Build.** Run `build_exhaust_dataset.py` against the experiment directory.81 Aggregate shape (default) recursively copies EVERY file under82 `analysis-committed/` -- any depth, flat or celled layout -- byte-for-byte83 into `<out-dir>/`, preserving relative paths. There is no filename84 allowlist: `analysis-committed/` is already the repo's containment85 boundary, so the only filter applied is the hard-exclusion deny-list (a86 file whose relative PATH matches OpenMOSS/Cheng IDK or87 `bridge_llama2_7b_chat` is skipped and recorded with a reason; a file88 whose CONTENT matches one aborts the whole build). Row-level shape89 (`--rows-dir <dir>`) reads locally staged `<cell_id>.jsonl` files and90 gives every row one of the three dispositions above, per row, based on91 its own `source` field -- a single `rows.jsonl` can mix full-text and92 text-free rows from different sources. Excluded rows are counted, not93 written in any form. The build never writes inside the experiment's own94 directory; only `--out-dir` is touched.953. **Verify.** Run `verify_exhaust.py --dataset-dir <out-dir> --experiment-dir96 experiments/<slug>` against the build output. It checks schema/sha25697 consistency, row counts against `PROVENANCE.json`, a containment lint over98 every file (including an independent PER-ROW re-check of each row's99 license-gate disposition, so a file mixing sources only passes if each row100 matches its own source's rule), that any `permitted-with-conditions`101 source's disclosure text actually landed in the README, that102 `license-gates.md`'s machine-readable table is well-formed and still103 carries both hard exclusions, and (aggregate shape) a completeness check104 that independently re-walks the source `analysis-committed/` tree right105 now and requires the staged files plus the recorded exclusions to equal it106 exactly -- omitting `--experiment-dir` FAILS this check rather than107 skipping it silently, so always pass it. A single failure fails the whole108 gate loudly; it does not report a partial pass.1094. **Upload.** Run `upload_exhaust.py --dataset-dir <out-dir>`. Default is a110 dry run: it prints the resolved repo id (`professorsynapse/eh-<slug>` for111 aggregate, `professorsynapse/eh-<slug>-rows` for row-level), the file list,112 and the provenance summary, and touches no network. Pass `--live` only113 after the publication gate below is satisfied, and only with `HF_TOKEN` set114 in the environment (never printed, never logged).1155. **Record.** After a real upload, copy the printed HF revision SHA into the116 experiment's `NOTEBOOK.md` and into `docs/public-artifacts.md` (see that117 file's own row format). An upload without a recorded revision is not118 finished.119120## Publication gate121122Do not run `upload_exhaust.py --live` until:1231241. The experiment's `experiment.yaml` status is terminal (`resolved`,125 `null-result`, or `falsified`), not `draft`/`signed`/`running`.1262. `verify_exhaust.py` passed with zero errors on the exact dir about to be127 uploaded.1283. For a row-level dataset, every source appearing has an explicit129 `permitted`, `permitted-with-conditions`, or `text-free-only` verdict in130 `reference/license-gates.md` -- not `pending-audit`. For any131 `permitted-with-conditions` source, its disclosure text must actually be132 in the built README (verified, not just built).1334. The user has explicitly approved this specific upload. A prior approval134 for a different experiment or a different dataset shape does not carry135 over.136137This mirrors the model-artifact publication gate in138`.skills/experiment-runner/reference/hf-publication.md`; that reference stays139the source of truth for model checkpoints, this skill is the analogous gate140for exhaust data.141142## Invariants143144- The public git repo never receives question text, answer aliases, row-level145 generation text, or token IDs. HF is the sanctioned text channel, and only146 for license-permitted sources. Do not commit a built dataset dir; the147 `--out-dir` you point the builder at should be a scratch location outside148 version control.149- Two hard exclusions apply regardless of anything else: OpenMOSS/Cheng IDK150 data and `bridge_llama2_7b_chat`. These are enforced structurally in code151 (`scripts/build_exhaust_dataset.py` and `scripts/verify_exhaust.py`), not152 only by the license-gates table, so an accidental table edit cannot reopen153 them.154- A source with no entry in `reference/license-gates.md` is `pending-audit`155 (excluded entirely), never `text-free-only` and never permitted. Never156 infer a permissive verdict from a different dataset's card or from memory157 of a prior release.158- `HF_TOKEN` comes from the environment only. Never print, log, or echo it.159- Quick commands:160161```bash162python3 .skills/data-exhaust/scripts/build_exhaust_dataset.py \163 --experiment-dir experiments/<slug> \164 --out-dir /tmp/<slug>-exhaust-aggregate165166python3 .skills/data-exhaust/scripts/verify_exhaust.py \167 --dataset-dir /tmp/<slug>-exhaust-aggregate \168 --experiment-dir experiments/<slug> # required for the aggregate completeness check169170python3 .skills/data-exhaust/scripts/upload_exhaust.py \171 --dataset-dir /tmp/<slug>-exhaust-aggregate # dry run by default; add --live once approved172```173174## Skill maintenance175176Edit the canonical tree under `.skills/data-exhaust/` only. `.agents/` and177`.claude/` are generated mirrors; never hand-edit them. After canonical edits:178179```bash180python3 bin/sync_skills.py --write --skill data-exhaust181python3 bin/sync_skills.py --check --skill data-exhaust182```