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,119 @@
1
+ # repro/outputs-before-decision
2
+
3
+ **Level:** error · **Reads:** `repro/studies.json`, each study's `plan.json`, the files under a
4
+ decide-first study's folder, and git's history of them · **Reported on:** the first line of the
5
+ paper's `PIPELINE-STATUS.md`, once per finding
6
+
7
+ ## What it catches
8
+
9
+ A study decided before its run — its plan's `mode` is `decide_before_run`, the default
10
+ `paperlint studies new` writes for a measurement — whose history does not show that order:
11
+
12
+ ```json
13
+ {
14
+ "mode": {
15
+ "kind": "decide_before_run",
16
+ "outputs": ["out/**", "RESULTS.md"],
17
+ "decision": {
18
+ "file": "repro/P1-S01-outcome/decision.md",
19
+ "note": "exclusions, veto, and the sample approved"
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ - **`output-before-decision`** — an output (a file the `outputs` globs match, under the study's
26
+ folder) whose FIRST commit is not a strict descendant of the decision file's FIRST commit: earlier,
27
+ on a line of history the decision is not on, or in the very same commit, which says nothing about
28
+ which came first. An output committed and later deleted still counts.
29
+ - **`undecided-output`** — an output in the tree, or one ever committed and since deleted, while the
30
+ decision file has never been committed.
31
+ - **`decision-is-output`** — the decision file lies in the study's folder and an output glob matches
32
+ it: the decision would be a file its own run produces.
33
+ - **`decision-deleted`** — the decision file was committed and later deleted: it is not in HEAD, so
34
+ the study has no decision its outputs could follow, and the order is not judged against it.
35
+ - **`decision-not-file`** — the decision path names a directory or a symbolic link: git answers for
36
+ a directory with the commits of anything under it, and records a link rather than the file it
37
+ points to, so no order is read from either. (A path that names the paper's folder
38
+ itself, `.`, or ends in `/` is refused when the plan is parsed.)
39
+ - **`shallow`** — the clone is shallow, so every file's first commit is the clone's: no order can be
40
+ read, and the rule says so instead of judging.
41
+ - **`no-history`** — git could not answer (not installed, not a repository).
42
+
43
+ The study's own files — `plan.json`, `README.md` and the decision file — are never outputs. A symbolic
44
+ link in the study's folder is one entry, as git stores it, and is never followed: a link to the folder
45
+ itself cannot loop the walk, and one to a tree outside brings none of its files into the study.
46
+
47
+ ## Why
48
+
49
+ A measurement's inputs — which runs count, which units are excluded, whether the sample is approved
50
+ — must be decided before anyone has seen an outcome; otherwise the decision can follow the result.
51
+ "Decide first, then compute" needs no key and no encryption: the decision is committed, the run
52
+ starts only after (`paperlint studies gate` exits 0 only then), and git's history is the proof of
53
+ order. This rule re-reads that proof on every lint, so a reviewer who clones the repository can run
54
+ the same check and get the same answer. The design is
55
+ [`docs/design/study-modes.md`](../../design/study-modes.md).
56
+
57
+ **It asks git for an order, never pins a sha**, which is why it can live as a lint rule (rules in
58
+ `eslint-rules/` may not run git: a pinned sha is deleted by a squash or a gc). A squash merge keeps
59
+ the order when the decision was merged before the run — the decide-first workflow — and a shallow
60
+ checkout is named, not read as a violation.
61
+
62
+ ## Examples
63
+
64
+ Failing:
65
+
66
+ ```text
67
+ papers/example/PIPELINE-STATUS.md
68
+ 1:1 error `repro/P1-S01-outcome/RESULTS.md`, an output of `P1-S01`, entered history in
69
+ 4f2a9c0e1b7d (2026-10-16T10:00:00+05:00), the very commit that first recorded its
70
+ decision, 4f2a9c0e1b7d (2026-10-16T10:00:00+05:00). The study is declared decided
71
+ before its run, and its history does not show that order. What to do: if the decision
72
+ really came first, nothing in this history can prove it — declare the study
73
+ `exploratory` with the reason, or run it again after the decision; never rewrite the
74
+ history to make it pass.
75
+ Rule repro/outputs-before-decision: https://github.com/zernie/paperlint/blob/main/docs/rules/repro/outputs-before-decision.md repro/outputs-before-decision
76
+ ```
77
+
78
+ Passing: `decision.md` committed in one commit; `RESULTS.md` and `out/` committed in a later commit
79
+ that descends from it. An `exploratory` study is not this rule's.
80
+
81
+ ## Options / preset fields
82
+
83
+ No rule options. The study declares its mode in its plan.
84
+
85
+ ## What it does not check
86
+
87
+ - What the decision says. The gate is that it is committed, not its content.
88
+ - **A decision amended after the outputs**: the order is between FIRST commits. A later edit of the
89
+ decision file is visible in its history (`git log -- <file>`) but not reported.
90
+ - That nobody ran the study before the decision and kept the outputs out of git. History records
91
+ what was committed; a run on a machine that never committed it leaves no trace.
92
+ - A study whose row names a folder that is not one inside `repro/` (`../x`), or a folder that is a
93
+ symbolic link: nothing is read through it, so it is no study here;
94
+ [`repro/study-registered`](study-registered.md) reports the row or reads its plan as missing.
95
+ - A study whose plan is still a draft: only a ready plan's mode decides anything, and
96
+ [`repro/study-registered`](study-registered.md) reports an unfilled plan once the study runs.
97
+ - Commit dates. They are shown in the message, but the order is ancestry, which a rebase can
98
+ rewrite only by making a new history that a reviewer comparing against the published one sees.
99
+
100
+ ## How to fix
101
+
102
+ Commit the decision first, then let the run start:
103
+
104
+ ```sh
105
+ git add papers/example/repro/P1-S01-outcome/decision.md && git commit -m "P1-S01: decision"
106
+ npx paperlint studies gate papers/example P1-S01 && ./run.sh
107
+ ```
108
+
109
+ In CI, check out the whole history: `actions/checkout` with `fetch-depth: 0`. A study whose outputs
110
+ were made before its decision cannot be repaired by editing history; declare it exploratory:
111
+
112
+ ```json
113
+ {
114
+ "mode": {
115
+ "kind": "exploratory",
116
+ "reason": "outputs existed before the exclusions were fixed"
117
+ }
118
+ }
119
+ ```
@@ -0,0 +1,70 @@
1
+ # repro/study-plan-name
2
+
3
+ **Level:** warning · **Reads:** `repro/studies.json` · **Reported on:** the first line of the
4
+ paper's `PIPELINE-STATUS.md`, once per study
5
+
6
+ ## What it catches
7
+
8
+ A study whose row in `repro/studies.json` names its plan as `prereg.json` — the name the plan had
9
+ before `paperlint studies new` wrote it as `plan.json`.
10
+
11
+ ```json
12
+ {
13
+ "id": "P1-S01",
14
+ "folder": "P1-S01-rule-census",
15
+ "plan": "prereg.json",
16
+ "…": "…"
17
+ }
18
+ ```
19
+
20
+ The file is still read as the study's plan — by a loose schema, since it was written by hand before
21
+ the plan had one: keys of its own are accepted, a seed makes its draw `seeded` and none makes it a
22
+ draw of `all`, a size is not read, and an absent check is `null` —
23
+ and [`repro/study-registered`](study-registered.md) judges it like any `plan.json`. This rule only
24
+ asks for the new name. A `prereg.json` moved to `plan.json` is parsed strictly: its own keys go in
25
+ `extra`.
26
+
27
+ ## Why
28
+
29
+ A study's plan was once a hand-written `prereg.json`. The name misled: it is not a pre-registration
30
+ with an outside registry, it is the study's own plan, fixed before the runs, with the author's check
31
+ of the drawn units. Papers that wrote one before `plan.json` existed keep working; this warning tells
32
+ them where the file moves, without failing their build.
33
+
34
+ ## Examples
35
+
36
+ Failing (a warning):
37
+
38
+ ```text
39
+ papers/example/PIPELINE-STATUS.md
40
+ 1:1 warning The plan of `P1-S01` is `prereg.json`, its old name; it is read as the plan. What to
41
+ do: rename it to `plan.json` in the study's folder (`git mv`) and set the row's `plan`
42
+ to `plan.json`.
43
+ Rule repro/study-plan-name: https://github.com/zernie/paperlint/blob/main/docs/rules/repro/study-plan-name.md repro/study-plan-name
44
+ ```
45
+
46
+ Passing: the row names `"plan": "plan.json"`, as `paperlint studies new` writes it; or the row
47
+ names no plan.
48
+
49
+ ## Options / preset fields
50
+
51
+ No rule options.
52
+
53
+ ## What it does not check
54
+
55
+ - A `prereg.json` in a study folder whose row names no `plan`: the file is not the study's plan
56
+ until the row says so, and is not read.
57
+ - Anything inside the plan: that is [`repro/study-registered`](study-registered.md).
58
+
59
+ ## How to fix
60
+
61
+ ```sh
62
+ git mv papers/example/repro/P1-S01-rule-census/prereg.json papers/example/repro/P1-S01-rule-census/plan.json
63
+ ```
64
+
65
+ then set the row's `"plan": "plan.json"` in `repro/studies.json`. To keep the old name for one
66
+ paper, switch the rule off in its `paperlint.json`:
67
+
68
+ ```json
69
+ { "rules": { "repro/study-plan-name": "off" } }
70
+ ```
@@ -1,8 +1,8 @@
1
1
  # repro/study-registered
2
2
 
3
3
  **Level:** error · **Reads:** the listing of the paper's `repro/`, `repro/studies.json`,
4
- `repro/README.md`, and each `result_file` the registry names · **Reported on:** the first line of the
5
- paper's `PIPELINE-STATUS.md`, once per finding
4
+ `repro/README.md`, and each `result_file` and `plan` the registry names · **Reported on:** the first
5
+ line of the paper's `PIPELINE-STATUS.md`, once per finding
6
6
 
7
7
  ## What it catches
8
8
 
@@ -46,9 +46,142 @@ is named `<id>-<slug>` after its row — `P1-S03-baseline`. The design: [`study-
46
46
  | `superseded_by` | optional: the id of the study that replaced it; required when `superseded` |
