paperlint 9.0.1 → 10.0.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 (150) hide show
  1. package/README.md +68 -128
  2. package/dist/build.d.ts +1 -1
  3. package/dist/build.js +1 -1
  4. package/dist/cli.d.ts +5 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +52 -6
  7. package/dist/cli.js.map +1 -1
  8. package/dist/domain/layout-tree.d.ts +19 -0
  9. package/dist/domain/layout-tree.d.ts.map +1 -0
  10. package/dist/domain/layout-tree.js +32 -0
  11. package/dist/domain/layout-tree.js.map +1 -0
  12. package/dist/domain/lookup-cache.d.ts +2 -2
  13. package/dist/domain/lookup-cache.js +2 -2
  14. package/dist/domain/paper-layout.d.ts +81 -0
  15. package/dist/domain/paper-layout.d.ts.map +1 -0
  16. package/dist/domain/paper-layout.js +259 -0
  17. package/dist/domain/paper-layout.js.map +1 -0
  18. package/dist/domain/repro-layout.d.ts +80 -0
  19. package/dist/domain/repro-layout.d.ts.map +1 -0
  20. package/dist/domain/repro-layout.js +247 -0
  21. package/dist/domain/repro-layout.js.map +1 -0
  22. package/dist/domain/scaffold-layout.d.ts +28 -0
  23. package/dist/domain/scaffold-layout.d.ts.map +1 -0
  24. package/dist/domain/scaffold-layout.js +81 -0
  25. package/dist/domain/scaffold-layout.js.map +1 -0
  26. package/dist/eslint-rules/prereg-coverage.d.ts +98 -0
  27. package/dist/eslint-rules/prereg-coverage.d.ts.map +1 -0
  28. package/dist/eslint-rules/prereg-coverage.js +274 -0
  29. package/dist/eslint-rules/prereg-coverage.js.map +1 -0
  30. package/dist/folder-layout-hook.d.ts +21 -0
  31. package/dist/folder-layout-hook.d.ts.map +1 -0
  32. package/dist/folder-layout-hook.js +117 -0
  33. package/dist/folder-layout-hook.js.map +1 -0
  34. package/dist/folder-layout-rule.d.ts +22 -0
  35. package/dist/folder-layout-rule.d.ts.map +1 -0
  36. package/dist/folder-layout-rule.js +107 -0
  37. package/dist/folder-layout-rule.js.map +1 -0
  38. package/dist/init.js +1 -1
  39. package/dist/init.js.map +1 -1
  40. package/dist/new-paper.d.ts +4 -1
  41. package/dist/new-paper.d.ts.map +1 -1
  42. package/dist/new-paper.js +40 -6
  43. package/dist/new-paper.js.map +1 -1
  44. package/dist/references.d.ts +1 -1
  45. package/dist/references.d.ts.map +1 -1
  46. package/dist/references.js +2 -2
  47. package/dist/references.js.map +1 -1
  48. package/dist/repro-layout-rule.d.ts +16 -0
  49. package/dist/repro-layout-rule.d.ts.map +1 -0
  50. package/dist/repro-layout-rule.js +81 -0
  51. package/dist/repro-layout-rule.js.map +1 -0
  52. package/docs/ci.md +60 -0
  53. package/docs/configuration.md +2 -0
  54. package/docs/e2e.md +2 -2
  55. package/docs/install.md +20 -16
  56. package/docs/layout.md +154 -0
  57. package/docs/optional-rules.md +5 -4
  58. package/docs/references.md +5 -2
  59. package/docs/rules/paper/allowed-files-only.md +192 -0
  60. package/docs/rules/repro/allowed-files-only.md +128 -0
  61. package/docs/rules/study/declare-sample.md +146 -0
  62. package/docs/rules.md +53 -50
  63. package/docs/skills.md +6 -3
  64. package/docs/venues.md +94 -0
  65. package/eslint-rules/prereg-coverage.test.ts +448 -0
  66. package/eslint-rules/prereg-coverage.ts +419 -0
  67. package/lib/skill-trigger-cases.harness.mjs +51 -5
  68. package/lib/skill-trigger-cases.mjs +53 -15
  69. package/package.json +1 -1
  70. package/plugin/hooks/hooks.json +9 -0
  71. package/skills/analyze-sibling-paper/SKILL.md +2 -2
  72. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +1 -1
  73. package/skills/build-benchmark/SKILL.md +14 -1
  74. package/skills/build-benchmark/SKILL.md.spec.ts +13 -0
  75. package/skills/build-label-page/SKILL.md +118 -0
  76. package/skills/build-label-page/SKILL.md.spec.ts +129 -0
  77. package/skills/build-label-page/build-label-page.eval.mjs +15 -0
  78. package/skills/build-label-page/build-label-page.test.ts +959 -0
  79. package/skills/build-label-page/references/checklist.md +148 -0
  80. package/skills/build-label-page/references/example-generator.py +803 -0
  81. package/skills/find-venue/SKILL.md +5 -3
  82. package/skills/find-venue/SKILL.md.spec.ts +4 -2
  83. package/skills/grade-paper-writing/SKILL.md +7 -7
  84. package/skills/grade-paper-writing/SKILL.md.spec.ts +6 -6
  85. package/skills/map-prior-work/SKILL.md +2 -2
  86. package/skills/map-prior-work/SKILL.md.spec.ts +1 -1
  87. package/skills/paper-pipeline/SKILL.md +8 -6
  88. package/skills/paper-pipeline/SKILL.md.spec.ts +7 -5
  89. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +19 -9
  90. package/skills/paper-pipeline/references/acceptance-gate.md +19 -19
  91. package/skills/paper-pipeline/references/artifact-checklist.md +5 -0
  92. package/skills/paper-pipeline/references/pipeline-status-template.md +30 -1
  93. package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +1 -1
  94. package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +1 -1
  95. package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +1 -1
  96. package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +1 -1
  97. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +48 -48
  98. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +8 -8
  99. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +48 -48
  100. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +2 -2
  101. package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +3 -3
  102. package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-after.log +81 -0
  103. package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-before.log +81 -0
  104. package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-setupdiff.log +59 -0
  105. package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.jsonl +1 -0
  106. package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.log +22 -0
  107. package/skills/paper-pipeline/repro/README.md +4 -0
  108. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +2 -2
  109. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +2 -2
  110. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +4 -2
  111. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +7 -2
  112. package/skills/paper-pipeline/scripts/generated-code.mjs +8 -4
  113. package/skills/paper-pipeline/scripts/generated-code.test.ts +15 -1
  114. package/skills/paper-status/SKILL.md +2 -2
  115. package/skills/paper-status/SKILL.md.spec.ts +1 -1
  116. package/skills/study-accepted-papers/SKILL.md +163 -161
  117. package/skills/study-accepted-papers/SKILL.md.spec.ts +167 -165
  118. package/skills/study-accepted-papers/references/base-rates.md +69 -0
  119. package/skills/study-accepted-papers/references/mechanisms.md +181 -0
  120. package/skills/verify-citations/SKILL.md +2 -2
  121. package/skills/verify-citations/SKILL.md.spec.ts +1 -1
  122. package/src/build.ts +1 -1
  123. package/src/cli.harness.mjs +42 -0
  124. package/src/cli.test.ts +90 -6
  125. package/src/cli.ts +78 -5
  126. package/src/domain/layout-tree.test.ts +32 -0
  127. package/src/domain/layout-tree.ts +47 -0
  128. package/src/domain/lookup-cache.ts +2 -2
  129. package/src/domain/paper-layout.test.ts +264 -0
  130. package/src/domain/paper-layout.ts +336 -0
  131. package/src/domain/repro-layout.test.ts +222 -0
  132. package/src/domain/repro-layout.ts +311 -0
  133. package/src/domain/scaffold-layout.ts +104 -0
  134. package/src/folder-layout-hook.test.ts +214 -0
  135. package/src/folder-layout-hook.ts +141 -0
  136. package/src/folder-layout-rule.test.ts +235 -0
  137. package/src/folder-layout-rule.ts +151 -0
  138. package/src/hooks-settings.harness.ts +9 -8
  139. package/src/init.ts +1 -1
  140. package/src/layout-docs.test.ts +52 -0
  141. package/src/new-paper.harness.mjs +23 -1
  142. package/src/new-paper.test.ts +299 -6
  143. package/src/new-paper.ts +51 -5
  144. package/src/paper-files.test.ts +9 -6
  145. package/src/references.test.ts +5 -5
  146. package/src/references.ts +2 -2
  147. package/src/repro-layout-rule.test.ts +114 -0
  148. package/src/repro-layout-rule.ts +121 -0
  149. package/src/rule-cases.test.ts +98 -0
  150. package/templates/paper/PIPELINE-STATUS.md +41 -7
