paperlint 10.1.1 → 10.3.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 (72) hide show
  1. package/README.md +2 -0
  2. package/dist/adapters/study-registry/index.d.ts +357 -0
  3. package/dist/adapters/study-registry/index.d.ts.map +1 -0
  4. package/dist/adapters/study-registry/index.js +221 -0
  5. package/dist/adapters/study-registry/index.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +42 -2
  8. package/dist/cli.js.map +1 -1
  9. package/dist/domain/repro-layout.d.ts +12 -4
  10. package/dist/domain/repro-layout.d.ts.map +1 -1
  11. package/dist/domain/repro-layout.js +49 -18
  12. package/dist/domain/repro-layout.js.map +1 -1
  13. package/dist/domain/scaffold-layout.d.ts +4 -3
  14. package/dist/domain/scaffold-layout.d.ts.map +1 -1
  15. package/dist/domain/scaffold-layout.js +7 -4
  16. package/dist/domain/scaffold-layout.js.map +1 -1
  17. package/dist/domain/study-registry.d.ts +372 -0
  18. package/dist/domain/study-registry.d.ts.map +1 -0
  19. package/dist/domain/study-registry.js +593 -0
  20. package/dist/domain/study-registry.js.map +1 -0
  21. package/dist/ports/study-registry-codec.d.ts +15 -0
  22. package/dist/ports/study-registry-codec.d.ts.map +1 -0
  23. package/dist/ports/study-registry-codec.js +2 -0
  24. package/dist/ports/study-registry-codec.js.map +1 -0
  25. package/dist/repro-layout-rule.d.ts.map +1 -1
  26. package/dist/repro-layout-rule.js +4 -3
  27. package/dist/repro-layout-rule.js.map +1 -1
  28. package/dist/studies.d.ts +27 -0
  29. package/dist/studies.d.ts.map +1 -0
  30. package/dist/studies.js +163 -0
  31. package/dist/studies.js.map +1 -0
  32. package/dist/study-registry-rule.d.ts +24 -0
  33. package/dist/study-registry-rule.d.ts.map +1 -0
  34. package/dist/study-registry-rule.js +145 -0
  35. package/dist/study-registry-rule.js.map +1 -0
  36. package/docs/design/study-registry.md +246 -0
  37. package/docs/layout.md +51 -46
  38. package/docs/rules/paper/allowed-files-only.md +3 -2
  39. package/docs/rules/repro/allowed-files-only.md +40 -30
  40. package/docs/rules/repro/study-plan-name.md +70 -0
  41. package/docs/rules/repro/study-registered.md +320 -0
  42. package/docs/rules/study/declare-sample.md +1 -1
  43. package/docs/rules.md +53 -51
  44. package/package.json +1 -1
  45. package/skills/build-benchmark/SKILL.md +39 -8
  46. package/skills/build-benchmark/SKILL.md.spec.ts +38 -7
  47. package/skills/build-label-page/SKILL.md +14 -9
  48. package/skills/build-label-page/SKILL.md.spec.ts +13 -8
  49. package/skills/paper-pipeline/references/artifact-checklist.md +2 -1
  50. package/skills/paper-pipeline/references/pipeline-status-template.md +5 -3
  51. package/skills/run-agent-grid/SKILL.md +13 -1
  52. package/skills/run-agent-grid/SKILL.md.spec.ts +12 -0
  53. package/src/adapters/study-registry/index.test.ts +484 -0
  54. package/src/adapters/study-registry/index.ts +328 -0
  55. package/src/cli.ts +53 -2
  56. package/src/domain/paper-layout.test.ts +1 -1
  57. package/src/domain/repro-layout.test.ts +18 -9
  58. package/src/domain/repro-layout.ts +58 -18
  59. package/src/domain/scaffold-layout.ts +7 -4
  60. package/src/domain/study-measurements.test.ts +629 -0
  61. package/src/domain/study-registry.test.ts +666 -0
  62. package/src/domain/study-registry.ts +1044 -0
  63. package/src/folder-layout-hook.test.ts +10 -0
  64. package/src/new-paper.test.ts +29 -2
  65. package/src/ports/study-registry-codec.ts +15 -0
  66. package/src/repro-layout-rule.ts +4 -3
  67. package/src/rule-cases.test.ts +68 -0
  68. package/src/studies-cli.test.ts +113 -0
  69. package/src/studies.test.ts +359 -0
  70. package/src/studies.ts +268 -0
  71. package/src/study-registry-rule.test.ts +456 -0
  72. package/src/study-registry-rule.ts +239 -0
