paperlint 10.2.0 → 10.4.0

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 (147) hide show
  1. package/.github/workflows/ci.yml +15 -4
  2. package/README.md +1 -0
  3. package/dist/adapters/git/index.d.ts +10 -0
  4. package/dist/adapters/git/index.d.ts.map +1 -0
  5. package/dist/adapters/git/index.js +151 -0
  6. package/dist/adapters/git/index.js.map +1 -0
  7. package/dist/adapters/memory/index.d.ts +6 -0
  8. package/dist/adapters/memory/index.d.ts.map +1 -1
  9. package/dist/adapters/memory/index.js +8 -0
  10. package/dist/adapters/memory/index.js.map +1 -1
  11. package/dist/adapters/node/index.d.ts +1 -0
  12. package/dist/adapters/node/index.d.ts.map +1 -1
  13. package/dist/adapters/node/index.js +1 -0
  14. package/dist/adapters/node/index.js.map +1 -1
  15. package/dist/adapters/node/list-entries.io.d.ts +3 -0
  16. package/dist/adapters/node/list-entries.io.d.ts.map +1 -0
  17. package/dist/adapters/node/list-entries.io.js +24 -0
  18. package/dist/adapters/node/list-entries.io.js.map +1 -0
  19. package/dist/adapters/study-registry/index.d.ts +412 -1
  20. package/dist/adapters/study-registry/index.d.ts.map +1 -1
  21. package/dist/adapters/study-registry/index.js +245 -8
  22. package/dist/adapters/study-registry/index.js.map +1 -1
  23. package/dist/cli.d.ts.map +1 -1
  24. package/dist/cli.js +38 -6
  25. package/dist/cli.js.map +1 -1
  26. package/dist/domain/decide-first.d.ts +100 -0
  27. package/dist/domain/decide-first.d.ts.map +1 -0
  28. package/dist/domain/decide-first.js +102 -0
  29. package/dist/domain/decide-first.js.map +1 -0
  30. package/dist/domain/index-columns.d.ts +45 -0
  31. package/dist/domain/index-columns.d.ts.map +1 -0
  32. package/dist/domain/index-columns.js +62 -0
  33. package/dist/domain/index-columns.js.map +1 -0
  34. package/dist/domain/study-mode.d.ts +37 -0
  35. package/dist/domain/study-mode.d.ts.map +1 -0
  36. package/dist/domain/study-mode.js +45 -0
  37. package/dist/domain/study-mode.js.map +1 -0
  38. package/dist/domain/study-registry.d.ts +253 -10
  39. package/dist/domain/study-registry.d.ts.map +1 -1
  40. package/dist/domain/study-registry.js +325 -27
  41. package/dist/domain/study-registry.js.map +1 -1
  42. package/dist/outputs-before-decision-rule.d.ts +17 -0
  43. package/dist/outputs-before-decision-rule.d.ts.map +1 -0
  44. package/dist/outputs-before-decision-rule.js +90 -0
  45. package/dist/outputs-before-decision-rule.js.map +1 -0
  46. package/dist/ports/git-history.d.ts +44 -0
  47. package/dist/ports/git-history.d.ts.map +1 -0
  48. package/dist/ports/git-history.js +2 -0
  49. package/dist/ports/git-history.js.map +1 -0
  50. package/dist/ports/list-entries.d.ts +17 -0
  51. package/dist/ports/list-entries.d.ts.map +1 -0
  52. package/dist/ports/list-entries.js +2 -0
  53. package/dist/ports/list-entries.js.map +1 -0
  54. package/dist/ports/study-registry-codec.d.ts +9 -2
  55. package/dist/ports/study-registry-codec.d.ts.map +1 -1
  56. package/dist/studies.d.ts +4 -0
  57. package/dist/studies.d.ts.map +1 -1
  58. package/dist/studies.js +55 -24
  59. package/dist/studies.js.map +1 -1
  60. package/dist/study-gate.d.ts +14 -0
  61. package/dist/study-gate.d.ts.map +1 -0
  62. package/dist/study-gate.js +95 -0
  63. package/dist/study-gate.js.map +1 -0
  64. package/dist/study-index-input.d.ts +5 -0
  65. package/dist/study-index-input.d.ts.map +1 -0
  66. package/dist/study-index-input.js +13 -0
  67. package/dist/study-index-input.js.map +1 -0
  68. package/dist/study-modes.d.ts +45 -0
  69. package/dist/study-modes.d.ts.map +1 -0
  70. package/dist/study-modes.js +70 -0
  71. package/dist/study-modes.js.map +1 -0
  72. package/dist/study-registry-rule.d.ts +29 -6
  73. package/dist/study-registry-rule.d.ts.map +1 -1
  74. package/dist/study-registry-rule.js +91 -40
  75. package/dist/study-registry-rule.js.map +1 -1
  76. package/dist/study-run.d.ts +19 -0
  77. package/dist/study-run.d.ts.map +1 -0
  78. package/dist/study-run.js +9 -0
  79. package/dist/study-run.js.map +1 -0
  80. package/docs/design/study-modes.md +247 -0
  81. package/docs/design/study-registry.md +137 -15
  82. package/docs/e2e.md +8 -4
  83. package/docs/layout.md +26 -21
  84. package/docs/rules/repro/outputs-before-decision.md +119 -0
  85. package/docs/rules/repro/study-plan-name.md +70 -0
  86. package/docs/rules/repro/study-registered.md +206 -15
  87. package/docs/rules.md +54 -52
  88. package/docs/testing.md +1 -0
  89. package/package.json +2 -1
  90. package/scripts/check.ts +6 -0
  91. package/skills/build-benchmark/SKILL.md +24 -12
  92. package/skills/build-benchmark/SKILL.md.spec.ts +23 -11
  93. package/skills/build-label-page/SKILL.md +9 -7
  94. package/skills/build-label-page/SKILL.md.spec.ts +8 -6
  95. package/skills/camera-ready/SKILL.md +6 -1
  96. package/skills/camera-ready/SKILL.md.spec.ts +5 -0
  97. package/skills/draft-paper/SKILL.md +11 -3
  98. package/skills/draft-paper/SKILL.md.spec.ts +10 -2
  99. package/skills/harden-paper/SKILL.md +12 -2
  100. package/skills/harden-paper/SKILL.md.spec.ts +11 -1
  101. package/skills/osf-artifact-upload/SKILL.md +7 -1
  102. package/skills/osf-artifact-upload/SKILL.md.spec.ts +6 -0
  103. package/skills/pc-panel-review/SKILL.md +10 -3
  104. package/skills/pc-panel-review/SKILL.md.spec.ts +9 -2
  105. package/skills/run-agent-grid/SKILL.md +10 -5
  106. package/skills/run-agent-grid/SKILL.md.spec.ts +9 -4
  107. package/skills/tighten-paper/SKILL.md +5 -1
  108. package/skills/tighten-paper/SKILL.md.spec.ts +4 -0
  109. package/src/adapters/git/history.test.ts +121 -0
  110. package/src/adapters/git/index.test.ts +268 -0
  111. package/src/adapters/git/index.ts +227 -0
  112. package/src/adapters/memory/index.test.ts +15 -1
  113. package/src/adapters/memory/index.ts +13 -0
  114. package/src/adapters/node/index.ts +1 -0
  115. package/src/adapters/node/list-entries.io.ts +26 -0
  116. package/src/adapters/node/list-entries.test.ts +43 -0
  117. package/src/adapters/study-registry/index.test.ts +343 -0
  118. package/src/adapters/study-registry/index.ts +374 -10
  119. package/src/adapters/study-registry/mode.test.ts +162 -0
  120. package/src/cli.ts +52 -6
  121. package/src/domain/decide-first.test.ts +258 -0
  122. package/src/domain/decide-first.ts +243 -0
  123. package/src/domain/index-columns.test.ts +216 -0
  124. package/src/domain/index-columns.ts +105 -0
  125. package/src/domain/study-measurements.test.ts +633 -0
  126. package/src/domain/study-mode.test.ts +55 -0
  127. package/src/domain/study-mode.ts +78 -0
  128. package/src/domain/study-registry.test.ts +48 -32
  129. package/src/domain/study-registry.ts +621 -36
  130. package/src/new-paper.test.ts +6 -1
  131. package/src/outputs-before-decision-rule.test.ts +206 -0
  132. package/src/outputs-before-decision-rule.ts +124 -0
  133. package/src/ports/git-history.ts +44 -0
  134. package/src/ports/list-entries.ts +18 -0
  135. package/src/ports/study-registry-codec.ts +13 -2
  136. package/src/rule-cases.test.ts +103 -1
  137. package/src/studies-cli.test.ts +2 -2
  138. package/src/studies-index-modes.test.ts +129 -0
  139. package/src/studies.test.ts +39 -9
  140. package/src/studies.ts +99 -29
  141. package/src/study-gate.test.ts +316 -0
  142. package/src/study-gate.ts +161 -0
  143. package/src/study-index-input.ts +23 -0
  144. package/src/study-modes.ts +120 -0
  145. package/src/study-registry-rule.test.ts +333 -7
  146. package/src/study-registry-rule.ts +145 -41
  147. package/src/study-run.ts +21 -0