package/docs/ci.md ADDED
@@ -0,0 +1,60 @@
1
+ # Run it in CI
2
+
3
+ Two jobs, one choice: how much of the paper CI judges.
4
+
5
+ | job | time | checks |
6
+ | -------------------------- | ----------------------------- | ------------------------------------------ |
7
+ | build, then lint | ~3 min first run, then cached | everything |
8
+ | lint only (`init` adds it) | seconds | the source; not pages, fonts or references |
9
+
10
+ `paperlint lint` exits non-zero on any error; warnings only print, unless you pass
11
+ `--max-warnings <n>`.
12
+
13
+ ## Build, then lint
14
+
15
+ Lint judges pages, fonts and references from what build measured, so the full job builds first.
16
+ The cache keeps TeX Live between runs:
17
+
18
+ ```yaml
19
+ name: papers
20
+ on: [push, pull_request]
21
+ jobs:
22
+ papers:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: 22
29
+ - run: npm ci
30
+ - uses: actions/cache@v4
31
+ with:
32
+ path: ~/.cache/paperlint/texlive
33
+ key: texlive-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
34
+ - run: npx paperlint toolchain
35
+ - run: npx paperlint build --all
36
+ - run: npx paperlint lint
37
+ ```
38
+
39
+ Commit `repro/data/references-cache.json` of each paper: with it, a build asks the citation
40
+ services only about new or edited entries, so an unchanged bibliography usually builds without
41
+ the network ([`references.md`](references.md)).
42
+
43
+ ## Lint only
44
+
45
+ Each paper then gets one warning that it was not built. Call the action from the installed package,
46
+ after `npm ci`: the action and the CLI it runs are then one revision, pinned by your lockfile.
47
+
48
+ ```yaml
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+ - uses: actions/setup-node@v4
52
+ with:
53
+ node-version: 22
54
+ - run: npm ci
55
+ - uses: ./node_modules/paperlint
56
+ ```
57
+
58
+ With no `paths` it lints the `papersDir` of your `paperlint.json`; pass `paths:` only to lint less.
59
+
60
+ This is the workflow `paperlint init` offers to write ([`install.md`](install.md)).
@@ -13,6 +13,8 @@ papers/
13
13
  paper.tex
