paperlint 4.0.0 → 4.1.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/workflows/ci.yml +16 -23
- package/CLAUDE.md +31 -11
- package/CONTRIBUTING.md +23 -7
- package/README.md +11 -8
- package/dist/adapters/banal/invocation.d.ts +1 -1
- package/dist/adapters/latex/document-class.d.ts +22 -0
- package/dist/adapters/latex/document-class.d.ts.map +1 -0
- package/dist/adapters/latex/document-class.js +102 -0
- package/dist/adapters/latex/document-class.js.map +1 -0
- package/dist/adapters/latex/hidden.d.ts +16 -0
- package/dist/adapters/latex/hidden.d.ts.map +1 -0
- package/dist/adapters/latex/hidden.js +49 -0
- package/dist/adapters/latex/hidden.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +29 -0
- package/dist/adapters/latex/index.d.ts.map +1 -0
- package/dist/adapters/latex/index.js +18 -0
- package/dist/adapters/latex/index.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts +54 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -0
- package/dist/adapters/latex/nodes.js +93 -0
- package/dist/adapters/latex/nodes.js.map +1 -0
- package/dist/adapters/latex/outline.d.ts +10 -0
- package/dist/adapters/latex/outline.d.ts.map +1 -0
- package/dist/adapters/latex/outline.js +39 -0
- package/dist/adapters/latex/outline.js.map +1 -0
- package/dist/adapters/latex/parse.d.ts +42 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -0
- package/dist/adapters/latex/parse.js +94 -0
- package/dist/adapters/latex/parse.js.map +1 -0
- package/dist/adapters/latex/rendered.d.ts +10 -0
- package/dist/adapters/latex/rendered.d.ts.map +1 -0
- package/dist/adapters/latex/rendered.js +39 -0
- package/dist/adapters/latex/rendered.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +12 -12
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/build.js +4 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -5
- package/dist/cli.js.map +1 -1
- package/dist/domain/tex-document.d.ts +84 -0
- package/dist/domain/tex-document.d.ts.map +1 -0
- package/dist/domain/tex-document.js +43 -0
- package/dist/domain/tex-document.js.map +1 -0
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +2 -2
- package/dist/facts-file.js.map +1 -1
- package/dist/new-paper.d.ts +5 -0
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +3 -0
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts +5 -0
- package/dist/package-dirs.d.ts.map +1 -0
- package/dist/package-dirs.js +14 -0
- package/dist/package-dirs.js.map +1 -0
- package/dist/ports/latex.d.ts +24 -0
- package/dist/ports/latex.d.ts.map +1 -0
- package/dist/ports/latex.js +2 -0
- package/dist/ports/latex.js.map +1 -0
- package/dist/presets.d.ts +15 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -6
- package/dist/presets.js.map +1 -1
- package/dist/tex-requirements.d.ts +11 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -8
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +83 -0
- package/dist/tex-venue-rules.d.ts.map +1 -0
- package/dist/tex-venue-rules.js +254 -0
- package/dist/tex-venue-rules.js.map +1 -0
- package/dist/venue-rules.d.ts +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/docs/configuration.md +3 -2
- package/docs/e2e.md +69 -40
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +21 -10
- package/docs/rules/tex/required-section.md +73 -0
- package/docs/rules/tex/template.md +62 -0
- package/docs/rules/tex/venue-leftover.md +71 -0
- package/docs/rules.md +70 -48
- package/docs/testing.md +17 -4
- package/docs/texlive-install-decision.md +1 -1
- package/docs/toolchain.md +2 -2
- package/eslint.config.mjs +155 -15
- package/fixtures/build-e2e/acmart/expect.json +26 -0
- package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/aidc/expect.json +14 -0
- package/fixtures/build-e2e/aidc/paper.tex +46 -0
- package/fixtures/build-e2e/aidc/paperlint.json +1 -0
- package/fixtures/build-e2e/broken/expect.json +11 -0
- package/fixtures/build-e2e/cite/expect.json +27 -0
- package/fixtures/build-e2e/empty/expect.json +12 -0
- package/fixtures/build-e2e/fallback/expect.json +53 -0
- package/fixtures/build-e2e/guards/expect.json +6 -0
- package/fixtures/build-e2e/no-source/expect.json +10 -0
- package/fixtures/build-e2e/unbalanced/expect.json +60 -0
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.ts +1 -1
- package/fixtures/render-paper/build-clean.sh +3 -5
- package/package.json +13 -2
- package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
- package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
- package/presets/aidc.jsonc +73 -0
- package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
- package/presets/ieee-conference.jsonc +66 -0
- package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
- package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
- package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
- package/scripts/check.harness.ts +64 -10
- package/scripts/check.test.ts +60 -13
- package/scripts/check.ts +83 -30
- package/scripts/test.test.ts +1 -1
- package/scripts/test.ts +1 -1
- package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
- package/skills/paper-pipeline/SKILL.md +2 -2
- package/skills/paper-pipeline/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
- package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/check-render.sh +16 -63
- package/skills/render-paper/render-paper.harness.mjs +17 -59
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +1 -1
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/latex/document-class.ts +147 -0
- package/src/adapters/latex/hidden.ts +70 -0
- package/src/adapters/latex/index.test.ts +361 -0
- package/src/adapters/latex/index.ts +55 -0
- package/src/adapters/latex/nodes.ts +141 -0
- package/src/adapters/latex/outline.ts +64 -0
- package/src/adapters/latex/parse.ts +136 -0
- package/src/adapters/latex/rendered.ts +59 -0
- package/src/adapters/pdfjs/fill.test.ts +1 -1
- package/src/adapters/pdfjs/fill.ts +18 -10
- package/src/build.harness.mjs +5 -7
- package/src/build.ts +4 -4
- package/src/cli.harness.mjs +1 -1
- package/src/cli.test.ts +2 -8
- package/src/cli.ts +33 -8
- package/src/domain/tex-document.test.ts +80 -0
- package/src/domain/tex-document.ts +110 -0
- package/src/engine.harness.mjs +1 -1
- package/src/facts-file.test.ts +4 -7
- package/src/facts-file.ts +2 -2
- package/src/latex-loop.harness.ts +1 -1
- package/src/new-paper.test.ts +28 -3
- package/src/new-paper.ts +8 -0
- package/src/package-dirs.ts +17 -0
- package/src/paper-settings-commands.test.ts +6 -1
- package/src/ports/latex.ts +29 -0
- package/src/presets.test.ts +65 -4
- package/src/presets.ts +38 -15
- package/src/rule-cases.test.ts +474 -0
- package/src/rule-docs.test.ts +159 -0
- package/src/tex-requirements.harness.ts +13 -23
- package/src/tex-requirements.ts +35 -8
- package/src/tex-venue-rules.test.ts +659 -0
- package/src/tex-venue-rules.ts +429 -0
- package/src/toolchain.harness.mjs +1 -1
- package/src/venue-rules.test.ts +96 -4
- package/src/venue-rules.ts +1 -1
- package/templates/paper/paper.tex +2 -1
- /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
- /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
package/eslint.config.mjs
CHANGED
|
@@ -19,6 +19,8 @@ import portRules from "#eslint-rules/install-path-literals";
|
|
|
19
19
|
import n from "eslint-plugin-n";
|
|
20
20
|
import tseslint from "typescript-eslint";
|
|
21
21
|
import boundaries from "eslint-plugin-boundaries";
|
|
22
|
+
import functional from "eslint-plugin-functional";
|
|
23
|
+
import sonarjs from "eslint-plugin-sonarjs";
|
|
22
24
|
import { dirname } from "node:path";
|
|
23
25
|
import { fileURLToPath } from "node:url";
|
|
24
26
|
|
|
@@ -27,6 +29,21 @@ import { fileURLToPath } from "node:url";
|
|
|
27
29
|
// or pinned at its current maximum in RATCHET.
|
|
28
30
|
const TS_FILES = ["**/*.ts", "**/*.mts"];
|
|
29
31
|
|
|
32
|
+
/** Collection helpers come from Remeda; these would be a second library for the same job. */
|
|
33
|
+
const ONE_COLLECTION_LIBRARY = ["lodash", "lodash-es", "ramda"].map((name) => ({
|
|
34
|
+
name,
|
|
35
|
+
message: "use remeda (see CLAUDE.md)",
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
/** An import of a skill's file, from code of the package itself (#133). */
|
|
39
|
+
const NO_SKILLS = [
|
|
40
|
+
{
|
|
41
|
+
regex: "(^|/)skills/",
|
|
42
|
+
message:
|
|
43
|
+
"The package's own code does not import from skills/: a skill is a consumer, not storage (#133). Move what you need into src/ or lib/.",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
30
47
|
/** `x as unknown as T` tells the checker to look away (#76). Shared: a block REPLACES a rule's options. */
|
|
31
48
|
const AS_UNKNOWN_AS = {
|
|
32
49
|
selector:
|
|
@@ -54,33 +71,99 @@ const GIT_IN_A_RULE = [
|
|
|
54
71
|
"A rule may not run git: a sha is deleted by routine maintenance and absent from a shallow CI checkout. Read the file content ESLint passes the rule.",
|
|
55
72
|
}));
|
|
56
73
|
|
|
57
|
-
|
|
74
|
+
/** Source code: a function is one job, and thirty lines is where a second job starts to hide. */
|
|
75
|
+
const MAX_LINES = { max: 30, skipComments: true, skipBlankLines: true };
|
|
76
|
+
/**
|
|
77
|
+
* Tests keep sixty: a `describe`/`it` callback is a list of cases, not a function to split, and
|
|
78
|
+
* cutting it by line count scatters one scenario across helpers nobody reads twice.
|
|
79
|
+
*/
|
|
80
|
+
const MAX_LINES_TESTS = { ...MAX_LINES, max: 60 };
|
|
81
|
+
const TEST_FILES = [
|
|
82
|
+
"**/*.test.{ts,mts}",
|
|
83
|
+
"**/*.harness.{ts,mts}",
|
|
84
|
+
"**/*.e2e.{ts,mts}",
|
|
85
|
+
"test/**/*.{ts,mts}",
|
|
86
|
+
];
|
|
58
87
|
|
|
59
|
-
// 🔴 A CEILING, NOT A PERMISSION: each number is the file's measured maximum on
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// when
|
|
88
|
+
// 🔴 A CEILING, NOT A PERMISSION: each number is the file's measured maximum on the day its limit
|
|
89
|
+
// arrived (complexity and depth on 2026-09-24, the files #78 moved to TypeScript on 2026-09-27,
|
|
90
|
+
// cognitive complexity and the 30-line source limit on 2026-09-28), so a function in these files
|
|
91
|
+
// can get simpler and cannot get worse. Lower a number when a refactor lowers the maximum; never
|
|
92
|
+
// raise one. Paying the debt down is issue #137. A file leaves this table when it passes the
|
|
93
|
+
// shared limits.
|
|
63
94
|
const RATCHET = {
|
|
64
|
-
"src/init.ts": { complexity: 59, "max-lines-per-function": 219 },
|
|
65
|
-
"src/cli.ts": { complexity: 43, "max-lines-per-function": 152 },
|
|
66
|
-
"src/doctor.ts": { complexity: 38, "max-lines-per-function": 125 },
|
|
67
|
-
"src/hooks-settings.ts": { complexity: 12, "max-depth": 4 },
|
|
68
|
-
"src/structure.ts": { complexity: 12, "max-depth": 4 },
|
|
69
|
-
"src/new-paper.ts": { complexity: 11 },
|
|
70
|
-
// Moved from JavaScript on 2026-09-27 (#78) and measured that day: the move changed their
|
|
71
|
-
// language, not their shape, so their debt is pinned here like the rest rather than paid in it.
|
|
72
95
|
"eslint-rules/latex-language.ts": {
|
|
73
96
|
complexity: 68,
|
|
97
|
+
"sonarjs/cognitive-complexity": 98,
|
|
74
98
|
"max-lines-per-function": 314,
|
|
75
99
|
"max-depth": 4,
|
|
76
100
|
},
|
|
77
|
-
"eslint-rules/paper-typography.ts": { complexity: 29, "max-depth": 5 },
|
|
78
101
|
"eslint-rules/paper-stages.ts": {
|
|
79
102
|
complexity: 15,
|
|
103
|
+
"sonarjs/cognitive-complexity": 21,
|
|
80
104
|
"max-lines-per-function": 105,
|
|
81
105
|
},
|
|
106
|
+
"eslint-rules/paper-typography.ts": {
|
|
107
|
+
complexity: 29,
|
|
108
|
+
"sonarjs/cognitive-complexity": 41,
|
|
109
|
+
"max-lines-per-function": 50,
|
|
110
|
+
"max-depth": 5,
|
|
111
|
+
},
|
|
112
|
+
"eslint-rules/pdf-last-page-balance.ts": { "max-lines-per-function": 31 },
|
|
113
|
+
"eslint-rules/review-frontmatter.ts": { "max-lines-per-function": 46 },
|
|
82
114
|
"eslint-rules/temp-root-realpath.ts": { complexity: 13 },
|
|
115
|
+
"eslint-rules/tex-build.ts": { "max-lines-per-function": 34 },
|
|
116
|
+
"lib/agent-cli-version.ts": { "max-lines-per-function": 42 },
|
|
83
117
|
"lib/markdown.ts": { complexity: 12 },
|
|
118
|
+
"scripts/check.ts": {
|
|
119
|
+
"sonarjs/cognitive-complexity": 13,
|
|
120
|
+
"max-lines-per-function": 39,
|
|
121
|
+
},
|
|
122
|
+
"skills/plan-paper-timeline/fixtures/fake-google-calendar.ts": {
|
|
123
|
+
"max-lines-per-function": 31,
|
|
124
|
+
},
|
|
125
|
+
"src/adapters/curl/download.io.ts": { "max-lines-per-function": 39 },
|
|
126
|
+
"src/adapters/references/index.ts": { "max-lines-per-function": 36 },
|
|
127
|
+
"src/build-engine.ts": { "max-lines-per-function": 33 },
|
|
128
|
+
"src/build.ts": {
|
|
129
|
+
"sonarjs/cognitive-complexity": 13,
|
|
130
|
+
"max-lines-per-function": 41,
|
|
131
|
+
},
|
|
132
|
+
"src/cli.ts": {
|
|
133
|
+
complexity: 43,
|
|
134
|
+
"sonarjs/cognitive-complexity": 30,
|
|
135
|
+
"max-lines-per-function": 152,
|
|
136
|
+
},
|
|
137
|
+
"src/doctor.ts": {
|
|
138
|
+
complexity: 38,
|
|
139
|
+
"sonarjs/cognitive-complexity": 20,
|
|
140
|
+
"max-lines-per-function": 125,
|
|
141
|
+
},
|
|
142
|
+
"src/domain/lookup-cache.ts": { "max-lines-per-function": 31 },
|
|
143
|
+
"src/hooks-settings.ts": {
|
|
144
|
+
complexity: 12,
|
|
145
|
+
"max-lines-per-function": 42,
|
|
146
|
+
"max-depth": 4,
|
|
147
|
+
},
|
|
148
|
+
"src/init.ts": {
|
|
149
|
+
complexity: 59,
|
|
150
|
+
"sonarjs/cognitive-complexity": 44,
|
|
151
|
+
"max-lines-per-function": 219,
|
|
152
|
+
},
|
|
153
|
+
"src/latex-log.ts": { "sonarjs/cognitive-complexity": 12 },
|
|
154
|
+
"src/link-skills.ts": { "max-lines-per-function": 32 },
|
|
155
|
+
"src/new-paper.ts": { complexity: 11, "max-lines-per-function": 38 },
|
|
156
|
+
"src/pdf-facts.ts": { "max-lines-per-function": 33 },
|
|
157
|
+
"src/presets.ts": { "max-lines-per-function": 50 },
|
|
158
|
+
"src/reference-rules.ts": { "max-lines-per-function": 36 },
|
|
159
|
+
"src/references.ts": { "max-lines-per-function": 43 },
|
|
160
|
+
"src/structure.ts": {
|
|
161
|
+
complexity: 12,
|
|
162
|
+
"sonarjs/cognitive-complexity": 24,
|
|
163
|
+
"max-lines-per-function": 39,
|
|
164
|
+
"max-depth": 4,
|
|
165
|
+
},
|
|
166
|
+
"src/toolchain.ts": { "max-lines-per-function": 50 },
|
|
84
167
|
};
|
|
85
168
|
|
|
86
169
|
// ── Hexagonal layers (src/CLAUDE.md, issue #76) ─────────────────────────────────────────
|
|
@@ -407,7 +490,11 @@ export default [
|
|
|
407
490
|
tsconfigRootDir: dirname(fileURLToPath(import.meta.url)),
|
|
408
491
|
},
|
|
409
492
|
},
|
|
410
|
-
plugins: {
|
|
493
|
+
plugins: {
|
|
494
|
+
"@typescript-eslint": tseslint.plugin,
|
|
495
|
+
local: localRules,
|
|
496
|
+
sonarjs,
|
|
497
|
+
},
|
|
411
498
|
rules: {
|
|
412
499
|
// The same macOS-only defect as in the `.mjs` block; clean here, so it opens at `error`.
|
|
413
500
|
"local/temp-root-realpath": "error",
|
|
@@ -437,6 +524,10 @@ export default [
|
|
|
437
524
|
// Branches per function. Ten is the rule's long-standing default; the worst function
|
|
438
525
|
// measured was 59, and one that size cannot be read, only re-run.
|
|
439
526
|
complexity: ["error", 10],
|
|
527
|
+
// `complexity` counts branches; cognitive complexity also charges NESTING, which is what makes
|
|
528
|
+
// a function hard to read. Only this one rule of the plugin: its recommended set is a style
|
|
529
|
+
// guide of its own, and each rule here is a decision.
|
|
530
|
+
"sonarjs/cognitive-complexity": ["error", 10],
|
|
440
531
|
// `x as unknown as T` tells the checker to look away: two spellings of one port were once
|
|
441
532
|
// reconciled that way (#76). A real conversion is a function; a real subset needs no cast.
|
|
442
533
|
"no-restricted-syntax": ["error", AS_UNKNOWN_AS],
|
|
@@ -448,8 +539,15 @@ export default [
|
|
|
448
539
|
"max-lines-per-function": ["error", MAX_LINES],
|
|
449
540
|
// Three levels of callbacks is the ceiling before a promise chain or a named step is due.
|
|
450
541
|
"max-nested-callbacks": ["error", 3],
|
|
542
|
+
// A `switch` over a union names every member (or says `default:` on purpose): a state added
|
|
543
|
+
// to the type is then a compile-time finding at every place that must handle it.
|
|
544
|
+
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
|
451
545
|
},
|
|
452
546
|
},
|
|
547
|
+
{
|
|
548
|
+
files: TEST_FILES,
|
|
549
|
+
rules: { "max-lines-per-function": ["error", MAX_LINES_TESTS] },
|
|
550
|
+
},
|
|
453
551
|
// Hexagonal layers, both axes (src/CLAUDE.md): knowledge by element, purity by file category.
|
|
454
552
|
IO_GLOBALS,
|
|
455
553
|
layerBoundaries(dirname(fileURLToPath(import.meta.url))),
|
|
@@ -601,6 +699,48 @@ export default [
|
|
|
601
699
|
// one whose firing path nothing exercises, which is rule 4 wearing a different hat. The
|
|
602
700
|
// pass/fail signal lives in `npm test`, not in the warning count of `npm run lint`.
|
|
603
701
|
},
|
|
702
|
+
{
|
|
703
|
+
files: ["**/*.{ts,mts,mjs,js,cjs}"],
|
|
704
|
+
rules: {
|
|
705
|
+
"no-restricted-imports": ["error", { paths: ONE_COLLECTION_LIBRARY }],
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
// 🔴 THE LINTER DOES NOT READ A SKILL. Skills are consumers of the package, not its storage: the
|
|
709
|
+
// venue presets once lived in a skill and were located through a skill's script, so renaming the
|
|
710
|
+
// skill broke `paperlint lint` (#133). The package's own code — src/, eslint-rules/, lib/ — may
|
|
711
|
+
// not import from skills/. Tests and harnesses may: they check the skills.
|
|
712
|
+
{
|
|
713
|
+
files: [
|
|
714
|
+
"src/**/*.{ts,mts,mjs,js}",
|
|
715
|
+
"eslint-rules/**/*.{ts,mts,mjs,js}",
|
|
716
|
+
"lib/**/*.{ts,mts,mjs,js}",
|
|
717
|
+
],
|
|
718
|
+
ignores: ["**/*.test.*", "**/*.harness.*"],
|
|
719
|
+
rules: {
|
|
720
|
+
"no-restricted-imports": [
|
|
721
|
+
"error",
|
|
722
|
+
{ paths: ONE_COLLECTION_LIBRARY, patterns: NO_SKILLS },
|
|
723
|
+
],
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
// 🔴 NEW CODE IS FUNCTIONAL: no `let`, no mutation, no loops, readonly types. Old code is listed
|
|
727
|
+
// in eslint-suppressions.json (ESLint's own bulk suppressions), which may only shrink: a fixed
|
|
728
|
+
// site that is not pruned fails `npm run lint`, and #134 burns the list down. Tests may mutate
|
|
729
|
+
// their fixtures.
|
|
730
|
+
{
|
|
731
|
+
files: ["src/**/*.ts", "eslint-rules/**/*.ts", "lib/**/*.ts"],
|
|
732
|
+
ignores: ["**/*.test.*", "**/*.harness.*"],
|
|
733
|
+
plugins: { functional },
|
|
734
|
+
rules: {
|
|
735
|
+
"functional/no-let": "error",
|
|
736
|
+
"functional/immutable-data": "error",
|
|
737
|
+
"functional/no-loop-statements": "error",
|
|
738
|
+
"functional/prefer-immutable-types": [
|
|
739
|
+
"error",
|
|
740
|
+
{ enforcement: "ReadonlyShallow", ignoreInferredTypes: true },
|
|
741
|
+
],
|
|
742
|
+
},
|
|
743
|
+
},
|
|
604
744
|
// Rule sources, whatever their extension; tests and harnesses plant git calls on purpose.
|
|
605
745
|
{
|
|
606
746
|
files: ["eslint-rules/*.ts", "eslint-rules/*.mjs"],
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "acmart in its own fonts: the class silently falls back to Computer Modern when one of its font packages is missing, and that is what this fixture exists to catch. Only the rules whose answer does not depend on banal's page classification are named: the paper is one sentence long, banal calls such a page a one-column cover, and pdf/geometry rightly reports that — so neither it nor the exit code is part of the claim.",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "pdf"
|
|
5
|
+
},
|
|
6
|
+
"fonts": {
|
|
7
|
+
"all": "^(LinLibertine|LinBiolinum)",
|
|
8
|
+
"none": "^(CMR|CMBX|CMTI|CMTT|CMSS|LMRoman)",
|
|
9
|
+
"matchLog": true
|
|
10
|
+
},
|
|
11
|
+
"lint": {
|
|
12
|
+
"paperlintJson": {
|
|
13
|
+
"extends": "paperlint:agenticdev",
|
|
14
|
+
"kind": "short",
|
|
15
|
+
"rules": {
|
|
16
|
+
"pdf/last-page-balance": "error"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"rules": {
|
|
20
|
+
"pdf/last-page-balance": [],
|
|
21
|
+
"pdf/profile": [],
|
|
22
|
+
"pdf/fresh": [],
|
|
23
|
+
"pdf/fonts": []
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "A real IEEEtran [conference,compsoc] paper for AIDC, built on paperlint's TeX Live: IEEE's Times, and not one finding from any rule — which is what judges pdf/limits, pdf/body-size and the rest against a real PDF of the class.",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "pdf"
|
|
5
|
+
},
|
|
6
|
+
"fonts": {
|
|
7
|
+
"some": "NimbusRomNo9L",
|
|
8
|
+
"none": "^(CMR|CMBX|CMTI|CMTT|CMSS|LMRoman)"
|
|
9
|
+
},
|
|
10
|
+
"lint": {
|
|
11
|
+
"exit": 0,
|
|
12
|
+
"findings": []
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
% A small real paper in the class AIDC requires, built by the e2e on paperlint's own TeX Live, so
|
|
2
|
+
% `paperlint toolchain` is proved to install what the ieee-conference preset declares, and every
|
|
3
|
+
% rule is judged against what a real build of the class measures. It conforms: the LLM Usage
|
|
4
|
+
% Statement closes it, and not one rule has anything to say.
|
|
5
|
+
\documentclass[conference,compsoc]{IEEEtran}
|
|
6
|
+
\begin{filecontents*}{refs.bib}
|
|
7
|
+
@article{knuth84,
|
|
8
|
+
title = {Literate Programming},
|
|
9
|
+
author = {Knuth, Donald E.},
|
|
10
|
+
journal = {The Computer Journal},
|
|
11
|
+
volume = {27},
|
|
12
|
+
number = {2},
|
|
13
|
+
pages = {97--111},
|
|
14
|
+
year = {1984},
|
|
15
|
+
doi = {10.1093/comjnl/27.2.97}
|
|
16
|
+
}
|
|
17
|
+
\end{filecontents*}
|
|
18
|
+
% banal calls a page with a few lines on it a one-column "cover", and pdf/geometry would rightly
|
|
19
|
+
% report that. So the body fills two columns of a page: one paragraph, set several times.
|
|
20
|
+
\newcommand{\filler}{This paragraph is ordinary prose, set in the two columns of the conference
|
|
21
|
+
format so that the page geometry is measured on a real body of text. It says nothing new, and it
|
|
22
|
+
does not need to: the fixture is about the class, the fonts and the page, not about its content.
|
|
23
|
+
A reviewer would find it dull, which is exactly what a measurement fixture should be.\par}
|
|
24
|
+
\begin{document}
|
|
25
|
+
\title{A Fixture Typeset in the Class the Venue Requires}
|
|
26
|
+
\author{\IEEEauthorblockN{A. Author}
|
|
27
|
+
\IEEEauthorblockA{Anonymous for double-blind review}}
|
|
28
|
+
\maketitle
|
|
29
|
+
\begin{abstract}
|
|
30
|
+
The abstract exists so the build has the front matter an IEEE paper carries.
|
|
31
|
+
\end{abstract}
|
|
32
|
+
\section{Introduction}
|
|
33
|
+
This paper has a body of ordinary prose, set in the two columns of the conference format, and
|
|
34
|
+
it cites one article~\cite{knuth84}.
|
|
35
|
+
\filler\filler\filler\filler
|
|
36
|
+
\section{Method}
|
|
37
|
+
The method is to build this file with pdflatex and bibtex, then read the facts of the PDF.
|
|
38
|
+
\filler\filler\filler\filler\filler\filler
|
|
39
|
+
\section{Conclusion}
|
|
40
|
+
The conclusion restates that the fixture builds.
|
|
41
|
+
\filler\filler\filler\filler
|
|
42
|
+
\bibliographystyle{IEEEtran}
|
|
43
|
+
\bibliography{refs}
|
|
44
|
+
\section*{LLM Usage Statement}
|
|
45
|
+
No LLMs were used in writing this fixture.
|
|
46
|
+
\end{document}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "paperlint:aidc", "kind": "short" }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Does not compile: an undefined control sequence on line 6. The build names pdflatex and quotes the log.",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "failed"
|
|
5
|
+
},
|
|
6
|
+
"says": [
|
|
7
|
+
"✗ compile: pdflatex exited with 1",
|
|
8
|
+
"./paper.tex:6: Undefined control sequence.",
|
|
9
|
+
"l.6 Text before, then \\undefinedmacro"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "The bibtex path: the citation and the \\ref resolve, and the leftover build.sh is ignored, not run.",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "pdf",
|
|
5
|
+
"pages": 1
|
|
6
|
+
},
|
|
7
|
+
"says": [
|
|
8
|
+
"build.sh is ignored — paperlint builds the paper itself"
|
|
9
|
+
],
|
|
10
|
+
"text": {
|
|
11
|
+
"contains": [
|
|
12
|
+
"[1]"
|
|
13
|
+
],
|
|
14
|
+
"lacks": [
|
|
15
|
+
"[?]",
|
|
16
|
+
"??"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"files": {
|
|
20
|
+
"exist": [
|
|
21
|
+
"paper.bbl"
|
|
22
|
+
],
|
|
23
|
+
"absent": [
|
|
24
|
+
"RAN"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "No pages: pdflatex exits 0 and writes no PDF, which is a failure, not a green paper.pdf.",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "failed"
|
|
5
|
+
},
|
|
6
|
+
"says": [
|
|
7
|
+
"✗ compile: pdflatex exited 0 but wrote no paper.pdf — does the document have any pages?"
|
|
8
|
+
],
|
|
9
|
+
"saysNot": [
|
|
10
|
+
"✓"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Computer Modern standing in for acmart's silent fallback. It builds green; declared an agenticdev paper afterwards, the venue rules reject it.",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "pdf"
|
|
5
|
+
},
|
|
6
|
+
"fonts": {
|
|
7
|
+
"some": "^(CMR|CMBX|CMTI|CMTT|CMSS|LMRoman)",
|
|
8
|
+
"matchLog": true
|
|
9
|
+
},
|
|
10
|
+
"lint": {
|
|
11
|
+
"paperlintJson": {
|
|
12
|
+
"extends": "paperlint:agenticdev",
|
|
13
|
+
"kind": "short"
|
|
14
|
+
},
|
|
15
|
+
"exit": 1,
|
|
16
|
+
"rules": {
|
|
17
|
+
"pdf/fonts": [
|
|
18
|
+
{
|
|
19
|
+
"severity": 2,
|
|
20
|
+
"says": [
|
|
21
|
+
"LinLibertine"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"severity": 2,
|
|
26
|
+
"says": [
|
|
27
|
+
"LinBiolinum"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"pdf/geometry": [
|
|
32
|
+
{
|
|
33
|
+
"severity": 2,
|
|
34
|
+
"says": [
|
|
35
|
+
"page width is 8.264 in, agenticdev requires 8.5 in"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"severity": 2,
|
|
40
|
+
"says": [
|
|
41
|
+
"page height is 11.694 in, agenticdev requires 11 in"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"severity": 2,
|
|
46
|
+
"says": [
|
|
47
|
+
"column(s), agenticdev requires 2"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "No paper.tex: a refusal, named separately, and a stale paper.pdf is removed.",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "failed"
|
|
5
|
+
},
|
|
6
|
+
"says": [
|
|
7
|
+
"✗ nothing to compile: no paper.tex",
|
|
8
|
+
"paper.pdf removed — a stale PDF must not pass for this build"
|
|
9
|
+
]
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "A two-column acmart paper whose last page ends unbalanced. The build measures it without judging; the optional lint rule reports it.",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "pdf"
|
|
5
|
+
},
|
|
6
|
+
"says": [
|
|
7
|
+
"✓ paper.pdf",
|
|
8
|
+
"facts: _build/paper.facts.json, last page 621.5 / 264.8 pt"
|
|
9
|
+
],
|
|
10
|
+
"saysNot": [
|
|
11
|
+
" balance:"
|
|
12
|
+
],
|
|
13
|
+
"files": {
|
|
14
|
+
"lacks": {
|
|
15
|
+
"paper.bbl": [
|
|
16
|
+
"\\balance"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"facts": {
|
|
21
|
+
"schema": 2,
|
|
22
|
+
"lastPageColumnsPt": [
|
|
23
|
+
621.5,
|
|
24
|
+
264.8
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"lint": {
|
|
28
|
+
"paperlintJson": {
|
|
29
|
+
"rules": {
|
|
30
|
+
"pdf/last-page-balance": "error"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"exit": 1,
|
|
34
|
+
"rules": {
|
|
35
|
+
"pdf/last-page-balance": [
|
|
36
|
+
{
|
|
37
|
+
"severity": 2,
|
|
38
|
+
"says": [
|
|
39
|
+
"621.5 and 264.8 pt",
|
|
40
|
+
"pbalance"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"pdf/fresh": [],
|
|
45
|
+
"pdf/profile": [],
|
|
46
|
+
"pdf/fonts": [],
|
|
47
|
+
"pdf/geometry": [],
|
|
48
|
+
"pdf/limits": [],
|
|
49
|
+
"pdf/body-size": [],
|
|
50
|
+
"pdf/measured": [
|
|
51
|
+
{
|
|
52
|
+
"severity": 1,
|
|
53
|
+
"says": [
|
|
54
|
+
"names no venue preset yet"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -13,7 +13,7 @@ poppler's numbers, so a pass means agreement with the tool pdf.js replaced.
|
|
|
13
13
|
| `encrypted.pdf` | `t3-all.pdf` saved with an AES-256 user password (PyMuPDF) | pdf.js refuses it with `PasswordException`; the reader reports `encrypted` |
|
|
14
14
|
| `hidden-text.pdf` | `hidden-text.tex`, pdflatex | two-column body text beside near-white, rotated and render-mode-3 text; page 2 holds only hidden text, so banal calls it blank |
|
|
15
15
|
|
|
16
|
-
`test/e2e/banal.ts` also reads these files, with the real banal: it pins what banal 1.2 printed for
|
|
16
|
+
`test/e2e/tex/banal.e2e.ts` also reads these files, with the real banal: it pins what banal 1.2 printed for
|
|
17
17
|
each one on poppler's `pdftohtml` 24.02.0 (2026-09-25), so a pass there means paperlint's pdf.js-written
|
|
18
18
|
XML measures the same as poppler's. `hidden-text.pdf` exists for that test: each kind of hidden text
|
|
19
19
|
changes banal's answer if it is counted.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ONE reader of `baseline.json`, shared by the two runs that compare against it: the harness
|
|
3
|
-
* beside this file (the repository's own `bin/paperlint.mjs`) and `test/e2e/install.ts` (the binary
|
|
3
|
+
* beside this file (the repository's own `bin/paperlint.mjs`) and `test/e2e/install/install.e2e.ts` (the binary
|
|
4
4
|
* a consumer actually got). Two copies of "growth fails, a drop never does" would drift the first
|
|
5
5
|
* time one of them is tightened, and the two runs would then disagree about the same article.
|
|
6
6
|
*/
|
|
@@ -5,18 +5,16 @@
|
|
|
5
5
|
# A paper's own build script must not hard-code a path into the agent's skills directory:
|
|
6
6
|
# once the skills move into a package that address points at nothing, and LaTeX's reaction to
|
|
7
7
|
# a missing \input is SILENCE, not an error. So the script resolves the venues directory
|
|
8
|
-
# through the package
|
|
8
|
+
# through the installed package, and then VERIFIES that kpsewhich
|
|
9
9
|
# can actually see the file before handing anything to pdflatex.
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
ROOT=$(git rev-parse --show-toplevel)
|
|
12
12
|
|
|
13
|
-
VENUES_PKG=$(node -e 'try{process.stdout.write(require.resolve("paperlint/
|
|
13
|
+
VENUES_PKG=$(cd "$ROOT" && node -e 'try{process.stdout.write(require.resolve("paperlint/presets/tex/paper-guards.tex"))}catch{}' 2>/dev/null || true)
|
|
14
14
|
if [ -n "$VENUES_PKG" ] && [ -f "$VENUES_PKG" ]; then
|
|
15
15
|
VENUES_DIR=$(dirname "$VENUES_PKG")
|
|
16
|
-
elif [ -f "$ROOT/.claude/skills/submit-paper/references/venues/paper-guards.tex" ]; then
|
|
17
|
-
VENUES_DIR="$ROOT/.claude/skills/submit-paper/references/venues"
|
|
18
16
|
else
|
|
19
|
-
echo "✗ paper-guards.tex found
|
|
17
|
+
echo "✗ paper-guards.tex not found in the installed paperlint package" >&2
|
|
20
18
|
exit 1
|
|
21
19
|
fi
|
|
22
20
|
export TEXINPUTS="$VENUES_DIR:"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperlint",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,21 +8,26 @@
|
|
|
8
8
|
"build": "tsc -p tsconfig.pkg.json && tsc",
|
|
9
9
|
"prepare": "tsc -p tsconfig.pkg.json && tsc",
|
|
10
10
|
"test": "node scripts/test.ts",
|
|
11
|
+
"test:e2e": "vitest run --project 'e2e-*'",
|
|
12
|
+
"test:e2e:install": "vitest run --project e2e-install",
|
|
13
|
+
"test:e2e:tex": "vitest run --project e2e-tex",
|
|
11
14
|
"coverage": "NODE_OPTIONS=\"--import=./test/coverage-src.ts $NODE_OPTIONS\" c8 --check-coverage npm test --",
|
|
12
15
|
"lint": "eslint .",
|
|
13
16
|
"check": "node scripts/check.ts",
|
|
14
|
-
"test:e2e": "node test/e2e/install.ts && node test/e2e/build.ts",
|
|
15
17
|
"fmt": "prettier --write .",
|
|
16
18
|
"fmt:check": "prettier --check ."
|
|
17
19
|
},
|
|
18
20
|
"dependencies": {
|
|
19
21
|
"@eslint/markdown": "^8.0.3",
|
|
20
22
|
"@eslint/plugin-kit": "^0.7.2",
|
|
23
|
+
"@unified-latex/unified-latex-types": "^1.8.4",
|
|
21
24
|
"@unified-latex/unified-latex-util-parse": "^1.8.4",
|
|
25
|
+
"@unified-latex/unified-latex-util-visit": "^1.8.4",
|
|
22
26
|
"ajv": "^6.12.6",
|
|
23
27
|
"eslint": "^10.9.1",
|
|
24
28
|
"js-yaml": "^4.1.0",
|
|
25
29
|
"markdown-it": "^14.3.0",
|
|
30
|
+
"remeda": "^2.50.0",
|
|
26
31
|
"ts-essentials": "^10.2.1",
|
|
27
32
|
"typescript": "^6.0.3",
|
|
28
33
|
"unpdf": "^1.8.1",
|
|
@@ -40,7 +45,9 @@
|
|
|
40
45
|
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
41
46
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
42
47
|
"eslint-plugin-boundaries": "^7.2.0",
|
|
48
|
+
"eslint-plugin-functional": "^10.0.1",
|
|
43
49
|
"eslint-plugin-n": "^18.3.0",
|
|
50
|
+
"eslint-plugin-sonarjs": "^4.2.1",
|
|
44
51
|
"prettier": "^3.8.1",
|
|
45
52
|
"semantic-release": "25.0.9",
|
|
46
53
|
"typescript-eslint": "^8.70.1",
|
|
@@ -107,6 +114,7 @@
|
|
|
107
114
|
"fixtures/real-markdown-paper/baseline.*",
|
|
108
115
|
"hooks",
|
|
109
116
|
"lib",
|
|
117
|
+
"presets",
|
|
110
118
|
"plugin",
|
|
111
119
|
"scripts",
|
|
112
120
|
"skills",
|
|
@@ -126,5 +134,8 @@
|
|
|
126
134
|
"types": "./dist/eslint-rules/*.d.ts",
|
|
127
135
|
"default": "./dist/eslint-rules/*.js"
|
|
128
136
|
}
|
|
137
|
+
},
|
|
138
|
+
"overrides": {
|
|
139
|
+
"deepmerge-ts": "^8.0.2"
|
|
129
140
|
}
|
|
130
141
|
}
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
// requirements. A paper may extend this family directly for an ACM venue nobody has profiled:
|
|
8
8
|
// page size, columns and fonts are checked; with no kinds, the page limit is not.
|
|
9
9
|
{
|
|
10
|
-
|
|
10
|
+
// `tex/template` requires a paper's class to be acmart WITH `sigconf`: acmart's default format is
|
|
11
|
+
// `manuscript`, a single-column layout no sigconf venue takes. Other options (review, anonymous,
|
|
12
|
+
// screen, nonacm) are the paper's to add.
|
|
13
|
+
"template": "\\documentclass[sigconf]{acmart}",
|
|
11
14
|
|
|
12
15
|
// TeX Live packages the acmart template needs ON TOP OF tex-base.jsonc, each with the files that
|
|
13
16
|
// prove it is present (checked with kpsewhich after every install). Read by `paperlint toolchain` and
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
{
|
|
7
7
|
"extends": "paperlint:acm-sigconf",
|
|
8
8
|
|
|
9
|
+
// What the venue is called in a paper's text (tex/venue-leftover). Its parent conference, ASE,
|
|
10
|
+
// is left out: a three-letter acronym that many sentences use for other things.
|
|
11
|
+
"aliases": ["AgenticDev"],
|
|
12
|
+
|
|
9
13
|
"format": {
|
|
10
14
|
// The limits are a property of the paper's TYPE at this venue, not of the venue itself.
|
|
11
15
|
// A given paper's type is stated in its <paper-dir>/paperlint.json (`kind`).
|