@@ -0,0 +1,247 @@
1
+ # Study modes: decide first, or exploratory — design
2
+
3
+ ## Why
4
+
5
+ A study's outcomes are computed by an automated runner in the same workspace where the people
6
+ deciding its inputs work: which runs count, which units are excluded, whether a veto applies, whether
7
+ the drawn sample is approved. If those decisions can be made — or changed — after an outcome has been
8
+ seen, the decision can follow the result, and nothing in the repository says which came first.
9
+
10
+ What is needed:
11
+
12
+ 1. every measurement says how its outcomes are kept from the decisions on its inputs, and none can
13
+ say nothing;
14
+ 2. the default needs no key and no outside service: the decision is recorded first, the run starts
15
+ after, and anyone with the repository can re-check the order;
16
+ 3. a study that cannot work that way is declared exploratory, with its reason, and reported as such;
17
+ 4. the paper's Methods and its reviewers read each study's mode, decision commit and first output
18
+ from one generated place, not from prose someone keeps in sync by hand.
19
+
20
+ ## What is decided
21
+
22
+ ### 1. One mode per measurement, in its plan
23
+
24
+ A study's `plan.json` holds one `mode`, a union on its `kind` (`src/domain/study-mode.ts`):
25
+
26
+ ```json
27
+ { "mode": { "kind": "decide_before_run",
28
+ "outputs": ["out/**", "RESULTS.md"],
29
+ "decision": { "file": "repro/P1-S03-x/decision.md", "note": "exclusions and veto" } } }
30
+
31
+ { "mode": { "kind": "exploratory", "reason": "a first look at the logs, to find the question" } }
32
+ ```
33
+
34
+ - **Exactly one, by construction.** The modes are one discriminated union, each variant closed: a
35
+ second mode's key in the same object is an unknown key, and an unknown `kind` is refused. This is
36
+ the parser's (`src/adapters/study-registry/`), not a later check.
37
+ - **None is not ready.** The parser is told the study's `kind` (from its row). A `measurement`'s plan
38
+ without a mode is a **draft** naming `mode` among its empty texts, like an empty `question`: legal
39
+ while the study is `designing`, reported by `repro/study-registered` (`bad-plan`, `unfilled`) once
40
+ it runs, and a parse error when the author's check is already on it (the author approved an
41
+ incomplete plan). A spike, a tool, a data set, an artifact or a salvage may omit it.
42
+ - **The default is written for you.** `paperlint studies new` writes a measurement's plan with
43
+ `decide_before_run`, its decision file named (`repro/<folder>/decision.md`), its outputs and note
44
+ left to fill — a draft until they are. The decision file itself is NOT created: an empty
45
+ placeholder that got committed would open the gate on nothing.
46
+ - **Paths.** `outputs` are globs relative to the study's folder, inside it, never the study's
47
+ `README.md`. A decision `file` is relative to the **paper's** folder, so it may live beside the
48
+ study or anywhere in the paper (`research/decisions.md`); it may not leave the paper, and it is a
49
+ file's path — names joined by `/`, none empty, `.` or `..` — so `.` (the paper itself) or a path
50
+ ending in `/` cannot be written. A name that is a directory on disk is the gate's to refuse and the
51
+ rule's to report (`decision-not-file`): git answers for a directory with the commits of anything
52
+ under it. Whether a
53
+ decision inside the study's folder is matched by an output glob needs the folder, which the plan
54
+ does not hold: the rule reports it (`decision-is-output`) and the gate refuses it.
55
+ - **Empty texts of a mode** — no outputs yet, an empty decision file or note, an empty reason — are a
56
+ draft's empty texts, named field by field (`mode.outputs`, `mode.decision.file`, …).
57
+
58
+ ### 2. `paperlint studies gate <paper-dir> <id>`
59
+
60
+ Exit 0 only if the decision file exists and is committed as it stands (git's last commit for it, and
61
+ nothing changed since), printing the commit and its date; exit 1 saying why otherwise (not
62
+ committed, changed since its commit, committed and since deleted — not in HEAD, though `git log -1`
63
+ names the deleting commit and the tree is clean — git unable to answer, no decision file named, a
64
+ decision that is a directory, a decision its own outputs match). A runner calls it first:
65
+
66
+ ```sh
67
+ npx paperlint studies gate papers/my-paper P1-S03 && ./run.sh
68
+ ```
69
+
70
+ For an exploratory study it exits 0 and says it has no gate. A study that declares no mode, or whose
71
+ plan is still a draft, is refused.
72
+
73
+ ### 3. `repro/outputs-before-decision` — the proof of order a reviewer can re-run
74
+
75
+ An error, reported on `PIPELINE-STATUS.md`. For each decide-first study: every output whose first
76
+ commit is not a strict descendant of the decision file's first commit (earlier, on another line of
77
+ history, or in the same commit) is a finding; deleted outputs count. An output in the tree or in history while the
78
+ decision was never committed is a finding, and so is a decision committed and since deleted
79
+ (`decision-deleted`): it is not in HEAD, so no order is judged against it. A shallow clone, and a
80
+ history git cannot read, are
81
+ findings that say so — never a pass. The study's folder is walked without following a symbolic link
82
+ (`ListEntries`, `src/ports/list-entries.ts`): a link is one entry, as git stores it, so a link to the
83
+ folder itself cannot loop the walk and one outside brings no foreign file into the study.
84
+
85
+ **Why a lint rule may ask git here.** Rules in `eslint-rules/` may not run git
86
+ (`eslint.config.mjs`, `GIT_IN_A_RULE`): a pinned sha is deleted by a squash or a gc, and a shallow CI
87
+ checkout has none. This rule pins no sha; it asks the history the checkout has for an order. A squash
88
+ merge keeps that order when the decision was merged before the run — which is the decide-first
89
+ workflow — and a shallow checkout is named by the rule (`fetch-depth: 0`).
90
+
91
+ ### 4. The index carries each study's mode
92
+
93
+ `repro/README.md`'s study index (`paperlint studies index`, docs/design/study-registry.md) has three
94
+ columns for this (`src/domain/index-columns.ts`):
95
+
96
+ | column | cell |
97
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------- |
98
+ | `mode` | `decide_before_run`, `exploratory: <reason>`, or `—` for a study whose plan has none |
99
+ | `decision` | the decision file's first commit (12 characters) and its date (`, deleted since` when it is not in HEAD), or `not committed` |
100
+ | `first output` | the first commit of the oldest output and its date, or `none` |
101
+
102
+ A sample row:
103
+
104
+ ```markdown
105
+ | id | title | status | result | in paper | mode | decision | first output | folder |
106
+ | ------ | ------- | ------ | -------- | -------- | ----------------- | ----------------------- | ----------------------- | ---------------------------------- |
107
+ | P1-S01 | Outcome | done | 41 of 60 | yes | decide_before_run | 3f2a9c1d7e40 2026-10-14 | 8b51e0aa42c9 2026-10-15 | [P1-S01-outcome/](P1-S01-outcome/) |
108
+ ```
109
+
110
+ - **The mode is compared, the commits are not.** The mode comes from the plan, which is in the tree,
111
+ so `repro/study-registered` reports an index whose mode column is not the plan's. The two commits
112
+ are what git's history said when `studies index` last ran: a squash merge or a rebase gives the
113
+ same files new commits, and a shallow CI clone has none to read, so the lint never compares those
114
+ two columns — comparing them would turn every squash merge red. `studies index` rewrites the README
115
+ whenever any column moved, and in a shallow clone writes `unknown (shallow clone)`. The order
116
+ itself is `repro/outputs-before-decision`'s to judge, not the table's.
117
+ - **The skills read the table.** `draft-paper` writes the Methods sentence from it (decided before
118
+ the run, the decision's commit and date; exploratory studies labelled with their reason);
119
+ `harden-paper` and `pc-panel-review` check that every confirmatory claim rests on a
120
+ `decide_before_run` study whose decision commit precedes its first output, and flag a
121
+ "pre-registered" or "decided before" claim the table does not support; `tighten-paper` keeps that
122
+ sentence; `osf-artifact-upload` and `camera-ready` ship each plan, its decision file and the index,
123
+ with the whole history. Each regenerates the index first, so the commits are this checkout's.
124
+
125
+ ### 5. Reads stay inside the paper
126
+
127
+ **What these checks guard against.** paperlint's checks guard against accidents and honest
128
+ mistakes — a run started before the decision was committed, a decision edited or deleted afterwards,
129
+ a path typed wrong — not against an author who controls the repository's history and sets out to
130
+ forge it. The path rules below exist to keep every read inside the paper, so a mistyped row cannot
131
+ make a command or a rule read, judge or write files that belong to nothing in it.
132
+
133
+ Every command and rule that reads a study's folder — `studies gate`, `studies index` and `new`,
134
+ `repro/outputs-before-decision`, and `repro/study-registered`'s reads of plans and result files —
135
+ reads a row only through one predicate, `readableFolders` (`src/study-registry-rule.ts`): its
136
+ `folder` is one folder directly inside `repro/` (`isFolderName`: not empty, not `.` or `..`, no
137
+ separator or NUL) and not a symbolic link (`ListEntries`, read without following links). Such a row
138
+ is never read through: `studies index` and `new` refuse the registry, the gate refuses the study,
139
+ the order rule treats it as no study, and `repro/study-registered` reads its plan and results as
140
+ missing. A plan's path is held inside its folder by the registry rule's own `isInside`, the same on
141
+ every platform (a backslash is refused outright); the decision file is held inside the paper by the
142
+ schema (`staysInside`) and must be a regular file on disk — a directory or a symbolic link is refused
143
+ by the gate and reported by the rule (`decision-not-file`). Only a **ready** plan's mode decides
144
+ anything: the gate refuses a draft, naming its empty fields, and the order rule judges no draft — a
145
+ draft may name no outputs, or an empty decision file.
146
+
147
+ ### 6. Legacy plans
148
+
149
+ A plan still named `prereg.json` is parsed by its loose legacy schema, which never asks for a mode,
150
+ and `repro/study-plan-name` keeps warning that it moves to `plan.json` — where the strict parser asks
151
+ for one. So an old paper keeps linting as before until it renames the file. A `plan.json` written by
152
+ the release before modes existed, for a measurement, reads as a draft whose empty text is `mode`:
153
+ silent while the study is `designing`, reported once it runs, and an unreadable plan when the
154
+ author's check is already on it (the message names `mode`). Adding one line — usually the
155
+ decide-first mode — is the migration. An index generated before the mode columns existed is out of
156
+ sync until `paperlint studies index` regenerates it.
157
+
158
+ ### 7. Why: confirmatory vs exploratory, and venues that review the plan first
159
+
160
+ Read for this note (2026-10-16):
161
+
162
+ - Nosek, Ebersole, DeHaven, Mellor, "The preregistration revolution", _PNAS_ 115(11):2600–2606,
163
+ 2018 (metadata and abstract from Crossref, doi:10.1073/pnas.1708274114). The abstract, verbatim:
164
+
165
+ > Progress in science relies in part on generating hypotheses with existing observations and
166
+ > testing hypotheses with new observations. This distinction between postdiction and prediction
167
+ > is appreciated conceptually but is not respected in practice. Mistaking generation of
168
+ > postdictions with testing of predictions reduces the credibility of research findings. […] An
169
+ > effective solution is to define the research questions and analysis plan before observing the
170
+ > research outcomes—a process called preregistration. Preregistration distinguishes analyses and
171
+ > outcomes that result from predictions from those that result from postdictions.
172
+
173
+ This is the distinction the modes encode: decided first is prediction; exploratory is postdiction,
174
+ legitimate when it is labelled.
175
+
176
+ - The MSR (Mining Software Repositories) Registered Reports track, 2020 edition
177
+ (https://2020.msrconf.org/track/msr-2020-Registered-Reports; the 2021, 2025 and 2026 editions have a
178
+ track page at the same address, the 2019 site has none there). Quoted from the 2020 page:
179
+
180
+ > Methods and proposed analyses are pre-registered and reviewed prior to research being conducted.
181
+
182
+ > (MSR 2020) Phase 1: Introduction, Methods (including proposed analyses), and Pilot Data (where
183
+ > applicable).
184
+
185
+ > (EMSE) Phase 2: full study, after data collection and analysis.
186
+
187
+ > Additional exploratory analyses in Phase 2 may be conducted, if they are justified.
188
+
189
+ > Any deviation from the protocol must be justified and logged in detail to ensure replicability.
190
+
191
+ A venue of this field already reviews the plan before the data, and accepts exploratory analyses
192
+ beside it only when they are justified. The modes, the order rule and the index give a paper the
193
+ same evidence in its own repository: what was fixed before the run, with its commit, and what was
194
+ exploratory, with its reason.
195
+
196
+ ## Alternatives considered
197
+
198
+ - **Encrypting the outputs until the decision (age, to a key the author holds): rejected.** It needs
199
+ an external binary that `paperlint toolchain` does not install, and a key whose custody has no good
200
+ home: a session in the cloud has nowhere private to keep the identity, and a key in a CI secret is
201
+ readable by any workflow that can be edited. Decide first already proves the order from git with no
202
+ key at all, so encryption would add a dependency and a secret to guard to buy what the history
203
+ shows. Blind analysis in physics (Klein & Roodman, _Annu. Rev. Nucl. Part. Sci._ 55, 2005) and
204
+ MacCoun & Perlmutter, "Hide results to seek the truth" (_Nature_ 526, 2015) — both cited from
205
+ memory, not re-read for this note — are where the idea comes from; what is kept is that the
206
+ decision is fixed and recorded before the outcome is seen.
207
+ - **Paragraph links (`% study: <id>` comments in `paper.tex`) checked by lint rules: rejected.** Such
208
+ rules check only the paragraphs that carry a convention authors do not write, so an unlinked
209
+ overclaim passes silently. The review skills check the Methods sentence and every confirmatory
210
+ claim against the index instead, which covers the whole text.
211
+ - **A separate `studies methods` command printing the modes and commits: rejected.** The same facts
212
+ are three columns of the `studies index` table the registry already generates; a second command
213
+ would be a second rendering of one set of facts.
214
+ - **Comparing the decision and first-output columns in the lint: rejected.** A squash merge renames
215
+ the commits, so the index would go stale on every merge, and a shallow CI clone cannot read them;
216
+ the order rule judges the history directly instead (§4).
217
+ - **An outside pre-registration (OSF, AsPredicted) as the record: not required.** A timestamped
218
+ record before the outcome is what they give; here the repository's history is that record,
219
+ re-checkable by anyone who clones it. A paper may still register outside and cite it.
220
+
221
+ ## Where each part lives
222
+
223
+ | part | file | layer |
224
+ | ------------------------------------------------------------- | ------------------------------------- | ------- |
225
+ | the modes, what is an output, which decision a mode waits on | `src/domain/study-mode.ts` | domain |
226
+ | the order judge and its messages | `src/domain/decide-first.ts` | domain |
227
+ | the index's mode columns and the history they show | `src/domain/index-columns.ts` | domain |
228
+ | the index, which columns the lint compares, `isFolderName` | `src/domain/study-registry.ts` | domain |
229
+ | the schema of `mode`; readiness by kind | `src/adapters/study-registry/` | adapter |
230
+ | git: committed state, first commits, ancestry, shallow | `src/adapters/git/` | adapter |
231
+ | what the app asks of git | `src/ports/git-history.ts` | port |
232
+ | a directory's entries, links not followed | `src/ports/list-entries.ts` | port |
233
+ | the studies with a mode, and the files under a study's folder | `src/study-modes.ts` | app |
234
+ | the order rule | `src/outputs-before-decision-rule.ts` | app |
235
+ | the plans and histories `studies index` and `new` write from | `src/study-index-input.ts` | app |
236
+ | `gate`; `new` and `index` | `src/study-gate.ts`, `src/studies.ts` | app |
237
+ | wiring: git with PATH and HOME, the clock | `src/cli.ts` | root |
238
+
239
+ ## What it does not check
240
+
241
+ - What a decision says. The gate is that it is committed, not its content.
242
+ - A decision amended after the outputs: `repro/outputs-before-decision` orders FIRST commits; later
243
+ edits of the decision are in its history but not reported.
244
+ - That nobody ran a decide-first study before its decision and kept the outputs out of git. The rule
245
+ keeps early outputs out of the repository's history, not out of a person's hands.
246
+ - Files outside the study's folder as outputs: outputs are relative to it by construction.
247
+ - Whether the paper's text matches the index: that is the review skills' job, not a lint rule's.
@@ -47,9 +47,13 @@ two commands that write it.
47
47
  | `superseded_by` | optional; an id in the registry. Required when `superseded` |
48
48
  | `in_paper` | `yes`, `no`, `registry` (listed only in the registry, not in the text) |
49
49
 
50
- **A row may carry more fields**, and they are kept: a paper merges its own measurement metadata
51
- into the rows (`role`, `frame`, `method`, `outcome`, …). The top level is closed: a misspelt
52
- `prefix` is an error, not an ignored key.
50
+ | `measurement` | optional; the id of the measurement the study belongs to (6) |
51
+ | `plan` | optional; the study's plan, a path inside its folder: `plan.json` (7) |
52
+
53
+ **A row may carry more fields**, and they are kept: a paper merges its own metadata into the rows
54
+ (`frame`, `method`, `outcome`, …). The top level is closed — `prefix`, `studies`, and the optional
55
+ `author`, `requirements` and `measurements` (6) — so a misspelt key is an error, not an ignored
56
+ key.
53
57
 
54
58
  2. **The rule `repro/study-registered`, an error,** reported on the first line of
55
59
  `PIPELINE-STATUS.md` like `repro/allowed-files-only`, once per finding:
@@ -74,8 +78,9 @@ two commands that write it.
74
78
  3. **Two commands**, in `src/studies.ts`, wired in `src/cli.ts`:
75
79
 
76
80
  - `paperlint studies new <paper-dir> "<title>" [--kind measurement]` takes the next id, creates
77
- `repro/<id>-<slug>/README.md` (frontmatter `id:` and `title:`), appends the row with
78
- `status: designing`, `in_paper: no` and today's date, and regenerates the index block. It refuses a
81
+ `repro/<id>-<slug>/README.md` (frontmatter `id:` and `title:`) and the plan's template,
82
+ `plan.json` (7), appends the row with `status: designing`, `in_paper: no`, today's date and
83
+ `plan: "plan.json"`, and regenerates the index block. It refuses a
79
84
  title over 8 words, a title with no letter or digit to make a slug from, an unknown kind, a folder
80
85
  that already exists, and a paper with no `studies.json` (the prefix is the author's choice, so the
81
86
  refusal names the file to write and does not guess one).
@@ -93,19 +98,133 @@ two commands that write it.
93
98
  `paperlint studies new`, and update its row (status, result, result file, in paper) when it ends,
94
99
  then run `paperlint studies index`.
95
100
 
101
+ 6. **Measurements above studies, and the author's requirements.** One measurement (a census, a
102
+ benchmark) spans several study folders: a pilot, the main run, a replication. Before this, a paper
103
+ put the measurement's fields on the row of its first study under prefixed names
104
+ (`measurement_title`, `measurement_status`) and the other rows carried `measurement: <id>`, which
105
+ nothing checked; the author's verbatim requirements went to a separate file nothing read. Now:
106
+
107
+ ```json
108
+ {
109
+ "prefix": "P1",
110
+ "author": "A. Author",
111
+ "requirements": [
112
+ {
113
+ "id": "R1",
114
+ "text": "Every sample is a random draw from all rules found.",
115
+ "date": "2026-10-10",
116
+ "source": "chat"
117
+ }
118
+ ],
119
+ "measurements": [
120
+ {
121
+ "id": "M1",
122
+ "title": "Rule census",
123
+ "rq": "RQ1",
124
+ "role": "headline",
125
+ "approval": {
126
+ "by": "A. Author",
127
+ "date": "2026-10-12",
128
+ "quote": "This is the one."
129
+ },
130
+ "requirements": ["R1"]
131
+ }
132
+ ],
133
+ "studies": [
134
+ { "id": "P1-S01", "measurement": "M1", "plan": "plan.json", "…": "…" }
135
+ ]
136
+ }
137
+ ```
138
+
139
+ `role` is `headline` (the paper's claim rests on it) or `baseline`. A measurement keeps extra
140
+ fields like a row; a requirement and an approval are exactly their fields. The rule reports, each
141
+ finding its own message id: a row naming an undeclared measurement (`unknown-measurement`), a
142
+ measurement no row names (`empty-measurement`), a measurement naming an undeclared requirement
143
+ (`unknown-requirement`), and a headline without `approval`, or approved by anyone but `author`
144
+ when the registry names one (`headline-approval`). These cross-references are the rule's, not the
145
+ parser's: one dangling id must not fail the whole parse and hide every other finding. **These checks run only when the registry has
146
+ `measurements`**: a registry written before them, whose rows already carry `measurement` as a field
147
+ of their own, stays valid.
148
+
149
+ 7. **The study's plan, `plan.json`.** Each study folder held a hand-written `prereg.json`, its
150
+ fields differing study to study, read only by the experimental `study/declare-sample`. The name
151
+ misled — it is not an outside pre-registration — so the plan is `plan.json`, written as a
152
+ template by `paperlint studies new` beside the README, and named in the row's `plan`:
153
+
154
+ ```json
155
+ {
156
+ "question": "",
157
+ "population": {
158
+ "source": "",
159
+ "draw": { "kind": "all", "method": "" }
160
+ },
161
+ "arms": [],
162
+ "primary_outcome": "",
163
+ "author_check": null
164
+ }
165
+ ```
166
+
167
+ The template's draw is of all the population (`"kind": "all"`, a census); a sampled study changes
168
+ it to `{ "kind": "seeded", "method": "random", "seed": 7 }`. `arms` is empty for a census;
169
+ `author_check` becomes `{ "by", "date", "quote" }` once the author has seen the drawn units.
170
+ **Parse, don't validate:**
171
+
172
+ - **Closed at every level.** A misspelt key is an error; the paper's own keys (hypotheses,
173
+ models, budget) go in the one optional key `extra`, an object of anything.
174
+ - **One spelling of "none".** `author_check` is required, and `null` is "not yet"; an absent key
175
+ is an error, never a second way to say the same.
176
+ - **No contradictory state.** Each of these is a parse error, not a later finding:
177
+ - an `author_check` on a draft — the author approved a plan whose texts are empty;
178
+ - a draw that is not one of two kinds: `seeded`, whose `seed` is required and not null, or
179
+ `all` (a census), which has no seed;
180
+ - two arms of one name;
181
+ - a key of `extra` that is a field of the plan, which would say one thing twice.
182
+
183
+ The population's size is not stored: it is counted from the source, and a stored copy could
184
+ only contradict it. Nothing downstream read it.
185
+
186
+ - **Complete or not is a type.** The parser returns a plan `ready` (every required text filled)
187
+ or a `draft` with the list of its empty texts, or why it is no plan. The rule never looks at a
188
+ text again: a study that has run on a `draft` is `bad-plan`, with the parser's list.
189
+ - **The schemas are the domain's types.** The domain's types are written by hand (the domain
190
+ imports no zod); the adapter holds a compile-time proof that each schema's output — registry,
191
+ study, measurement, requirement, approval, author check, plan, ready-or-draft — and its domain type are the
192
+ same type both ways, so a field added to one alone stops the build.
193
+
194
+ The schemas live beside the registry's, in `src/adapters/study-registry/`. The rule reports a
195
+ `plan` outside the folder, missing, or not of the schema, and — once the study has run (`running`,
196
+ `done`, `stopped`, `superseded`) — a draft (`bad-plan`); a study of a headline measurement
197
+ that has run with no `author_check` in its plan, no plan, or a check by anyone but `author`
198
+ (`author-check`). A `designing` study's template is clean, so `studies new` never leaves the lint
199
+ red.
200
+
201
+ **Compatibility.** A row may name `prereg.json` as its plan: it is parsed by a loose schema of
202
+ its own — keys of its own accepted at every level, those at the top kept in `extra`, a seed makes
203
+ the draw `seeded`, none (or `null`) makes it `all`; a size is not read; an absent check is
204
+ `null` — and judged as the plan, the same contradictions refused; a second rule, `repro/study-plan-name`, warns that it moves to `plan.json` — a warning cannot
205
+ come from an error rule, since ESLint sets the level per rule. A `prereg.json` no row names is not
206
+ read, so no existing paper turns red.
207
+
208
+ 8. **A study's mode.** A measurement's plan declares how its outcomes are kept from the decisions
209
+ on its inputs: `decide_before_run` (the default `studies new` writes) or `exploratory` with its
210
+ reason. `paperlint studies gate` and the rule `repro/outputs-before-decision` hold it, and the
211
+ index carries it in three columns: the mode, the decision's first commit and date, and the first
212
+ output's commit. Its own note: [`study-modes.md`](study-modes.md).
213
+
96
214
  ## Where each part lives