14
14
  PIPELINE-STATUS.md
15
15
  paperlint.json THIS PAPER: its venue preset, its kind, its own rules — `paperlint new` writes it
16
+ reviews/ research/ figures/ versions/ submission/ repro/
17
+ the paper's layout — `paperlint new` makes them (an empty one holds a `.gitkeep`)
16
18
  venues/usenix-sec.jsonc (optional, your own) a VENUE PRESET: format, page limits, TeX packages, rules
17
19
  ```
18
20
 
package/docs/e2e.md CHANGED
@@ -54,8 +54,8 @@ depend on each other — and drives the installed binary:
54
54
  publishes, once each — and says they need `npm install` in a fresh clone; a second `init`
55
55
  leaves that file byte-identical
56
56
  - `paperlint lint` passes the staged corpus
57
- - `paperlint new demo` scaffolds a paper from the templates that shipped in the tarball, its own lint is
58
- clean, and `paperlint lint` stays clean with it in the corpus
57
+ - `paperlint new demo` scaffolds a paper from the templates that shipped in the tarball (and the
58
+ layout's folders), its own lint is clean, and `paperlint lint` stays clean with it in the corpus
59
59
  - `hooks.json` arrived and parses, and every hook command named in it resolves to a file
60
60
  - every skill arrived, and every script path named inside a skill resolves **in the consumer**
61
61
 
package/docs/install.md CHANGED
@@ -50,9 +50,9 @@ question (Ctrl+D) also takes the default.
50
50
  | what | where | when |
51
51
  | -------------------------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
52
  | `{ "papersDir": … }` naming your papers directory | `paperlint.json` | only when it is not the default `papers` |
53
- | the three hook commands, merged in beside your own entries | `.claude/settings.json` | by default. A human at a terminal is asked [Y/n]; an agent, CI or `--yes` gets YES; `--no-hooks` skips. Hand-wired under another spelling: nothing written, so nothing runs twice |
53
+ | the four hook commands, merged in beside your own entries | `.claude/settings.json` | by default. A human at a terminal is asked [Y/n]; an agent, CI or `--yes` gets YES; `--no-hooks` skips. Hand-wired under another spelling: nothing written, so nothing runs twice |
54
54
  | a GitHub Actions workflow: `npm ci`, then `./node_modules/paperlint` | `.github/workflows/` | only if you say yes; it asks once, and only when a human is at a terminal (stdin and stdout, no `CI`, no `--yes`) |
55
- | a first paper, via `paperlint new` | `<papers>/<name>/` | only when the papers directory holds none: asked of a human at a terminal, otherwise only with `--paper <name>` |
55
+ | a first paper, via `paperlint new`, with its layout's folders | `<papers>/<name>/` | only when the papers directory holds none: asked of a human at a terminal, otherwise only with `--paper <name>` |
56
56
  | one relative symlink per shipped skill, into the installed package | `.claude/skills/<name>` | always — except where that name is already taken (a directory, a file, a link elsewhere): that entry is left as it is and named in the report |
57
57
 
58
58
  It installs no software you did not say yes to, and touches nothing else. Commit `.claude/settings.json` so every clone
@@ -61,9 +61,11 @@ gets the hooks; the hook commands run files inside `node_modules`, so a fresh cl
61
61
 
62
62
  ## Why it is shaped this way
63
63
 
64
- **One file, at the project root.** A hook cannot import code or walk up a tree looking for a
65
- config; it can only read a path it can spell, and `$CLAUDE_PROJECT_DIR/paperlint.json` is one. The
66
- same file is read by the CLI, the three hooks, `eslint-rules/papers.mjs`,
64
+ **One file, at the project root.** A vigiles hook cannot import code or walk up a tree looking for
65
+ a config; it can only read a path it can spell, and `$CLAUDE_PROJECT_DIR/paperlint.json` is one.
66
+ (`paper-folder-layout` is not a vigiles hook: it runs inside the CLI, because it must call the
67
+ function its rule reports with.) The same file is read by the CLI, the three vigiles hooks,
68
+ `eslint-rules/papers.mjs`,
67
69
  `lib/skill-trigger-cases.mjs` and `skills/paper-pipeline/scripts/consumer.mjs`. It is optional:
68
70
  absent, every reader takes the same defaults.
69
71
 
@@ -147,17 +149,19 @@ are probably smaller now; re-measure before quoting them.
147
149
  | of which `typescript` (via `vigiles`) | 23 MB |
148
150
  | of which `vigiles` itself | 6 MB |
149
151
 
150
- Neither `paperlint lint` nor any of the three hooks loads `@ast-grep` or `typescript` (traced);
151
- `vigiles` itself is loaded.
152
+ Neither `paperlint lint` nor any of the four hooks loads `@ast-grep` or `typescript` (traced;
153
+ `paper-folder-layout` on 2026-10-12: 223 CommonJS modules, none from either); `vigiles` itself is
154
+ loaded.
152
155
 
153
156
  ## Troubleshooting
154
157
 
155
- | symptom | cause and fix |
156
- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
157
- | `init`: skills "nothing linked", or hooks that fail to start | the package is not installed in this project (`npx` ran a temporary copy). Run `npm i -D paperlint`, then `npx paperlint init` again |
158
- | `paperlint lint`: `no papers in papers/` | there is no paper where `papersDir` points. Create one with `npx paperlint new <name>`, or set `"papersDir"` in `paperlint.json` |
159
- | hooks fail with `Cannot find module` in a fresh clone | the hook commands run files in `node_modules`: run `npm install` |
160
- | `paperlint: not built` | installed from git with `--ignore-scripts`, or a clone before building: run `npm run build` in the package |
161
- | a skill does not show up in Claude Code | its name was already taken in `.claude/skills/`. `init` names it and leaves it alone; rename or remove yours and run `npx paperlint init` again |
162
- | **every** shell command is blocked in Claude Code | `paper-edit-guard` cannot parse the root `paperlint.json` (merge-conflict markers, a typo), and refuses rather than guess. Fix it with a normal file edit — file edits are not blocked |
163
- | anything else | `npx paperlint doctor` — it checks the setup and exits non-zero on anything miswired |
158
+ | symptom | cause and fix |
159
+ | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160
+ | `init`: skills "nothing linked", or hooks that fail to start | the package is not installed in this project (`npx` ran a temporary copy). Run `npm i -D paperlint`, then `npx paperlint init` again |
161
+ | `paperlint lint`: `no papers in papers/` | there is no paper where `papersDir` points. Create one with `npx paperlint new <name>`, or set `"papersDir"` in `paperlint.json` |
162
+ | hooks fail with `Cannot find module` in a fresh clone | the hook commands run files in `node_modules`: run `npm install` |
163
+ | `paperlint: not built` | installed from git with `--ignore-scripts`, or a clone before building: run `npm run build` in the package |
164
+ | a skill does not show up in Claude Code | its name was already taken in `.claude/skills/`. `init` names it and leaves it alone; rename or remove yours and run `npx paperlint init` again |
165
+ | **every** shell command is blocked in Claude Code | `paper-edit-guard` cannot parse the root `paperlint.json` (merge-conflict markers, a typo), and refuses rather than guess. Fix it with a normal file edit — file edits are not blocked |
166
+ | a Write of a new file is denied: "… is at the root of a paper folder" or "… is loose at the top of the paper's repro/ folder" | `paper-folder-layout` refuses to create an entry the paper layout does not allow; the reason says where it goes. Write it there. Existing files are never refused, and the hook does not see shell moves |
167
+ | anything else | `npx paperlint doctor` — it checks the setup and exits non-zero on anything miswired |
package/docs/layout.md ADDED
@@ -0,0 +1,154 @@
1
+ # Where things live, and what checks them when
2
+
3
+ A paper moves through eight stages ([the flow](#the-flow)). Each stage writes into one
4
+ place, and a check guards that place. This page is the map: the places, who writes each one, and the
5
+ moment each check runs. The skills, stage by stage: [`skills.md`](skills.md).
6
+
7
+ ## The flow
8
+
9
+ Each box names the stage's skills, what it writes, and what checks it. Stage 3 is drawn from the
10
+ inside: a study's pre-registration and its human labels are made there.
11
+
12
+ ```mermaid
13
+ flowchart TB
14
+ N["<b>paperlint new</b><br/>creates the paper folder<br/>checked from here on:<br/>every lint · paper/allowed-files-only<br/>every new file · hook paper-folder-layout"]
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
+ 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
+ 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/"]
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)"]
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
+ 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"]
26
+ D --> P --> A --> R --> L --> Y
27
+ end
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"]
29
+ S5["<b>5 · review</b><br/>grade-paper-writing · verify-citations<br/>pc-panel-review · harden-paper<br/>study-accepted-papers, mode C or D<br/>writes: reviews/, the scorecard<br/>in PIPELINE-STATUS.md<br/>checked by: review/frontmatter"]
30
+ S6["<b>6 · submit</b><br/>paperlint build · submit-paper<br/>writes: paper.pdf, _build/,<br/>versions/, submission/<br/>checked by: format/page-limit, pdf/*,<br/>anonymity/identity, paper/stages"]
31
+ S7["<b>7 · camera-ready</b><br/>camera-ready<br/>writes: versions/, talk/<br/>checked by: paper/stages, talk/*"]
32
+ S8["<b>8 · extend</b><br/>extend-paper<br/>writes: a new paper folder"]
33
+ N --> S1 --> S2 --> D
34
+ Y --> S4 --> S5 --> S6 --> S7 --> S8
35
+ S2 -. fills .-> V
36
+ V -. read again .-> S5
37
+ ```
38
+
39
+ The cylinder is a venue's corpus of accepted papers: it lives beside the papers, not inside one
40
+ ([`venues.md`](venues.md)).
41
+
42
+ ## The map
43
+
44
+ The papers directory is `papersDir` in the project's `paperlint.json` (default `papers/`). A folder
45
+ in it is a paper when it holds `PIPELINE-STATUS.md`, `paper.tex` or `paperlint.json`; `paperlint new`
46
+ creates one.
47
+
48
+ <!-- layout:begin — drawn from src/domain/paper-layout.ts (PAPERS_LAYOUT_TREE); src/layout-docs.test.ts fails when this block differs -->
49
+
50
+ ```text
51
+ <papers-root>/
52
+ ├── venues/ ← outside every paper folder: one folder per venue
53
+ │ └── <venue>/ ← that venue's accepted papers, shared by every paper aimed at it
54
+ └── <paper>/ ← one folder per paper, named after the work; its root holds only what is below
55
+ ├── PIPELINE-STATUS.md ← the paper's one live-state file: stages, plan, scorecard
56
+ ├── paper.tex ← the source; `paperlint build` compiles it
57
+ ├── paperlint.json ← the paper's settings
58
+ ├── README.md ← what the paper is
59
+ ├── CLAUDE.md ← the paper's agent rules
60
+ ├── CLAIMS.md ← the claims ledger
61
+ ├── NOTES.md ← each number in the paper → the data it comes from
62
+ ├── SUBMIT-CHECKLIST.md ← the venue's format and submission mechanics
63
+ ├── .gitignore ← keeps the build's byproducts out of git
64
+ ├── paper.pdf ← the built PDF
65
+ ├── *.tex *.bib *.cls *.sty *.bst ← what TeX and bibtex look up beside paper.tex
66
+ ├── _build/ ← the facts `paperlint build` writes
67
+ ├── versions/ ← each stage's frozen PDF and source
68
+ ├── reviews/ ← dated reviews and pass reports
69
+ ├── siblings/ ← the cards of competing papers
70
+ ├── research/ ← dated research and decision records
71
+ ├── repro/ ← the reproduction artifact
72
+ │ ├── README.md ← the index: one line per study
73
+ │ ├── .gitignore ← keeps the scripts' byproducts out of git
74
+ │ ├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
75
+ │ ├── 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
+ │ └── README.md ← required: what it asks, how to run it, what it found
78
+ ├── figures/ ← graphics and their sources
79
+ ├── sections/ ← the body, when it is split
80
+ ├── talk/ ← the talk's files
81
+ ├── submission/ ← forms, portal records, mail with the venue, the anonymized upload
82
+ └── extra/ ← anything; never judged
83
+ ```
84
+
85
+ <!-- layout:end -->
86
+
87
+ Two places with similar names are not in this drawing:
88
+
89
+ - `venues/` at the **project root** holds your own venue presets and their cards
90
+ (`venues/<name>.jsonc`, `venues/<name>.md`), for a venue paperlint does not ship.
91
+ - `<papers-root>/venues/<venue>/` (drawn above) holds a venue's **accepted papers**.
92
+
93
+ Both are explained in [`venues.md`](venues.md).
94
+
95
+ ## Each place: who writes it, and what checks it
96
+
97
+ "Stage" is the stage of [the pipeline](#the-flow) that writes the place first.
98
+ "Checked by" is the rule `paperlint lint` runs over it; "nothing" means no rule reads its content.
99
+
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 |
122
+
123
+ ## What runs when
124
+
125
+ A check runs at one of four moments:
126
+
127
+ - **Before a write** — a Claude Code hook. It sees an agent's Edit or Write, not a shell `mv` or a
128
+ script, so it saves a round trip and guarantees nothing.
129
+ - **On lint** — `paperlint lint`, when you run it. Every rule runs, whatever wrote the files. This is
130
+ the guarantee. No hook runs it for you.
131
+ - **In CI** — the same lint, on every push, if the project has the workflow `paperlint init` offers.
132
+ The lint-only job skips what needs a build (pages, fonts, references).
133
+ - **On demand** — a skill, when you ask for what it does. A skill stops nothing; it writes.
134
+
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/` |
152
+
153
+ The hooks are wired by `paperlint init` ([`install.md`](install.md)). A rule's level and options:
154
+ [`rules.md`](rules.md).
@@ -8,10 +8,11 @@ rules apply to that paper. The root `paperlint.json` can turn it on for every pa
8
8
  with a block ([`configuration.md`](configuration.md#the-rules-key-turning-rules-on-and-off)); a
9
9
  paper's own file still has the last word for that paper.
10
10
 
11
- | rule | what it checks | who needs it |
12
- | ----------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `pdf/last-page-balance` | the two columns of the last page end at about the same height | two-column papers whose publisher asks for it — see below |
14
- | [`tex/heading-case`](rules/tex/heading-case.md) | the words of the paper's title and of each heading are in the capitalization the venue asks | papers whose venue names a case style paperlint builds; only the AgenticDev preset turns it on (`chicago-headline` for the title and every heading) — [page](rules/tex/heading-case.md) |
11
+ | rule | what it checks | who needs it |
12
+ | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `pdf/last-page-balance` | the two columns of the last page end at about the same height | two-column papers whose publisher asks for it — see below |
14
+ | [`tex/heading-case`](rules/tex/heading-case.md) | the words of the paper's title and of each heading are in the capitalization the venue asks | papers whose venue names a case style paperlint builds; only the AgenticDev preset turns it on (`chicago-headline` for the title and every heading) — [page](rules/tex/heading-case.md) |
15
+ | [`study/declare-sample`](rules/study/declare-sample.md) | experimental: a study's `prereg.json` declares its population, screens, coverage and the author's check of the drawn units | papers with a sampled study; no preset turns it on — [page](rules/study/declare-sample.md) |
15
16
 
16
17
  ## `pdf/last-page-balance`
17
18
 
@@ -61,18 +61,21 @@ is stale and `paper/sources-fresh` says so: build again to write it. A database
61
61
  the paper directory, on its search path (`BIBINPUTS`), is not read: the step says which and
62
62
  `paper/refs-checked` warns.
63
63
 
64
- ## `repro/references-cache.json` — commit it
64
+ ## `repro/data/references-cache.json` — commit it
65
65
 
66
66
  Asking five services about every entry is slow. DBLP asks clients to pause between requests, and a
67
67
  slow service is retried. Before this cache existed, a 27-reference paper spent 217 seconds on the
68
68
  check, on every build, and a 51-reference paper did not finish inside a 12-minute CI job
69
69
  ([#107](https://github.com/zernie/paperlint/issues/107)).
70
70
 
71
- So the build keeps every answer it receives in `<paper>/repro/references-cache.json`, and the next
71
+ So the build keeps every answer it receives in `<paper>/repro/data/references-cache.json`, and the next
72
72
  build asks only what that file cannot answer:
73
73
 
74
74
  - An unchanged bibliography whose answers are under 30 days old asks nothing and makes no network request at all, not even the check
75
75
  for whether the services can be reached.
76
+ - A cache at the old place, `repro/references-cache.json`, is not read: `repro/allowed-files-only` reports
77
+ it with the command that moves it (`git mv repro/references-cache.json repro/data/`). Until it is
78
+ moved, the build asks every question again.
76
79
  - Adding or editing an entry asks only about that entry. Answers are stored per identifier (DOI,
77
80
  arXiv id, or title), so changing an entry's DOI or title is what makes it be asked again.
78
81
  - A request that failed is not an answer and is not stored, so the next build asks it again.
@@ -0,0 +1,192 @@
1
+ # paper/allowed-files-only
2
+
3
+ **Level:** error · **Reads:** the paper folder's root (a directory listing), its `paperlint.json` ·
4
+ **Reported on:** the first line of the paper's `PIPELINE-STATUS.md`, once per stray entry
5
+
6
+ ## What it catches
7
+
8
+ A file or folder at the root of a paper folder that is not part of paperlint's layout. The root
9
+ holds a small, known set; every kind of material has one folder; anything else goes into `extra/`,
10
+ which is never judged.
11
+
12
+ It runs on every `paperlint lint` and in CI, from the moment `paperlint new` creates the folder; a
13
+ hook refuses a new stray entry before it is written. Which stage writes each place, and what checks
14
+ it: [the map](../../layout.md).
15
+
16
+ <!-- layout:begin — drawn from src/domain/paper-layout.ts (PAPER_LAYOUT_TREE); src/layout-docs.test.ts fails when this block differs -->
17
+
18
+ ```text
19
+ <paper>/
20
+ ├── PIPELINE-STATUS.md ← the paper's one live-state file: stages, plan, scorecard
21
+ ├── paper.tex ← the source; `paperlint build` compiles it
22
+ ├── paperlint.json ← the paper's settings
23
+ ├── README.md ← what the paper is
24
+ ├── CLAUDE.md ← the paper's agent rules
25
+ ├── CLAIMS.md ← the claims ledger
26
+ ├── NOTES.md ← each number in the paper → the data it comes from
27
+ ├── SUBMIT-CHECKLIST.md ← the venue's format and submission mechanics
28
+ ├── .gitignore ← keeps the build's byproducts out of git
29
+ ├── paper.pdf ← the built PDF
30
+ ├── *.tex *.bib *.cls *.sty *.bst ← what TeX and bibtex look up beside paper.tex
31
+ ├── _build/ ← the facts `paperlint build` writes
32
+ ├── versions/ ← each stage's frozen PDF and source
33
+ ├── reviews/ ← dated reviews and pass reports
34
+ ├── siblings/ ← the cards of competing papers
35
+ ├── research/ ← dated research and decision records
36
+ ├── repro/ ← the reproduction artifact
37
+ │ ├── README.md ← the index: one line per study
38
+ │ ├── .gitignore ← keeps the scripts' byproducts out of git
39
+ │ ├── tools/ ← scripts used across the paper (numbers, figures, anonymity check, harnesses)
40
+ │ ├── 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
+ │ └── README.md ← required: what it asks, how to run it, what it found
43
+ ├── figures/ ← graphics and their sources
44
+ ├── sections/ ← the body, when it is split
45
+ ├── talk/ ← the talk's files
46
+ ├── submission/ ← forms, portal records, mail with the venue, the anonymized upload
47
+ └── extra/ ← anything; never judged
48
+ ```
49
+
50
+ <!-- layout:end -->
51
+
52
+ `paperlint new` makes the folders a paper uses from its first day: `versions/`, `reviews/`, `research/`,
53
+ `repro/` (with its index, `tools/` and `data/`), `figures/` and `submission/`, each empty one with a
54
+ `.gitkeep` so the layout is in the first commit. It leaves out `siblings/` (made with the first
55
+ competitor's card), `sections/` (when the body is split), `talk/` (when the paper declares a talk),
56
+ `extra/` (never judged) and `_build/` (the build's). No rule reports a `.gitkeep` inside these folders.
57
+
58
+ `extra/` is never judged by any rule, not only this one: `paperlint lint` does not read a folder named
59
+ `extra/`, wherever it is. An archived snapshot of an older paper kept there, with its own
60
+ `PIPELINE-STATUS.md`, is not taken for a second paper.
61
+
62
+ Never reported: the byproducts a TeX run leaves beside `paper.tex` (`paper.aux`, `.log`, `.out`,
63
+ `.toc`, `.lof`, `.lot`, `.lol`, `.loa`, `.fls`, `.xdv`, `.dvi`, `.synctex.gz`; bibtex's `.bbl`, `.blg`;
64
+ makeindex's `.idx`, `.ind`, `.ilg`; glossaries' `.glo`, `.gls`, `.glg`, `.acn`, `.acr`, `.alg`, `.ist`,
65
+ `.nlo`, `.nls`; biber's `.bcf`, `.run.xml`; beamer's `.nav`, `.snm`, `.vrb`; latexmk's `.fdb_latexmk`;
66
+ the list follows latexmk's clean lists; only under the job name `paper`), the `.aux` that
67
+ `\include{chapter}` writes beside a root `chapter.tex`, and the operating system's `.DS_Store` and
68
+ `Thumbs.db`.
69
+
70
+ A venue's corpus of accepted papers is not in the paper folder at all: it is shared by every paper
71
+ that targets the venue, in `<papers-root>/venues/<venue>/` ([what it holds](../../venues.md)).
72
+
73
+ The paper's settings can move three entries, and what they name at the root is allowed while they
74
+ name it: `pdf` (the built PDF under another path), `talk.dir` (another talk folder), and a preset
75
+ file of the paper's own (`extends`, or the `extends` of any of its `cycles`).
76
+
77
+ ## Why
78
+
79
+ A paper folder collects files the way a desk does: a plan, mails, three drafts of the PDF, an old
80
+ attempt's checklist, a corpus. After a few months nobody can say which file is current, where the
81
+ evidence for a decision is, or where a new thing goes, and an agent asked to continue the work reads
82
+ the wrong one. A fixed root is what build tools settled on long ago (Cargo, Maven): a known set, one
83
+ folder per kind of material, and one place for the rest.
84
+
85
+ A structure linter over file names cannot do this from inside ESLint: ESLint is handed only the files
86
+ paperlint lints, so a stray `.pdf`, `.py` or `.md` is never one of them. The rule lists the folder
87
+ itself.
88
+
89
+ ## Examples
90
+
91
+ Failing: a plan beside the paper.
92
+
93
+ ```text
94
+ papers/agent-rule-drift/PIPELINE-STATUS.md
95
+ 1:1 error `PLAN.md` is at the root of a paper folder (the folder holding PIPELINE-STATUS.md),
96
+ and that root holds only paperlint's layout. Where it goes: a section of
97
+ PIPELINE-STATUS.md — the paper's one live-state file holds its stages, its plan
98
+ ("Plan / now") and its scorecard; move what is still live there and delete the file.
99
+ The layout (extra/ takes anything and is never judged; the build's paper.* byproducts are never reported):
100
+ <paper>/
101
+ ├── PIPELINE-STATUS.md ← the paper's one live-state file: stages, plan, scorecard
102
+ …
103
+ └── extra/ ← anything; never judged
104
+ Rule paper/allowed-files-only: https://github.com/zernie/paperlint/blob/main/docs/rules/paper/allowed-files-only.md paper/allowed-files-only
105
+ ```
106
+
107
+ Each message names the entry and where it goes, and draws the whole layout above, so it reads on its own. A
108
+ name met in real paper folders has one destination:
109
+
110
+ | entry | where it goes |
111
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------- |
112
+ | `PLAN.md`, `plans/` | a section of `PIPELINE-STATUS.md` ("Plan / now") |
113
+ | `DECISION-LOG.md` | `research/` |
114
+ | `EMAILS.md` | `submission/` |
115
+ | `data/` | `repro/data/` |
116
+ | `artifact/` | `repro/` (scripts in `repro/tools/`, data in `repro/data/`); the anonymized upload copy in `submission/` |
117
+ | `archive/` | `versions/` for frozen PDFs, `submission/` for an earlier attempt's forms and mails, `extra/` for the rest |
118
+ | `venue-corpus/`, `venues/` | `<papers-root>/venues/<venue>/`, outside the paper folder |
119
+
120
+ Any other name gets the choice by what it is: a record or research → `research/`; about submitting
121
+ → `submission/`; the paper's live state → a section of `PIPELINE-STATUS.md`; reproduction → `repro/`;
122
+ a frozen PDF of a stage → `versions/`; unclear → `extra/`.
123
+
124
+ Passing: the folder `paperlint new` writes (`PIPELINE-STATUS.md`, `paper.tex`, `paperlint.json`),
125
+ and any folder holding only the entries above.
126
+
127
+ ## Options / preset fields
128
+
129
+ No rule options. The paper's `pdf`, `talk.dir` and own preset files add what they name (above).
130
+
131
+ ## What it does not check
132
+
133
+ - What is inside the allowed folders. `reviews/`, `siblings/` and `repro/` have their own rules
134
+ ([`repro/allowed-files-only`](../repro/allowed-files-only.md) for `repro/`); the rest are free.
135
+ - Whether a `.tex` at the root is included by anything: a fragment the body no longer `\input`s
136
+ passes. Old versions of the source belong in `versions/`.
137
+ - A folder without `PIPELINE-STATUS.md`: it is not a paper to paperlint.
138
+
139
+ ## The hook: a new stray entry is refused before it is written
140
+
141
+ `paperlint init` also wires the hook `paper-folder-layout` (PreToolUse on `Edit|Write|MultiEdit`,
142
+ `plugin/hooks/hooks.json`). When an agent is about to create a file that would add an entry the
143
+ layout does not allow — at a paper folder's root, or at the top of its `repro/`
144
+ ([`repro/allowed-files-only`](../repro/allowed-files-only.md)) — the hook denies the call, and the reason it gives is the
145
+ rule's own message: where the entry goes, and the layout drawn. The agent writes the file where it
146
+ belongs instead, in the same turn. The text comes from the functions the rules report with
147
+ (`strayTexts`, `reproTexts`); there is no second list.
148
+
149
+ What it never refuses:
150
+
151
+ - **a file that already exists** — editing is never blocked, wherever the file is, so a paper whose
152
+ folder predates the layout keeps working and the rule reports what is out of place;
153
+ - a new file inside an entry that already exists, even a stray one: the entry is not new, and the
154
+ rule names it once;
155
+ - anything outside a paper folder.
156
+
157
+ A new study folder in `repro/` is started with its `README.md`; any other first file in it is
158
+ refused with `repro/allowed-files-only`'s "without a README.md" message.
159
+
160
+ 🔴 **What it cannot see: the shell.** `mv`, `cp`, `git mv`, or a script that writes files are not an
161
+ Edit or a Write, so the hook never sees them. The guarantee is the lint: both rules fail `paperlint
162
+ lint`, and CI, whatever wrote the entry. The hook only saves the round trip. It does not read the
163
+ paper's `rules` either: a paper that switched a layout rule off is still refused a new stray entry
164
+ (an existing one is never refused).
165
+
166
+ Its first step is cheap: it looks for the nearest folder above the target file that holds
167
+ `PIPELINE-STATUS.md`, one file check per ancestor, and allows the call when there is none — no git,
168
+ no settings, no lint before that. Measured on 2026-10-12: that step takes about 19 µs. The process
169
+ around it is what every `paperlint hook` costs, the CLI's import: about 650 ms per write on the
170
+ measuring machine, with or without a paper folder (`paper-skills-nudge`, a vigiles hook, measured 730
171
+ to 850 ms on the same machine). The hook fails open: a payload it cannot read is reported on stderr
172
+ and the call goes ahead.
173
+
174
+ A project whose papers all live under one folder can skip the process for every other write by
175
+ giving the hook's entry in its own `.claude/settings.json` an `if`, Claude Code's permission-rule
176
+ filter, for example `"if": "Write(**/papers/**)"`. paperlint does not write one: where papers live is
177
+ a project's choice, and a filter that misses a papers folder would make the hook silently dead there.
178
+
179
+ ## How to fix
180
+
181
+ Move the entry where its message says, and update whatever names its path:
182
+
183
+ ```sh
184
+ git mv papers/agent-rule-drift/EMAILS.md papers/agent-rule-drift/submission/EMAILS.md
185
+ ```
186
+
187
+ To keep something as it is, move it under `extra/`. To switch the rule off for one paper, set it in
188
+ that paper's `paperlint.json`:
189
+
190
+ ```json
191
+ { "rules": { "paper/allowed-files-only": "off" } }
192
+ ```