publishable 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. publishable-0.1.0/.gitignore +226 -0
  2. publishable-0.1.0/CITATION.cff +26 -0
  3. publishable-0.1.0/LICENSE +21 -0
  4. publishable-0.1.0/PKG-INFO +402 -0
  5. publishable-0.1.0/README.md +372 -0
  6. publishable-0.1.0/docs/design-principles.md +222 -0
  7. publishable-0.1.0/docs/experimental-designs.md +402 -0
  8. publishable-0.1.0/docs/reference.md +4391 -0
  9. publishable-0.1.0/pyproject.toml +130 -0
  10. publishable-0.1.0/src/publishable/__init__.py +39 -0
  11. publishable-0.1.0/src/publishable/apparatus.py +1003 -0
  12. publishable-0.1.0/src/publishable/artifacts.py +1408 -0
  13. publishable-0.1.0/src/publishable/base_experiment.py +76 -0
  14. publishable-0.1.0/src/publishable/base_step.py +53 -0
  15. publishable-0.1.0/src/publishable/cli.py +6261 -0
  16. publishable-0.1.0/src/publishable/coercion.py +264 -0
  17. publishable-0.1.0/src/publishable/config.py +95 -0
  18. publishable-0.1.0/src/publishable/contrasts.py +261 -0
  19. publishable-0.1.0/src/publishable/correction.py +556 -0
  20. publishable-0.1.0/src/publishable/demo.py +690 -0
  21. publishable-0.1.0/src/publishable/diagnostics.py +77 -0
  22. publishable-0.1.0/src/publishable/diff.py +614 -0
  23. publishable-0.1.0/src/publishable/docs.py +763 -0
  24. publishable-0.1.0/src/publishable/envelope.py +366 -0
  25. publishable-0.1.0/src/publishable/errors.py +21 -0
  26. publishable-0.1.0/src/publishable/estimate.py +33 -0
  27. publishable-0.1.0/src/publishable/freeze.py +608 -0
  28. publishable-0.1.0/src/publishable/generators/__init__.py +0 -0
  29. publishable-0.1.0/src/publishable/generators/experiment.py +176 -0
  30. publishable-0.1.0/src/publishable/generators/report.py +88 -0
  31. publishable-0.1.0/src/publishable/generators/step.py +59 -0
  32. publishable-0.1.0/src/publishable/generators/template.py +110 -0
  33. publishable-0.1.0/src/publishable/hashes.py +184 -0
  34. publishable-0.1.0/src/publishable/hypotheses.py +341 -0
  35. publishable-0.1.0/src/publishable/lineage.py +782 -0
  36. publishable-0.1.0/src/publishable/manifest.py +106 -0
  37. publishable-0.1.0/src/publishable/materialize.py +174 -0
  38. publishable-0.1.0/src/publishable/param.py +213 -0
  39. publishable-0.1.0/src/publishable/plugin_scaffold.py +227 -0
  40. publishable-0.1.0/src/publishable/plugins.py +316 -0
  41. publishable-0.1.0/src/publishable/provenance.py +211 -0
  42. publishable-0.1.0/src/publishable/py.typed +0 -0
  43. publishable-0.1.0/src/publishable/readme_templates/CITATION.cff.tmpl +8 -0
  44. publishable-0.1.0/src/publishable/readme_templates/LICENSE.mit.tmpl +7 -0
  45. publishable-0.1.0/src/publishable/readme_templates/README.md.tmpl +49 -0
  46. publishable-0.1.0/src/publishable/readme_templates/__init__.py +0 -0
  47. publishable-0.1.0/src/publishable/readme_templates/gitignore.tmpl +7 -0
  48. publishable-0.1.0/src/publishable/replication.py +478 -0
  49. publishable-0.1.0/src/publishable/report.py +1583 -0
  50. publishable-0.1.0/src/publishable/reproduce.py +1565 -0
  51. publishable-0.1.0/src/publishable/run_identity.py +399 -0
  52. publishable-0.1.0/src/publishable/run_record.py +324 -0
  53. publishable-0.1.0/src/publishable/runner.py +939 -0
  54. publishable-0.1.0/src/publishable/scaffold.py +85 -0
  55. publishable-0.1.0/src/publishable/scope.py +71 -0
  56. publishable-0.1.0/src/publishable/secrets.py +135 -0
  57. publishable-0.1.0/src/publishable/sourceimport.py +132 -0
  58. publishable-0.1.0/src/publishable/stats.py +3454 -0
  59. publishable-0.1.0/src/publishable/strata.py +40 -0
  60. publishable-0.1.0/src/publishable/study.py +454 -0
  61. publishable-0.1.0/src/publishable/sweep.py +1157 -0
  62. publishable-0.1.0/src/publishable/templates/__init__.py +0 -0
  63. publishable-0.1.0/src/publishable/templates/base.py +44 -0
  64. publishable-0.1.0/src/publishable/templates/builtin/__init__.py +0 -0
  65. publishable-0.1.0/src/publishable/templates/builtin/generic.py +24 -0
  66. publishable-0.1.0/src/publishable/templates/discovery.py +439 -0
  67. publishable-0.1.0/src/publishable/templates/registry.py +193 -0
  68. publishable-0.1.0/src/publishable/units.py +3484 -0
  69. publishable-0.1.0/src/publishable/uv_support.py +64 -0
  70. publishable-0.1.0/src/publishable/validate.py +6795 -0