97
215
 
98
- | part | file | layer |
99
- | --------------------------------------- | ----------------------------------- | ------- |
100
- | the types, the judge, the index, ids | `src/domain/study-registry.ts` | domain |
101
- | the schema (zod), parsing the text | `src/adapters/study-registry/` | adapter |
102
- | what the app asks of a parser | `src/ports/study-registry-codec.ts` | port |
103
- | the rule: list the folder, read, report | `src/study-registry-rule.ts` | app |
104
- | the two commands | `src/studies.ts` | app |
105
- | wiring, the clock for today's date | `src/cli.ts` | root |
216
+ | part | file | layer |
217
+ | -------------------------------------- | ----------------------------------- | ------- |
218
+ | the types, the judge, the index, ids | `src/domain/study-registry.ts` | domain |
219
+ | the plan's type and template | `src/domain/study-registry.ts` | domain |
220
+ | the schemas (zod) of registry and plan | `src/adapters/study-registry/` | adapter |
221
+ | what the app asks of a parser | `src/ports/study-registry-codec.ts` | port |
222
+ | both rules: list, read, report | `src/study-registry-rule.ts` | app |
223
+ | the two commands | `src/studies.ts` | app |
224
+ | wiring, the clock for today's date | `src/cli.ts` | root |
106
225
 
