paperlint 9.0.0 → 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.
- package/.github/CLAUDE.md +22 -0
- package/.github/CLAUDE.md.spec.ts +20 -0
- package/CLAUDE.md +59 -515
- package/CONTRIBUTING.md +2 -2
- package/README.md +68 -128
- package/dist/CLAUDE.md.spec.d.ts +3 -0
- package/dist/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/CLAUDE.md.spec.js +62 -0
- package/dist/CLAUDE.md.spec.js.map +1 -0
- package/dist/build.d.ts +1 -1
- package/dist/build.js +2 -2
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -6
- package/dist/cli.js.map +1 -1
- package/dist/domain/layout-tree.d.ts +19 -0
- package/dist/domain/layout-tree.d.ts.map +1 -0
- package/dist/domain/layout-tree.js +32 -0
- package/dist/domain/layout-tree.js.map +1 -0
- package/dist/domain/lookup-cache.d.ts +2 -2
- package/dist/domain/lookup-cache.js +2 -2
- package/dist/domain/paper-layout.d.ts +81 -0
- package/dist/domain/paper-layout.d.ts.map +1 -0
- package/dist/domain/paper-layout.js +259 -0
- package/dist/domain/paper-layout.js.map +1 -0
- package/dist/domain/repro-layout.d.ts +80 -0
- package/dist/domain/repro-layout.d.ts.map +1 -0
- package/dist/domain/repro-layout.js +247 -0
- package/dist/domain/repro-layout.js.map +1 -0
- package/dist/domain/scaffold-layout.d.ts +28 -0
- package/dist/domain/scaffold-layout.d.ts.map +1 -0
- package/dist/domain/scaffold-layout.js +81 -0
- package/dist/domain/scaffold-layout.js.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts +3 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
- package/dist/eslint-rules/install-path-literals.d.ts +1 -1
- package/dist/eslint-rules/install-path-literals.js +4 -4
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +1 -1
- package/dist/eslint-rules/prereg-coverage.d.ts +98 -0
- package/dist/eslint-rules/prereg-coverage.d.ts.map +1 -0
- package/dist/eslint-rules/prereg-coverage.js +274 -0
- package/dist/eslint-rules/prereg-coverage.js.map +1 -0
- package/dist/folder-layout-hook.d.ts +21 -0
- package/dist/folder-layout-hook.d.ts.map +1 -0
- package/dist/folder-layout-hook.js +117 -0
- package/dist/folder-layout-hook.js.map +1 -0
- package/dist/folder-layout-rule.d.ts +22 -0
- package/dist/folder-layout-rule.d.ts.map +1 -0
- package/dist/folder-layout-rule.js +107 -0
- package/dist/folder-layout-rule.js.map +1 -0
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/lib/agent-cli-version.js +1 -1
- package/dist/lib/agent-cli-version.js.map +1 -1
- package/dist/new-paper.d.ts +4 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +40 -6
- package/dist/new-paper.js.map +1 -1
- package/dist/references.d.ts +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/repro-layout-rule.d.ts +16 -0
- package/dist/repro-layout-rule.d.ts.map +1 -0
- package/dist/repro-layout-rule.js +81 -0
- package/dist/repro-layout-rule.js.map +1 -0
- package/dist/tex-requirements.js +1 -1
- package/dist/toolchain.js +1 -1
- package/docs/ci.md +60 -0
- package/docs/configuration.md +2 -0
- package/docs/e2e.md +3 -3
- package/docs/incidents.md +5 -5
- package/docs/install.md +20 -16
- package/docs/layout.md +154 -0
- package/docs/optional-rules.md +5 -4
- package/docs/references.md +5 -2
- package/docs/rules/paper/allowed-files-only.md +192 -0
- package/docs/rules/repro/allowed-files-only.md +128 -0
- package/docs/rules/study/declare-sample.md +146 -0
- package/docs/rules.md +53 -50
- package/docs/skills.md +6 -3
- package/docs/venues.md +94 -0
- package/eslint-rules/CLAUDE.md +44 -0
- package/eslint-rules/CLAUDE.md.spec.ts +43 -0
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/install-path-literals.ts +4 -4
- package/eslint-rules/pdf-last-page-balance.ts +1 -1
- package/eslint-rules/prereg-coverage.test.ts +448 -0
- package/eslint-rules/prereg-coverage.ts +419 -0
- package/eslint.config.mjs +6 -3
- package/hooks/CLAUDE.md +34 -0
- package/hooks/CLAUDE.md.spec.ts +30 -0
- package/lib/agent-cli-version.ts +1 -1
- package/lib/skill-trigger-cases.harness.mjs +51 -5
- package/lib/skill-trigger-cases.mjs +53 -15
- package/package.json +2 -2
- package/plugin/CLAUDE.md +22 -0
- package/plugin/CLAUDE.md.spec.ts +22 -0
- package/plugin/hooks/hooks.json +9 -0
- package/scripts/coverage-config.test.ts +2 -1
- package/skills/CLAUDE.md +19 -0
- package/skills/CLAUDE.md.spec.ts +18 -0
- package/skills/analyze-sibling-paper/SKILL.md +2 -2
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +1 -1
- package/skills/build-benchmark/SKILL.md +14 -1
- package/skills/build-benchmark/SKILL.md.spec.ts +13 -0
- package/skills/build-label-page/SKILL.md +118 -0
- package/skills/build-label-page/SKILL.md.spec.ts +129 -0
- package/skills/build-label-page/build-label-page.eval.mjs +15 -0
- package/skills/build-label-page/build-label-page.test.ts +959 -0
- package/skills/build-label-page/references/checklist.md +148 -0
- package/skills/build-label-page/references/example-generator.py +803 -0
- package/skills/find-venue/SKILL.md +5 -3
- package/skills/find-venue/SKILL.md.spec.ts +4 -2
- package/skills/grade-paper-writing/SKILL.md +7 -7
- package/skills/grade-paper-writing/SKILL.md.spec.ts +6 -6
- package/skills/map-prior-work/SKILL.md +2 -2
- package/skills/map-prior-work/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +8 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +7 -5
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +19 -9
- package/skills/paper-pipeline/references/acceptance-gate.md +19 -19
- package/skills/paper-pipeline/references/artifact-checklist.md +5 -0
- package/skills/paper-pipeline/references/pipeline-status-template.md +30 -1
- package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +48 -48
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +8 -8
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +48 -48
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +2 -2
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +3 -3
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-after.log +81 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-before.log +81 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-setupdiff.log +59 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.jsonl +1 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.log +22 -0
- package/skills/paper-pipeline/repro/README.md +4 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +2 -2
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +2 -2
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +4 -2
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +7 -2
- package/skills/paper-pipeline/scripts/generated-code.mjs +8 -4
- package/skills/paper-pipeline/scripts/generated-code.test.ts +15 -1
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
- package/skills/study-accepted-papers/SKILL.md +163 -161
- package/skills/study-accepted-papers/SKILL.md.spec.ts +167 -165
- package/skills/study-accepted-papers/references/base-rates.md +69 -0
- package/skills/study-accepted-papers/references/mechanisms.md +181 -0
- package/skills/verify-citations/SKILL.md +2 -2
- package/skills/verify-citations/SKILL.md.spec.ts +1 -1
- package/src/CLAUDE.md +23 -1
- package/src/CLAUDE.md.spec.ts +72 -0
- package/src/build.ts +2 -2
- package/src/cli.harness.mjs +42 -0
- package/src/cli.test.ts +90 -6
- package/src/cli.ts +78 -5
- package/src/domain/layout-tree.test.ts +32 -0
- package/src/domain/layout-tree.ts +47 -0
- package/src/domain/lookup-cache.ts +2 -2
- package/src/domain/paper-layout.test.ts +264 -0
- package/src/domain/paper-layout.ts +336 -0
- package/src/domain/repro-layout.test.ts +222 -0
- package/src/domain/repro-layout.ts +311 -0
- package/src/domain/scaffold-layout.ts +104 -0
- package/src/folder-layout-hook.test.ts +214 -0
- package/src/folder-layout-hook.ts +141 -0
- package/src/folder-layout-rule.test.ts +235 -0
- package/src/folder-layout-rule.ts +151 -0
- package/src/hooks-settings.harness.ts +9 -8
- package/src/init.ts +1 -1
- package/src/layout-docs.test.ts +52 -0
- package/src/new-paper.harness.mjs +23 -1
- package/src/new-paper.test.ts +299 -6
- package/src/new-paper.ts +51 -5
- package/src/paper-files.test.ts +9 -6
- package/src/references.test.ts +5 -5
- package/src/references.ts +2 -2
- package/src/repro-layout-rule.test.ts +114 -0
- package/src/repro-layout-rule.ts +121 -0
- package/src/rule-cases.test.ts +98 -0
- package/src/tex-requirements.ts +1 -1
- package/src/toolchain.ts +1 -1
- package/templates/paper/PIPELINE-STATUS.md +41 -7
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Mechanisms of a strong accept — the generic list (M1–M10)
|
|
2
|
+
|
|
3
|
+
What separates a venue's award winners from its ordinary accepted papers, written so it can be checked
|
|
4
|
+
against any draft. The ids are the rows of the scorecard section in the paper's `PIPELINE-STATUS.md`;
|
|
5
|
+
the skill scores a draft against them in mode C and checks the wording in mode D.
|
|
6
|
+
|
|
7
|
+
**Where the list comes from, and how far it reaches.** One run of this skill on one software-engineering
|
|
8
|
+
venue (zernie/paperlint#207): every award winner the venue listed for seven years against the ordinary
|
|
9
|
+
papers of the same years, then twelve papers read in full, with two award / no-award pairs on the same
|
|
10
|
+
question. It is a list of **hypotheses to re-test on your venue**, not a law. Each entry says what
|
|
11
|
+
supports it and what it is not. The examples below are award papers, named by title; what is said about
|
|
12
|
+
each comes from reading its full text.
|
|
13
|
+
|
|
14
|
+
**What did NOT separate the winners** (the same run, issue #207): form. Body words, figures, tables,
|
|
15
|
+
title shape, "first / gap / doubt" phrasing and statistics markers: 13 features, none significant after
|
|
16
|
+
correction, on 21 winners against 112 ordinary papers. Releasing an artifact did not separate them
|
|
17
|
+
either, because most ordinary papers release one too. Do not rebuild a paper's form to match the
|
|
18
|
+
winners. In that run, an apparent "winners have many figures" pattern came from one year of five papers
|
|
19
|
+
and did not hold across seven.
|
|
20
|
+
|
|
21
|
+
The two published sources on how reviewers in software engineering judge papers point the same way.
|
|
22
|
+
The ICSE 2018 PC report found best-paper nominees furthest above accepted papers on significance and
|
|
23
|
+
soundness, not on presentation. Ernst et al. (EMSE 2021) found negative reviews come from "a mismatch
|
|
24
|
+
between the method and the claims" and "overly grandiose claims". Both are quoted in
|
|
25
|
+
[`base-rates.md`](base-rates.md).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
| id | mechanism | the check |
|
|
30
|
+
| ------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
31
|
+
| **M1** | a non-model oracle under the headline | what decides the headline number: execution, a developer's action, or people reading, with their agreement reported? |
|
|
32
|
+
| **M2** | the claim is aimed at a belief stated on record | which sentence, by whom, does the result confirm or refute, quoted and cited? |
|
|
33
|
+
| **M3** | the construct is the claim | does what the oracle decides equal what the claim sentence asserts? |
|
|
34
|
+
| **M4** | the denominator in the claim sentence, no drift | does every claim sentence carry its scope, and does the abstract say no more than the body? |
|
|
35
|
+
| **M5** | a negative or "X alone" claim shape | is the claim a counter-example to a stated universal, or "X alone is not Y"? |
|
|
36
|
+
| **M6** | the generalised-over factor is a column | if the claim names a class (models, tools, languages), is that class a manipulated column with every cell printed? |
|
|
37
|
+
| **M7** | the phenomenon measured directly | is the thing the claim is about an outcome of its own, not inferred from a downstream proxy? |
|
|
38
|
+
| **M8** | a reusable thing the field pays for once | which benchmark, method, labelled set or actionable number will a reader reuse? |
|
|
39
|
+
| **M9** | relevance to every reviewer on the PC | would a reviewer outside the topic care, e.g. because it is about how the venue does its own science? |
|
|
40
|
+
| **M10** | where the paper's own result loses | is the case where the method or tool fails reported as prominently as where it wins? |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## M1 — a non-model oracle under the headline
|
|
45
|
+
|
|
46
|
+
**What it is.** The number in the title or abstract is decided by something other than a model's
|
|
47
|
+
judgement: running the code, what developers actually did, or people reading, with how much they agreed.
|
|
48
|
+
|
|
49
|
+
**Supports it.** In the run, no winner rested its headline on model-only labels. Award examples:
|
|
50
|
+
_Does Functional Package Management Enable Reproducible Builds at Scale? Yes._ (MSR 2025) rebuilds
|
|
51
|
+
packages and compares the result bit for bit; _A Large-scale Study about Quality and Reproducibility of
|
|
52
|
+
Jupyter Notebooks_ (MSR 2019) re-executes the notebooks; _Can LLMs Replace Manual Annotation of
|
|
53
|
+
Software Engineering Artifacts?_ (MSR 2025) keeps human annotations as the reference the models are
|
|
54
|
+
measured against.
|
|
55
|
+
|
|
56
|
+
**What it is not.** Not sufficient: in the run's pairs, papers with a human anchor still lost on M3 and
|
|
57
|
+
M4. Not "report κ": winners with and without an agreement statistic were indistinguishable. The point is
|
|
58
|
+
what decides the headline, not the statistic beside it.
|
|
59
|
+
|
|
60
|
+
## M2 — the claim is aimed at a belief stated on record
|
|
61
|
+
|
|
62
|
+
**What it is.** The introduction quotes a position the field has written down (a claim in a cited
|
|
63
|
+
paper, a practice the venue's own papers follow, a widely read critique) and the result confirms or
|
|
64
|
+
refutes it.
|
|
65
|
+
|
|
66
|
+
**Supports it.** All the award papers read in full did this; the non-award papers read mostly did not.
|
|
67
|
+
The reproducible-builds paper answers a doubt that a cited interview study put on record.
|
|
68
|
+
|
|
69
|
+
**What it is not.** Not "no study has done X": a gap is not a belief. Doubt words ("first", "gap",
|
|
70
|
+
"little is known") did not separate winners in the corpus count. What separates is a named position
|
|
71
|
+
with a source.
|
|
72
|
+
|
|
73
|
+
## M3 — the construct is the claim
|
|
74
|
+
|
|
75
|
+
**What it is.** What the oracle measures is exactly what the claim sentence asserts, so a reviewer
|
|
76
|
+
cannot ask "but is that what you mean?".
|
|
77
|
+
|
|
78
|
+
**Supports it.** Bitwise equality _is_ reproducibility (the reproducible-builds paper); re-execution
|
|
79
|
+
_is_ reproducibility (the notebooks paper). _Clarity Is Not Assumed_ (ASE 2026) defines its construct
|
|
80
|
+
formally before measuring it, so a change in the outcome can mean only one thing. In the run's
|
|
81
|
+
award / no-award pair on one question, this was the deciding difference, not the number of raters.
|
|
82
|
+
|
|
83
|
+
**What it is not.** Not the same as having an oracle (M1): a valid oracle on a nearby construct still
|
|
84
|
+
fails here.
|
|
85
|
+
|
|
86
|
+
## M4 — the denominator in the claim sentence, no drift
|
|
87
|
+
|
|
88
|
+
**What it is.** Each claim sentence carries its scope ("in the 1,000 repositories that…", "for the
|
|
89
|
+
evaluated models"), and the abstract and introduction claim no more than the body shows.
|
|
90
|
+
|
|
91
|
+
**Supports it.** The reproducible-builds abstract scopes its main sentence to one package manager. _The
|
|
92
|
+
Hidden Cost of Readability_ (ICSE 2026) has a title that asserts only what its oracle measured. In the
|
|
93
|
+
run's pairs, the non-award paper lost where its introduction or abstract said more than its own tables.
|
|
94
|
+
|
|
95
|
+
**What it is not.** Not hedging. A bold title wins when the fragile half of the claim lives, scoped, in
|
|
96
|
+
the body; the defect is drift between the levels, not confidence.
|
|
97
|
+
|
|
98
|
+
## M5 — a negative or "X alone" claim shape
|
|
99
|
+
|
|
100
|
+
**What it is.** Refute a stated universal with one large counter-example, or show that one factor alone
|
|
101
|
+
does not produce the outcome everyone assumes it does.
|
|
102
|
+
|
|
103
|
+
**Supports it.** The reproducible-builds paper refutes "reproducible builds do not scale" and, with
|
|
104
|
+
equal weight, the opposite myth about its own tool.
|
|
105
|
+
|
|
106
|
+
**What it is not.** A universal positive ("every X is Y") has the opposite logic: no single census can
|
|
107
|
+
carry it, so it either narrows or needs many independent settings.
|
|
108
|
+
|
|
109
|
+
## M6 — the generalised-over factor is a column
|
|
110
|
+
|
|
111
|
+
**What it is.** When the claim names a class (language models, tools, languages, ecosystems), the paper
|
|
112
|
+
varies that factor alone inside one design, prints every cell, and names the member that deviates.
|
|
113
|
+
|
|
114
|
+
**Supports it.** _The Hidden Cost of Readability_ runs ten models in one design and names the outliers.
|
|
115
|
+
_Understanding Software Engineering Agents: A Study of Thought-Action-Result Trajectories_ (ASE 2025)
|
|
116
|
+
names in its threats that comparing distinct models complicates a fair comparison, and scopes its
|
|
117
|
+
claims accordingly.
|
|
118
|
+
|
|
119
|
+
**What it is not.** Not necessary for an award: most winners in the run were observational. It is
|
|
120
|
+
necessary to use the class noun without bounding it to the members measured.
|
|
121
|
+
|
|
122
|
+
## M7 — the phenomenon measured directly
|
|
123
|
+
|
|
124
|
+
**What it is.** The thing the claim is about is an outcome of its own (for example, disagreement between
|
|
125
|
+
readings measured as a rate), beside the downstream outcome (for example, correctness) that it affects.
|
|
126
|
+
|
|
127
|
+
**Supports it.** _Clarity Is Not Assumed_ measures conflict between a model's outputs directly, beside
|
|
128
|
+
correctness. In the run, that was what separated it from a no-award paper on the same question, which
|
|
129
|
+
measured correctness alone.
|
|
130
|
+
|
|
131
|
+
**What it is not.** Only applies when the claim is about the phenomenon itself; a claim about an effect
|
|
132
|
+
is carried by M3 and M5.
|
|
133
|
+
|
|
134
|
+
## M8 — a reusable thing the field pays for once
|
|
135
|
+
|
|
136
|
+
**What it is.** A named benchmark, a method another team can apply, a labelled set, or an actionable
|
|
137
|
+
number a practitioner can use.
|
|
138
|
+
|
|
139
|
+
**Supports it.** The ambiguity benchmark of _Clarity Is Not Assumed_; the trajectory analysis method of
|
|
140
|
+
the agents paper; the decision procedure of the annotation paper.
|
|
141
|
+
|
|
142
|
+
**What it is not.** A released artifact alone is an admission ticket, not a differentiator: most
|
|
143
|
+
ordinary accepted papers release one. The differentiator is that a reader reuses it.
|
|
144
|
+
|
|
145
|
+
## M9 — relevance to every reviewer on the PC
|
|
146
|
+
|
|
147
|
+
**What it is.** A result that matters to reviewers outside the paper's topic, most often because it is
|
|
148
|
+
about how the venue's community does its own research (its datasets, its labelling practice, its
|
|
149
|
+
reproducibility).
|
|
150
|
+
|
|
151
|
+
**Supports it.** In the run, meta-research was over-represented among winners; examples are the
|
|
152
|
+
annotation paper and _Escaping the Time Pit_ (MSR 2021). This was a post-hoc observation by one coder.
|
|
153
|
+
Treat it as the weakest entry.
|
|
154
|
+
|
|
155
|
+
**What it is not.** Not a form. It is a topic choice, so it is a mode A or B decision, rarely a mode C
|
|
156
|
+
fix.
|
|
157
|
+
|
|
158
|
+
## M10 — where the paper's own result loses
|
|
159
|
+
|
|
160
|
+
**What it is.** The cases where the paper's method, tool or hypothesis fails are reported in the same
|
|
161
|
+
place and with the same prominence as where it wins.
|
|
162
|
+
|
|
163
|
+
**Supports it.** The reproducible-builds paper names a "false myth" about its own tool; the annotation
|
|
164
|
+
paper reports the tasks where models failed beside the ones where they succeeded.
|
|
165
|
+
|
|
166
|
+
**What it is not.** Not a threats section. This is what keeps M2 and M5 from reading as advocacy.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Using the list
|
|
171
|
+
|
|
172
|
+
- **Mode A (idea) and B (design):** M1, M2, M3, M6 and M7 are design decisions. Write each as a
|
|
173
|
+
requirement for the study before anything runs: what the oracle is, which stated belief is targeted,
|
|
174
|
+
which factor is a column.
|
|
175
|
+
- **Mode C (draft):** score every row of the scorecard section in `PIPELINE-STATUS.md`. Each row gets a
|
|
176
|
+
status (pass / partial / fail), evidence as `file:line` in the paper source, an owner, and a plan or an
|
|
177
|
+
explicit "accept ceiling" with the reason.
|
|
178
|
+
- **Mode D (final):** M4 and M2, sentence by sentence, in the abstract and the introduction.
|
|
179
|
+
- **Re-test the list on your venue.** Add a mechanism your venue's winners share, with its evidence, in
|
|
180
|
+
the venue corpus (`<papers-root>/venues/<venue>/`). Mark one that does not hold there. The ids M1–M10
|
|
181
|
+
stay stable so scorecards stay comparable.
|
|
@@ -4,7 +4,7 @@ description: Verify every citation is a real work with correct metadata, and tha
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:47a4b53679a716dd compiled from skills/verify-citations/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# verify-citations — every cite real, the delta explicit
|
|
10
10
|
|
|
@@ -134,7 +134,7 @@ built and records the verdicts, with the SHA-256 of the bibliography it checked,
|
|
|
134
134
|
changed since, and `paper/refs-checked` warns when nothing was recorded or the build had no network. The
|
|
135
135
|
step never fails the build.
|
|
136
136
|
|
|
137
|
-
What the services ANSWERED is kept in `<paper>/repro/references-cache.json` — **commit it**. A build asks
|
|
137
|
+
What the services ANSWERED is kept in `<paper>/repro/data/references-cache.json` — **commit it**. A build asks
|
|
138
138
|
only the questions that file cannot answer (a new or edited entry, or an answer older than 30 days), so an
|
|
139
139
|
unchanged bibliography builds with no network at all, in CI too, until its answers age out. It holds responses, never verdicts: the verdicts are derived again on
|
|
140
140
|
every build. A service that refuses (429, HTML instead of JSON, a timeout) is not asked again for the rest of the
|
|
@@ -150,7 +150,7 @@ built and records the verdicts, with the SHA-256 of the bibliography it checked,
|
|
|
150
150
|
changed since, and \`paper/refs-checked\` warns when nothing was recorded or the build had no network. The
|
|
151
151
|
step never fails the build.
|
|
152
152
|
|
|
153
|
-
What the services ANSWERED is kept in \`<paper>/repro/references-cache.json\` — **commit it**. A build asks
|
|
153
|
+
What the services ANSWERED is kept in \`<paper>/repro/data/references-cache.json\` — **commit it**. A build asks
|
|
154
154
|
only the questions that file cannot answer (a new or edited entry, or an answer older than 30 days), so an
|
|
155
155
|
unchanged bibliography builds with no network at all, in CI too, until its answers age out. It holds responses, never verdicts: the verdicts are derived again on
|
|
156
156
|
every build. A service that refuses (429, HTML instead of JSON, a timeout) is not asked again for the rest of the
|
package/src/CLAUDE.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- vigiles:sha256:f51051aae2ef09f8 compiled from src/CLAUDE.md.spec.ts -->
|
|
2
|
+
|
|
3
|
+
# CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## `src/`
|
|
2
6
|
|
|
3
7
|
The package's TypeScript: the CLI, the build, the toolchain installer, and the measuring that feeds
|
|
4
8
|
the facts file. Loaded when working under `src/`; the repository-wide rules are in the root
|
|
5
9
|
`CLAUDE.md`.
|
|
6
10
|
|
|
11
|
+
## The two layer axes
|
|
12
|
+
|
|
7
13
|
**`src/` is hexagonal on two axes, and the linter enforces both** (`eslint.config.mjs`; fixtures in
|
|
8
14
|
`test/fixtures/layers/`, run by `test/eslint-layers.test.ts`; issue #76).
|
|
9
15
|
|
|
@@ -49,3 +55,19 @@ against `process.cwd()`, so lint started from any other directory classifies not
|
|
|
49
55
|
`checkAllOrigins: true`: without it no `module:` policy is evaluated, so axis B and the library bans
|
|
50
56
|
report nothing — dropping it turns the fixtures `src/domain/reads-disk.ts` and
|
|
51
57
|
`src/adapters/one/disk.ts` clean (measured).
|
|
58
|
+
|
|
59
|
+
## Readers and commands
|
|
60
|
+
|
|
61
|
+
`@retorquere/bibtex-parser` is the one `.bib` reader, behind `src/ports/bib-reader.ts`.
|
|
62
|
+
Rejected on 2026-09-27, so not proposed again: a separate `paperlint refs` command; the slowness it
|
|
63
|
+
would have moved was a cache thrown away after every build (`docs/references.md`).
|
|
64
|
+
|
|
65
|
+
## Rules
|
|
66
|
+
|
|
67
|
+
### Layers
|
|
68
|
+
|
|
69
|
+
**Enforced by:** `eslint/boundaries/dependencies`
|
|
70
|
+
|
|
71
|
+
### Effect Globals
|
|
72
|
+
|
|
73
|
+
**Enforced by:** `eslint/no-restricted-globals`
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Compiled to src/CLAUDE.md by `npx vigiles compile`. Edit THIS file, never the markdown.
|
|
2
|
+
import { enforce, file, instructionFile, prose } from "vigiles/spec";
|
|
3
|
+
|
|
4
|
+
export default instructionFile({
|
|
5
|
+
sections: {
|
|
6
|
+
"`src/`": prose`The package's TypeScript: the CLI, the build, the toolchain installer, and the measuring that feeds
|
|
7
|
+
the facts file. Loaded when working under \`src/\`; the repository-wide rules are in the root
|
|
8
|
+
\`CLAUDE.md\`.`,
|
|
9
|
+
|
|
10
|
+
"The two layer axes": prose`**\`src/\` is hexagonal on two axes, and the linter enforces both** (\`eslint.config.mjs\`; fixtures in
|
|
11
|
+
\`test/fixtures/layers/\`, run by ${file("test/eslint-layers.test.ts")}; issue #76).
|
|
12
|
+
|
|
13
|
+
**Axis A — knowledge. Core means the DOMAIN, not "no I/O".** A file belongs to an adapter when it
|
|
14
|
+
exists because of one external program or format: _if banal / pdf.js / TeX Live / ESLint / Claude
|
|
15
|
+
Code disappeared tomorrow, which files would change? Those are its adapter, wherever they sit
|
|
16
|
+
today._ The domain is what would not change if every one of them were swapped. Purity is not the
|
|
17
|
+
test — a parser for banal's JSON is pure and is still banal's.
|
|
18
|
+
|
|
19
|
+
| element | where | holds | may import |
|
|
20
|
+
| --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| \`domain\` | \`src/domain/\` | values and decisions in the paper's vocabulary | \`domain\`; \`ts-essentials\`, \`node:path\`, \`node:crypto\` |
|
|
22
|
+
| \`port\` | \`src/ports/\` | interfaces **shaped by what the inside needs**, never by what a tool does (Graça: "created to fit the Application Core needs and not simply mimic the tools APIs") | \`domain\`, \`port\`; the same three externals |
|
|
23
|
+
| \`app\` | \`src/*.ts\` | use cases over ports; **no I/O defaults** — a use case without its deps does not compile | \`domain\`, \`port\`, \`app\`, the package's \`.mjs\`; Node's \`util\`/\`url\`/\`module\` |
|
|
24
|
+
| \`adapter\` | \`src/adapters/<tool>/\` | everything that knows \`<tool>\` exists, pure parsers included; used through its \`index.ts\` | \`domain\`, \`port\`, its own folder, its tool's packages, the package's \`.mjs\`. **Never** \`app\`, **never** another adapter |
|
|
25
|
+
| \`root\` | \`src/cli.ts\` | reads \`process.*\`, builds adapters, wires ports | anything |
|
|
26
|
+
|
|
27
|
+
Folders are ELEMENTS of \`eslint-plugin-boundaries\`, single files are CATEGORIES (\`root\`, \`app\`,
|
|
28
|
+
\`test\`, \`io\`): v7 matches element patterns as folders. Everyone — tests included — reaches an
|
|
29
|
+
adapter through its \`index.ts\`; the files inside one adapter import each other freely.
|
|
30
|
+
|
|
31
|
+
**Axis B — purity.** An effect (disk, process, network, env, clock, TTY) may be written only in
|
|
32
|
+
\`src/adapters/<tool>/<x>.io.ts\` and \`src/cli.ts\`. Every other file, adapters' parsers included,
|
|
33
|
+
takes a port. Test: _can a call here fail for a reason not in its arguments?_ Imports are checked
|
|
34
|
+
through the \`io\` file category in \`boundaries/dependencies\`; the \`process\` and \`fetch\` globals by
|
|
35
|
+
\`no-restricted-globals\`.
|
|
36
|
+
|
|
37
|
+
**The legacy app files are held by a ratchet, not exempted.** A site that still breaks either axis
|
|
38
|
+
carries \`// eslint-disable-next-line <rule> -- legacy <I/O|layer>, moves behind a port in #76\`.
|
|
39
|
+
Unused directives are errors (\`reportUnusedDisableDirectives\`), so a fixed site must drop its
|
|
40
|
+
comment; the per-file counts are frozen in \`scripts/layer-legacy.frozen.json\` and checked by
|
|
41
|
+
${file("scripts/layer-legacy-frozen.ts")} (\`npm run check\`, CI), which counts ESLint's own
|
|
42
|
+
\`suppressedMessages\` and fails when a file's count grows, a file not in the list carries one, or
|
|
43
|
+
anything under \`src/domain/\`, \`src/ports/\` or \`src/adapters/\` does. A NEW module that needs the
|
|
44
|
+
world is a new \`*.io.ts\` in the adapter of the program it talks to — never a new directive.
|
|
45
|
+
|
|
46
|
+
Parse at the boundary, into types that cannot hold an invalid state (a banal path that exists only
|
|
47
|
+
after its sha256 and probe passed; a \`Ready\` only an installer mints; an outcome as a discriminated
|
|
48
|
+
union, not a string); the inside never re-validates.
|
|
49
|
+
|
|
50
|
+
⚠️ **Two settings are load-bearing.** \`boundaries/root-path\`: without it the plugin matches paths
|
|
51
|
+
against \`process.cwd()\`, so lint started from any other directory classifies nothing and passes.
|
|
52
|
+
\`checkAllOrigins: true\`: without it no \`module:\` policy is evaluated, so axis B and the library bans
|
|
53
|
+
report nothing — dropping it turns the fixtures \`src/domain/reads-disk.ts\` and
|
|
54
|
+
\`src/adapters/one/disk.ts\` clean (measured).`,
|
|
55
|
+
|
|
56
|
+
"Readers and commands": prose`\`@retorquere/bibtex-parser\` is the one \`.bib\` reader, behind ${file("src/ports/bib-reader.ts")}.
|
|
57
|
+
Rejected on 2026-09-27, so not proposed again: a separate \`paperlint refs\` command; the slowness it
|
|
58
|
+
would have moved was a cache thrown away after every build (${file("docs/references.md")}).`,
|
|
59
|
+
},
|
|
60
|
+
rules: {
|
|
61
|
+
layers: enforce(
|
|
62
|
+
"eslint/boundaries/dependencies",
|
|
63
|
+
"Both axes: what a layer may import, and where an effect may be written.",
|
|
64
|
+
{ render: "id" },
|
|
65
|
+
),
|
|
66
|
+
"effect-globals": enforce(
|
|
67
|
+
"eslint/no-restricted-globals",
|
|
68
|
+
"The process and fetch globals only where an effect may be written.",
|
|
69
|
+
{ render: "id" },
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
});
|
package/src/build.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* The decision of the LaTeX loop is `latex-loop.ts`, reading the log is `latex-log.ts`; both are
|
|
35
35
|
* pure. This file is the shell: it runs processes through the injected `run` (the existing port —
|
|
36
36
|
* `spawnSync` by default) and reads the files a pass left behind. Where paperlint's own files live is
|
|
37
|
-
* answered by `package-dirs.ts`, the one module allowed to know it (
|
|
37
|
+
* answered by `package-dirs.ts`, the one module allowed to know it (root CLAUDE.md).
|
|
38
38
|
*/
|
|
39
39
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
40
40
|
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
@@ -615,7 +615,7 @@ export const measureStep: BuildStep = {
|
|
|
615
615
|
* `_build/references.json` for the offline lint rules (`reference-rules.ts`). OPTIONAL and NEVER
|
|
616
616
|
* FAILING: without network the PDF is still built, and the record says `not-checked`.
|
|
617
617
|
*
|
|
618
|
-
* What the services answered is read from and added to `<paper>/repro/references-cache.json`
|
|
618
|
+
* What the services answered is read from and added to `<paper>/repro/data/references-cache.json`
|
|
619
619
|
* (`references.ts`, `domain/lookup-cache.ts`), committed with the paper, so only a new or edited
|
|
620
620
|
* entry is asked, and an unchanged bibliography is checked with no network at all (#107).
|
|
621
621
|
*/
|
package/src/cli.harness.mjs
CHANGED
|
@@ -1233,6 +1233,48 @@ check(
|
|
|
1233
1233
|
}
|
|
1234
1234
|
}
|
|
1235
1235
|
|
|
1236
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1237
|
+
// `paperlint hook paper-folder-layout` the way Claude Code runs it: the payload on stdin, the answer
|
|
1238
|
+
// on stdout, through the real bin. The in-process tests inject both streams; this is the one place
|
|
1239
|
+
// the defaults are read and written for real.
|
|
1240
|
+
{
|
|
1241
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-hook-")));
|
|
1242
|
+
try {
|
|
1243
|
+
const paper = join(root, "papers", "p");
|
|
1244
|
+
mkdirSync(paper, { recursive: true });
|
|
1245
|
+
writeFileSync(join(paper, "PIPELINE-STATUS.md"), "# S\n");
|
|
1246
|
+
const hook = (file) =>
|
|
1247
|
+
spawnSync(
|
|
1248
|
+
process.execPath,
|
|
1249
|
+
[
|
|
1250
|
+
join(HERE, "..", "bin", "paperlint.mjs"),
|
|
1251
|
+
"hook",
|
|
1252
|
+
"paper-folder-layout",
|
|
1253
|
+
],
|
|
1254
|
+
{
|
|
1255
|
+
cwd: root,
|
|
1256
|
+
encoding: "utf8",
|
|
1257
|
+
input: JSON.stringify({ tool_input: { file_path: file } }),
|
|
1258
|
+
},
|
|
1259
|
+
);
|
|
1260
|
+
const stray = hook(join(paper, "PLAN.md"));
|
|
1261
|
+
check(
|
|
1262
|
+
"the layout hook refuses a new stray PLAN.md on stdout as a PreToolUse deny, exit 0",
|
|
1263
|
+
stray.status === 0 &&
|
|
1264
|
+
/^\{"hookSpecificOutput":\{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"`PLAN\.md` is at the root/.test(
|
|
1265
|
+
stray.stdout,
|
|
1266
|
+
),
|
|
1267
|
+
);
|
|
1268
|
+
const outside = hook(join(root, "notes.md"));
|
|
1269
|
+
check(
|
|
1270
|
+
"and says nothing about a file outside every paper folder",
|
|
1271
|
+
outside.status === 0 && outside.stdout === "",
|
|
1272
|
+
);
|
|
1273
|
+
} finally {
|
|
1274
|
+
rmSync(root, { recursive: true, force: true });
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1236
1278
|
console.log(
|
|
1237
1279
|
`✓ ${String(check.count)} assertions passed — paperlint lint, one command instead of hand-rolled config`,
|
|
1238
1280
|
);
|
package/src/cli.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { spawn } from "node:child_process";
|
|
|
9
9
|
import { existsSync, readdirSync } from "node:fs";
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
|
-
import { test } from "vitest";
|
|
12
|
+
import { test, vi } from "vitest";
|
|
13
13
|
import {
|
|
14
14
|
runNode,
|
|
15
15
|
useTempDir,
|
|
@@ -200,6 +200,70 @@ test("runHook: a runtime with no cli.js beside it, and a hook killed by a signal
|
|
|
200
200
|
);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
test("runHook paper-folder-layout: in-process, on the real disk — a refusal on stdout for a new stray file, nothing for an allowed one, a complaint and exit 0 on a broken payload", () => {
|
|
204
|
+
const dir = join(root, "layout-hook");
|
|
205
|
+
writeTree(dir, {
|
|
206
|
+
"papers/p/PIPELINE-STATUS.md": "# status\n",
|
|
207
|
+
"papers/p/paper.tex": "x",
|
|
208
|
+
"papers/p/NOTES.md": "x",
|
|
209
|
+
});
|
|
210
|
+
const hook = (payload: string) => {
|
|
211
|
+
const out: string[] = [];
|
|
212
|
+
const err: string[] = [];
|
|
213
|
+
const code = runHook("paper-folder-layout", {
|
|
214
|
+
stdin: () => payload,
|
|
215
|
+
out: (t) => out.push(t),
|
|
216
|
+
err: (m: string) => err.push(m),
|
|
217
|
+
run: () => {
|
|
218
|
+
throw new Error("an in-process hook spawns nothing");
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
return { code, out, err };
|
|
222
|
+
};
|
|
223
|
+
const write = (file: string) =>
|
|
224
|
+
JSON.stringify({ tool_input: { file_path: join(dir, file) } });
|
|
225
|
+
const stray = hook(write("papers/p/PLAN.md"));
|
|
226
|
+
assert.equal(stray.code, 0);
|
|
227
|
+
assert.match(
|
|
228
|
+
stray.out.join(""),
|
|
229
|
+
/^\{"hookSpecificOutput":\{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"`PLAN\.md` is at the root of a paper folder/,
|
|
230
|
+
);
|
|
231
|
+
assert.deepEqual(hook(write("papers/p/NOTES.md")), {
|
|
232
|
+
code: 0,
|
|
233
|
+
out: [],
|
|
234
|
+
err: [],
|
|
235
|
+
});
|
|
236
|
+
const broken = hook("{ not json");
|
|
237
|
+
assert.equal(broken.code, 0);
|
|
238
|
+
assert.match(
|
|
239
|
+
broken.err.join(""),
|
|
240
|
+
/^paperlint: the paper-folder-layout hook did not run: /,
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("runHook paper-folder-layout: by default its output goes to stdout, where Claude Code reads it", () => {
|
|
245
|
+
const dir = join(root, "layout-hook-stdout");
|
|
246
|
+
writeTree(dir, { "papers/p/PIPELINE-STATUS.md": "# status\n" });
|
|
247
|
+
const write = vi
|
|
248
|
+
.spyOn(process.stdout, "write")
|
|
249
|
+
.mockImplementation(() => true);
|
|
250
|
+
try {
|
|
251
|
+
const code = runHook("paper-folder-layout", {
|
|
252
|
+
stdin: () =>
|
|
253
|
+
JSON.stringify({
|
|
254
|
+
tool_input: { file_path: join(dir, "papers/p/PLAN.md") },
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
assert.equal(code, 0);
|
|
258
|
+
assert.match(
|
|
259
|
+
String(write.mock.calls[0]?.[0]),
|
|
260
|
+
/^\{"hookSpecificOutput":\{"hookEventName":"PreToolUse","permissionDecision":"deny"/,
|
|
261
|
+
);
|
|
262
|
+
} finally {
|
|
263
|
+
write.mockRestore();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
203
267
|
test("chooseVenue: a preset by path (inside and outside the paper), by prefixed name, and a broken one", async () => {
|
|
204
268
|
const dir = join(root, "venues");
|
|
205
269
|
writeTree(dir, {
|
|
@@ -447,14 +511,34 @@ test("silentOptionalRules: a rule no block names for any paper is silent — a g
|
|
|
447
511
|
);
|
|
448
512
|
});
|
|
449
513
|
|
|
514
|
+
test("silentOptionalRules: a rule that runs on the paper's PIPELINE-STATUS.md is reached through it", async () => {
|
|
515
|
+
const eslint = {
|
|
516
|
+
calculateConfigForFile: (f: string) =>
|
|
517
|
+
Promise.resolve(
|
|
518
|
+
f.endsWith("PIPELINE-STATUS.md")
|
|
519
|
+
? { rules: { "study/declare-sample": [2] } }
|
|
520
|
+
: { rules: {} },
|
|
521
|
+
),
|
|
522
|
+
};
|
|
523
|
+
const sampleOn = {
|
|
524
|
+
rules: [
|
|
525
|
+
{ basePath: ".", rules: { "study/declare-sample": "error" as const } },
|
|
526
|
+
],
|
|
527
|
+
};
|
|
528
|
+
assert.deepEqual(
|
|
529
|
+
await silentOptionalRules(eslint, ["p/paper.tex"], sampleOn),
|
|
530
|
+
[],
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
|
|
450
534
|
test("init with no path sets up the current directory, and its first paper is paper.tex", async () => {
|
|
451
535
|
const dir = writeTree(join(root, "init-here"), { "package.json": "{}" });
|
|
452
536
|
const r = await cli(["init", "--yes", "--no-hooks", "--paper", "first"], dir);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
r.out + r.err
|
|
457
|
-
);
|
|
537
|
+
const made = readdirSync(join(dir, "papers", "first"));
|
|
538
|
+
// The three files, then the layout's folders (src/new-paper.test.ts holds the exact tree).
|
|
539
|
+
for (const f of ["PIPELINE-STATUS.md", "paper.tex", "paperlint.json"])
|
|
540
|
+
assert.ok(made.includes(f), `${f} is missing:\n${r.out + r.err}`);
|
|
541
|
+
assert.equal(made.includes("main.tex"), false);
|
|
458
542
|
});
|
|
459
543
|
|
|
460
544
|
test("init and doctor with a declaration that names no directory: nothing to compare", async () => {
|