Validation recipes¶
Three small acceptance checks that catch a wrong analysis a plain run would not, and how to correct a wrong expectation afterwards. Each recipe is built from what the repository already has: a validation binding, the data-contract checks, and a development fixture that proves the check fails when it should. Nothing here adds a mandatory step; a plan without a validation-bindings block is approved and run as before.
A bound check is one claim, not a certificate. verify reads the check’s result file and reports passed, failed, not run, changed, unattributable or stale (see Validation in the report). It does not run the check and does not say the analysis is right.
On this page: How a recipe is written · 1. Sample and key join · 2. Contrast direction · 3. Complete result table · Correcting a wrong expectation · Limits
How a recipe is written¶
Every recipe states the same nine things, so you can judge it before approving it:
the claim it supports, and nothing more;
the inputs it reads;
the expected answer and where that answer comes from;
the invocation the plan names;
the binding that freezes the check and its expectation at approval;
what pass, fail and unavailable each mean;
a deliberate failure, with the development fixture that reproduces it;
a benign control that must still pass;
the limit of the claim.
The fixtures live in tests/e2e/defects.py and run on the fixture project (tests/fixtures/mycelium-project). Their ids (V-DC08, V-DC09, V-DC12) are in the table of each recipe. They are development tests: they show the check works on that project, not that it works on yours.
1. Sample and key join¶
Claim |
Sample ids match between the metadata and the counts, and the analysis gives the same answer when the metadata rows are reordered. |
Inputs |
|
Expected answer |
Equal id sets on both sides (from the data-contract check); identical log2 fold changes after a row permutation (the answer is the analysis’s own output, so the check compares the analysis with itself under a reordering, not with an outside truth). |
Invocation |
|
Pass / fail / unavailable |
Pass: both result files say passed. Fail: an id is on one side only, or any log2 fold change moves. Not run: no matched run of the invocation under this approval. |
Deliberate failure |
|
Benign control |
The unmutated step 02: both checks pass. The shuffle itself is the control, since a correct keyed analysis ignores row order. |
Limit |
It shows that this code ignores metadata row order on the test data. It does not show that the pairing logic is right on real data. |
<!-- validation-bindings {"version": 1, "checks": [
{"id": "sample-alignment", "plan_row": "4", "required": true, "defined_by": "researcher-approved",
"test_paths": ["analysis/vaccine-response/checks/check_sample_alignment.py",
"analysis/vaccine-response/checks/sample_alignment_contract.json"],
"invocation": ["python3", "analysis/vaccine-response/checks/check_sample_alignment.py"],
"evidence_path": "analysis/vaccine-response/outputs/sample_alignment_check.json"},
{"id": "shuffle-invariance", "plan_row": "5", "required": true, "defined_by": "researcher-approved",
"test_paths": ["analysis/vaccine-response/checks/check_shuffle.py"],
"invocation": ["python3", "analysis/vaccine-response/checks/check_shuffle.py"],
"evidence_path": "analysis/vaccine-response/outputs/shuffle_check.json"}]} -->
The contract file is a data-contract JSON; its sample_alignment check names key and a reference (here the counts header). Keys are compared as sets, so a different order is not a failure for keyed data.
2. Contrast direction¶
Claim |
On a tiny dataset whose answer is known, the real analysis step reports a positive log2 fold change for a gene that is higher in the vaccine arm and a negative one for a gene that is lower. |
Inputs |
A small dataset built inside the check (donors, arms and counts written in the script) and the analysis step itself. |
Expected answer |
Sign of two genes, from the dataset’s construction. It is independent of the analysis only as far as you wrote it independently: a check built from the same assumption as the code passes a wrong code. |
Invocation |
|
Pass / fail / unavailable |
Pass: signs are as stated. Fail: a sign is reversed. Not run: no matched run. An unavailable backend is not a pass; a check that cannot import the step reports |
Deliberate failure |
|
Benign control |
The unmutated step 02. |
Limit |
It tests the direction the check encodes, on one synthetic dataset. A reference level that differs on real data is not seen. |
<!-- validation-bindings {"version": 1, "checks": [
{"id": "contrast-direction", "plan_row": "4", "required": true, "defined_by": "researcher-approved",
"test_paths": ["analysis/vaccine-response/checks/check_contrast_direction.py"],
"invocation": ["python3", "analysis/vaccine-response/checks/check_contrast_direction.py"],
"evidence_path": "analysis/vaccine-response/outputs/contrast_direction_check.json"}]} -->
3. Complete result table¶
Claim |
Every gene that was tested is a row of the result table, and no other gene is. |
Inputs |
|
Expected answer |
The gene column of |
Invocation |
|
Pass / fail / unavailable |
Pass: the two gene sets are equal. Fail: genes are in one table and not the other; the result file names them. Not run: no matched run, or the result table did not exist when the check ran. |
Deliberate failure |
|
Benign control |
The unmutated step 02 writes all 60 genes and the check passes. |
Limit |
It compares the table with the declared universe. It does not say the universe was the right one, and a table filtered before testing is complete by this definition. |
<!-- validation-bindings {"version": 1, "checks": [
{"id": "complete-table", "plan_row": "9", "required": true, "defined_by": "researcher-approved",
"test_paths": ["analysis/vaccine-response/checks/check_complete_table.py",
"analysis/vaccine-response/checks/complete_table_contract.json"],
"invocation": ["python3", "analysis/vaccine-response/checks/check_complete_table.py"],
"evidence_path": "analysis/vaccine-response/outputs/complete_table_check.json"}]} -->
The contract is the same sample_alignment kind used in recipe 1, pointed at the result table instead of the metadata:
{"schema": "mycelium-extra.data_contract.v1", "table": "analysis/vaccine-response/outputs/de_results.tsv",
"checks": [{"id": "complete-table", "kind": "sample_alignment", "key": "gene",
"reference": {"path": "data/processed/vaccine-cohort/counts.tsv", "column": "gene"}}]}
Renamed columns need an explicit mapping in the check (for example comparing the gene_symbol column to the reference gene column). Meaning that no check compares, such as the unit of an effect size, stays your judgement.
Correcting a wrong expectation¶
A check can be faithful to a wrong expected answer: the plan, the code and the expectation share one mistake, every check passes, and the result is still wrong. Approval froze the expectation, so it does not change by itself. When you find the mistake:
Keep the original. Leave the old expectation file and the saved provenance as they are. The run conformed to the plan it was approved under; that stays true.
Record the correction. Use Mycelium’s usual decision log, and
decision-statusfor a decision that is being superseded. Say which file was wrong and why.Find the affected analyses. From the repository root, run
verify refs <old path> --sha <old sha256>(see Advanced features). Each check that pinned the file is listed as definite (same path and same digest), possible (same path, different or unrecorded digest, or a bundle with no validation record) or has no recorded reference. No recorded reference is not proof the file was unused.Decide. Rerun under a new approval, amend the finding, ask for review, or keep the result with a stated limitation. Nothing rewrites an old receipt or a finding for you.
An edit to a pinned expectation is a changed dependency, not automatically a confirmed correction: the gate denies runs that depend on it until you approve the plan again.
Limits¶
Hooks are not a security boundary, and a check the gate did not see run has no record; see Validation bindings for what the card says about each check.
A passing check is one observation. It is not scientific validation, and
defined_byis a declaration, not authenticated authorship.A standalone preflight followed by a separately permitted command is not prevented by bindings. To make an analysis conditional on a preflight, put both in one entry point that runs the analysis only when the preflight succeeds.
The fixture results show these checks work on the fixture project and the host and versions the tests ran on.