107
226
  The judge is pure: it takes the folder names, the parsed registry (or why it is absent or unreadable),
108
- the README's text, and which result files exist, and returns findings. The rule and the commands are
227
+ the README's text, which result files exist, and each named plan parsed, and returns findings. The rule and the commands are
109
228
  the only places that touch the disk, through the `Files` and `ListDir` ports.
110
229
 
111
230
  ## Generated index, and prettier
@@ -127,4 +246,7 @@ a cell is escaped and a line break becomes a space.
127
246
 
128
247
  - Whether the `question` is one sentence, or the `result` one line, beyond their being strings.
129
248
  - What a study's `README.md` says, or whether its frontmatter matches the row.
130
- - Files inside a study folder other than `result_file`.
249
+ - Files inside a study folder other than `result_file` and the `plan` its row names.
250
+ - `study/declare-sample` still reads only `prereg.json`, and asks for different fields
251
+ (`strata`, `screens`, `coverage`, and an `author_check` of `who`, `when`, `units_seen`,
252
+ `units_drawn`, `verdict`); merging the two shapes is its own decision.
package/docs/e2e.md CHANGED
@@ -8,10 +8,14 @@ or CI:
8
8
  | -------------------------- | ------------- | -------------------------------------------- | --------------------------- |
9
9
  | `npm run test:e2e:install` | `e2e-install` | npm and pnpm, and the network for `npm pack` | `test/e2e/install/*.e2e.ts` |