@@ -0,0 +1,246 @@
1
+ # The study registry — design
2
+
3
+ A paper's `repro/` holds one folder per study. Named `<slug>-YYYY-MM-DD`, they sort by date and say
4
+ nothing else: a real paper reached 79 study folders, and nobody could tell which one a number in the
5
+ paper came from, which ones had ended, or which were current. This note fixes the shape of the
6
+ registry that gives every study a stable number and a plain name, the lint rule that holds it, and the
7
+ two commands that write it.
8
+
9
+ ## What is decided
10
+
11
+ 1. **One registry, `repro/studies.json`, JSON with a schema.** Never markdown: a table in a README is
12
+ what people edit by hand and what a parser has to guess at. The schema (zod, in
13
+ `src/adapters/study-registry/`) is checked where the file is read; nothing after that re-validates.
14
+
15
+ ```json
16
+ {
17
+ "prefix": "P1",
18
+ "studies": [
19
+ {
20
+ "id": "P1-S01",
21
+ "folder": "P1-S01-baseline-pass-rate",
22
+ "title": "Baseline pass rate",
23
+ "question": "How often does the unmodified agent pass the task?",
24
+ "kind": "measurement",
25
+ "status": "done",
26
+ "date": "2026-10-15",
27
+ "result": "41 of 60 runs pass",
28
+ "result_file": "results.tsv",
29
+ "in_paper": "yes"
30
+ }
31
+ ]
32
+ }
33
+ ```
34
+
35
+ | field | values |
36
+ | --------------- | ------------------------------------------------------------------------- |
37
+ | `prefix` | the paper's prefix: a capital letter, then capitals or digits (`P1`) |
38
+ | `id` | `<prefix>-S<two or more digits>`, unique |
39
+ | `folder` | `<id>-<slug>`: the study's folder at the top of `repro/` |
40
+ | `title` | a plain name, at most 8 words |
41
+ | `question` | one sentence: what the study asks |
42
+ | `kind` | `measurement`, `spike`, `tooling`, `data`, `artifact`, `salvage` |
43
+ | `status` | `designing`, `running`, `done`, `stopped`, `superseded`, `dropped` |
44
+ | `date` | `YYYY-MM-DD`, the day it was registered |
45
+ | `result` | optional; one line. Required when `done` |
46
+ | `result_file` | optional; a path inside the folder. Required, and must exist, when `done` |
47
+ | `superseded_by` | optional; an id in the registry. Required when `superseded` |
48
+ | `in_paper` | `yes`, `no`, `registry` (listed only in the registry, not in the text) |
49
+
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.
57
+
58
+ 2. **The rule `repro/study-registered`, an error,** reported on the first line of
59
+ `PIPELINE-STATUS.md` like `repro/allowed-files-only`, once per finding:
60
+
61
+ - every folder at the top of `repro/` that is not `tools/`, `data/` or a byproduct the layout
62
+ ignores (`__pycache__/`, `node_modules/`, …) has exactly one row, and every row's folder exists;
63
+ - the folder is `<id>-<slug>`, the id is `<prefix>-S<digits>`, ids are unique;
64
+ - `done` has a `result` and an existing `result_file`; `superseded` names an existing id in
65
+ `superseded_by`; a title has at most 8 words;
66
+ - **the index is in sync:** the block of `repro/README.md` between the two markers below equals the
67
+ text generated from `studies.json`. The comparison is of strings. The markdown is output only and
68
+ is never parsed.
69
+
70
+ ```
71
+ <!-- studies:begin — generated from studies.json by `paperlint studies index`; do not edit -->
72
+ <!-- studies:end -->
73
+ ```
74
+
75
+ - a paper with study folders and no `repro/studies.json` gets one finding naming the file.
76
+ - a `studies.json` that is not JSON, or not of the schema, gets one finding with the reasons.
77
+
78
+ 3. **Two commands**, in `src/studies.ts`, wired in `src/cli.ts`:
79
+
80
+ - `paperlint studies new <paper-dir> "<title>" [--kind measurement]` takes the next id, creates
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
84
+ title over 8 words, a title with no letter or digit to make a slug from, an unknown kind, a folder
85
+ that already exists, and a paper with no `studies.json` (the prefix is the author's choice, so the
86
+ refusal names the file to write and does not guess one).
87
+ - `paperlint studies index <paper-dir>` regenerates the index block: a table with the columns id ·
88
+ title · status · result · in paper · folder. With no markers in `repro/README.md` the block is
89
+ appended; with no `repro/README.md` the file is written.
90
+
91
+ 4. **The layout accepts `<id>-<slug>` beside `<slug>-YYYY-MM-DD`.** `repro/allowed-files-only` keeps
92
+ accepting the dated form, so a paper that turns the registry off is not broken by this change; it
93
+ also allows `studies.json` at the top of `repro/`. A folder in the dated form is still reported by
94
+ `repro/study-registered` as unregistered — that is the point of the registry — and the paper decides
95
+ whether to register it or turn the rule off.
96
+
97
+ 5. **Skills.** `build-benchmark`, `run-agent-grid` and `build-label-page` create a study only with
98
+ `paperlint studies new`, and update its row (status, result, result file, in paper) when it ends,
99
+ then run `paperlint studies index`.
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
+ ## Where each part lives
209
+
210
+ | part | file | layer |
211
+ | -------------------------------------- | ----------------------------------- | ------- |
212
+ | the types, the judge, the index, ids | `src/domain/study-registry.ts` | domain |
213
+ | the plan's type and template | `src/domain/study-registry.ts` | domain |
214
+ | the schemas (zod) of registry and plan | `src/adapters/study-registry/` | adapter |
215
+ | what the app asks of a parser | `src/ports/study-registry-codec.ts` | port |
216
+ | both rules: list, read, report | `src/study-registry-rule.ts` | app |
217
+ | the two commands | `src/studies.ts` | app |
218
+ | wiring, the clock for today's date | `src/cli.ts` | root |
219
+
220
+ The judge is pure: it takes the folder names, the parsed registry (or why it is absent or unreadable),
221
+ the README's text, which result files exist, and each named plan parsed, and returns findings. The rule and the commands are
222
+ the only places that touch the disk, through the `Files` and `ListDir` ports.
223
+
224
+ ## Generated index, and prettier
225
+
226
+ The index is a table whose columns are padded to their widest cell, the way prettier writes a
227
+ markdown table, so formatting `repro/README.md` with prettier does not make the block stale. A `|` in
228
+ a cell is escaped and a line break becomes a space.
229
+
230
+ ## Rejected, by the author on 2026-10-15
231
+
232
+ - Commands that change a study's status: the row is edited in `studies.json`, the rule checks it.
233
+ - Detecting automatically whether a study is cited in the paper: `in_paper` is declared.
234
+ - An adopt or migration command for existing folders: there are no other users; the folders are
235
+ renamed by hand once.
236
+ - A PreToolUse hook for the registry: the lint is the guarantee, and the layout hook already refuses
237
+ a loose entry.
238
+
239
+ ## What it does not check
240
+
241
+ - Whether the `question` is one sentence, or the `result` one line, beyond their being strings.
242
+ - What a study's `README.md` says, or whether its frontmatter matches the row.
243
+ - Files inside a study folder other than `result_file` and the `plan` its row names.
244
+ - `study/declare-sample` still reads only `prereg.json`, and asks for different fields
245
+ (`strata`, `screens`, `coverage`, and an `author_check` of `who`, `when`, `units_seen`,
246
+ `units_drawn`, `verdict`); merging the two shapes is its own decision.
package/docs/layout.md CHANGED
@@ -15,14 +15,14 @@ flowchart TB
15
15
  S1["<b>1 · idea</b><br/>research-ideate · map-prior-work<br/>analyze-sibling-paper<br/>writes: siblings/<br/>checked by: sibling/frontmatter"]