@@ -0,0 +1,226 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+
220
+ # macOS
221
+ .DS_Store
222
+
223
+ # `docs/superpowers/` and the SDD workspace under `.superpowers/sdd/` are TRACKED —
224
+ # they are the development record. See CLAUDE.md § The development record.
225
+ # Only the derived artifacts inside the workspace are ignored, by
226
+ # `.superpowers/sdd/.gitignore`.
@@ -0,0 +1,26 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ type: software
4
+ title: publishable
5
+ abstract: >-
6
+ Run experiments so the record is publishable by default — one config in,
7
+ one file a reviewer can re-run exactly. Every parameter lives in a single
8
+ config file, and every run pins code, environment, and input data by hash.
9
+ authors:
10
+ - family-names: Lee
11
+ given-names: Kyungjoon
12
+ email: joon_lee@hms.harvard.edu
13
+ affiliation: Harvard Medical School
14
+ repository-code: "https://github.com/seouri/publishable"
15
+ url: "https://github.com/seouri/publishable"
16
+ license: MIT
17
+ version: 0.1.0
18
+ date-released: "2026-08-26"
19
+ keywords:
20
+ - reproducibility
21
+ - research-software
22
+ - experiments
23
+ - provenance
24
+ - preregistration
25
+ - open-science
26
+ - python
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joon Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,402 @@
1
+ Metadata-Version: 2.5
2
+ Name: publishable
3
+ Version: 0.1.0
4
+ Summary: Run experiments so the record is publishable by default
5
+ Project-URL: Homepage, https://github.com/seouri/publishable
6
+ Project-URL: Repository, https://github.com/seouri/publishable
7
+ Project-URL: Documentation, https://github.com/seouri/publishable/blob/main/docs/reference.md
8
+ Project-URL: Issues, https://github.com/seouri/publishable/issues
9
+ Author-email: Kyungjoon Lee <joon_lee@hms.harvard.edu>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: experiments,open-science,preregistration,provenance,reproducibility,research-software
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: numpy>=1.26
25
+ Requires-Dist: pyarrow>=15.0
26
+ Requires-Dist: python-dotenv>=1.2.3
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: scipy>=1.11
29
+ Description-Content-Type: text/markdown
30
+
31
+ # publishable
32
+
33
+ **Every parameter in one file. Every run reproducible from one command.**
34
+
35
+ You have an experiment to run. Here's the whole arc with `publishable`:
36
+
37
+ 1. **Design the run.** One config file holds every parameter, the conditions you're comparing, and the repeats you'll average over. `publishable` generates it fully populated — you edit rather than author.
38
+ 2. **Run it.** `publishable run config.yaml`. No flags. Conditions and repeats expand on their own, and each one gets its own place in the output tree.
39
+ 3. **Read the results.** Estimates, confidence intervals over your units, and effect sizes against your baseline, already computed and sitting next to the run that produced them.
40
+ 4. **Publish it.** Hand a collaborator, a reviewer, or your future self one file. `publishable reproduce` rebuilds exactly what ran — same commit, same locked environment, the input checked against a recorded manifest — and stops at the two things only a person can supply: your copy of the data, and your credentials.
41
+
42
+ Nothing about what ran ends up in a shell history, so nothing has to be reconstructed later.
43
+
44
+ ## Try it
45
+
46
+ Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/). Nothing else to configure:
47
+
48
+ ```bash
49
+ uv tool install publishable # or: pipx install publishable
50
+ publishable demo
51
+ ```
52
+
53
+ `demo` builds a complete worked example — synthetic data, a three-step pipeline, a parameter sweep — and then walks you through running it, one command at a time. It opens with the part you can't easily make yourself:
54
+
55
+ ```
56
+ Created publishable-demo/
57
+ 240 synthetic units ~/publishable-demo-data/input/
58
+ template templates/correlation.py
59
+ experiment src/correlation_pilot/
60
+ config configs/correlation-pilot/config.yaml
61
+
62
+ Your data sits outside the repo, where real data belongs. Everything from
63
+ here is the CLI you'd use on an experiment of your own.
64
+
65
+ Next: a look at the config that describes this run
66
+ [Enter] to continue · q to stop here
67
+ ```
68
+
69
+ Six stops, the middle three with the same beat: the real command, its real output, then what it meant. After the config comes `validate`:
70
+
71
+ ```
72
+ Next: publishable validate configs/correlation-pilot/config.yaml
73
+ [Enter] to run it · q to stop here
74
+
75
+ ✓ config valid · configs/correlation-pilot/config.yaml
76
+
77
+ validate read your config and your data. It created nothing and reached
78
+ nothing off this machine — the 240 units it resolved came from the
79
+ index.csv outside the repo, and `input_dir` being outside is enforced.
80
+ ```
81
+
82
+ Nothing you press changes what runs — every stop is proceed-or-quit, and the config written at the first stop is the one executed at the fifth. `q` prints the remaining commands and `publishable demo` picks up where you left off. Two stops later, past `dry-run`:
83
+
84
+ ```
85
+ Next: publishable run configs/correlation-pilot/config.yaml
86
+ [Enter] to run it · q to stop here
87
+
88
+ warning W-ENV-UNLOCKED environment
89
+ no uv.lock found at ~/publishable-demo; the environment is not
90
+ pinned, and `reproduce` will not be able to restore it
91
+ 1 problem (0 errors, 1 warning)
92
+ run.yaml → ~/publishable-demo-data/results/run_2026-08-07T09-14-03Z_2f5c8d0/run.yaml
93
+
94
+ W-ENV-UNLOCKED fired because this project has no uv.lock: its pyproject
95
+ depends on `publishable`, which cannot resolve until the package is
96
+ published, so there is nothing to pin yet. Nothing was suppressed.
97
+
98
+ run printed no table — its whole output is that warning and the path to the
99
+ record. Everything below is `demo` reading the record back:
100
+
101
+ condition r 95% CI vs baseline (paired, 95% CI)
102
+ 00_baseline 0.697 [0.630, 0.757] —
103
+ 01_method=spearman 0.666 [0.582, 0.739] -0.031 [-0.068, -0.002]
104
+ 02_method=kendall 0.482 [0.413, 0.550] -0.215 [-0.240, -0.190]
105
+
106
+ intervals over 228 of 240 units (12 failed) · seed spread std 0.003 of recorded `pred`
107
+
108
+ `pred` and `truth` are recorded columns, so each publishes its own
109
+ metric and joins the correction family beside `r` — six members, and
110
+ two of them nobody reads. A template that derived twenty diagnostics
111
+ would correct every interval in the run for numbers nobody reads.
112
+ ```
113
+
114
+ Those are the numbers `demo` computes on your machine, from the 240 rows it generated — `run` itself prints no table, no banner and no progress bar, so everything under the record's path is `demo` saying what the record means. The sweep is `3 conditions × 5 repeats = 15` repeat-scoped executions and **19 in all**: the plan also runs `step01_load_cohort` once for the whole sweep and `step02_fit_model` once per condition, and 19 is the figure `dry-run` prints. Absolute paths are elided to `~` above, and the `run.yaml` path is illustrative either way — a run ID carries the timestamp and the code hash of *your* run.
115
+
116
+ That `run.yaml` is the point, and the last stop opens it and leaves you there. It carries the results *and* everything needed to regenerate them — so on any other machine:
117
+
118
+ ```bash
119
+ publishable reproduce <path-to-run.yaml>
120
+ ```
121
+
122
+ clones the exact commit, restores the locked environment, writes the config back out, and prints what's left to fill in. Neither data nor credentials travel, so the last step is yours — and `run` then verifies the input against the recorded manifest before spending anything.
123
+
124
+ > **v0.x — the design is settled, interfaces may still shift before 1.0.** Issues and design feedback are very welcome.
125
+
126
+ ---
127
+
128
+ ## Is this for you?
129
+
130
+ **A good fit if** you run experiments with parameters you sweep, repeats you average over, and results that end up in a paper — especially with data that can't live in your git repo. What a run executes is your pipeline over that data; measurements taken outside the pipeline arrive as input and are [pinned by hash, not produced](https://github.com/seouri/publishable/blob/main/docs/design-principles.md#what-core-does-not-promise).
131
+
132
+ **Probably not** if you want a pipeline scheduler, a live dashboard, or something to retrofit onto existing scripts. `publishable` is [greenfield only](https://github.com/seouri/publishable/blob/main/docs/design-principles.md#greenfield-only).
133
+
134
+ **Designs it speaks natively:** within- and between-subjects (with recorded randomization), factorial over parameters or over crossed arms, ablation, dose-response, train-test holdout, repeated cross-validation, bootstrap, permutation, technical-vs-biological replication, clustered units, matched case-control. The statistics follow the design — bootstrap resamples get percentile intervals, technical replicates never enter `n`, and a multi-condition sweep won't report uncorrected comparisons without warning you. Modelling beyond summary statistics is yours: see [Experimental designs](https://github.com/seouri/publishable/blob/main/docs/experimental-designs.md) for what's supported, what needs an override, and the [errors core refuses to let you make](https://github.com/seouri/publishable/blob/main/docs/experimental-designs.md#mistakes-core-prevents).
135
+
136
+ | Tool | Optimizes for | `publishable` differs by |
137
+ |---|---|---|
138
+ | **MLflow / W&B** | Tracking and comparing runs as they happen | Producing a self-contained record built for *publication*, not a server you query |
139
+ | **Hydra** | Flexible config composition and CLI overrides | Refusing overrides entirely — the file is the only description of the run |
140
+ | **DVC** | Versioning data and pipelines in git | Keeping data structurally *out* of the repo, for governed and clinical work |
141
+ | **Snakemake / Nextflow** | Expressing and scheduling complex DAGs | A linear pipeline, with statistics and provenance as first-class concerns |
142
+ | **Sacred** | Lightweight run capture | Pinning code, environment, *and* input data, with pre-registration and effect sizes built in |
143
+
144
+ These overlap, and several compose fine with `publishable`. The distinguishing bet: the deliverable is a **paper**, so the tool should produce something a reviewer can check, not just something you can query.
145
+
146
+ ---
147
+
148
+ ## How it fits together
149
+
150
+ Five words carry the whole model:
151
+
152
+ | Word | Means |
153
+ |---|---|
154
+ | **Unit** | the thing you measure — a patient, sample, trial, respondent |
155
+ | **Step** | one stage of your pipeline, one file in `src/` |
156
+ | **Condition** | one parameter combination you're *comparing* |
157
+ | **Repeat** | one re-execution you're *averaging over* — a seed or a cross-validation fold |
158
+ | **Run** | one execution of the whole thing: every step, every condition, every repeat |
159
+
160
+ Statistics aggregate *within* a condition and compare *across* conditions. Getting that backwards is the most common way a reproducible pipeline still produces a wrong number, which is why the two are named separately rather than both being called "runs."
161
+
162
+ ### Where things live
163
+
164
+ Your repo holds code and configs — never data:
165
+
166
+ ```
167
+ my-study/
168
+ ├── src/cohort_pilot/ # your pipeline, one file per step → code_hash
169
+ │ ├── experiment.py # declares step order
170
+ │ └── steps/
171
+ │ ├── step01_load_cohort.py
172
+ │ ├── step02_fit_model.py
173
+ │ ├── step03_analyze.py
174
+ │ └── step04_compare_methods.py
175
+ ├── templates/ # this project's own templates, if any → code_hash
176
+ ├── configs/cohort-pilot/
177
+ │ └── config.yaml # every parameter → parameters_hash
178
+ ├── pyproject.toml + uv.lock # the environment → locked
179
+ └── .env # credentials, never committed
180
+ ```
181
+
182
+ Your data lives outside it, and so does everything a run produces:
183
+
184
+ ```
185
+ ~/results/cohort-pilot/
186
+ └── run_2026-08-07T09-14-03Z_8e21ab3/
187
+ ├── run.yaml ← results + all three hashes. This is the deliverable.
188
+ ├── conditions/
189
+ │ ├── 00_baseline/ ← one folder per condition, self-describing
190
+ │ │ ├── seed17/ ← one folder per repeat
191
+ │ │ │ └── step03_analyze/scores.parquet
192
+ │ │ └── seed42/…
193
+ │ └── 01_method=spearman/…
194
+ └── summary/ ← steps that compare across conditions
195
+ ```
196
+
197
+ The directory structure *is* the experiment structure, so finding an artifact never requires reading code. Nothing here is ever overwritten: a second run creates a new `run_<id>/` beside this one.
198
+
199
+ ### And then
200
+
201
+ ```
202
+ run.yaml ──► publishable reproduce ──► a checkout anyone can run
203
+ └─► publishable study add ──► a bundle beside your manuscript
204
+ ```
205
+
206
+ Full vocabulary: [Ontology](https://github.com/seouri/publishable/blob/main/docs/design-principles.md#ontology).
207
+
208
+ ---
209
+
210
+ ## What you actually write
211
+
212
+ One config, generated fully populated by `publishable generate experiment` (or its shorter alias `publishable init`), so you edit rather than author from scratch:
213
+
214
+ ```yaml
215
+ parameters:
216
+ analysis:
217
+ method: pearson # choices: pearson | spearman | kendall
218
+ min_samples: 30 # integer >= 2
219
+
220
+ sweep:
221
+ baseline: {analysis.method: pearson}
222
+ grid:
223
+ analysis.method: [spearman, kendall]
224
+
225
+ replication:
226
+ repeats:
227
+ - {kind: seed, n: 5} # seed | batch | fold — what a re-execution varies
228
+ ```
229
+
230
+ And steps that never mention sweeps — each condition is resolved before your code runs:
231
+
232
+ ```python
233
+ class Step(BaseStep):
234
+ scope = "repeat" # run once per repeat, per condition
235
+
236
+ def run(self, cfg, io):
237
+ # io.units — your patients, samples, trials: whatever you're measuring
238
+ # cfg.parameters — already resolved to THIS condition's values
239
+ result = analyze(io.units, method=cfg.parameters.analysis.method)
240
+
241
+ for unit, pred in zip(io.units, result.per_unit):
242
+ io.record(unit.key, {"pred": pred, "truth": unit.label}) # the per-unit table —
243
+ # what every interval is over
244
+ io.write("scores.parquet", result.rows) # rows in, rows back out — this condition +
245
+ # repeat's own folder
246
+ return {"r": result.r} # recorded per repeat, without an interval
247
+ ```
248
+
249
+ Write it once for a single condition; adding a sweep later changes nothing here.
250
+
251
+ Statistics come back computed, next to the hashes that make them checkable:
252
+
253
+ ```yaml
254
+ results:
255
+ conditions:
256
+ - label: method=spearman
257
+ aggregated:
258
+ step03_analyze: {r: {value: 0.607, basis: units, n: {completed: 228},
259
+ ci95: [0.517, 0.683], repeat_spread: {std: 0.014}}}
260
+ vs_baseline:
261
+ step03_analyze: {r: {delta: 0.026, paired: true, ci95: [-0.007, 0.059]}}
262
+ provenance:
263
+ code_hash: sha256:8e21… # your src/** + templates/**, from a clean tree
264
+ parameters_hash: sha256:1a2b… # this exact parameter set
265
+ input_manifest_hash: sha256:3d8a… # the data it actually read
266
+ ```
267
+
268
+ The `r` carrying an interval is the *derived* one: template `generic`'s `aggregate` recomputes it from the per-unit table the step recorded, and being recomputable on a resampled table is what earns it a `ci95`. The `r` the step returned sits beside it under `per_repeat`, without one. That is the whole rule — [an interval is over units, never over executions](https://github.com/seouri/publishable/blob/main/docs/reference.md#the-unit-table-is-the-inference-base).
269
+
270
+ ---
271
+
272
+ ## Start your own
273
+
274
+ ```bash
275
+ # 1. Scaffold a repo. Runs `git init`; writes README, LICENSE, CITATION.cff.
276
+ publishable new my-study && cd my-study
277
+
278
+ # 2. Create an experiment. Data paths must live outside the repo.
279
+ publishable generate experiment cohort-pilot \
280
+ --template generic \
281
+ --input-dir ~/data/cohort-2026 \
282
+ --output-dir ~/results/cohort-pilot
283
+
284
+ # 3. Fill in the two fields `init` leaves for you — metadata.description and
285
+ # metadata.authors — and put your data where the config points.
286
+ # `validate` names anything still missing, and costs nothing.
287
+ publishable validate configs/cohort-pilot/config.yaml
288
+
289
+ # 4. Run it. The scaffold's starter step records one number per unit, so this
290
+ # produces a real estimate with an interval — replace it with your own.
291
+ git add -A && git commit -m "Scaffold cohort-pilot"
292
+ publishable run configs/cohort-pilot/config.yaml
293
+
294
+ # 5. Now make it yours: add steps, implement them, commit.
295
+ publishable generate step cohort-pilot analyze
296
+ git add src/ && git commit -m "Implement cohort analysis"
297
+ publishable validate configs/cohort-pilot/config.yaml
298
+ publishable dry-run configs/cohort-pilot/config.yaml
299
+ publishable run configs/cohort-pilot/config.yaml
300
+
301
+ # 6. When you publish, collect the runs you're reporting.
302
+ publishable study new ~/papers/triage/study --title "Cohort triage pilot"
303
+ publishable study add ~/papers/triage/study ~/results/cohort-pilot/latest/run.yaml --as main
304
+ ```
305
+
306
+ Step 4 works before you've written any code — the starter step records a number, so a first run produces a real estimate with an interval — so you can see the whole loop before committing to it.
307
+
308
+ ### The loop you'll actually live in
309
+
310
+ Once the code is committed, changing an experiment means editing one file. No commit, no flags, no bookkeeping:
311
+
312
+ ```bash
313
+ # edit configs/cohort-pilot/config.yaml — say, min_samples: 30 → 50
314
+ publishable run configs/cohort-pilot/config.yaml
315
+
316
+ publishable diff ~/results/cohort-pilot/run_A/run.yaml \
317
+ ~/results/cohort-pilot/run_B/run.yaml
318
+ ```
319
+
320
+ ```
321
+ A run record run_A completed
322
+ B run record run_B completed
323
+ code_hash identical sha256:8e21…
324
+ input_manifest identical sha256:3d8a…
325
+ uv.lock identical sha256:6b1f…
326
+ parameters_hash DIFFERS
327
+ parameters.analysis.min_samples 30 → 50
328
+ ```
329
+
330
+ That's the payoff of hashing code and parameters separately: you get to *prove* only one thing changed, which is the claim a comparison rests on.
331
+
332
+ ---
333
+
334
+ ## What you get
335
+
336
+ - **One file, no flags.** No *operation* command takes an argument other than a path; creation commands take what's needed to bring something into existence, and they're the only exception. A selector flag would live in a shell history that nobody archives.
337
+ - **Code and parameters hashed separately.** `code_hash` covers your code trees only, so "same code, different parameters" is a *provable* claim — even across commits weeks apart.
338
+ - **Code, environment, and data all pinned — and the apparatus too, when you measure through one.** A content hash over your code trees, with the commit recorded so a reproduction can fetch them; `uv.lock` for the environment; a content manifest for the input data, which is the one most tools leave open. And when measurement goes through an apparatus core can't install — a hosted model deployment, an instrument — a plugin probe records its revision per condition and a change fails the run. `uv.lock` pins the client; that record pins the server.
339
+ - **Artifacts are append-only and atomic.** Nothing is ever overwritten or deleted, and a crash mid-write leaves nothing behind rather than a half-file that blocks the retry.
340
+ - **Code and data never share a repo.** Data paths are structurally forbidden inside the git repo — code is shareable, governed data isn't, and they need different protocols.
341
+ - **Intervals over units, not over executions.** `n` counts the things your claim generalizes over. Repeats are reported as pipeline stability, separately and labelled, because an interval across five seeds narrows as you add seeds and says nothing about your cohort. Where core can't compute an interval honestly, it reports the estimate and omits the interval.
342
+ - **Statistics that match your design.** Declaring how units are allocated and how repeats are structured determines the analysis: paired or unpaired, t-based or percentile, clustered or not. A t-interval over bootstrap resamples is wrong, and core won't compute one — nor will it count technical replicates as `n`.
343
+ - **Pre-registration for free.** The config is written and hashed *before* the run, so declared hypotheses can be checked against results — and after-the-fact additions don't match the hash.
344
+ - **Stated limits.** Core documents what it [does not promise](https://github.com/seouri/publishable/blob/main/docs/design-principles.md#what-core-does-not-promise) — bit-identical reruns against external services, verification of your Python, or scientific validity.
345
+
346
+ ---
347
+
348
+ ## Commands
349
+
350
+ Creation commands take a name and what's needed to create it. **Everything else takes paths only.**
351
+
352
+ | Command | Does |
353
+ |---|---|
354
+ | `demo` | Build a complete worked example, then walk through running it command by command |
355
+ | `new` · `plugin new` · `generate` · `init` | Scaffold a project, a plugin, an experiment, a step |
356
+ | `validate` · `dry-run` | Check values, ranges, and the full execution plan before spending a run |
357
+ | `run` · `draft` · `resume` | Execute; `draft` permits a dirty tree, `resume` continues an interrupted run |
358
+ | `report` · `diff` · `freeze` | Render results, compare two runs hash by hash, re-check the apparatus mid-run |
359
+ | `reproduce` | Clone the recorded commit and prepare it to run — no git commands typed |
360
+ | `study new` · `study add` | Assemble the runs a paper reports, outside the repo |
361
+ | `docs` · `list-templates` | Rewrite the managed README regions; list every template this build knows, with its parameters where it can read them |
362
+
363
+ Full details: [CLI reference](https://github.com/seouri/publishable/blob/main/docs/reference.md#cli-reference) — which also carries the `Status` column saying which of these the current build executes.
364
+
365
+ ---
366
+
367
+ ## Extending it
368
+
369
+ Core knows nothing about LLMs, cohorts, instruments, or solvers. Domain knowledge lives in **plugins**, and a plugin doesn't need a PR here:
370
+
371
+ ```bash
372
+ publishable generate experiment triage-pilot \
373
+ --plugin someuser/publishable-llm \
374
+ --template llm_diagnostic \
375
+ --input-dir ~/data/xray-2026 --output-dir ~/results/triage-pilot
376
+ ```
377
+
378
+ `--plugin owner/repo` is `uv add git+…` and nothing more — so the plugin lands in your lockfile and gets captured in provenance like any other dependency. Write your own with `publishable plugin new`, which scaffolds an installable package whose docs generate from its parameter spec.
379
+
380
+ The reference LLM plugin is [`publishable-llm`](https://github.com/someuser/publishable-llm).
381
+
382
+ ---
383
+
384
+ ## Documentation
385
+
386
+ - **[Experimental designs](https://github.com/seouri/publishable/blob/main/docs/experimental-designs.md)** — how to express each design, and the mistakes core prevents
387
+ - **[Reference](https://github.com/seouri/publishable/blob/main/docs/reference.md)** — config schema, CLI, `io` API, templates, sweeps, artifact layout
388
+ - **[Design principles](https://github.com/seouri/publishable/blob/main/docs/design-principles.md)** — why the rules are what they are; read before proposing a change
389
+ - **[Plugin guide](https://github.com/seouri/publishable/blob/main/docs/reference.md#plugins-where-domain-knowledge-lives)** — building and sharing templates
390
+ - **[The development record](https://github.com/seouri/publishable/blob/main/docs/superpowers/README.md)** — non-normative: how each slice was designed, measured and reviewed, kept because a tool arguing the record should be publishable owes its own
391
+
392
+ ## Contributing
393
+
394
+ Most new templates should be a [plugin](https://github.com/seouri/publishable/blob/main/docs/reference.md#plugins-where-domain-knowledge-lives), not a PR here — that's what `--plugin owner/repo` is for. Upstream contributions are for core mechanisms: the config envelope, provenance capture, the artifact model, the validation engine, the CLI.
395
+
396
+ A good test of whether something belongs in core: **would it be identical for a wet-lab assay, a simulation sweep, and an LLM benchmark?** If not, it's a plugin.
397
+
398
+ Design disagreements are welcome as issues. If a rule seems arbitrary, [design-principles.md](https://github.com/seouri/publishable/blob/main/docs/design-principles.md) probably explains it — and if it doesn't, that's a documentation bug worth filing.
399
+
400
+ ## License
401
+
402
+ MIT — see [LICENSE](https://github.com/seouri/publishable/blob/main/LICENSE).