10
10
  | `npm run test:e2e:tex` | `e2e-tex` | TeX Live from `paperlint toolchain`, banal | `test/e2e/tex/*.e2e.ts` |
11
- | `npm run test:e2e` | both | | |
12
-
13
- Both areas are part of `npm run check`, so nobody has to remember to call them. CI runs each in the
14
- job that has its environment: `test:e2e:install` in `gates`, `test:e2e:tex` in `build-e2e`.
11
+ | `npm run test:e2e:studies` | `e2e-studies` | git | `test/e2e/studies/*.e2e.ts` |
12
+ | `npm run test:e2e` | all three | | |
13
+
14
+ All three areas are part of `npm run check`, so nobody has to remember to call them. CI runs each in
15
+ the job that has its environment: `test:e2e:install` and `test:e2e:studies` in `gates`, `test:e2e:tex`
16
+ in `build-e2e`. The studies run drives a study decided before its run through the real binary in a
17
+ real git repository: the gate, the order the lint reads, a shallow clone named, and the two commits
18
+ `studies index` writes into the index.
15
19
 
16
20
  This page says what they prove, what they deliberately do not, and when a change owes a new one.
17
21
 
package/docs/layout.md CHANGED
@@ -18,8 +18,8 @@ flowchart TB
18
18
  subgraph S3["3 · study: repro/#lt;id#gt;-#lt;slug#gt;/"]