16
16
  S2["<b>2 · venue</b><br/>find-venue · plan-paper-timeline<br/>study-accepted-papers, mode A or B<br/>writes: paperlint.json, a report in reviews/<br/>checked by: review/frontmatter"]
17
17
  V[("<b>#lt;papers-root#gt;/venues/#lt;venue#gt;/</b><br/>beside the papers, not in one:<br/>the venue's accepted papers,<br/>shared by every paper aimed at it<br/>checked by: nothing")]
18
- subgraph S3["3 · study: repro/#lt;slug#gt;-YYYY-MM-DD/"]
18
+ subgraph S3["3 · study: repro/#lt;id#gt;-#lt;slug#gt;/"]
19
19
  direction TB
20
- D["<b>design</b> · build-benchmark<br/>writes: the study's README.md<br/>checked by: repro/allowed-files-only"]
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)"]
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>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
- Y["<b>analysis</b> · build-benchmark<br/>writes: the numbers, NOTES.md"]
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"]
26
26
  D --> P --> A --> R --> L --> Y
27
27
  end
28
28
  S4["<b>4 · draft</b><br/>draft-paper · argument-arc · tighten-paper<br/>writes: paper.tex, sections/, figures/<br/>checked by: the tex/* rules<br/>hook paper-edit-guard: no shell writes"]
