Experiment Rigor
Turn an experiment into a typed record.yaml whose report.md is derived, not
written. This is a rigid skill: the scripts under scripts/ are the
mechanism, and each bright line below is a gate that exits non-zero with a stable
error code (validate.py), not advice to remember. The dividing line the
discipline draws: methods uncertainty is disqualifying, effect uncertainty is
declarable — a reader who cannot reconstruct what was manipulated, where, and
how it was measured has found a defect; a wide confidence interval is an honest
result.
Paths below are relative to ${CLAUDE_PLUGIN_ROOT}/skills/experiment-rigor/, and
PyYAML is the scripts' only non-stdlib dependency — run them as the gates do:
uv run --no-project --with pyyaml -- python \
"${CLAUDE_PLUGIN_ROOT}/skills/experiment-rigor/scripts/validate.py" <record.yaml>
The same two gates ship as pre-commit hooks (experiment-rigor-validate,
experiment-rigor-render-check); the README names the install surface.
The tier ladder
tier is a field, not a second skill. It selects the required fields and which
gates apply. Tier-0 is the exception: it names no artifact, so it never appears
as a tier: value.
- check (tier-0) — the structured check. An evaluation act answered inline in
the response: no file, no record, no gate. An evaluation act asks a question
whose correct answer is a valuative claim ("is this effective", "which is
better"); an execution or lookup request asks for an action or a fact and
owes nothing here. The five-element shape — method, metric, result(s) with
denominators, conclusion, and a one-line "what this updates" — the boundary,
and two worked examples live in
references/report-skeleton.md. This rung is
guidance, not a bright line, and entry above it is unchanged: a record is owed
when a decision rides on the result.
- probe — cheap and exploratory. Refuses a confirmatory verdict or any
posterior; a probe that wants either graduates to measurement.
- measurement — a frozen pre-registration and a reported interval on every
rate.
- decision — adds the comprehension gate: fresh-context readers reconstruct
the Methods before the result is allowed to move a decision.
The loop
- Copy the tier skeleton from
templates/<tier>.yaml; the field guide is
templates/SCHEMA.md (generated from templates/schema.json, the canonical
schema).
- Author the record. While drafting,
validate.py <record> --schema-only checks
shape without the context gates (they are skipped and listed).
- Freeze the plan: commit the pre-registration and record
plan_frozen_at.commit before the first run.
- Run the experiment; fill results.
from_fathom.py maps a fathom ledger into
the run-derived fields.
validate.py <record> runs the full gate. render.py <record> derives
report.md; render.py --check is the drift gate over a committed pair. Never
hand-edit report.md.
The activation line
Whenever the frame engages, one plain line opens the work product, naming the
tier and the artifact behind it. At probe and above it is generated, not typed:
render.py --activation-line <record> prints it, and --check-activation-line "<line>" <record> exits non-zero when the tier or the path disagrees — the line
is a claim tied to the artifact, not a badge. At tier-0 the artifact reference is
the literal inline, which nothing resolves.
[experiment-rigor | check -> inline]
[experiment-rigor | measurement -> experiments/retry-backoff/record.yaml]
Bright lines
Each is a gate in validate.py; the error code is named so a failure points at
the rule.
- The freeze (
ER-PREREG). The pre-registration subset — design.cells, each
outcome's role, operationalization, and verifier hash, and analysis_plan —
is reconstructed with git show on plan_frozen_at.commit and compared to the
analyzed record. Any drift fails; a cut forced after the freeze is an amendment,
not a drift — design.amendments[] re-plans declared cells' planned_n under
analysis_plan.amendments[]'s chronology. A confirmatory verdict is legal only on an
outcome whose frozen role is confirmatory. The two guards the freeze buys —
a null bank, and a pre-committed rule for what a finding must survive — are
worked, with the day they both fired, in references/two-guards.md.
- Declared-cells reconciliation (
ER-RECON). N_expected is the sum of
design.cells[].planned_n (as amended), and it must equal the disposition total and every
outcome's sum of arm denominators. The model tier is one named factor level, not
a separate multiplier.
- The confirmatory / exploratory partition.
outcomes[].role belongs to the
frozen plan. An outcome added after the freeze carries added_after_freeze: true
and role: exploratory — the quarantine — and may report only an
exploratory_signal or inconclusive verdict, never a confirmatory one.
- The small-n CI refusal (
ER-STATS). No CLT / normal method below a cell
denominator of 30. Allowed: wilson, clopper_pearson, beta_binomial. Every
stated interval is recomputed from stats.py and must match to four decimals.
references/small-n-stats.md covers the rest.
- A rate needs both a numerator and a denominator (
ER-SCHEMA). A rate lives
under results.<outcome>.arms.<arm> with both; a loose rate at the outcome
level fails.
- Record is source, report is derived (
ER-PARITY, render.py --check). The
record is the single source of truth; report.md is regenerated and must not
contradict it.
- Probe self-labeling (
ER-PROBE). A probe carrying a confirmatory verdict or
a posterior fails; the message names the graduation path to measurement.
- Threat coverage (
ER-THREAT). Every core threat in the closed enum carries
a row with a status and a statement; silence on one fails. The enum is in references/threats-catalog.md.
What binds, role-generically
- Readiness before build. When a spec-readiness gate is installed (for
example keel's Definition of Ready), a decision-tier experiment that will inform
a build defers spec readiness to it rather than re-deciding it here. Absent one,
the experiment stands alone.
- Fresh-context readers. The decision-tier comprehension block records
independent readers who reconstruct the Methods with no access to this
conversation. Any fresh-context reader tooling that is installed can produce
those reads; the block records the transcript path and the four verbatim
answers, and their genuineness is ceded to review. The gate checks presence,
resolution, and unanimity, not sincerity.
Boundaries
Running the scenario matrix, scoring a bank, or driving the paid run is fathom's
fathom-eval; this skill structures and gates the record that results. Measuring
one skill's trigger activation is evaluate-skill. Judging whether a spec is
ready to decompose is a readiness gate (keel's Definition of Ready). A throwaway
spot-check you will decide nothing on needs none of this.
Correct-usage check
1---2name: experiment-rigor3description: Structure an experiment and its write-up so the Methods reconstruct without the conversation and the uncertainty is declared, not hidden — a typed record.yaml across a probe / measurement / decision tier ladder, every load-bearing rule a script that exits non-zero rather than a line of prose. Use when you pre-register an A/B, freeze a plan before running it, write up an experiment or a comparison, add error bars or a confidence interval to a rate, reconcile declared cells against the runs that happened, separate confirmatory from exploratory outcomes, name the threats to validity, design the 2x2, or ask whether a skill, tier, model, or strategy actually helps and how you would show it rigorously. Covers the small-n refusal (no CLT below 30 — Wilson, Clopper-Pearson, or a within-experiment Beta-Binomial), the design-arithmetic reconciliation (declared cells == disposition == denominators), the plan freeze and its drift gate, the rate that needs both a numerator and a denominator, probe self-labeling, and recor4---56# Experiment Rigor78Turn an experiment into a typed `record.yaml` whose `report.md` is derived, not9written. This is a **rigid** skill: the scripts under `scripts/` are the10mechanism, and each bright line below is a gate that exits non-zero with a stable11error code (`validate.py`), not advice to remember. The dividing line the12discipline draws: **methods uncertainty is disqualifying, effect uncertainty is13declarable** — a reader who cannot reconstruct what was manipulated, where, and14how it was measured has found a defect; a wide confidence interval is an honest15result.1617Paths below are relative to `${CLAUDE_PLUGIN_ROOT}/skills/experiment-rigor/`, and18PyYAML is the scripts' only non-stdlib dependency — run them as the gates do:1920```bash21uv run --no-project --with pyyaml -- python \22 "${CLAUDE_PLUGIN_ROOT}/skills/experiment-rigor/scripts/validate.py" <record.yaml>23```2425The same two gates ship as pre-commit hooks (`experiment-rigor-validate`,26`experiment-rigor-render-check`); the README names the install surface.2728## The tier ladder2930`tier` is a field, not a second skill. It selects the required fields and which31gates apply. Tier-0 is the exception: it names no artifact, so it never appears32as a `tier:` value.3334- **check** (tier-0) — the structured check. An evaluation act answered inline in35 the response: no file, no record, no gate. An *evaluation act* asks a question36 whose correct answer is a valuative claim ("is this effective", "which is37 better"); an *execution or lookup request* asks for an action or a fact and38 owes nothing here. The five-element shape — method, metric, result(s) with39 denominators, conclusion, and a one-line "what this updates" — the boundary,40 and two worked examples live in `references/report-skeleton.md`. This rung is41 guidance, not a bright line, and entry above it is unchanged: a record is owed42 when a decision rides on the result.43- **probe** — cheap and exploratory. Refuses a confirmatory verdict or any44 posterior; a probe that wants either graduates to measurement.45- **measurement** — a frozen pre-registration and a reported interval on every46 rate.47- **decision** — adds the comprehension gate: fresh-context readers reconstruct48 the Methods before the result is allowed to move a decision.4950## The loop51521. Copy the tier skeleton from `templates/<tier>.yaml`; the field guide is53 `templates/SCHEMA.md` (generated from `templates/schema.json`, the canonical54 schema).552. Author the record. While drafting, `validate.py <record> --schema-only` checks56 shape without the context gates (they are skipped and listed).573. Freeze the plan: commit the pre-registration and record58 `plan_frozen_at.commit` before the first run.594. Run the experiment; fill results. `from_fathom.py` maps a fathom ledger into60 the run-derived fields.615. `validate.py <record>` runs the full gate. `render.py <record>` derives62 `report.md`; `render.py --check` is the drift gate over a committed pair. Never63 hand-edit `report.md`.6465## The activation line6667Whenever the frame engages, one plain line opens the work product, naming the68tier and the artifact behind it. At `probe` and above it is generated, not typed:69`render.py --activation-line <record>` prints it, and `--check-activation-line70"<line>" <record>` exits non-zero when the tier or the path disagrees — the line71is a claim tied to the artifact, not a badge. At tier-0 the artifact reference is72the literal `inline`, which nothing resolves.7374```text75[experiment-rigor | check -> inline]76[experiment-rigor | measurement -> experiments/retry-backoff/record.yaml]77```7879## Bright lines8081Each is a gate in `validate.py`; the error code is named so a failure points at82the rule.8384- **The freeze (`ER-PREREG`).** The pre-registration subset — `design.cells`, each85 outcome's `role`, operationalization, and verifier hash, and `analysis_plan` —86 is reconstructed with `git show` on `plan_frozen_at.commit` and compared to the87 analyzed record. Any drift fails; a cut forced after the freeze is an amendment,88 not a drift — `design.amendments[]` re-plans declared cells' `planned_n` under89 `analysis_plan.amendments[]`'s chronology. A confirmatory verdict is legal only on an90 outcome whose frozen `role` is `confirmatory`. The two guards the freeze buys —91 a null bank, and a pre-committed rule for what a finding must survive — are92 worked, with the day they both fired, in `references/two-guards.md`.93- **Declared-cells reconciliation (`ER-RECON`).** `N_expected` is the sum of94 `design.cells[].planned_n` (as amended), and it must equal the disposition total and every95 outcome's sum of arm denominators. The model tier is one named factor level, not96 a separate multiplier.97- **The confirmatory / exploratory partition.** `outcomes[].role` belongs to the98 frozen plan. An outcome added after the freeze carries `added_after_freeze: true`99 and `role: exploratory` — the quarantine — and may report only an100 `exploratory_signal` or `inconclusive` verdict, never a confirmatory one.101- **The small-n CI refusal (`ER-STATS`).** No CLT / normal method below a cell102 denominator of 30. Allowed: `wilson`, `clopper_pearson`, `beta_binomial`. Every103 stated interval is recomputed from `stats.py` and must match to four decimals.104 `references/small-n-stats.md` covers the rest.105- **A rate needs both a numerator and a denominator (`ER-SCHEMA`).** A rate lives106 under `results.<outcome>.arms.<arm>` with both; a loose rate at the outcome107 level fails.108- **Record is source, report is derived (`ER-PARITY`, `render.py --check`).** The109 record is the single source of truth; `report.md` is regenerated and must not110 contradict it.111- **Probe self-labeling (`ER-PROBE`).** A probe carrying a confirmatory verdict or112 a posterior fails; the message names the graduation path to measurement.113- **Threat coverage (`ER-THREAT`).** Every core threat in the closed enum carries114 a row with a status and a statement; silence on one fails. The enum is in `references/threats-catalog.md`.115116## What binds, role-generically117118- **Readiness before build.** When a spec-readiness gate is installed (for119 example keel's Definition of Ready), a decision-tier experiment that will inform120 a build defers spec readiness to it rather than re-deciding it here. Absent one,121 the experiment stands alone.122- **Fresh-context readers.** The decision-tier comprehension block records123 independent readers who reconstruct the Methods with no access to this124 conversation. Any fresh-context reader tooling that is installed can produce125 those reads; the block records the transcript path and the four verbatim126 answers, and their genuineness is ceded to review. The gate checks presence,127 resolution, and unanimity, not sincerity.128129## Boundaries130131Running the scenario matrix, scoring a bank, or driving the paid run is fathom's132`fathom-eval`; this skill structures and gates the record that results. Measuring133one skill's trigger activation is `evaluate-skill`. Judging whether a spec is134ready to decompose is a readiness gate (keel's Definition of Ready). A throwaway135spot-check you will decide nothing on needs none of this.136137## Correct-usage check138139- [ ] The record passes `validate.py` at its declared tier (exit 0).140- [ ] The Methods reconstruct from the record alone — what was manipulated, where141 it was placed, how each outcome was operationalized, whether execution was142 real — without the conversation.143- [ ] Every reported rate carries a recomputable interval; no CLT below 30.144- [ ] `report.md` is derived (`render.py`), not hand-edited (`render.py --check`145 clean).146- [ ] The work product opens with the activation line — `inline` at tier-0, and at147 `probe` and above the generated line, `--check-activation-line` clean.