19
19
  direction TB
20
20
  D["<b>design</b> · build-benchmark<br/>paperlint studies new<br/>writes: the study's folder, README.md,<br/>its row in repro/studies.json<br/>checked by: repro/allowed-files-only,<br/>repro/study-registered"]
21
- P["<b>pre-register</b> · build-benchmark<br/>writes: prereg.json, before the draw<br/>checked by: study/declare-sample (experimental, off)"]
22
- A["<b>author check</b> · you see the drawn units<br/>writes: author_check in prereg.json<br/>checked by: study/declare-sample (experimental, off)"]
21
+ P["<b>plan</b> · build-benchmark<br/>fills: plan.json, before the draw<br/>checked by: repro/study-registered,<br/>once the study runs"]
22
+ A["<b>author check</b> · you see the drawn units<br/>writes: author_check in plan.json<br/>checked by: repro/study-registered,<br/>for a headline measurement"]
23
23
  R["<b>run</b> · your scripts<br/>writes: the study folder,<br/>repro/tools/, repro/data/<br/>checked by: repro/allowed-files-only"]
24
24
  L["<b>labels</b> · build-label-page<br/>a phone page; the answers go<br/>into the study's data/"]
25
25
  Y["<b>analysis</b> · build-benchmark<br/>writes: the numbers, NOTES.md,<br/>the study's row: status, result<br/>then paperlint studies index<br/>checked by: repro/study-registered"]
