paperlint 7.2.0 → 8.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/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -6
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +3 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -42
- package/dist/cli.js.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.js +2 -2
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +22 -91
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.js.map +1 -1
- package/dist/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +0 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-prose.d.ts +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +7 -9
- package/docs/e2e.md +4 -6
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- package/docs/rules/tex/claim-provenance.md +0 -1
- package/docs/rules.md +2 -6
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +2 -2
- package/eslint-rules/paper-typography.test.mjs +17 -85
- package/eslint-rules/paper-typography.ts +22 -98
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +0 -1
- package/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +0 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +1 -3
- package/presets/realm.jsonc +1 -1
- package/scripts/harness-api.frozen.json +0 -5
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +14 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +108 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
- package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +1 -1
- package/src/build.ts +1 -6
- package/src/cli.harness.mjs +26 -46
- package/src/cli.test.ts +32 -40
- package/src/cli.ts +14 -54
- package/src/folder-venue-rule.test.ts +1 -1
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/lint-fix.test.ts +10 -4
- package/src/new-paper.harness.mjs +9 -28
- package/src/new-paper.test.ts +6 -6
- package/src/new-paper.ts +11 -30
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-prose.test.ts +49 -0
- package/src/paper-prose.ts +67 -0
- package/src/parse-args.test.ts +0 -1
- package/src/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- package/src/types.ts +0 -2
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
|
@@ -4,7 +4,7 @@ description: "Use when a paper \"feels long / bloated / overly complex / hard to
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Agent]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:c57d6da372b28acf compiled from skills/tighten-paper/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# tighten-paper — the developmental edit
|
|
10
10
|
|
|
@@ -34,31 +34,12 @@ counter, is not this skill; it is the damage this skill exists to undo.
|
|
|
34
34
|
If you are invoked while the author is still adding material, say so and decline the pass. The right
|
|
35
35
|
answer is "not yet", not a smaller cut.
|
|
36
36
|
|
|
37
|
-
## 🏗 START WITH THE
|
|
37
|
+
## 🏗 START WITH THE SHAPE — weigh every section before judging it
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
🔴 **THE SCRIPT NO LONGER FINDS ANYTHING — it is an INVENTORY (2026-08-26).** All sixteen of its
|
|
45
|
-
checks moved into ESLint rules (`eslint-rules/paper-structure.mjs`). The findings you used to read
|
|
46
|
-
here now come from:
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
npx eslint --no-config-lookup --config eslint.config.mjs <paper.md>
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
⚠️ Do NOT pass `--flags-only`. It is kept only because two harnesses assert on its silence, and it
|
|
53
|
-
prints nothing and exits 0 whatever the paper looks like. Measured 26.08 on `the reference paper`:
|
|
54
|
-
`--flags-only` printed 0 lines while ESLint on the same bytes reported **6 structural findings**
|
|
55
|
-
(subsection-size ×2 · section-lead · free-section-size ×2 · block-ungraded). A mode that cannot
|
|
56
|
-
speak is not a check, and reading its silence as "clean" is the defect this pipeline is named for.
|
|
57
|
-
|
|
58
|
-
What the inventory prints, and why it stays a script: the outline **in order** — every heading, its
|
|
59
|
-
own words, its total with subsections, its share of the body, and the `carries:`/`score:`/`verdict:`
|
|
60
|
-
note the section wrote about itself, printed beside its weight. That comparison is the whole point
|
|
61
|
-
and a linter cannot make it: a finding has nowhere to put a table.
|
|
39
|
+
Write the outline out **in order**: every heading, its own words before its first subsection, its
|
|
40
|
+
total with subsections, its share of the body — and beside each, the `carries:`/`score:`/`verdict:`
|
|
41
|
+
note the section wrote about itself. That comparison, a section's weight printed next to what it
|
|
42
|
+
claims about itself, is the whole point.
|
|
62
43
|
|
|
63
44
|
🔴 **Why this exists, and why word counts alone mislead.** Asked whether a paper short-changed its
|
|
64
45
|
contribution, per-section word counts said no — the contribution had 38% more words than related
|
|
@@ -67,7 +48,7 @@ owned **0, 28 and 38** words before their first subsection. Nothing in the toolc
|
|
|
67
48
|
question, because every metric was per-sentence or per-document and none was per-section. The author's
|
|
68
49
|
word for the result was a "salad" — a jumbled mix — and he was right while the numbers said fine.
|
|
69
50
|
|
|
70
|
-
**What the
|
|
51
|
+
**What the count does NOT decide, and you must:** the ORDER, the NAMES, and whether a section earns
|
|
71
52
|
its place. Those are judgement, and pretending otherwise would be the failure this whole pipeline
|
|
72
53
|
exists to prevent.
|
|
73
54
|
|
|
@@ -188,18 +169,24 @@ called it "fat for no reason" after a pass that cut almost nothing and left it 1
|
|
|
188
169
|
|
|
189
170
|
A verdict that lives in a chat log is gone by the next session, and the next session then re-derives
|
|
190
171
|
it or ignores it. **Every section and subsection gets a one-line verdict comment immediately above its
|
|
191
|
-
heading**, in
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
172
|
+
heading**, in `paper.tex`, as LaTeX `%` comments, stamped with the date and the skill that
|
|
173
|
+
produced it:
|
|
174
|
+
|
|
175
|
+
```latex
|
|
176
|
+
% TIGHTEN 2026-08-06 · score 8/10 · verdict: KEEP · 190w
|
|
177
|
+
% carries: tools get close but none resolves a NAMED rule against a committed config
|
|
178
|
+
% because: it is the only place the tool-ecosystem gap is closed; a reviewer who skips it
|
|
179
|
+
% asks "hasn't this been done" and gets no answer
|
|
180
|
+
\section{Related tools}
|
|
181
|
+
|
|
182
|
+
% TIGHTEN 2026-08-06 · score 5/10 · verdict: SHORTEN -89w · 210w
|
|
183
|
+
% carries: why a linter for the rules file cannot work
|
|
184
|
+
% because: the point lands in two sentences; the rest re-establishes what §2 already did
|
|
185
|
+
\subsection{Why not a linter}
|
|
186
|
+
|
|
187
|
+
% TIGHTEN 2026-08-06 · score 2/10 · verdict: MOVE→artifact · 45w
|
|
188
|
+
% carries: nothing the reader must hold to judge the contribution
|
|
189
|
+
\subsection{Implementation notes}
|
|
203
190
|
```
|
|
204
191
|
|
|
205
192
|
🔴 **Before assigning any `MOVE→` verdict, read `paper-pipeline/references/body-vs-appendix.md`.**
|
|
@@ -224,7 +211,7 @@ written and **not one ever concluded "cut me"**. Author, 2026-08-06, reading the
|
|
|
224
211
|
value on every page and compared nothing.
|
|
225
212
|
|
|
226
213
|
The pairing is the mechanism: **a low score beside a KEEP verdict is a visible contradiction**, and
|
|
227
|
-
|
|
214
|
+
the outline above puts the two side by side. Prose cannot contradict itself; a number can.
|
|
228
215
|
|
|
229
216
|
**Calibration, so the scale does not drift to "everything is an 8":**
|
|
230
217
|
- **9–10** — remove it and a reviewer cannot assess the contribution. The claim, its evidence, its price.
|
|
@@ -279,10 +266,10 @@ Rules for these comments:
|
|
|
279
266
|
unpriced prose accumulates, because nothing there costs a page.
|
|
280
267
|
- **Stale beats absent, but not by much.** A comment whose date precedes the last substantive edit to
|
|
281
268
|
its section is stale; re-grade it rather than trusting it.
|
|
282
|
-
- **A verdict nobody executed is a note, not a decision.**
|
|
283
|
-
|
|
269
|
+
- **A verdict nobody executed is a note, not a decision.** List the blocks still carrying a
|
|
270
|
+
non-KEEP verdict — a paper full of unexecuted CUTs is worse than one never graded,
|
|
284
271
|
because it reads as decided.
|
|
285
|
-
- They are working notes:
|
|
272
|
+
- They are working notes: a `%` comment never reaches the PDF.
|
|
286
273
|
- The chat summary still gets written — but it is the *derivative*, and the paper is the record.
|
|
287
274
|
|
|
288
275
|
## 🧱 Enforce subsections — a section without them is a wall
|
|
@@ -40,31 +40,12 @@ counter, is not this skill; it is the damage this skill exists to undo.
|
|
|
40
40
|
If you are invoked while the author is still adding material, say so and decline the pass. The right
|
|
41
41
|
answer is "not yet", not a smaller cut.
|
|
42
42
|
|
|
43
|
-
## 🏗 START WITH THE
|
|
43
|
+
## 🏗 START WITH THE SHAPE — weigh every section before judging it
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
🔴 **THE SCRIPT NO LONGER FINDS ANYTHING — it is an INVENTORY (2026-08-26).** All sixteen of its
|
|
51
|
-
checks moved into ESLint rules (\`eslint-rules/paper-structure.mjs\`). The findings you used to read
|
|
52
|
-
here now come from:
|
|
53
|
-
|
|
54
|
-
\`\`\`
|
|
55
|
-
npx eslint --no-config-lookup --config eslint.config.mjs <paper.md>
|
|
56
|
-
\`\`\`
|
|
57
|
-
|
|
58
|
-
⚠️ Do NOT pass \`--flags-only\`. It is kept only because two harnesses assert on its silence, and it
|
|
59
|
-
prints nothing and exits 0 whatever the paper looks like. Measured 26.08 on \`the reference paper\`:
|
|
60
|
-
\`--flags-only\` printed 0 lines while ESLint on the same bytes reported **6 structural findings**
|
|
61
|
-
(subsection-size ×2 · section-lead · free-section-size ×2 · block-ungraded). A mode that cannot
|
|
62
|
-
speak is not a check, and reading its silence as "clean" is the defect this pipeline is named for.
|
|
63
|
-
|
|
64
|
-
What the inventory prints, and why it stays a script: the outline **in order** — every heading, its
|
|
65
|
-
own words, its total with subsections, its share of the body, and the \`carries:\`/\`score:\`/\`verdict:\`
|
|
66
|
-
note the section wrote about itself, printed beside its weight. That comparison is the whole point
|
|
67
|
-
and a linter cannot make it: a finding has nowhere to put a table.
|
|
45
|
+
Write the outline out **in order**: every heading, its own words before its first subsection, its
|
|
46
|
+
total with subsections, its share of the body — and beside each, the \`carries:\`/\`score:\`/\`verdict:\`
|
|
47
|
+
note the section wrote about itself. That comparison, a section's weight printed next to what it
|
|
48
|
+
claims about itself, is the whole point.
|
|
68
49
|
|
|
69
50
|
🔴 **Why this exists, and why word counts alone mislead.** Asked whether a paper short-changed its
|
|
70
51
|
contribution, per-section word counts said no — the contribution had 38% more words than related
|
|
@@ -73,7 +54,7 @@ owned **0, 28 and 38** words before their first subsection. Nothing in the toolc
|
|
|
73
54
|
question, because every metric was per-sentence or per-document and none was per-section. The author's
|
|
74
55
|
word for the result was a "salad" — a jumbled mix — and he was right while the numbers said fine.
|
|
75
56
|
|
|
76
|
-
**What the
|
|
57
|
+
**What the count does NOT decide, and you must:** the ORDER, the NAMES, and whether a section earns
|
|
77
58
|
its place. Those are judgement, and pretending otherwise would be the failure this whole pipeline
|
|
78
59
|
exists to prevent.
|
|
79
60
|
|
|
@@ -194,18 +175,24 @@ called it "fat for no reason" after a pass that cut almost nothing and left it 1
|
|
|
194
175
|
|
|
195
176
|
A verdict that lives in a chat log is gone by the next session, and the next session then re-derives
|
|
196
177
|
it or ignores it. **Every section and subsection gets a one-line verdict comment immediately above its
|
|
197
|
-
heading**, in
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
178
|
+
heading**, in \`paper.tex\`, as LaTeX \`%\` comments, stamped with the date and the skill that
|
|
179
|
+
produced it:
|
|
180
|
+
|
|
181
|
+
\`\`\`latex
|
|
182
|
+
% TIGHTEN 2026-08-06 · score 8/10 · verdict: KEEP · 190w
|
|
183
|
+
% carries: tools get close but none resolves a NAMED rule against a committed config
|
|
184
|
+
% because: it is the only place the tool-ecosystem gap is closed; a reviewer who skips it
|
|
185
|
+
% asks "hasn't this been done" and gets no answer
|
|
186
|
+
\\section{Related tools}
|
|
187
|
+
|
|
188
|
+
% TIGHTEN 2026-08-06 · score 5/10 · verdict: SHORTEN -89w · 210w
|
|
189
|
+
% carries: why a linter for the rules file cannot work
|
|
190
|
+
% because: the point lands in two sentences; the rest re-establishes what §2 already did
|
|
191
|
+
\\subsection{Why not a linter}
|
|
192
|
+
|
|
193
|
+
% TIGHTEN 2026-08-06 · score 2/10 · verdict: MOVE→artifact · 45w
|
|
194
|
+
% carries: nothing the reader must hold to judge the contribution
|
|
195
|
+
\\subsection{Implementation notes}
|
|
209
196
|
\`\`\`
|
|
210
197
|
|
|
211
198
|
🔴 **Before assigning any \`MOVE→\` verdict, read \`paper-pipeline/references/body-vs-appendix.md\`.**
|
|
@@ -230,7 +217,7 @@ written and **not one ever concluded "cut me"**. Author, 2026-08-06, reading the
|
|
|
230
217
|
value on every page and compared nothing.
|
|
231
218
|
|
|
232
219
|
The pairing is the mechanism: **a low score beside a KEEP verdict is a visible contradiction**, and
|
|
233
|
-
|
|
220
|
+
the outline above puts the two side by side. Prose cannot contradict itself; a number can.
|
|
234
221
|
|
|
235
222
|
**Calibration, so the scale does not drift to "everything is an 8":**
|
|
236
223
|
- **9–10** — remove it and a reviewer cannot assess the contribution. The claim, its evidence, its price.
|
|
@@ -285,10 +272,10 @@ Rules for these comments:
|
|
|
285
272
|
unpriced prose accumulates, because nothing there costs a page.
|
|
286
273
|
- **Stale beats absent, but not by much.** A comment whose date precedes the last substantive edit to
|
|
287
274
|
its section is stale; re-grade it rather than trusting it.
|
|
288
|
-
- **A verdict nobody executed is a note, not a decision.**
|
|
289
|
-
|
|
275
|
+
- **A verdict nobody executed is a note, not a decision.** List the blocks still carrying a
|
|
276
|
+
non-KEEP verdict — a paper full of unexecuted CUTs is worse than one never graded,
|
|
290
277
|
because it reads as decided.
|
|
291
|
-
- They are working notes:
|
|
278
|
+
- They are working notes: a \`%\` comment never reaches the PDF.
|
|
292
279
|
- The chat summary still gets written — but it is the *derivative*, and the paper is the record.
|
|
293
280
|
|
|
294
281
|
## 🧱 Enforce subsections — a section without them is a wall
|
|
@@ -50,15 +50,6 @@ test("checkAuthors sorts every kind of entry into its bucket", async () => {
|
|
|
50
50
|
entry("nomatch", "Ada Lovelace", "Nobody Has This"),
|
|
51
51
|
entry("onlycorr", "Ada Lovelace", "Only On CoRR"),
|
|
52
52
|
entry("down", "Ada Lovelace", "Service Down"),
|
|
53
|
-
{
|
|
54
|
-
type: "mdref",
|
|
55
|
-
key: "sw",
|
|
56
|
-
unparsed: true,
|
|
57
|
-
author: "",
|
|
58
|
-
title: "",
|
|
59
|
-
booktitle: "",
|
|
60
|
-
journal: "",
|
|
61
|
-
},
|
|
62
53
|
];
|
|
63
54
|
const answers = {
|
|
64
55
|
"Same Paper": [hit("Same Paper.", ["Ada Lovelace", "Alan Turing"])],
|
|
@@ -101,10 +92,6 @@ test("checkAuthors sorts every kind of entry into its bucket", async () => {
|
|
|
101
92
|
},
|
|
102
93
|
],
|
|
103
94
|
skipped: [
|
|
104
|
-
{
|
|
105
|
-
key: "sw",
|
|
106
|
-
why: "no author/title — a reference to software or a dataset, DBLP does not apply",
|
|
107
|
-
},
|
|
108
95
|
{
|
|
109
96
|
key: "etal",
|
|
110
97
|
why: "author list ends in `and others` — completeness not checkable",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bib-authors.mjs as a process: which file it picks from a paper directory, the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* bib-authors.mjs as a process: which file it picks from a paper directory, the bibliography
|
|
3
|
+
* sources it reads (.bib, a .tex's filecontents or its sibling refs.bib), the file it refuses, and
|
|
4
|
+
* the report and exit code for each outcome. DBLP is a preload that answers
|
|
5
5
|
* by title from a table (the documented `result.hits.hit[].info` shape) — no network.
|
|
6
6
|
*/
|
|
7
7
|
import assert from "node:assert/strict";
|
|
@@ -58,8 +58,10 @@ writeTree(root, {
|
|
|
58
58
|
"sibling/paper.tex": "\\documentclass{article}\n",
|
|
59
59
|
"sibling/refs.bib": bib("good", "Ada Lovelace", "All Good"),
|
|
60
60
|
"nobib/paper.tex": "\\documentclass{article}\n",
|
|
61
|
-
"
|
|
62
|
-
|
|
61
|
+
"etal/notes.txt": "not a bibliography\n",
|
|
62
|
+
"etal/refs.bib":
|
|
63
|
+
bib("good", "Ada Lovelace", "All Good") +
|
|
64
|
+
bib("etal", "Ada Lovelace and others", "Truncated"),
|
|
63
65
|
"down/refs.bib": bib("down", "Ada Lovelace", "Service Down"),
|
|
64
66
|
"empty/.keep": "",
|
|
65
67
|
});
|
|
@@ -98,7 +100,7 @@ test("refusals: no argument, no such path, a directory with nothing to read, a .
|
|
|
98
100
|
{
|
|
99
101
|
status: 2,
|
|
100
102
|
stdout: "",
|
|
101
|
-
stderr: "bib-authors: no .bib
|
|
103
|
+
stderr: "bib-authors: no .bib or .tex in <root>/empty\n",
|
|
102
104
|
},
|
|
103
105
|
{
|
|
104
106
|
status: 2,
|
|
@@ -162,15 +164,25 @@ test("a .tex with no filecontents reads the refs.bib beside it — PASS, exit 0"
|
|
|
162
164
|
});
|
|
163
165
|
});
|
|
164
166
|
|
|
165
|
-
test("a
|
|
166
|
-
const r = run(join(root, "
|
|
167
|
+
test("a file that is neither a .bib nor a .tex is refused — not swapped for the refs.bib beside it", () => {
|
|
168
|
+
const r = run(join(root, "etal", "notes.txt"));
|
|
169
|
+
assert.deepEqual(r, {
|
|
170
|
+
status: 2,
|
|
171
|
+
stdout: "",
|
|
172
|
+
stderr:
|
|
173
|
+
"bib-authors: <root>/etal/notes.txt is not a .bib or a .tex — give the paper's .bib or paper.tex\n",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("an `and others` entry is printed as not applicable, not dropped", () => {
|
|
178
|
+
const r = run(join(root, "etal"));
|
|
167
179
|
assert.deepEqual(r, {
|
|
168
180
|
status: 0,
|
|
169
181
|
stderr: "",
|
|
170
182
|
stdout:
|
|
171
|
-
"== bib-authors: <root>/
|
|
172
|
-
" · n/a
|
|
173
|
-
"--
|
|
183
|
+
"== bib-authors: <root>/etal/refs.bib ==\n" +
|
|
184
|
+
" · n/a etal — author list ends in `and others` — completeness not checkable\n\n" +
|
|
185
|
+
"-- 2 entries · 0 difference(s) · 1 not applicable · 0 NOT CHECKED\nPASS: no author-list disagreement.\n",
|
|
174
186
|
});
|
|
175
187
|
});
|
|
176
188
|
|
|
@@ -45,90 +45,12 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
47
47
|
import { join, extname } from "node:path";
|
|
48
|
-
import { headings as mdHeadings, requireMarkdown } from "#lib/markdown";
|
|
49
48
|
import { isMain } from "../../paper-pipeline/scripts/consumer.mjs";
|
|
50
49
|
|
|
51
50
|
const DBLP = "https://dblp.org/search/publ/api";
|
|
52
51
|
|
|
53
52
|
/* ---------- input: a .bib, a .tex with filecontents, or a paper dir ---------- */
|
|
54
53
|
|
|
55
|
-
/**
|
|
56
|
-
* Some papers carry no BibTeX at all. `compile-rules-2026` writes its 67 references as a
|
|
57
|
-
* hand-numbered markdown list that `repro/md2acl.py` turns into ACL format at build time:
|
|
58
|
-
*
|
|
59
|
-
* 12. N. F. Liu, K. Lin, J. Hewitt. *Lost in the Middle: How Language Models Use Long
|
|
60
|
-
* Contexts.* TACL, 2024. arXiv:2307.03172.
|
|
61
|
-
*
|
|
62
|
-
* 🔴 Exempting such a paper would be the wrong call, and measurably so: a hand-written author
|
|
63
|
-
* list has NO machine-checkable source, so it is the MORE exposed of the two formats, not the
|
|
64
|
-
* less. The class this script hunts lives exactly where an entry names a published venue and an
|
|
65
|
-
* arXiv id in the same breath — which is most of this list.
|
|
66
|
-
*
|
|
67
|
-
* Authors are taken only from the text BEFORE the first `*`, on purpose: entries here sometimes
|
|
68
|
-
* append a second work ("— and the reanalysis: S. B. Hossain, ...") after the title, and folding
|
|
69
|
-
* those names in would invent co-authors that the first work does not have.
|
|
70
|
-
*/
|
|
71
|
-
export function parseMarkdownRefs(text) {
|
|
72
|
-
// 🔴 The heading comes FROM THE PARSER, not from a regex (the base's rule "parse markdown with a
|
|
73
|
-
// parser"). The previous `text.search(/^#+\s*References\s*$/m)` opened the bibliography on a
|
|
74
|
-
// `## References` line QUOTED inside a ``` fence — and the papers in this repo quote their own
|
|
75
|
-
// markup in chunks. Exactly this assert already stands at a neighbour
|
|
76
|
-
// (`paper-pipeline/scripts/extract-ref-facts.harness.mjs`: "a heading inside a ``` block is not a
|
|
77
|
-
// heading"), that is, the class was known and was being reproduced here again.
|
|
78
|
-
// `requireMarkdown()` — so that a missing markdown-it fails LOUDLY: with an empty parser
|
|
79
|
-
// `headings()` returns [], and the gate would report "no references" instead of refusing. That is
|
|
80
|
-
// the same trade of one silent failure for another that the rule was written against.
|
|
81
|
-
requireMarkdown();
|
|
82
|
-
const h = mdHeadings(text).find((x) => /^References\s*$/u.test(x.text));
|
|
83
|
-
if (!h) return [];
|
|
84
|
-
const body = text.slice(h.offset);
|
|
85
|
-
const out = [];
|
|
86
|
-
// 🔴 `$(?![\s\S])`, NOT `\Z`. JavaScript has no `\Z` anchor — outside a unicode-mode pattern it
|
|
87
|
-
// is an identity escape meaning the LETTER Z, so the first version of this lookahead ended entry
|
|
88
|
-
// 1 in the middle of "J. Zhou", at the Z. Measured: 48 of 67 references parsed, and the missing
|
|
89
|
-
// 19 looked like ordinary gaps rather than a bug. Same family as the `\b`-over-Cyrillic defect
|
|
90
|
-
// already recorded in this repo: an escape that means one thing in Perl/Python and another here.
|
|
91
|
-
const re = /^(\d+)\.[ \t]+([\s\S]*?)(?=^\d+\.[ \t]|^#|$(?![\s\S]))/gm;
|
|
92
|
-
let m;
|
|
93
|
-
while ((m = re.exec(body))) {
|
|
94
|
-
const entry = m[2].replace(/\s*\n\s*/g, " ").trim();
|
|
95
|
-
const t = entry.match(/\*([^*]+)\*/);
|
|
96
|
-
if (!t) {
|
|
97
|
-
// No italic title and no author list — in this corpus these are SOFTWARE references
|
|
98
|
-
// (npm packages, repos). Nothing for DBLP to disagree with. They are surfaced as
|
|
99
|
-
// not-applicable rather than dropped: an entry that silently vanishes between the file
|
|
100
|
-
// and the report is indistinguishable from an entry that passed.
|
|
101
|
-
out.push({
|
|
102
|
-
type: "mdref",
|
|
103
|
-
key: `ref${m[1]}`,
|
|
104
|
-
unparsed: true,
|
|
105
|
-
author: "",
|
|
106
|
-
title: "",
|
|
107
|
-
booktitle: "",
|
|
108
|
-
journal: "",
|
|
109
|
-
});
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
const authorPart = entry
|
|
113
|
-
.slice(0, entry.indexOf("*"))
|
|
114
|
-
.trim()
|
|
115
|
-
.replace(/[.,;]\s*$/, "");
|
|
116
|
-
const rest = entry.slice(entry.indexOf(t[0]) + t[0].length);
|
|
117
|
-
out.push({
|
|
118
|
-
type: "mdref",
|
|
119
|
-
key: `ref${m[1]}`,
|
|
120
|
-
author: authorPart
|
|
121
|
-
.split(/,\s*|\s+and\s+/)
|
|
122
|
-
.filter(Boolean)
|
|
123
|
-
.join(" and "),
|
|
124
|
-
title: t[1].replace(/\.$/, "").trim(),
|
|
125
|
-
booktitle: rest.trim(),
|
|
126
|
-
journal: "",
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
54
|
function bibTextFrom(target) {
|
|
133
55
|
let file = target;
|
|
134
56
|
if (!existsSync(file)) die(`no such path: ${file}`);
|
|
@@ -161,16 +83,16 @@ function bibTextFrom(target) {
|
|
|
161
83
|
}
|
|
162
84
|
return undefined;
|
|
163
85
|
};
|
|
164
|
-
const found =
|
|
165
|
-
|
|
166
|
-
pick(".tex") ||
|
|
167
|
-
names.find((f) => f === "paper.md" || f === "draft.md");
|
|
168
|
-
if (!found) die(`no .bib, .tex or paper.md in ${file}`);
|
|
86
|
+
const found = pick(".bib") || pick(".tex");
|
|
87
|
+
if (!found) die(`no .bib or .tex in ${file}`);
|
|
169
88
|
file = join(file, found);
|
|
170
89
|
}
|
|
171
90
|
const text = readFileSync(file, "utf-8");
|
|
172
|
-
if (file.endsWith(".md")) return { text, file, markdown: true };
|
|
173
91
|
if (file.endsWith(".bib")) return { text, file };
|
|
92
|
+
// Only a .bib and a .tex are read: any other file would fall through to the refs.bib beside it,
|
|
93
|
+
// and the report would name a file nobody asked about.
|
|
94
|
+
if (!file.endsWith(".tex"))
|
|
95
|
+
die(`${file} is not a .bib or a .tex — give the paper's .bib or paper.tex`);
|
|
174
96
|
// A .tex may carry the bibliography inline via filecontents — that is how our papers do it.
|
|
175
97
|
const m = text.match(
|
|
176
98
|
/\\begin\{filecontents\*?\}(?:\[[^\]]*\])?\{[^}]*\.bib\}\r?\n([\s\S]*?)\\end\{filecontents\*?\}/,
|
|
@@ -358,13 +280,6 @@ export async function checkAuthors(
|
|
|
358
280
|
const matched = []; // compared against the published record, and equal
|
|
359
281
|
const entries = parsed.filter((e) => e.title && e.author);
|
|
360
282
|
|
|
361
|
-
for (const e of parsed.filter((x) => x.unparsed)) {
|
|
362
|
-
skipped.push({
|
|
363
|
-
key: e.key,
|
|
364
|
-
why: "no author/title — a reference to software or a dataset, DBLP does not apply",
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
283
|
for (const e of entries) {
|
|
369
284
|
if (!claimsPublished(e)) continue; // a preprint entry is allowed to carry preprint metadata
|
|
370
285
|
if (truncated(e.author)) {
|
|
@@ -438,8 +353,8 @@ async function main() {
|
|
|
438
353
|
if (!args[0])
|
|
439
354
|
die("usage: bib-authors.mjs <paper-dir|file.bib|file.tex> [--json]");
|
|
440
355
|
|
|
441
|
-
const { text, file
|
|
442
|
-
const parsed =
|
|
356
|
+
const { text, file } = bibTextFrom(args[0]);
|
|
357
|
+
const parsed = parseBib(text);
|
|
443
358
|
const entries = parsed.filter((e) => e.title && e.author);
|
|
444
359
|
const { findings, skipped, unchecked } = await checkAuthors(parsed);
|
|
445
360
|
if (asJson) {
|
|
@@ -14,12 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import assert from "node:assert/strict";
|
|
16
16
|
import { test } from "vitest";
|
|
17
|
-
import {
|
|
18
|
-
surnames,
|
|
19
|
-
compare,
|
|
20
|
-
claimsPublished,
|
|
21
|
-
parseMarkdownRefs,
|
|
22
|
-
} from "./bib-authors.mjs";
|
|
17
|
+
import { surnames, compare, claimsPublished } from "./bib-authors.mjs";
|
|
23
18
|
|
|
24
19
|
const t = test;
|
|
25
20
|
|
|
@@ -135,41 +130,3 @@ t("a proceedings entry IS in scope", () => {
|
|
|
135
130
|
true,
|
|
136
131
|
);
|
|
137
132
|
});
|
|
138
|
-
|
|
139
|
-
console.log("bib-authors — where the bibliography begins");
|
|
140
|
-
|
|
141
|
-
// Both halves. "Silent on a fence" without "finds the real one" is indistinguishable from a broken parse.
|
|
142
|
-
t("a real ## References is parsed", () => {
|
|
143
|
-
const md = [
|
|
144
|
-
"# Paper",
|
|
145
|
-
"text",
|
|
146
|
-
"",
|
|
147
|
-
"## References",
|
|
148
|
-
"",
|
|
149
|
-
"1. A. Author. *A title*. In NeurIPS, 2023.",
|
|
150
|
-
].join("\n");
|
|
151
|
-
const refs = parseMarkdownRefs(md);
|
|
152
|
-
assert.equal(refs.length, 1);
|
|
153
|
-
assert.match(refs[0].title, /A title/);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
t("a ## References inside a ``` fence does NOT open the bibliography", () => {
|
|
157
|
-
const md = [
|
|
158
|
-
"# Paper",
|
|
159
|
-
"",
|
|
160
|
-
"The section is written like this:",
|
|
161
|
-
"",
|
|
162
|
-
"```markdown",
|
|
163
|
-
"## References",
|
|
164
|
-
"",
|
|
165
|
-
"1. Z. Ghost. *Not a reference, but a markup example*. In Nowhere, 2020.",
|
|
166
|
-
"```",
|
|
167
|
-
"",
|
|
168
|
-
"The end.",
|
|
169
|
-
].join("\n");
|
|
170
|
-
assert.deepEqual(
|
|
171
|
-
parseMarkdownRefs(md),
|
|
172
|
-
[],
|
|
173
|
-
"quoted markup is not a bibliography — the ^#+ regex counted it as a heading",
|
|
174
|
-
);
|
|
175
|
-
});
|
package/src/build.harness.mjs
CHANGED
|
@@ -482,7 +482,7 @@ try {
|
|
|
482
482
|
);
|
|
483
483
|
|
|
484
484
|
// ── no paper.tex: a REFUSAL, not a skip ───────────────────────────────────────────────
|
|
485
|
-
const bare = paper("bare", { "
|
|
485
|
+
const bare = paper("bare", { "PIPELINE-STATUS.md": "# x" });
|
|
486
486
|
const ntex = fakeTex();
|
|
487
487
|
const nr = await buildPaper(bare, {
|
|
488
488
|
cwd: root,
|
|
@@ -546,7 +546,7 @@ try {
|
|
|
546
546
|
|
|
547
547
|
// (c) no paper.tex: refused, and a PDF left from some earlier build goes too.
|
|
548
548
|
const bareStale = paper("bare-stale", {
|
|
549
|
-
"
|
|
549
|
+
"PIPELINE-STATUS.md": "# x",
|
|
550
550
|
"paper.pdf": "%PDF-stale-from-yesterday",
|
|
551
551
|
});
|
|
552
552
|
const bsTex = fakeTex();
|
package/src/build.test.ts
CHANGED
|
@@ -162,7 +162,7 @@ test("documentclass: an empty name is no class; a macro inside the name contribu
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
test("facts of a paper with no paper.tex: no class", () => {
|
|
165
|
-
assert.deepEqual(readFacts(paper({ "
|
|
165
|
+
assert.deepEqual(readFacts(paper({ "PIPELINE-STATUS.md": "# P\n" })), {
|
|
166
166
|
main: null,
|
|
167
167
|
documentclass: null,
|
|
168
168
|
venue: null,
|
package/src/build.ts
CHANGED
|
@@ -89,12 +89,7 @@ import { messageOf, printed } from "./domain/text.ts";
|
|
|
89
89
|
const PIPED: ["ignore", "pipe", "pipe"] = ["ignore", "pipe", "pipe"];
|
|
90
90
|
|
|
91
91
|
/** A directory counts as a paper by the same markers as `structure.ts` — one shared dictionary. */
|
|
92
|
-
export const PAPER_MARKERS = [
|
|
93
|
-
"PIPELINE-STATUS.md",
|
|
94
|
-
"paper.tex",
|
|
95
|
-
"paper.md",
|
|
96
|
-
CONFIG_FILE,
|
|
97
|
-
];
|
|
92
|
+
export const PAPER_MARKERS = ["PIPELINE-STATUS.md", "paper.tex", CONFIG_FILE];
|
|
98
93
|
|
|
99
94
|
/** The source paperlint compiles, and the job name every output file carries. */
|
|
100
95
|
export const MAIN = "paper.tex";
|