47
47
  | `in_paper` | `yes`, `no`, or `registry` (listed in the registry, not in the paper's text) |
48
48
 
49
- A row may carry more fields — a paper's own measurement metadata (`role`, `frame`, `method`,
50
- `outcome`, …) — and `paperlint studies new` keeps them when it rewrites the file. The top level holds
51
- only `prefix` and `studies`.
49
+ | `measurement` | optional: the id of the measurement the study belongs to (below) |
50
+ | `plan` | optional: the study's plan, a file inside its folder — `plan.json` (below) |
51
+
52
+ A row may carry more fields — a paper's own metadata (`frame`, `method`, `outcome`, …) — and
53
+ `paperlint studies new` keeps them when it rewrites the file. Beside `prefix` and `studies`, the top
54
+ level holds only the optional `author`, `requirements` and `measurements`; any other key is an error.
55
+
56
+ ### Measurements, requirements, the author
57
+
58
+ One measurement — a census, a benchmark — often spans several studies: a pilot, the main run, a
59
+ replication. A measurement is declared once, at the top level, and each of its studies' rows names
60
+ it in `measurement`. The author's requirements on the measurements are kept in their words, beside
61
+ them:
62
+
63
+ ```json
64
+ {
65
+ "prefix": "P1",
66
+ "author": "A. Author",
67
+ "requirements": [
68
+ {
69
+ "id": "R1",
70
+ "text": "Every sample is a random draw from all rules found, no filter by difficulty.",
71
+ "date": "2026-10-10",
72
+ "source": "chat"
73
+ }
74
+ ],
75
+ "measurements": [
76
+ {
77
+ "id": "M1",
78
+ "title": "Rule census",
79
+ "rq": "RQ1",
80
+ "role": "headline",
81
+ "approval": {
82
+ "by": "A. Author",
83
+ "date": "2026-10-12",
84
+ "quote": "This is the one."
85
+ },
86
+ "requirements": ["R1"]
87
+ }
88
+ ],
89
+ "studies": [
90
+ {
91
+ "id": "P1-S01",
92
+ "folder": "P1-S01-census-pilot",
93
+ "measurement": "M1",
94
+ "plan": "plan.json",
95
+ "…": "…"
96
+ },
97
+ {
98
+ "id": "P1-S02",
99
+ "folder": "P1-S02-census",
100
+ "measurement": "M1",
101
+ "plan": "plan.json",
102
+ "…": "…"
103
+ }
104
+ ]
105
+ }
106
+ ```
107
+
108
+ | field | what it holds |
109
+ | ----------------------------- | -------------------------------------------------------------------------------- |
110
+ | `author` | optional: the paper's author, the one person who approves a headline measurement |
111
+ | `requirements[].id` | the requirement's id, named by the measurements that must meet it |
112
+ | `requirements[].text` | the author's words, verbatim |
113
+ | `requirements[].date` | `YYYY-MM-DD` |
114
+ | `requirements[].source` | where they said it: a chat, a message, a review comment |
115
+ | `measurements[].id` | the measurement's id, named by its studies' rows |
116
+ | `measurements[].title` | a plain name |
117
+ | `measurements[].rq` | optional: the research question it answers |
118
+ | `measurements[].role` | `headline` (the paper's claim rests on it) or `baseline` |
119
+ | `measurements[].approval` | the author's approval: `by`, `date`, their words in `quote`, optional `source` |
120
+ | `measurements[].requirements` | optional: the ids of the requirements it must meet |
121
+
122
+ A measurement may carry more fields, as a row does; a requirement and an approval are exactly
123
+ their fields. **A registry without `measurements` is judged as before**: a row's `measurement` is
124
+ then a field of the paper's own, and nothing checks it.
125
+
126
+ ### The study's plan, `plan.json`
127
+
128
+ Each study's plan is a JSON file in its folder, fixed before the runs: what it measures, the
129
+ population and how the sample is drawn from it, the arms compared, the primary outcome, and the
130
+ author's check of the drawn units. `paperlint studies new` writes the template beside the study's
131
+ `README.md` and names it in the row's `plan`:
132
+
133
+ ```json
134
+ {
135
+ "question": "",
136
+ "population": {
137
+ "source": "",
138
+ "draw": { "kind": "all", "method": "" }
139
+ },
140
+ "arms": [],
141
+ "primary_outcome": "",
142
+ "author_check": null,
143
+ "mode": {
144
+ "kind": "decide_before_run",
145
+ "outputs": [],
146
+ "decision": { "file": "repro/P1-S02-census/decision.md", "note": "" }
147
+ }
148
+ }
149
+ ```
150
+
151
+ | field | what it holds |
152
+ | ------------------------ | -------------------------------------------------------------------------------- |
153
+ | `question` | what the study measures, one sentence |
154
+ | `population.source` | where the units come from: the census, crawl or registry, with its date |
155
+ | `population.draw.kind` | `all` (the whole population, a census: no seed) or `seeded` (a sample) |
156
+ | `population.draw.method` | how the sample is drawn: `census`, `random`, `stratified` … |
157
+ | `population.draw.seed` | `seeded` only, and required there: the draw's seed, a string or a number |
158
+ | `arms` | the conditions compared, each `{ "name": … }`, each name once; `[]` for a census |
159
+ | `primary_outcome` | the one number the study reports first |
160
+ | `author_check` | `null` until the author has seen the drawn units; then `by`, `date`, `quote` |
161
+ | `extra` | optional: the paper's own keys (hypotheses, models, budget), kept unread |
162
+ | `mode` | a measurement's: exactly one of `decide_before_run` or `exploratory` |
163
+
164
+ `mode` is required of a `measurement` (other kinds may omit it): how its outcomes are kept from the
165
+ decisions on its inputs — decided before the run (the default the template writes; its `outputs`,
166
+ and its `decision` file relative to the paper, committed before the run) or `exploratory` with its
167
+ `reason`. Without it a measurement's plan is a draft whose empty text is `mode`. The modes, and the
168
+ rule that holds the order in git, [`repro/outputs-before-decision`](outputs-before-decision.md), are
169
+ in [`docs/design/study-modes.md`](../../design/study-modes.md).
170
+
171
+ The template's draw is of all the population; a sampled study changes it to
172
+ `{ "kind": "seeded", "method": "random", "seed": 7 }`. The population's size is not stored: it is
173
+ counted from the source, and a stored copy could only contradict it.
174
+
175
+ The plan is closed at every level, and holds no contradiction. Each of these makes the file "not a
176
+ study plan" (`bad-plan`, with the reason): a misspelt key; a key of the paper's own outside `extra`,
177
+ or a key of `extra` that is a field of the plan; a missing key — `null` is the one way to say "not
178
+ yet"; a `seeded` draw without its seed, or an `all` draw with one; two arms of one name; and an
179
+ `author_check` on a plan whose required texts are still empty — the author approves a complete plan.
180
+
181
+ A plan named `prereg.json`, its old name, is parsed by a loose schema — keys of its own accepted at
182
+ every level, those at the top kept in `extra`; a seed makes the draw `seeded`, none (or `null`)
183
+ makes it `all`; a size is not read; an absent check is `null` — and
184
+ [`repro/study-plan-name`](study-plan-name.md) warns that it moves to `plan.json`.
52
185
 
53
186
  The rule reports:
54
187
 
@@ -56,12 +189,13 @@ The rule reports:
56
189
  managers leave there, like `__pycache__/` and `node_modules/`, are not studies);