@@ -110,7 +110,9 @@ Both are explained in [`venues.md`](venues.md).
110
110
  | `repro/`, its top | 3 study | `build-benchmark`, your scripts | [`repro/allowed-files-only`](rules/repro/allowed-files-only.md); the hook `paper-folder-layout` refuses a new loose entry |
111
111
  | `repro/studies.json` | 3 study | `paperlint studies new` appends a row; you edit a row when its study ends; `paperlint studies index` regenerates the index in `repro/README.md` | [`repro/study-registered`](rules/repro/study-registered.md): a row per study folder, its fields, the index in sync |
112
112
  | `repro/<id>-<slug>/` | 3 study | `paperlint studies new` (the folder and its `README.md`), then `build-benchmark` (the design, in its `README.md`) | [`repro/allowed-files-only`](rules/repro/allowed-files-only.md): the name, and a `README.md` in it; [`repro/study-registered`](rules/repro/study-registered.md): its row |
113
- | `repro/<id>-<slug>/prereg.json` | 3 study | `build-benchmark`, before the sample is drawn; you add the author's check | [`study/declare-sample`](rules/study/declare-sample.md) |
113
+ | `repro/<id>-<slug>/plan.json` | 3 study | `paperlint studies new` writes the template; `build-benchmark` fills it before the sample is drawn; you add the author's check | [`repro/study-registered`](rules/repro/study-registered.md): its fields, filled once the study runs, and `author_check` in a headline measurement |
114
+ | `repro/<id>-<slug>/prereg.json` | 3 study | the plan's old name, still read when the row names it | [`repro/study-plan-name`](rules/repro/study-plan-name.md) (warning); [`study/declare-sample`](rules/study/declare-sample.md), off by default |
115
+ | `repro/<id>-<slug>/decision.md` | 3 study | you: the decisions on the inputs of a study decided before its run, committed before the run (`paperlint studies gate`); the plan names it, and it may live elsewhere in the paper | [`repro/outputs-before-decision`](rules/repro/outputs-before-decision.md) |
114
116
  | `repro/<id>-<slug>/data/` | 3 study | `build-label-page` exports the labels here | nothing |
115
117
  | `repro/tools/`, `repro/data/` | 3 study | your scripts; `paperlint build` keeps `repro/data/references-cache.json` | nothing inside them |
116
118
  | `NOTES.md` | 3, then 4 | you: each number → the data it comes from; `build-label-page` adds a line per label file | nothing |
@@ -134,24 +136,27 @@ A check runs at one of four moments:
134
136
  The lint-only job skips what needs a build (pages, fonts, references).
135
137
  - **On demand** — a skill, when you ask for what it does. A skill stops nothing; it writes.
136
138
 