@@ -69,11 +69,12 @@ creates one.
69
69
  ├── siblings/ ← the cards of competing papers
70
70
  ├── research/ ← dated research and decision records
71
71
  ├── repro/ ← the reproduction artifact
72
- │ ├── README.md ← the index: one line per study
72
+ │ ├── README.md ← the index: one line per study, generated from studies.json
73
+ │ ├── studies.json ← the study registry: id, title, status and result of each study
73
74
  │ ├── .gitignore ← keeps the scripts' byproducts out of git
74
75
  │ ├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
75
76
  │ ├── data/ ← shared data (tables, caches)
76
- │ └── <slug>-YYYY-MM-DD/ ← one folder per study or spike (<slug>-YYYY-MM for one that ran over weeks)
77
+ │ └── <id>-<slug>/ ← one folder per study or spike, named by its row in studies.json (P1-S03-baseline)
77
78
  │ └── README.md ← required: what it asks, how to run it, what it found
78
79
  ├── figures/ ← graphics and their sources
79
80
  ├── sections/ ← the body, when it is split
@@ -97,28 +98,30 @@ Both are explained in [`venues.md`](venues.md).
97
98
  "Stage" is the stage of [the pipeline](#the-flow) that writes the place first.
98
99
  "Checked by" is the rule `paperlint lint` runs over it; "nothing" means no rule reads its content.
99
100
 
100
- | place | stage | written by | checked by |
101
- | ------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
102
- | `<papers-root>/venues/<venue>/` | 2 venue | `study-accepted-papers`, its first run for that venue (mode A, usually) | nothing ([why](venues.md#what-checks-it)) |
103
- | the paper folder's root | before 1 | `paperlint new`: the three files, and an empty `.gitkeep` folder for `reviews/`, `research/`, `figures/`, `versions/`, `submission/`, `repro/tools/`, `repro/data/` (`repro/` gets a stub index) | [`paper/allowed-files-only`](rules/paper/allowed-files-only.md); the hook `paper-folder-layout` refuses a new stray entry |
104
- | `PIPELINE-STATUS.md` | every stage | `paperlint new`, then `paper-pipeline`, `paper-status`; `study-accepted-papers` (the scorecard) | `paper/stages`, `paper/source`, [`cycle/stage`](rules/cycle/stage.md) ([rules](rules.md)) |
105
- | `paperlint.json` | before 1, then 2 | `paperlint new`; `find-venue` names the venue | `paperlint lint` refuses an unknown key; [`cycle/record`](rules/cycle/record.md) |
106
- | `siblings/` | 1 idea | `map-prior-work`, `analyze-sibling-paper` | `sibling/frontmatter` ([records](configuration.md#records-in-frontmatter-checked-by-shipped-json-schemas)) |
107
- | `reviews/` | 2, then 4–5 | `study-accepted-papers` (its report), `grade-paper-writing`, `cold-read-diff`, the review skills | `review/frontmatter` ([records](configuration.md#records-in-frontmatter-checked-by-shipped-json-schemas)) |
108
- | `research/` | any | you and the skills: dated research and decision records | nothing |
109
- | `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 |
110
- | `repro/<slug>-YYYY-MM-DD/` | 3 study | `build-benchmark` (the design, in its `README.md`) | [`repro/allowed-files-only`](rules/repro/allowed-files-only.md): the dated name, and a `README.md` in it |
111
- | `repro/<slug>-YYYY-MM-DD/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) |
112
- | `repro/<slug>-YYYY-MM-DD/data/` | 3 study | `build-label-page` exports the labels here | nothing |
113
- | `repro/tools/`, `repro/data/` | 3 study | your scripts; `paperlint build` keeps `repro/data/references-cache.json` | nothing inside them |
114
- | `NOTES.md` | 3, then 4 | you: each number → the data it comes from; `build-label-page` adds a line per label file | nothing |
115
- | `paper.tex`, `sections/`, `*.bib` | 4 draft | `draft-paper`, `argument-arc`, `tighten-paper` | the `tex/*`, `paper/*` and `bib/*` rules ([rules](rules.md)); the hook `paper-edit-guard` refuses a shell write |
116
- | `CLAIMS.md`, `figures/` | 4 draft | you | nothing |
117
- | `paper.pdf`, `_build/` | 6 submit | `paperlint build` | `pdf/*`, [`format/page-limit`](rules/format/page-limit.md), [`anonymity/identity`](rules/anonymity/identity.md), the reference rules |
118
- | `versions/` | 6, then 7 | you, at submission and at camera-ready (`submit-paper` says how): the frozen PDF and source of each stage | `paper/stages`, `paper/source` |
119
- | `submission/`, `SUBMIT-CHECKLIST.md` | 6 submit | you, while `submit-paper` and `submission-portal` do the submitting: forms, portal records, mail with the venue | [`cycle/evidence`](rules/cycle/evidence.md), for the files a closed cycle names |
120
- | `talk/` | 7 camera-ready | you | the `talk/*` rules ([talk](talk.md)) |
121
- | `extra/` | any | you | nothing, ever |
101
+ | place | stage | written by | checked by |
102
+ | ------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
103
+ | `<papers-root>/venues/<venue>/` | 2 venue | `study-accepted-papers`, its first run for that venue (mode A, usually) | nothing ([why](venues.md#what-checks-it)) |
104
+ | the paper folder's root | before 1 | `paperlint new`: the three files, and an empty `.gitkeep` folder for `reviews/`, `research/`, `figures/`, `versions/`, `submission/`, `repro/tools/`, `repro/data/` (`repro/` gets a stub index) | [`paper/allowed-files-only`](rules/paper/allowed-files-only.md); the hook `paper-folder-layout` refuses a new stray entry |
105
+ | `PIPELINE-STATUS.md` | every stage | `paperlint new`, then `paper-pipeline`, `paper-status`; `study-accepted-papers` (the scorecard) | `paper/stages`, `paper/source`, [`cycle/stage`](rules/cycle/stage.md) ([rules](rules.md)) |
106
+ | `paperlint.json` | before 1, then 2 | `paperlint new`; `find-venue` names the venue | `paperlint lint` refuses an unknown key; [`cycle/record`](rules/cycle/record.md) |
107
+ | `siblings/` | 1 idea | `map-prior-work`, `analyze-sibling-paper` | `sibling/frontmatter` ([records](configuration.md#records-in-frontmatter-checked-by-shipped-json-schemas)) |
108
+ | `reviews/` | 2, then 4–5 | `study-accepted-papers` (its report), `grade-paper-writing`, `cold-read-diff`, the review skills | `review/frontmatter` ([records](configuration.md#records-in-frontmatter-checked-by-shipped-json-schemas)) |
109
+ | `research/` | any | you and the skills: dated research and decision records | nothing |
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
+ | `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
+ | `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>/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>/data/` | 3 study | `build-label-page` exports the labels here | nothing |
116
+ | `repro/tools/`, `repro/data/` | 3 study | your scripts; `paperlint build` keeps `repro/data/references-cache.json` | nothing inside them |
117
+ | `NOTES.md` | 3, then 4 | you: each number → the data it comes from; `build-label-page` adds a line per label file | nothing |
118
+ | `paper.tex`, `sections/`, `*.bib` | 4 draft | `draft-paper`, `argument-arc`, `tighten-paper` | the `tex/*`, `paper/*` and `bib/*` rules ([rules](rules.md)); the hook `paper-edit-guard` refuses a shell write |
119
+ | `CLAIMS.md`, `figures/` | 4 draft | you | nothing |
120
+ | `paper.pdf`, `_build/` | 6 submit | `paperlint build` | `pdf/*`, [`format/page-limit`](rules/format/page-limit.md), [`anonymity/identity`](rules/anonymity/identity.md), the reference rules |
121
+ | `versions/` | 6, then 7 | you, at submission and at camera-ready (`submit-paper` says how): the frozen PDF and source of each stage | `paper/stages`, `paper/source` |
122
+ | `submission/`, `SUBMIT-CHECKLIST.md` | 6 submit | you, while `submit-paper` and `submission-portal` do the submitting: forms, portal records, mail with the venue | [`cycle/evidence`](rules/cycle/evidence.md), for the files a closed cycle names |
123
+ | `talk/` | 7 camera-ready | you | the `talk/*` rules ([talk](talk.md)) |
124
+ | `extra/` | any | you | nothing, ever |
122
125
 
123
126
  ## What runs when
124
127
 
@@ -132,23 +135,25 @@ A check runs at one of four moments:
132
135
  The lint-only job skips what needs a build (pages, fonts, references).
133
136
  - **On demand** — a skill, when you ask for what it does. A skill stops nothing; it writes.
134
137
 
135
- | what | kind | when it runs | what it stops |
136
- | ------------------------------------------------------------------------------------------- | ----- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
137
- | `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 |
138
- | `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 |
139
- | `paper-skills-nudge` | hook | after an agent edits a paper's source | nothing: it shows the agent the pre-submit checklist |
140
- | `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 |
141
- | [`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) |
142
- | [`repro/allowed-files-only`](rules/repro/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a loose entry at the top of `repro/`, an undated study folder, a study folder without `README.md` (error) |
143
- | [`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) |
144
- | [`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 |
145
- | `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) |
146
- | `paper/stages`, `paper/source` | rule | every `paperlint lint`, and CI | a stage whose frozen PDF or source is missing or changed (error) |
147
- | the rules on the source (`tex/*`, `paper/*`, `bib/*`) | rule | every `paperlint lint`, and CI | what each one's [row](rules.md) says |
148
- | 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 |
149
- | `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` |
150
- | `build-benchmark` | skill | when you ask; stage 3 | nothing: it writes the study's design and its `prereg.json` |
151
- | `build-label-page` | skill | when you ask; stage 3 | nothing: it builds the labelling page, and exports the answers into the study's `data/` |
138
+ | what | kind | when it runs | what it stops |
139
+ | ------------------------------------------------------------------------------------------- | ----- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | `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 |
141
+ | `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 |
142
+ | `paper-skills-nudge` | hook | after an agent edits a paper's source | nothing: it shows the agent the pre-submit checklist |
143
+ | `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 |
144
+ | [`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) |
145
+ | [`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) |
146
+ | [`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) |
147
+ | [`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` |
148
+ | [`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) |
149
+ | [`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 |
150
+ | `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) |
151
+ | `paper/stages`, `paper/source` | rule | every `paperlint lint`, and CI | a stage whose frozen PDF or source is missing or changed (error) |
152
+ | the rules on the source (`tex/*`, `paper/*`, `bib/*`) | rule | every `paperlint lint`, and CI | what each one's [row](rules.md) says |
153
+ | 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 |
154
+ | `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` |
155
+ | `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 |
156
+ | `build-label-page` | skill | when you ask; stage 3 | nothing: it builds the labelling page, and exports the answers into the study's `data/` |
152
157
 
153
158
  The hooks are wired by `paperlint init` ([`install.md`](install.md)). A rule's level and options:
154
159
  [`rules.md`](rules.md).
@@ -34,11 +34,12 @@ it: [the map](../../layout.md).
34
34
  ├── siblings/ ← the cards of competing papers
35
35
  ├── research/ ← dated research and decision records
36
36
  ├── repro/ ← the reproduction artifact
37
- │ ├── README.md ← the index: one line per study
37
+ │ ├── README.md ← the index: one line per study, generated from studies.json
38
+ │ ├── studies.json ← the study registry: id, title, status and result of each study
38
39
  │ ├── .gitignore ← keeps the scripts' byproducts out of git
39
40
  │ ├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
40
41
  │ ├── data/ ← shared data (tables, caches)
41
- │ └── <slug>-YYYY-MM-DD/ ← one folder per study or spike (<slug>-YYYY-MM for one that ran over weeks)
42
+ │ └── <id>-<slug>/ ← one folder per study or spike, named by its row in studies.json (P1-S03-baseline)
42
43
  │ └── README.md ← required: what it asks, how to run it, what it found
43
44
  ├── figures/ ← graphics and their sources
44
45
  ├── sections/ ← the body, when it is split
@@ -7,8 +7,8 @@ per misplaced entry
7
7
  ## What it catches
8
8
 
9
9
  An entry at the top of a paper's `repro/` that is not part of its layout, and a study folder there
10
- without a `README.md`. `repro/` holds an index, the tools and data the whole paper shares, and one
11
- dated folder per study. Stage 3 of the pipeline, the study, writes here; the rule runs on every
10
+ without a `README.md`. `repro/` holds an index, the study registry, the tools and data the whole
11
+ paper shares, and one folder per study. Stage 3 of the pipeline, the study, writes here; the rule runs on every
12
12
  `paperlint lint` and in CI, and a hook refuses a new loose entry before it is written. The whole map:
13
13
  [`layout.md`](../../layout.md).
14
14
 
@@ -16,12 +16,13 @@ dated folder per study. Stage 3 of the pipeline, the study, writes here; the rul
16
16
 
17
17
  ```text
18
18
  repro/
19
- ├── README.md ← the index: one line per study
20
- ├── .gitignore ← keeps the scripts' byproducts out of git
21
- ├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
22
- ├── data/ ← shared data (tables, caches)
23
- └── <slug>-YYYY-MM-DD/ ← one folder per study or spike (<slug>-YYYY-MM for one that ran over weeks)
24
- └── README.md ← required: what it asks, how to run it, what it found
19
+ ├── README.md ← the index: one line per study, generated from studies.json
20
+ ├── studies.json ← the study registry: id, title, status and result of each study
21
+ ├── .gitignore ← keeps the scripts' byproducts out of git
22
+ ├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
23
+ ├── data/ ← shared data (tables, caches)
24
+ └── <id>-<slug>/ ← one folder per study or spike, named by its row in studies.json (P1-S03-baseline)
25
+ └── README.md ← required: what it asks, how to run it, what it found
25
26
  ```
26
27
 
27
28
  <!-- layout:end -->
@@ -33,10 +34,16 @@ above, and it already holds the index.
33
34
  A loose note belongs in the folder of the study it is about, or in the paper's `research/` when it is
34
35
  research or a decision rather than the record of a run.
35
36
 
36
- **The date in a study's name** is the day the study began: `ablation-2026-10-12`. A month is
37
- accepted too, `human-labels-2026-10`: a study that ran over weeks (labelling, reading a corpus) has a
38
- month as its honest date, and both forms sort by time. The date must be a real one — no month 13, no
39
- 30 February. The slug is lowercase letters and digits, words joined by single hyphens.
37
+ **A study's name** is `<id>-<slug>`: the id of its row in `repro/studies.json` — the paper's prefix,
38
+ `-S`, two or more digits — then a slug, lowercase letters and digits in words joined by single
39
+ hyphens: `P1-S03-baseline`. `npx paperlint studies new` makes the folder, its `README.md` and its row.
40
+ This rule accepts the shape; whether the row exists, and matches, is
41
+ [`repro/study-registered`](study-registered.md).
42
+
43
+ **The dated form, `<slug>-YYYY-MM-DD`**, is accepted too, so a paper that keeps its dated folders and
44
+ turns `repro/study-registered` off is not broken: `ablation-2026-10-12`, the day the study began. A
45
+ month is accepted as well, `human-labels-2026-10`, for a study that ran over weeks. The date must be a
46
+ real one — no month 13, no 30 February.
40
47
 
41
48
  Never reported: what interpreters, package managers, test runners and the operating system leave in
42
49
  the folder they run in — `__pycache__/`, `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`,
@@ -55,14 +62,15 @@ or where a new run goes. Each kind of material now has one place, and each study
55
62
  Failing: a script loose at the top.
56
63
 
57
64
  ```text
58
- papers/agent-rule-drift/PIPELINE-STATUS.md
65
+ papers/example/PIPELINE-STATUS.md
59
66
  1:1 error `repro/paper_numbers.py` is loose at the top of the paper's repro/ folder, which holds
60
- only an index, the shared tools and data, and one dated folder per study. Where it
61
- goes: repro/tools/ — the scripts used across the paper (numbers, figures, the anonymity
62
- check, harnesses); a script only one study runs goes in that study's folder instead.
67
+ only an index, the study registry, the shared tools and data, and one folder per study.
68
+ Where it goes: repro/tools/ — the scripts used across the paper (numbers, figures, the
69
+ anonymity check, harnesses); a script only one study runs goes in that study's folder
70
+ instead.
63
71
  What repro/ holds (what interpreters and package managers leave there, like __pycache__/ and node_modules/, is never reported):
64
72
  repro/
65
- ├── README.md ← the index: one line per study
73
+ ├── README.md ← the index: one line per study, generated from studies.json
66
74
  …
67
75
  Rule repro/allowed-files-only: https://github.com/zernie/paperlint/blob/main/docs/rules/repro/allowed-files-only.md repro/allowed-files-only
68
76
  ```
@@ -70,14 +78,14 @@ Rule repro/allowed-files-only: https://github.com/zernie/paperlint/blob/main/doc
70
78
  Each message names the entry and where it goes, and draws the layout above, so it reads on its own. Where
71
79
  an entry goes depends on what it is:
72
80
 
73
- | entry | where it goes |
74
- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
75
- | a script (`.py`, `.ipynb`, `.sh`, `.js`, `.mjs`, `.ts`, `.r`, `.jl`, `.sql` …), `Makefile`, `requirements.txt`, `package.json` | `repro/tools/`, or the folder of the one study that runs it |
76
- | data (`.tsv`, `.csv`, `.json`, `.jsonl`, `.yaml`, `.txt`, `.log`, `.parquet`, `.zip` …) | `repro/data/`, or the folder of the one study that produced it |
77
- | a note (`.md`) | the dated folder of the study it is about, or the paper's `research/` |
78
- | a folder whose name is not `<slug>-YYYY-MM-DD` | rename it to `<slug>-YYYY-MM-DD` and give it a `README.md`; its first commit's date is a fine default |
79
- | a study folder without `README.md` | add one: what the study asks, how to run it, what it found; and a line in `repro/README.md` |
80
- | anything else | by what it is, as above |
81
+ | entry | where it goes |
82
+ | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
+ | a script (`.py`, `.ipynb`, `.sh`, `.js`, `.mjs`, `.ts`, `.r`, `.jl`, `.sql` …), `Makefile`, `requirements.txt`, `package.json` | `repro/tools/`, or the folder of the one study that runs it |
84
+ | data (`.tsv`, `.csv`, `.json`, `.jsonl`, `.yaml`, `.txt`, `.log`, `.parquet`, `.zip` …) | `repro/data/`, or the folder of the one study that produced it |
85
+ | a note (`.md`) | the folder of the study it is about, or the paper's `research/` |
86
+ | a folder whose name is neither `<id>-<slug>` nor `<slug>-YYYY-MM-DD` | rename it to `<id>-<slug>` with its row in `repro/studies.json` and a `README.md`; with `repro/study-registered` off, `<slug>-YYYY-MM-DD`, dated by its first commit |
87
+ | a study folder without `README.md` | add one: what the study asks, how to run it, what it found; and the study's row in `repro/studies.json` |
88
+ | anything else | by what it is, as above |
81
89
 
82
90
  Three files paperlint itself once kept at the top of `repro/` are named with their new place and the
83
91
  command that moves them: `references-cache.json` → `repro/data/` (read by `paperlint build`),
@@ -98,7 +106,8 @@ No rule options.
98
106
  ## What it does not check
99
107
 
100
108
  - What is inside `tools/`, `data/` and the study folders.
101
- - Whether `repro/README.md` exists, or has a line for every study.
109
+ - Whether every study folder has its row in `repro/studies.json`, and whether `repro/README.md`'s index
110
+ is in sync: that is [`repro/study-registered`](study-registered.md).
102
111
  - What a study's `README.md` says.
103
112
  - `repro/` itself: whether the paper has one is `paper/allowed-files-only`'s concern, and that rule never
104
113
  looks inside it. Each entry is judged by exactly one of the two rules.
@@ -106,18 +115,19 @@ No rule options.
106
115
  ## The hook
107
116
 
108
117
  The hook `paper-folder-layout` refuses, before it is written, a new file that would add an entry
109
- this rule reports — a loose file at the top of `repro/`, an undated folder, or a study folder whose
118
+ this rule reports — a loose file at the top of `repro/`, a folder that is not a study's, or a study folder whose
110
119
  first file is not its `README.md` — with this rule's message as the reason. It never refuses a file
111
120
  that already exists, and it cannot see a shell `mv` or `cp`: the lint is the guarantee. What it does
112
121
  and does not do is on [`paper/allowed-files-only`'s page](../paper/allowed-files-only.md#the-hook-a-new-stray-entry-is-refused-before-it-is-written).
113
122
 
114
123
  ## How to fix
115
124
 
116
- Move or rename the entry where its message says, and update whatever names its path:
125
+ Move or rename the entry where its message says, and update whatever names its path; a renamed study
126
+ folder gets its row in `repro/studies.json` in the same commit:
117
127
 
118
128
  ```sh
119
- git mv papers/agent-rule-drift/repro/paper_numbers.py papers/agent-rule-drift/repro/tools/
120
- git mv papers/agent-rule-drift/repro/baseline papers/agent-rule-drift/repro/baseline-2026-08-05
129
+ git mv papers/example/repro/paper_numbers.py papers/example/repro/tools/
130
+ git mv papers/example/repro/baseline papers/example/repro/P1-S04-baseline
121
131
  ```
122
132
 
123
133
  To switch the rule off for one paper — say, one whose `repro/` predates the layout — set it in that