57
190
  - a row whose folder does not exist, and a folder named by more than one row;
58
191
  - an id that is not `<prefix>-S<digits>`, an id on more than one row, and a folder that is not
59
- `<id>-<slug>`;
192
+ `<id>-<slug>` — a row naming `../x` among them, through which no file is read;
60
193
  - a title over 8 words;
61
194
  - `done` without `result` or `result_file`, and a `result_file` that is not a file inside the folder;
62
195
  - `superseded` without `superseded_by`, or naming an id that is not in the registry;
63
- - an index out of sync: the block of `repro/README.md` between the markers below must equal the
64
- text the registry generates, character for character;
196
+ - an index out of sync: the block of `repro/README.md` between the markers below must hold the
197
+ cells the registry and the plans generate, whatever padding a formatter gave the table — every
198
+ column but the two `studies index` reads from git (below);
65
199
 
66
200
  ```text
67
201
  <!-- studies:begin — generated from studies.json by `paperlint studies index`; do not edit -->
@@ -69,7 +203,21 @@ The rule reports:
69
203
  ```
70
204
 
71
205
  - a `repro/` with study folders and no `repro/studies.json` (one finding naming the file), and a
72
- `studies.json` that is not JSON or not of the shape above (one finding with the reasons).
206
+ `studies.json` that is not JSON or not of the shape above (one finding with the reasons);
207
+ - when the registry has `measurements`: a row whose `measurement` is not one of them
208
+ (`unknown-measurement`), a measurement no row names (`empty-measurement`), and a measurement that
209
+ must meet a requirement the registry does not hold (`unknown-requirement`);
210
+ - a `headline` measurement with no `approval`, or, when the registry names its `author`, one approved
211
+ by anyone else (`headline-approval`);
212
+ - a row's `plan` that is outside its folder, not a file, or not of the plan's shape — and, once the
213
+ study has run (`running`, `done`, `stopped`, `superseded`), a plan that leaves `question`,
214
+ `population.source`, `population.draw.method` or `primary_outcome` empty, or — for a measurement —
215
+ declares no `mode` or leaves one of its texts empty (`bad-plan`);
216
+ - a study of a `headline` measurement that has run with no `author_check` in its plan — or no plan —
217
+ or, when the registry names its `author`, a check by anyone else (`author-check`).
218
+
219
+ A study that is still `designing` owes neither a filled plan nor the author's check: the template
220
+ `studies new` writes is clean until the first run. A `dropped` study owes neither.
73
221
 