137
- | what | kind | when it runs | what it stops |
138
- | ------------------------------------------------------------------------------------------- | ----- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
139
- | `paper-folder-layout` | hook | before an agent creates a file (Edit, Write) | a new entry the layout does not allow, at a paper's root or the top of its `repro/`; never an existing file |
140
- | `paper-edit-guard` | hook | before an agent's shell command | a shell command that writes or moves a paper's source, which would skip the hooks that run after an edit |
141
- | `paper-skills-nudge` | hook | after an agent edits a paper's source | nothing: it shows the agent the pre-submit checklist |
142
- | `paper-status-gates` | hook | after an agent edits a paper's source | nothing: it prints the paper's readiness and its unrun gates, for a person reading the transcript |
143
- | [`paper/allowed-files-only`](rules/paper/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a stray file or folder at a paper's root (error) |
144
- | [`repro/allowed-files-only`](rules/repro/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a loose entry at the top of `repro/`, a folder that is not a study's, a study folder without `README.md` (error) |
145
- | [`repro/study-registered`](rules/repro/study-registered.md) | rule | every `paperlint lint`, and CI | a study folder without its row in `repro/studies.json`, a row that does not match, an index out of sync (error) |
146
- | [`study/declare-sample`](rules/study/declare-sample.md) | rule | every `paperlint lint`, and CI, once a paper turns it on | a study's `prereg.json` without its population, strata, screens, coverage or the author's check (experimental, off by default) |
147
- | [`paper/folder-venue-leftover`](rules/paper/folder-venue-leftover.md) | rule | every `paperlint lint`, and CI | nothing: it warns when the folder is named after another venue |
148
- | `review/frontmatter`, `sibling/frontmatter` | rule | every `paperlint lint`, and CI | a review whose findings break the schema (error); a sibling card that does not say how much was read (warning) |
149
- | `paper/stages`, `paper/source` | rule | every `paperlint lint`, and CI | a stage whose frozen PDF or source is missing or changed (error) |
150
- | the rules on the source (`tex/*`, `paper/*`, `bib/*`) | rule | every `paperlint lint`, and CI | what each one's [row](rules.md) says |
151
- | the rules on what `paperlint build` measured (`pdf/*`, `format/page-limit`, the references) | rule | every `paperlint lint` after a build; CI's full job | a PDF over the page limit, in the wrong font or format, or with a bad reference |
152
- | `study-accepted-papers` | skill | when you ask; stage 2 (modes A, B) and stage 5 (modes C, D) | nothing: it fills the venue's corpus, and writes the strong-accept scorecard in `PIPELINE-STATUS.md` |
153
- | `build-benchmark` | skill | when you ask; stage 3 | nothing: it registers the study with `paperlint studies new`, writes its design and its `prereg.json`, and updates its row when it ends |
154
- | `build-label-page` | skill | when you ask; stage 3 | nothing: it builds the labelling page, and exports the answers into the study's `data/` |
139
+ | what | kind | when it runs | what it stops |
140
+ | ------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `paper-folder-layout` | hook | before an agent creates a file (Edit, Write) | a new entry the layout does not allow, at a paper's root or the top of its `repro/`; never an existing file |
142
+ | `paper-edit-guard` | hook | before an agent's shell command | a shell command that writes or moves a paper's source, which would skip the hooks that run after an edit |
143
+ | `paper-skills-nudge` | hook | after an agent edits a paper's source | nothing: it shows the agent the pre-submit checklist |
144
+ | `paper-status-gates` | hook | after an agent edits a paper's source | nothing: it prints the paper's readiness and its unrun gates, for a person reading the transcript |
145
+ | [`paper/allowed-files-only`](rules/paper/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a stray file or folder at a paper's root (error) |
146
+ | [`repro/allowed-files-only`](rules/repro/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a loose entry at the top of `repro/`, a folder that is not a study's, a study folder without `README.md` (error) |
147
+ | [`repro/study-registered`](rules/repro/study-registered.md) | rule | every `paperlint lint`, and CI | a study folder without its row in `repro/studies.json`, a row that does not match, a measurement or plan that does not hold, an index out of sync (error) |
148
+ | [`repro/study-plan-name`](rules/repro/study-plan-name.md) | rule | every `paperlint lint`, and CI | nothing: it warns when a study's plan is still named `prereg.json` |
149
+ | [`repro/outputs-before-decision`](rules/repro/outputs-before-decision.md) | rule | every `paperlint lint`, and CI (with the whole history) | an output of a study decided before its run that git shows was not made after the decision |
150
+ | `paperlint studies gate` | command | before a decide-first study's run | the run, until the decision is committed |
151
+ | [`study/declare-sample`](rules/study/declare-sample.md) | rule | every `paperlint lint`, and CI, once a paper turns it on | a study's `prereg.json` without its population, strata, screens, coverage or the author's check (experimental, off by default) |
152
+ | [`paper/folder-venue-leftover`](rules/paper/folder-venue-leftover.md) | rule | every `paperlint lint`, and CI | nothing: it warns when the folder is named after another venue |
153
+ | `review/frontmatter`, `sibling/frontmatter` | rule | every `paperlint lint`, and CI | a review whose findings break the schema (error); a sibling card that does not say how much was read (warning) |
154
+ | `paper/stages`, `paper/source` | rule | every `paperlint lint`, and CI | a stage whose frozen PDF or source is missing or changed (error) |
155
+ | the rules on the source (`tex/*`, `paper/*`, `bib/*`) | rule | every `paperlint lint`, and CI | what each one's [row](rules.md) says |
156
+ | the rules on what `paperlint build` measured (`pdf/*`, `format/page-limit`, the references) | rule | every `paperlint lint` after a build; CI's full job | a PDF over the page limit, in the wrong font or format, or with a bad reference |
157
+ | `study-accepted-papers` | skill | when you ask; stage 2 (modes A, B) and stage 5 (modes C, D) | nothing: it fills the venue's corpus, and writes the strong-accept scorecard in `PIPELINE-STATUS.md` |
158
+ | `build-benchmark` | skill | when you ask; stage 3 | nothing: it registers the study with `paperlint studies new`, writes its design, fills its `plan.json`, and updates its row when it ends |
159
+ | `build-label-page` | skill | when you ask; stage 3 | nothing: it builds the labelling page, and exports the answers into the study's `data/` |
155
160
 
156
161
  The hooks are wired by `paperlint init` ([`install.md`](install.md)). A rule's level and options:
157
162
  [`rules.md`](rules.md).