paperlint 2.1.0 → 3.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/CLAUDE.md +6 -1
- package/README.md +233 -224
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +38 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +28 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +277 -265
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +143 -172
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +2 -13
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +63 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +69 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +119 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +60 -23
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +267 -192
- package/eslint-rules/paper-typography.test.mjs +297 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +4 -13
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build.harness.mjs +8 -4
- package/src/build.ts +62 -6
- package/src/cli.harness.mjs +147 -292
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +344 -304
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +186 -230
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/new-paper.test.ts +1 -13
- package/src/new-paper.ts +2 -17
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +84 -121
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +19 -7
- package/src/structure.harness.mjs +3 -1
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +20 -11
- package/src/venue-rules.ts +11 -13
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
package/docs/rules.md
CHANGED
|
@@ -3,29 +3,63 @@
|
|
|
3
3
|
The README lists what each check catches in one line. This page says exactly which file each one
|
|
4
4
|
reads and when it fails. Errors fail `paperlint lint`; warnings print and do not.
|
|
5
5
|
|
|
6
|
-
| Rule | Level | Reads | Fails when
|
|
7
|
-
| ------------------------------ | ----- | --------------------------------------------- |
|
|
8
|
-
| `paper/stages` | error | `PIPELINE-STATUS.md` | a declared stage's PDF is missing, or the `bytes:` it names does not match the file's real size, or a frozen PDF exists that no stage declares
|
|
9
|
-
| `paper/source` | error | `PIPELINE-STATUS.md` | a declared stage has no frozen `.tex` beside its PDF (a commit hash does not count — squash and gc destroy it)
|
|
10
|
-
| `paper/author-list` |
|
|
11
|
-
| `paper/research-question` | warn | `paper.tex`, `paper.md`, `PIPELINE-STATUS.md` | a stage is declared and either the scorecard has no `researchQuestion` field, or it has one the paper does not contain. The declared sentence is compared against the source with whitespace collapsed — your words, not a pattern. Advisory, because nothing here can judge whether what you declared _is_ a research question
|
|
12
|
-
| `paper/
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `pdf/
|
|
6
|
+
| Rule | Level | Reads | Fails when |
|
|
7
|
+
| ------------------------------ | ----- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| `paper/stages` | error | `PIPELINE-STATUS.md` | a declared stage's PDF is missing, or the `bytes:` it names does not match the file's real size, or a frozen PDF exists that no stage declares |
|
|
9
|
+
| `paper/source` | error | `PIPELINE-STATUS.md` | a declared stage has no frozen `.tex` beside its PDF (a commit hash does not count — squash and gc destroy it) |
|
|
10
|
+
| `paper/author-list` | error | `paper.tex` → `_build/references.json` | an entry's authors are not those of the version it cites — typically the arXiv preprint's list under a published venue. Checked online by `paperlint build` (DBLP), reported on the entry's own line |
|
|
11
|
+
| `paper/research-question` | warn | `paper.tex`, `paper.md`, `PIPELINE-STATUS.md` | a stage is declared and either the scorecard has no `researchQuestion` field, or it has one the paper does not contain. The declared sentence is compared against the source with whitespace collapsed — your words, not a pattern. Advisory, because nothing here can judge whether what you declared _is_ a research question |
|
|
12
|
+
| `paper/section-word` | warn | `paper.tex`, `paper.md` | `§` or `\S\ref` instead of the word Section. **Fixable:** `--fix` writes `Section~\ref{…}` / `Section 5` |
|
|
13
|
+
| `paper/leading-zero` | warn | `paper.tex`, `paper.md` | a decimal below 1 without its leading zero (`.05` instead of `0.05`, IEEE / ISO 80000-1 style), counted only where the reader sees it — prose, math and table cells, not macro options, column specs, comments, code or tikz. **Fixable** |
|
|
14
|
+
| `paper/figure-ref-style` | warn | `paper.tex` | `Fig.~\ref` and `Figure~\ref` mixed in one document; each minority occurrence is reported. **Fixable:** `--fix` writes the majority form |
|
|
15
|
+
| `bib/reachable-entry` | warn | `paper.tex` (its `filecontents` bibliography) | an entry has no doi, url or arXiv id — a reader has nothing to follow. Reported on the entry; no fix, the link has to be looked up |
|
|
16
|
+
| `paper/cite-exists` | error | `paper.tex` → `_build/references.json` | an entry's doi or arXiv id resolves to nothing, or to a different work. Checked online by `paperlint build`; a work that was merely not found is not a finding |
|
|
17
|
+
| `paper/refs-fresh` | error | `paper.tex` → `_build/references.json` | the bibliography changed since the build checked it (its SHA-256 differs) — run `npx paperlint build` |
|
|
18
|
+
| `paper/refs-checked` | warn | `paper.tex` → `_build/references.json` | the paper has a bibliography and no build has checked it, or the last build could not (no network) — so the two rules above did not run |
|
|
19
|
+
| `tex/future-promise` | warn | `paper.tex` | a camera-ready build still says "will be released" about something already handed over |
|
|
20
|
+
| `tex/acm-frontmatter-override` | error | `paper.tex` | an `acmart` build overrides ACM's front-matter commands and drops template elements from page 1 |
|
|
21
|
+
| `review/frontmatter` | error | `reviews/*.md` | the review's frontmatter fails paperlint's JSON Schema — a `findings` record without `id`/`status`, an unknown field, or an **open** finding with no `cause` (`skill-defect` · `missing-skill` · `hook` · `rule`). A review with no `findings` key is not checked for findings |
|
|
22
|
+
| `sibling/frontmatter` | warn | `siblings/*.md` (not `README.md`) | a sibling card's frontmatter has no `read:` saying how much of the competing paper was read (`full` · `abstract` · `none`) |
|
|
23
|
+
| `pdf/fresh` | error | `paper.tex` → `_build/paper.facts.json` | the paper names a venue and the facts cannot be judged: not JSON, a schema other than 2, or they describe a PDF that is gone or differs from the one on disk (its SHA-256) |
|
|
24
|
+
| `pdf/profile` | error | `paper.tex` → `paperlint.json` | `paperlint.json` is not JSON or has an unknown key, its `extends` does not resolve (not found, a cycle, a chain longer than four, a preset that fails the schema, an npm name), it names no `kind` while the preset has kinds, or names a kind the preset does not have |
|
|
25
|
+
| `pdf/fonts` | error | `paper.tex` → `_build/paper.facts.json` | a font the pages draw is Type 3 or not embedded, or no font starts with the family the venue preset names for body text (`fonts_text`) or headings (`fonts_title`) |
|
|
26
|
+
| `pdf/geometry` | error | `paper.tex` → `_build/paper.facts.json` | the page width or height is more than `dimTol` (default 0.05 in) off the preset's, or the column count differs |
|
|
27
|
+
| `pdf/limits` | error | `paper.tex` → `_build/paper.facts.json` | body or reference pages exceed the limit of the paper's kind, or the reference font size is outside the preset's range widened by `body_pt_tol` |
|
|
28
|
+
| `pdf/body-size` | warn | `paper.tex` → `_build/paper.facts.json` | the body font size is more than `body_pt_tol` off the preset's. A warning: banal measures the mode of the rendered text, not the declared size (9.30 pt measured at a declared 9) |
|
|
29
|
+
| `pdf/measured` | warn | `paper.tex` → `_build/paper.facts.json` | the paper's `paperlint.json` names no venue preset yet; or it names one and there are no facts (it was not built), or the facts carry no page geometry (banal was not found) — so the checks above did not run |
|
|
24
30
|
|
|
25
31
|
Optional rules — off unless you turn them on in the `rules` setting, because only some venues need
|
|
26
32
|
them — are on their own page: [`optional-rules.md`](optional-rules.md). Today there is one,
|
|
27
33
|
`pdf/last-page-balance`.
|
|
28
34
|
|
|
35
|
+
## A deliberate exception: a disable directive, with the reason
|
|
36
|
+
|
|
37
|
+
When a finding is right in general and wrong for one line — a quoted `.05`, a bibliography entry
|
|
38
|
+
for an invited talk that has no link — keep the exception where it is, with ESLint's own directive
|
|
39
|
+
in a `%` comment (`<!-- … -->` in markdown). The part after `--` is the reason, and it stays in the
|
|
40
|
+
file for the next reader:
|
|
41
|
+
|
|
42
|
+
```latex
|
|
43
|
+
% eslint-disable-next-line paper/leading-zero -- quoted verbatim from the reviewer
|
|
44
|
+
They wrote ".05" in their report.
|
|
45
|
+
Also here: .06 % eslint-disable-line paper/leading-zero -- quoted too
|
|
46
|
+
|
|
47
|
+
% eslint-disable paper/section-word -- a table reproduced from the venue's own template
|
|
48
|
+
…
|
|
49
|
+
% eslint-enable paper/section-word
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This works inside the `filecontents` bibliography of `paper.tex` too, above the entry:
|
|
53
|
+
|
|
54
|
+
```latex
|
|
55
|
+
% eslint-disable-next-line bib/reachable-entry -- an invited talk, no recording exists
|
|
56
|
+
@misc{smith2024talk, …}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A directive that no longer silences anything is itself reported, so a stale exception does not
|
|
60
|
+
linger. There is no per-paper allowance or counter: the typography rules fix themselves with
|
|
61
|
+
`paperlint lint --fix`, and what is left is either fixed or excepted by name, on its line.
|
|
62
|
+
|
|
29
63
|
Besides these rules, `paperlint lint` reports a paper directory that is missing a required file (by
|
|
30
64
|
default `PIPELINE-STATUS.md`) as an error. Which files are required is configurable — see
|
|
31
65
|
[`configuration.md`](configuration.md#required-files).
|
|
@@ -33,7 +67,7 @@ default `PIPELINE-STATUS.md`) as an error. Which files are required is configura
|
|
|
33
67
|
## Checks against the venue
|
|
34
68
|
|
|
35
69
|
A paper says where it is submitted in a `paperlint.json` beside `paper.tex`
|
|
36
|
-
([`configuration.md`](configuration.md#
|
|
70
|
+
([`configuration.md`](configuration.md#a-papers-paperlintjson)):
|
|
37
71
|
|
|
38
72
|
```json
|
|
39
73
|
{ "extends": "paperlint:aisec", "kind": "research" }
|
|
@@ -69,12 +103,13 @@ and the split into body and reference pages. They report on the paper's `paper.t
|
|
|
69
103
|
| the paper | what you get |
|
|
70
104
|
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
71
105
|
| has a `paperlint.json` that extends no preset (`"extends": null` is what `paperlint new` writes) | `pdf/measured` warning naming the file to set — no venue chosen yet |
|
|
72
|
-
| has no `paperlint.json` at all
|
|
106
|
+
| has no `paperlint.json` at all | nothing |
|
|
73
107
|
| extends a preset that does not resolve (a typo, a missing file) | `pdf/profile` error, listing the shipped presets |
|
|
74
108
|
| has not been built (no facts file) | `pdf/measured` warning — it does not fail the run, because lint often runs where nothing is built (the CI action only lints) |
|
|
75
109
|
| was built without banal | `pdf/measured` warning; fonts are still checked, the rest is not |
|
|
76
110
|
| has facts about another PDF than the one on disk | `pdf/fresh` error, and nothing else is judged |
|
|
77
|
-
| names no `kind
|
|
111
|
+
| names no `kind` while its preset has kinds, or a kind the preset lacks | `pdf/profile` error; everything but the page limit is still checked |
|
|
112
|
+
| names no `kind` and its preset has none (`paperlint:acm-sigconf`) | nothing — that preset has no page limit to check; everything else is |
|
|
78
113
|
|
|
79
114
|
The venue comes from `paperlint.json`, not from the facts, so changing it needs no rebuild: the
|
|
80
115
|
measurements do not depend on it. Messages name the venue by the preset's `name`, else by the file
|
|
@@ -100,6 +135,7 @@ Put it in your repository and extend it by a path relative to the file that name
|
|
|
100
135
|
|
|
101
136
|
```
|
|
102
137
|
package.json
|
|
138
|
+
paperlint.json (optional)
|
|
103
139
|
venues/
|
|
104
140
|
usenix-sec.jsonc
|
|
105
141
|
papers/
|
|
@@ -180,8 +216,9 @@ researchQuestion: "Does pruning the state space reduce review cost?"
|
|
|
180
216
|
---
|
|
181
217
|
```
|
|
182
218
|
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
A stage name may use `a-z`, `0-9`, `.`, `_` and `-`. You write `bytes:` and `sourceBytes:` yourself
|
|
220
|
+
(`wc -c < versions/2026-07-22-submitted.pdf`), or the agent does, once, when the stage is recorded;
|
|
221
|
+
no command in this package generates them. They are not a checksum to maintain: a
|
|
185
222
|
frozen PDF is never supposed to change, so `bytes:` disagreeing with the file means the file was
|
|
186
223
|
replaced after it was declared, and that is exactly the finding. If you really did re-freeze a
|
|
187
224
|
stage, update the number in the same commit.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bib/reachable-entry` — a bibliography entry a reader cannot follow: no doi, no url, no arXiv
|
|
3
|
+
* id. Reported ON THE ENTRY, one finding per entry; there is no fix, because the link has to be
|
|
4
|
+
* looked up. Default severity: warn.
|
|
5
|
+
*
|
|
6
|
+
* Provenance: Reviewer A on HotCRP #20 (2026-08-23), "not every entry has a DOI/link" — one
|
|
7
|
+
* paper had 0 of 47 entries reachable. Until 3.0.0 this was one count inside `paper/typography`,
|
|
8
|
+
* silenced per paper by a declared debt; now each entry is its own finding, and an entry that
|
|
9
|
+
* genuinely has no link (a talk, a personal communication) carries a disable directive with its
|
|
10
|
+
* reason, in the bibliography itself:
|
|
11
|
+
*
|
|
12
|
+
* % eslint-disable-next-line bib/reachable-entry -- an invited talk, no recording exists
|
|
13
|
+
* @misc{smith2024talk, …}
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ NOT "no doi". Measured 2026-08-24: ICLR/NeurIPS/TMLR issue no DOIs at all, so a doi-only
|
|
16
|
+
* rule would demand something that does not exist and get muted for lying. A url or an arXiv id
|
|
17
|
+
* counts.
|
|
18
|
+
*
|
|
19
|
+
* The bibliography read is the one INSIDE `paper.tex` (`filecontents`), where the papers this
|
|
20
|
+
* package targets keep it. The entry boundaries and fields are found by a line-anchored `@` and
|
|
21
|
+
* a field name followed by `=` — BibTeX's own lexical shape, one lexeme at a time, not a parse:
|
|
22
|
+
* the parser (`@retorquere/bibtex-parser`) is an OPTIONAL peer of this package.
|
|
23
|
+
*/
|
|
24
|
+
import { bibRange } from "./paper-typography.mjs";
|
|
25
|
+
|
|
26
|
+
/** Entry types that are not references. */
|
|
27
|
+
const NOT_AN_ENTRY = /^@(comment|string|preamble)\b/i;
|
|
28
|
+
const HAS_LINK = /\b(doi|url)\s*=/i;
|
|
29
|
+
const HAS_ARXIV = /arxiv[:\s]*\d{4}\.\d{4,5}/i;
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
rules: {
|
|
33
|
+
"reachable-entry": {
|
|
34
|
+
meta: {
|
|
35
|
+
type: "suggestion",
|
|
36
|
+
docs: {
|
|
37
|
+
description:
|
|
38
|
+
"a bibliography entry carries a doi, a url or an arXiv id — something a reader can follow",
|
|
39
|
+
},
|
|
40
|
+
schema: [],
|
|
41
|
+
messages: {
|
|
42
|
+
unreachable:
|
|
43
|
+
"`{{key}}` has no doi, url or arXiv id — a reader has nothing to follow. If none exists, keep the exception with `% eslint-disable-next-line bib/reachable-entry -- <why>` above the entry",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
create(context) {
|
|
47
|
+
return {
|
|
48
|
+
"root:exit"() {
|
|
49
|
+
const sc = context.sourceCode;
|
|
50
|
+
const raw = sc.raw ?? sc.text;
|
|
51
|
+
const bib = typeof raw === "string" ? bibRange(raw) : null;
|
|
52
|
+
if (!bib) return;
|
|
53
|
+
const starts = [...bib.body.matchAll(/^@/gm)].map((m) => m.index);
|
|
54
|
+
starts.forEach((s, i) => {
|
|
55
|
+
const entry = bib.body.slice(s, starts[i + 1] ?? bib.body.length);
|
|
56
|
+
if (NOT_AN_ENTRY.test(entry)) return;
|
|
57
|
+
if (HAS_LINK.test(entry) || HAS_ARXIV.test(entry)) return;
|
|
58
|
+
const head = entry.split("\n")[0];
|
|
59
|
+
const key = /\{\s*([^,\s]+)/.exec(head)?.[1] ?? "?";
|
|
60
|
+
const from = bib.bodyStart + s;
|
|
61
|
+
context.report({
|
|
62
|
+
loc: {
|
|
63
|
+
start: sc.getLocFromIndex(from),
|
|
64
|
+
end: sc.getLocFromIndex(from + head.length),
|
|
65
|
+
},
|
|
66
|
+
messageId: "unreachable",
|
|
67
|
+
data: { key },
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -64,7 +64,9 @@
|
|
|
64
64
|
* is not substituted). Anonymisation is normally done with exactly those, so coinage and
|
|
65
65
|
* jargon rules see less on an anonymised version than on a camera-ready.
|
|
66
66
|
* 8. `% eslint {"rules":…}` inside `.tex` does NOT work: `applyInlineConfig` returns empty.
|
|
67
|
-
* `% eslint-disable-next-line <rule> -- reason
|
|
67
|
+
* The disable directives DO work — `% eslint-disable-next-line <rule> -- reason`,
|
|
68
|
+
* `% eslint-disable-line`, `% eslint-disable` / `% eslint-enable` — and since 3.0.0 also
|
|
69
|
+
* inside a `filecontents` bibliography, whose `%` lines are made comment nodes below.
|
|
68
70
|
* 9. `\section*{…}` is untested and does not occur in the corpus (measured: 0 occurrences).
|
|
69
71
|
* When it appears, `plain()` will glue all arguments together and the heading may come out
|
|
70
72
|
* as `*X`.
|
|
@@ -207,6 +209,32 @@ export function texToMdast(src) {
|
|
|
207
209
|
});
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
// 🔴 A `filecontents` body is not LaTeX — it is the `.bib` the paper writes, usually in the
|
|
213
|
+
// preamble — so the parser returns it as one verbatim node and no comment nodes from it. A
|
|
214
|
+
// disable directive above a bibliography entry (`% eslint-disable-next-line bib/reachable-entry
|
|
215
|
+
// -- …`) was therefore invisible. Its `%` lines are made comments here, the only way
|
|
216
|
+
// `getInlineConfigNodes` sees them. BibTeX ignores text between entries, so the line is
|
|
217
|
+
// harmless to the build.
|
|
218
|
+
for (const n of ast.content)
|
|
219
|
+
if (
|
|
220
|
+
n.type === "verbatim" &&
|
|
221
|
+
/^filecontents\*?$/.test(String(n.env)) &&
|
|
222
|
+
P(n)
|
|
223
|
+
) {
|
|
224
|
+
let at = P(n).start.offset;
|
|
225
|
+
for (const line of src.slice(at, P(n).end.offset).split("\n")) {
|
|
226
|
+
const text = line.replace(/\r$/, "");
|
|
227
|
+
const pct = /^\s*%/.test(text) ? text.indexOf("%") : -1;
|
|
228
|
+
if (pct >= 0)
|
|
229
|
+
children.push({
|
|
230
|
+
type: "html",
|
|
231
|
+
value: text.slice(pct + 1),
|
|
232
|
+
position: { start: loc(at + pct), end: loc(at + text.length) },
|
|
233
|
+
});
|
|
234
|
+
at += line.length + 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
(function walk(node) {
|
|
211
239
|
if (Array.isArray(node)) return node.forEach((n) => walk(n));
|
|
212
240
|
if (!node || typeof node !== "object") return;
|
|
@@ -45,7 +45,7 @@ const since = () => {
|
|
|
45
45
|
|
|
46
46
|
assert.deepEqual(
|
|
47
47
|
Object.keys(stages.rules).sort(),
|
|
48
|
-
["
|
|
48
|
+
["source", "stages"],
|
|
49
49
|
"rule set changed",
|
|
50
50
|
);
|
|
51
51
|
|
|
@@ -237,120 +237,4 @@ console.log(
|
|
|
237
237
|
`✓ ${String(since())} assertions passed — paper/source, frozen bytes instead of a sha`,
|
|
238
238
|
);
|
|
239
239
|
|
|
240
|
-
// ── `paper/author-list`: a shipped paper owes itself a run of the author-list cross-check ────
|
|
241
|
-
//
|
|
242
|
-
// This rule's subject is DIFFERENT from its two neighbors above: those check the declaration
|
|
243
|
-
// against the bytes, this one checks the declaration against a record of a run. What they share
|
|
244
|
-
// is exactly one input, the `stages` field, and that's why the rule lives in this module.
|
|
245
|
-
{
|
|
246
|
-
const lintAuthors = (name, opts = {}) => {
|
|
247
|
-
const file = join(FIX, name, "PIPELINE-STATUS.md");
|
|
248
|
-
const msgs = linter.verify(
|
|
249
|
-
readFileSync(file, "utf-8"),
|
|
250
|
-
[
|
|
251
|
-
{
|
|
252
|
-
files: ["**/*.md"],
|
|
253
|
-
plugins: { markdown, paper: stages },
|
|
254
|
-
language: "markdown/gfm",
|
|
255
|
-
languageOptions: { frontmatter: "yaml" },
|
|
256
|
-
rules: { "paper/author-list": ["error", opts] },
|
|
257
|
-
},
|
|
258
|
-
],
|
|
259
|
-
file,
|
|
260
|
-
);
|
|
261
|
-
assert.deepEqual(
|
|
262
|
-
msgs.filter((m) => m.fatal),
|
|
263
|
-
[],
|
|
264
|
-
`${name}: the rule threw`,
|
|
265
|
-
);
|
|
266
|
-
return msgs.map((m) => m.message);
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
// ── stays silent where it must ──
|
|
270
|
-
check(
|
|
271
|
-
"a run is recorded in the scorecard — silent",
|
|
272
|
-
lintAuthors("authors-ran").length === 0,
|
|
273
|
-
);
|
|
274
|
-
// A draft never asked anyone to read it, so it owes nothing. This is not a carve-out: the
|
|
275
|
-
// rule's subject is the DEBT of a shipped paper, and an unshipped one has no debt.
|
|
276
|
-
check(
|
|
277
|
-
"no stage declared at all — silent, a draft owes nothing",
|
|
278
|
-
lintAuthors("nothing").length === 0,
|
|
279
|
-
);
|
|
280
|
-
// An empty list is not "a stage exists": the record `stages: []` shows up on a paper that was
|
|
281
|
-
// set up but never submitted anywhere.
|
|
282
|
-
check(
|
|
283
|
-
"an empty stage list — silent",
|
|
284
|
-
linter.verify(
|
|
285
|
-
"---\nstages: []\n---\n# S\n",
|
|
286
|
-
[
|
|
287
|
-
{
|
|
288
|
-
files: ["**/*.md"],
|
|
289
|
-
plugins: { markdown, paper: stages },
|
|
290
|
-
language: "markdown/gfm",
|
|
291
|
-
languageOptions: { frontmatter: "yaml" },
|
|
292
|
-
rules: { "paper/author-list": "error" },
|
|
293
|
-
},
|
|
294
|
-
],
|
|
295
|
-
join(FIX, "x", "PIPELINE-STATUS.md"),
|
|
296
|
-
).length === 0,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
// ── fires on a planted defect ──
|
|
300
|
-
const owed = lintAuthors("ok");
|
|
301
|
-
check("a stage is declared, no run — a finding", owed.length === 1);
|
|
302
|
-
// 🔴 The message must name the CLASS, not just the fact of a miss: otherwise the reader takes
|
|
303
|
-
// it for a duplicate of the citation-existence check and closes it as noise. The class is
|
|
304
|
-
// preprint authors under a declared conference, and it is invisible to a check that a
|
|
305
|
-
// citation merely resolves.
|
|
306
|
-
check(
|
|
307
|
-
"and it names the class the cross-check catches, not just the miss",
|
|
308
|
-
/PREPRINT/.test(owed[0]),
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
// ── the whole reason the move was made ──
|
|
312
|
-
// The predecessor derived the stage with a REGEX OVER THE SCORECARD'S PROSE. Remeasured
|
|
313
|
-
// 09-17: for `agenticdev-2026` the prose reads `submitted`, while the frontmatter reads
|
|
314
|
-
// `submitted, camera-ready`. Here the list comes from the field, so both stages land in the
|
|
315
|
-
// finding's text.
|
|
316
|
-
const two = lintAuthors("twice");
|
|
317
|
-
check(
|
|
318
|
-
"the stage list in the message comes from the FIELD and carries all of them",
|
|
319
|
-
two.length === 1 && /submitted\/submitted/.test(two[0]),
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
// ── consumer data stays with the consumer ──
|
|
323
|
-
// The predecessor hardcoded the path `.claude/skills/verify-citations/...` into the message
|
|
324
|
-
// text — the address of ONE repository inside a public package.
|
|
325
|
-
const withCmd = lintAuthors("ok", {
|
|
326
|
-
command: "node scripts/bib-authors.mjs <paper>",
|
|
327
|
-
});
|
|
328
|
-
check(
|
|
329
|
-
"the run command comes in as an option and lands in the message",
|
|
330
|
-
/scripts\/bib-authors\.mjs/.test(withCmd[0]),
|
|
331
|
-
);
|
|
332
|
-
check(
|
|
333
|
-
"and without the option the message does not invent a path",
|
|
334
|
-
!/bib-authors\.mjs/.test(owed[0]),
|
|
335
|
-
);
|
|
336
|
-
// The marker is also data: the package cannot know EXACTLY how a given consumer records a run.
|
|
337
|
-
check(
|
|
338
|
-
"the marker is configurable — with a different marker the same paper becomes a debtor",
|
|
339
|
-
lintAuthors("authors-ran", { marker: "no-such-marker" }).length === 1,
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
// ── parsing versus grep: the one case where they diverge ──
|
|
343
|
-
// 🔴 This assert is the proof of the move to a parser. All the fixtures above pass IDENTICALLY
|
|
344
|
-
// either way, because their marker sits in a cell. Here it sits in PROSE — "still need to run
|
|
345
|
-
// bib-authors", an intention, not a record — and grep would read it as evidence of a run. The
|
|
346
|
-
// evidence must sit in the scorecard.
|
|
347
|
-
check(
|
|
348
|
-
"a marker in prose OUTSIDE the table is not a record of a run",
|
|
349
|
-
lintAuthors("marker-in-prose").length === 1,
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
console.log(
|
|
353
|
-
`✓ ${String(since())} assertions passed — paper/author-list, the debt of a shipped paper`,
|
|
354
|
-
);
|
|
355
|
-
|
|
356
240
|
console.log(`✓ ${String(n)} assertions passed in total`);
|
|
@@ -254,126 +254,6 @@ export default {
|
|
|
254
254
|
* because a stage frozen BEFORE this convention existed cannot be fixed at all — those
|
|
255
255
|
* bytes are gone. Failing a build over unrecoverable history is a gate nobody can clear.
|
|
256
256
|
*/
|
|
257
|
-
/**
|
|
258
|
-
* `paper/author-list` — a shipped paper OWES ITSELF a run of the author-list cross-check.
|
|
259
|
-
*
|
|
260
|
-
* The class this cross-check catches is invisible to an existence check on citations: the
|
|
261
|
-
* citation exists, the identifier resolves, and the authors are taken from the PREPRINT
|
|
262
|
-
* while the entry declares a conference. On the live corpus this turned up seven records
|
|
263
|
-
* across three papers, including a DROPPED LIVING PERSON (`schick2023toolformer` — Eric
|
|
264
|
-
* Hambro is missing; the NeurIPS version has nine authors, the preprint has eight). Two of
|
|
265
|
-
* the seven were found on a paper ALREADY SUBMITTED.
|
|
266
|
-
*
|
|
267
|
-
* 🔴 WHAT THE MOVE FIXED, and this is a measurement, not taste. The predecessor derived the
|
|
268
|
-
* declared stage with a REGEX OVER THE PROSE of that same file. Remeasured 2026-09-17 on the
|
|
269
|
-
* live corpus: for `agenticdev-2026` the prose reads `submitted`, the frontmatter declares
|
|
270
|
-
* `submitted, camera-ready` — the pattern `/camera-ready (?:uploaded|submitted|shipped)/i`
|
|
271
|
-
* does not catch the form the stage is actually recorded in. Today's set of findings did not
|
|
272
|
-
* change from this (both checks only asked "is there ANY stage at all"), but the MESSAGE
|
|
273
|
-
* printed the wrong list of stages. The subject here is the same `stages` field that
|
|
274
|
-
* `paper/stages` already checks against the bytes in both directions.
|
|
275
|
-
*
|
|
276
|
-
* 🔴 THE MARKER IS LOOKED FOR IN TABLE CELLS, NOT GREPPED OVER THE FILE. The first draft did
|
|
277
|
-
* `context.sourceCode.text.includes(marker)` and justified it with a comment saying "a
|
|
278
|
-
* footnote cell has no node of its own". That turned out to be SIMPLY WRONG — a 2026-09-17
|
|
279
|
-
* measurement showed the markdown parser hands back `table`, `tableRow` and `tableCell`
|
|
280
|
-
* (twelve cells on a three-row table). The base rule says, word for word, "parse markdown
|
|
281
|
-
* with a parser".
|
|
282
|
-
*
|
|
283
|
-
* Parsing is also STRICTER than grep, and the difference is real: a marker mentioned in
|
|
284
|
-
* prose outside the scorecard — in a heading, in a paragraph saying "still need to run
|
|
285
|
-
* bib-authors", in a quote from someone else — no longer counts as a record of a run. To
|
|
286
|
-
* grep, those three cases are indistinguishable from a genuine record.
|
|
287
|
-
*
|
|
288
|
-
* ⚠️ WHAT THIS STILL DOES NOT FIX: inside the cell the evidence is still PROSE, and a run
|
|
289
|
-
* phrased in different words will not be seen by the rule. The real fix is a frontmatter
|
|
290
|
-
* field (`gates.cites.ran`), not a smarter text search; that's separate work, touching four
|
|
291
|
-
* live scorecards.
|
|
292
|
-
*
|
|
293
|
-
* So severity is set by the CONSUMER, and the default is not `error`: the proof of a run is
|
|
294
|
-
* prose, and a false positive at a blocking level costs more than a miss.
|
|
295
|
-
*/
|
|
296
|
-
"author-list": {
|
|
297
|
-
meta: {
|
|
298
|
-
type: "suggestion",
|
|
299
|
-
docs: {
|
|
300
|
-
description:
|
|
301
|
-
"a paper that declares a stage records that the author-list check ran — a class the existence check cannot see",
|
|
302
|
-
},
|
|
303
|
-
schema: [
|
|
304
|
-
{
|
|
305
|
-
type: "object",
|
|
306
|
-
properties: {
|
|
307
|
-
// The run marker in the scorecard. This is consumer data: the package cannot
|
|
308
|
-
// know EXACTLY how a given consumer records that the cross-check happened.
|
|
309
|
-
marker: { type: "string" },
|
|
310
|
-
// What to run. This is a CONSUMER-SPECIFIC PATH, and it has no place in a public
|
|
311
|
-
// package: the predecessor hardcoded
|
|
312
|
-
// `.claude/skills/verify-citations/scripts/bib-authors.mjs` right into the message text.
|
|
313
|
-
command: { type: "string" },
|
|
314
|
-
},
|
|
315
|
-
additionalProperties: false,
|
|
316
|
-
},
|
|
317
|
-
],
|
|
318
|
-
messages: {
|
|
319
|
-
neverRan:
|
|
320
|
-
"stage «{{stages}}» is declared, but the scorecard records no author-list run (looked for «{{marker}}» in its table). It catches what an existence check cannot see: the citation resolves, the id resolves, and the authors are the PREPRINT's while the entry declares a conference{{how}}",
|
|
321
|
-
},
|
|
322
|
-
},
|
|
323
|
-
create(context) {
|
|
324
|
-
const opts = context.options?.[0] ?? {};
|
|
325
|
-
const marker = opts.marker ?? "bib-authors";
|
|
326
|
-
const command = opts.command ?? "";
|
|
327
|
-
|
|
328
|
-
// The verdict is deferred to the end of the file: the frontmatter node arrives FIRST,
|
|
329
|
-
// and the table comes after it. Reporting on `yaml` would mean a verdict rendered
|
|
330
|
-
// without ever seeing the scorecard.
|
|
331
|
-
let declaredAt = null;
|
|
332
|
-
let stages = [];
|
|
333
|
-
let recorded = false;
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
yaml(node) {
|
|
337
|
-
let data;
|
|
338
|
-
try {
|
|
339
|
-
data = load(node.value ?? "");
|
|
340
|
-
} catch {
|
|
341
|
-
return; // `paper/stages` has already reported the unreadable YAML
|
|
342
|
-
}
|
|
343
|
-
const raw = data?.stages;
|
|
344
|
-
if (!Array.isArray(raw)) return;
|
|
345
|
-
stages = raw.map((r) => r?.stage).filter(Boolean);
|
|
346
|
-
// One guard, not two: `raw.length === 0` would be a SPECIAL CASE of this same
|
|
347
|
-
// condition, and a mutation against it would turn out unkillable — the second guard
|
|
348
|
-
// mutes it. This also covers a non-empty list made of records with no `stage` field.
|
|
349
|
-
if (stages.length === 0) return; // nothing shipped — nothing is owed
|
|
350
|
-
declaredAt = node;
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
// The evidence is a SCORECARD CELL, not any occurrence of the string in the file. It
|
|
354
|
-
// has a node of its own; the first draft claimed otherwise and grepped the whole text.
|
|
355
|
-
tableCell(node) {
|
|
356
|
-
if (recorded) return;
|
|
357
|
-
if (context.sourceCode.getText(node).includes(marker))
|
|
358
|
-
recorded = true;
|
|
359
|
-
},
|
|
360
|
-
|
|
361
|
-
"root:exit"() {
|
|
362
|
-
if (!declaredAt || recorded) return;
|
|
363
|
-
context.report({
|
|
364
|
-
node: declaredAt,
|
|
365
|
-
messageId: "neverRan",
|
|
366
|
-
data: {
|
|
367
|
-
stages: stages.join("/"),
|
|
368
|
-
marker,
|
|
369
|
-
how: command ? `. Run: ${command}` : "",
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
},
|
|
373
|
-
};
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
|
|
377
257
|
source: {
|
|
378
258
|
meta: {
|
|
379
259
|
type: "problem",
|
|
@@ -76,82 +76,6 @@ process.exit(
|
|
|
76
76
|
],
|
|
77
77
|
],
|
|
78
78
|
},
|
|
79
|
-
{
|
|
80
|
-
// 🔴 A REGRESSION BACK TO GREP. The rule's first draft did exactly this and justified it
|
|
81
|
-
// by saying "a footnote cell has no node of its own" — a measurement showed the
|
|
82
|
-
// opposite: the parser hands back `tableCell`. The mutation is caught by ONE fixture out
|
|
83
|
-
// of six: in the rest the marker already sits in a cell, so grep and parsing are
|
|
84
|
-
// indistinguishable. Without `marker-in-prose` this regression would have passed
|
|
85
|
-
// silently, and the rule would again read an intention as a fact.
|
|
86
|
-
name: "the evidence is grepped over the WHOLE FILE again",
|
|
87
|
-
harness: HARNESS,
|
|
88
|
-
expect: "a marker in prose OUTSIDE the table is not a record of a run",
|
|
89
|
-
disables:
|
|
90
|
-
"parsing in favor of a string search: 'still need to run bib-authors' — an intention, " +
|
|
91
|
-
"not a record — counts as a run again, and the paper silently stops being a debtor",
|
|
92
|
-
edits: [
|
|
93
|
-
[
|
|
94
|
-
RULE,
|
|
95
|
-
"if (context.sourceCode.getText(node).includes(marker))\n recorded = true;",
|
|
96
|
-
"if (context.sourceCode.text.includes(marker))\n recorded = true;",
|
|
97
|
-
],
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
name: "the rule stops requiring an author cross-check run",
|
|
102
|
-
harness: HARNESS,
|
|
103
|
-
expect: "a stage is declared, no run — a finding",
|
|
104
|
-
disables:
|
|
105
|
-
"the debt itself: a shipped paper no longer owes anything, and the rule stays silent " +
|
|
106
|
-
"across the whole corpus — silence is its success state, so from the outside this is " +
|
|
107
|
-
"indistinguishable",
|
|
108
|
-
// Target retargeted 09-17: the old line disappeared when the rule moved to parsing, and
|
|
109
|
-
// the driver honestly said UNUSABLE — "the patch did not land", not "the test is weak".
|
|
110
|
-
// Now the mutation declares the run recorded before the file is even read.
|
|
111
|
-
edits: [[RULE, "let recorded = false;", "let recorded = true;"]],
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
name: "the exemption for an unshipped paper becomes WIDER than it should",
|
|
115
|
-
harness: HARNESS,
|
|
116
|
-
expect: "an empty stage list — silent",
|
|
117
|
-
disables:
|
|
118
|
-
"the distinction between 'no stages' and 'there are stages': a draft starts getting a " +
|
|
119
|
-
"finding, and a rule that scolds drafts gets turned off within a week",
|
|
120
|
-
edits: [
|
|
121
|
-
[
|
|
122
|
-
RULE,
|
|
123
|
-
"if (stages.length === 0) return; // nothing shipped — nothing is owed",
|
|
124
|
-
"",
|
|
125
|
-
],
|
|
126
|
-
],
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: "the stage list is taken from somewhere OTHER than the field again",
|
|
130
|
-
harness: HARNESS,
|
|
131
|
-
expect:
|
|
132
|
-
"the stage list in the message comes from the FIELD and carries all of them",
|
|
133
|
-
disables:
|
|
134
|
-
"the exact reason the move was made. The predecessor derived the stage with a regex " +
|
|
135
|
-
"over the prose, and on agenticdev printed `submitted` where `submitted, " +
|
|
136
|
-
"camera-ready` was declared. The mutation brings back a hardcoded list — the set of " +
|
|
137
|
-
"findings does not change, only the TEXT lies, and without this assert the regression " +
|
|
138
|
-
"would have passed silently",
|
|
139
|
-
edits: [[RULE, 'stages: stages.join("/"),', 'stages: "submitted",']],
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
name: "a consumer-specific path comes back into the package's text",
|
|
143
|
-
harness: HARNESS,
|
|
144
|
-
expect:
|
|
145
|
-
"the run command comes in as an option and lands in the message",
|
|
146
|
-
disables:
|
|
147
|
-
"the boundary 'the mechanism goes in the package, the data stays with the consumer': " +
|
|
148
|
-
"the command stops arriving as an option. The predecessor hardcoded " +
|
|
149
|
-
"`.claude/skills/verify-citations/...` — the path of one private repository — right " +
|
|
150
|
-
"into a public rule's message",
|
|
151
|
-
edits: [
|
|
152
|
-
[RULE, 'const command = opts.command ?? "";', 'const command = "";'],
|
|
153
|
-
],
|
|
154
|
-
},
|
|
155
79
|
],
|
|
156
80
|
}),
|
|
157
81
|
);
|