74
222
  ## Why
75
223
 
@@ -79,9 +227,24 @@ study began, not what it is. The registry gives each study a number that does no
79
227
  person can read, and says in one place where every study stands. A rule holds it, not a guideline: a
80
228
  row the rule does not check is a row that drifts.
81
229
 
82
- The index in `repro/README.md` is generated, so it is never edited by hand and never parsed: the rule
83
- compares strings. It is a table padded the way prettier writes one, so formatting the README leaves
84
- it in sync.
230
+ The index in `repro/README.md` is generated, so it is never edited by hand: the rule compares its
231
+ cells. It is a table padded the way prettier writes one, so formatting the README leaves it in sync.
232
+
233
+ Three of its columns say how each study kept its outcomes from its decisions
234
+ ([`docs/design/study-modes.md`](../../design/study-modes.md)):
235
+
236
+ | column | cell |
237
+ | -------------- | ------------------------------------------------------------------------------------ |
238
+ | `mode` | `decide_before_run`, `exploratory: <reason>`, or `—` for a study whose plan has none |
239
+ | `decision` | the decision file's first commit (12 characters) and its date, or `not committed` |
240
+ | `first output` | the first commit of the oldest output and its date, or `none` |
241
+
242
+ The `mode` comes from the plan, which is in the tree, so the rule compares it. The two commits are
243
+ what git's history said when `paperlint studies index` last ran: a squash merge or a rebase gives the
244
+ same files new commits, and a shallow CI clone has none to read, so the rule never compares those two
245
+ columns. Regenerate the index before reading them; the order itself is judged by
246
+ [`repro/outputs-before-decision`](outputs-before-decision.md), not by the table. In a shallow clone
247
+ `studies index` writes `unknown (shallow clone)`.
85
248
 
86
249
  ## Examples
87
250
 
@@ -98,6 +261,20 @@ Rule repro/study-registered: https://github.com/zernie/paperlint/blob/main/docs/
98
261
 
99
262
  Failing: a row set to `done` by hand, with no result, and the index not regenerated — two findings.
100
263
 
264
+ Failing: the census of the example above set to `running` while its plan is still the template —
265
+ the plan is reported for its empty fields, and the study for the author's check it lacks:
266
+
267
+ ```text
268
+ papers/example/PIPELINE-STATUS.md
269
+ 1:1 error The plan of `P1-S02`, `plan.json` in its folder, leaves `question`, `population.source`,
270
+ `population.draw.method`, `primary_outcome`, `mode.outputs` and `mode.decision.note`
271
+ empty, and the study is `running`: a plan is written before the runs. What to do: fill
272
+ them in, or set the study back to `designing`.
273
+ 1:1 error `P1-S02` is `running` in the headline measurement `M1`, and its plan has no
274
+ `author_check`. What to do: show the author the drawn units, then record their answer
275
+ in the plan's `author_check` — `by`, `date`, and their words in `quote`.
276
+ ```
277
+
101
278
  Passing: a paper with no study folder; and a `repro/` made only by `paperlint studies new`, its rows
102
279
  edited as the studies end and its index regenerated:
103
280
 
@@ -107,6 +284,7 @@ repro/
107
284
  ├── studies.json
108
285
  ├── P1-S01-baseline-pass-rate/
109
286
  │ ├── README.md
287
+ │ ├── plan.json
110
288
  │ └── results.tsv
111
289
  ├── tools/
112
290
  └── data/
@@ -115,9 +293,9 @@ repro/
115
293
  ```markdown
116
294
  <!-- studies:begin — generated from studies.json by `paperlint studies index`; do not edit -->
117
295
 
118
- | id | title | status | result | in paper | folder |
119
- | ------ | ------------------ | ------ | ------------------ | -------- | -------------------------------------------------------- |
120
- | P1-S01 | Baseline pass rate | done | 41 of 60 runs pass | yes | [P1-S01-baseline-pass-rate/](P1-S01-baseline-pass-rate/) |
296
+ | id | title | status | result | in paper | mode | decision | first output | folder |
297
+ | ------ | ------------------ | ------ | ------------------ | -------- | ----------------- | ----------------------- | ----------------------- | -------------------------------------------------------- |
298
+ | P1-S01 | Baseline pass rate | done | 41 of 60 runs pass | yes | decide_before_run | 3f2a9c1d7e40 2026-10-14 | 8b51e0aa42c9 2026-10-15 | [P1-S01-baseline-pass-rate/](P1-S01-baseline-pass-rate/) |
121
299
 
122
300
  <!-- studies:end -->
123
301
  ```
@@ -130,7 +308,15 @@ No rule options.
130
308
 
131
309
  - Whether `question` is one sentence and `result` one line, beyond their being text.
132
310
  - What a study's `README.md` says, or whether its frontmatter matches the row.
311
+ - Whether a plan's texts say anything true, beyond their being filled in; whether `arms` should be
312
+ empty; whether the person in `approval` or `author_check` really said it — the rule sees the
313
+ name, not who typed it.
314
+ - A repeated measurement or requirement id, and a requirement no measurement names.
315
+ - A `prereg.json` or `plan.json` that no row names.
133
316
  - Whether the paper cites a study: `in_paper` is declared, not detected.
317
+ - The index's `decision` and `first output` cells: they name commits a squash merge renames.
318
+ - Inside a study folder that is a symbolic link: nothing is read through it, so its plan and its
319
+ result file read as missing.
134
320
  - What may stand at the top of `repro/` besides the study folders, and whether a study folder has a
135
321
  `README.md`: that is [`repro/allowed-files-only`](allowed-files-only.md).
136
322
 
@@ -143,6 +329,11 @@ Create every new study with the command, which takes the next id, makes the fold
143
329
  npx paperlint studies new papers/example "Baseline pass rate" --kind measurement
144
330
  ```
145
331
 
332
+ `studies new` also writes the study's `plan.json`: fill it in before the first run, and, for a
333
+ study of a headline measurement, show the author the drawn units and record their answer in its
334
+ `author_check`. A measurement is added by hand to `measurements`, with the author's `approval` when
335
+ it is the headline, and each of its studies' rows names it in `measurement`.
336
+
146
337
  When a study ends, edit its row in `repro/studies.json` — `status`, `result`, `result_file`,
147
338
  `in_paper`, and `superseded_by` when another study replaced it — then regenerate the index:
148
339