paperlint 2.1.0 → 3.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/CLAUDE.md +6 -1
- package/README.md +243 -235
- 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-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +60 -7
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +94 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +500 -288
- 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/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.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 +144 -173
- 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 +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +49 -25
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +85 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +96 -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/toolchain.d.ts +5 -1
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +22 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +26 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +163 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +65 -24
- package/docs/skills.md +80 -0
- 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 +306 -191
- package/eslint-rules/paper-typography.test.mjs +337 -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/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- 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 +5 -14
- 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-engine.ts +6 -1
- package/src/build.harness.mjs +54 -6
- package/src/build.ts +89 -11
- package/src/cli.harness.mjs +149 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +637 -328
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/engine.harness.mjs +50 -0
- package/src/engine.ts +35 -0
- 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 +187 -231
- 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/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +2 -0
- package/src/new-paper.mutations.mjs +0 -14
- package/src/new-paper.test.ts +173 -14
- package/src/new-paper.ts +82 -30
- package/src/paper-files.test.ts +395 -0
- package/src/paper-files.ts +200 -0
- package/src/paper-settings-commands.test.ts +121 -122
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/parse-args.test.ts +48 -0
- 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/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +4 -2
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/toolchain.harness.mjs +41 -6
- package/src/toolchain.ts +29 -5
- package/src/types.ts +36 -18
- package/src/venue-rules.test.ts +23 -11
- package/src/venue-rules.ts +12 -14
- package/templates/paper/paperlint.json +1 -1
- 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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<paper>/paperlint.json` — the per-paper settings file
|
|
3
|
-
*
|
|
4
|
-
* block for that paper alone.
|
|
2
|
+
* `<paper>/paperlint.json` — the per-paper settings file: parsed strictly, merged over the root
|
|
3
|
+
* `paperlint.json`'s defaults, and `rules` turned into an ESLint block for that paper alone.
|
|
5
4
|
*/
|
|
6
5
|
import { describe, expect, it } from "vitest";
|
|
7
6
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
7
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
8
|
+
import { findProjectRoot } from "../lib/paper-config.mjs";
|
|
8
9
|
import {
|
|
9
|
-
migrationOf,
|
|
10
10
|
paperRules,
|
|
11
11
|
parsePaperSettings,
|
|
12
12
|
readPaperSettings,
|
|
@@ -16,9 +16,8 @@ const PAPER = "/work/papers/p";
|
|
|
16
16
|
const SHIPPED = new Set([
|
|
17
17
|
"pdf/last-page-balance",
|
|
18
18
|
"pdf/profile",
|
|
19
|
-
"paper/
|
|
19
|
+
"paper/section-word",
|
|
20
20
|
]);
|
|
21
|
-
const enc = (s: string) => new TextEncoder().encode(s);
|
|
22
21
|
|
|
23
22
|
describe("parsePaperSettings", () => {
|
|
24
23
|
it("reads extends, kind, pdf and rules; every absent field is null", () => {
|
|
@@ -58,10 +57,10 @@ describe("parsePaperSettings — optional keys", () => {
|
|
|
58
57
|
[
|
|
59
58
|
"an unknown key (a typo is not silent)",
|
|
60
59
|
{ venu: "aisec" },
|
|
61
|
-
/unknown key "venu"
|
|
60
|
+
/unknown key "venu" — known keys: papersDir, structure, rules, extends, kind, pdf/,
|
|
62
61
|
],
|
|
63
62
|
[
|
|
64
|
-
"
|
|
63
|
+
"a comment key other than `$comment`",
|
|
65
64
|
{ extends: "paperlint:aisec", _: "note" },
|
|
66
65
|
/unknown key "_"/,
|
|
67
66
|
],
|
|
@@ -76,10 +75,15 @@ describe("parsePaperSettings — optional keys", () => {
|
|
|
76
75
|
/"extends" must be a non-empty string/,
|
|
77
76
|
],
|
|
78
77
|
[
|
|
79
|
-
"rules that are
|
|
80
|
-
{ rules:
|
|
78
|
+
"rules that are neither an object nor a list of blocks",
|
|
79
|
+
{ rules: "pdf/profile" },
|
|
81
80
|
/"rules" must be an object/,
|
|
82
81
|
],
|
|
82
|
+
[
|
|
83
|
+
"🔴 papersDir — a project setting, refused in a paper's file",
|
|
84
|
+
{ papersDir: "papers" },
|
|
85
|
+
/"papersDir" is a project setting — set it in the root paperlint\.json/,
|
|
86
|
+
],
|
|
83
87
|
["not an object at all", ["aisec"], /must be a JSON object/],
|
|
84
88
|
])("refuses %s", (_, json, why) => {
|
|
85
89
|
const r = parsePaperSettings(json);
|
|
@@ -104,21 +108,6 @@ describe("readPaperSettings", () => {
|
|
|
104
108
|
expect(r.ok && r.value?.extends).toBe("paperlint:aisec");
|
|
105
109
|
});
|
|
106
110
|
|
|
107
|
-
it("🔴 a venue.json alone is NOT read — it is named, with the command that moves it", () => {
|
|
108
|
-
const files = memoryFiles({ [`${PAPER}/venue.json`]: '{"venue":"aisec"}' });
|
|
109
|
-
const r = readPaperSettings(files, PAPER);
|
|
110
|
-
expect(r).toEqual({ ok: false, error: { kind: "legacy" } });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("paperlint.json wins when both exist — the leftover is doctor's to report", () => {
|
|
114
|
-
const files = memoryFiles({
|
|
115
|
-
[`${PAPER}/venue.json`]: '{"venue":"realm"}',
|
|
116
|
-
[`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
|
|
117
|
-
});
|
|
118
|
-
const r = readPaperSettings(files, PAPER);
|
|
119
|
-
expect(r.ok && r.value?.extends).toBe("paperlint:aisec");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
111
|
it("not JSON: broken, with the parser's reason", () => {
|
|
123
112
|
const files = memoryFiles({
|
|
124
113
|
[`${PAPER}/paperlint.json`]: "{ extends: aisec",
|
|
@@ -129,52 +118,92 @@ describe("readPaperSettings", () => {
|
|
|
129
118
|
});
|
|
130
119
|
});
|
|
131
120
|
|
|
132
|
-
describe("
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
);
|
|
121
|
+
describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
|
|
122
|
+
const ROOT = "/work";
|
|
123
|
+
const withFiles = (f: Record<string, string>) =>
|
|
124
|
+
memoryFiles({ [`${ROOT}/package.json`]: "{}", ...f });
|
|
125
|
+
const read = (f: Record<string, string>) => {
|
|
126
|
+
const r = readPaperSettings(withFiles(f), PAPER);
|
|
127
|
+
if (!r.ok) throw new Error(r.error.why);
|
|
128
|
+
return r.value;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
it("no file at either level: null", () => {
|
|
132
|
+
expect(read({})).toBeNull();
|
|
133
|
+
});
|
|
138
134
|
|
|
139
|
-
it
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
'{ "extends": "paperlint:a", "kind": "short" }\n',
|
|
152
|
-
"drop-legacy",
|
|
153
|
-
],
|
|
154
|
-
[
|
|
155
|
-
"both, different",
|
|
156
|
-
'{"venue":"a"}',
|
|
157
|
-
'{"extends":"paperlint:b"}',
|
|
158
|
-
"conflict",
|
|
159
|
-
],
|
|
160
|
-
["venue.json that is not JSON", "{", null, "broken"],
|
|
161
|
-
["venue.json that is not an object", "[]", null, "broken"],
|
|
162
|
-
])("%s → %s", (_, legacy, current, want) => {
|
|
163
|
-
expect(plan(legacy, current).kind).toBe(want);
|
|
135
|
+
it("root only: its extends and kind are this paper's", () => {
|
|
136
|
+
expect(
|
|
137
|
+
read({
|
|
138
|
+
[`${ROOT}/paperlint.json`]:
|
|
139
|
+
'{"papersDir":"papers","extends":"paperlint:agenticdev","kind":"short"}',
|
|
140
|
+
}),
|
|
141
|
+
).toEqual({
|
|
142
|
+
extends: "paperlint:agenticdev",
|
|
143
|
+
kind: "short",
|
|
144
|
+
pdf: null,
|
|
145
|
+
rules: null,
|
|
146
|
+
});
|
|
164
147
|
});
|
|
165
148
|
|
|
166
|
-
it(
|
|
167
|
-
|
|
168
|
-
'{"
|
|
169
|
-
|
|
170
|
-
);
|
|
171
|
-
expect(r.kind === "move" && JSON.parse(r.text)).toEqual({
|
|
149
|
+
it("paper only: its own values", () => {
|
|
150
|
+
expect(
|
|
151
|
+
read({ [`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}' }),
|
|
152
|
+
).toEqual({
|
|
172
153
|
extends: "paperlint:aisec",
|
|
154
|
+
kind: null,
|
|
155
|
+
pdf: null,
|
|
156
|
+
rules: null,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("🔴 both: the paper's value wins, an absent one falls back to the root's, and `\"extends\": null` is absent", () => {
|
|
161
|
+
expect(
|
|
162
|
+
read({
|
|
163
|
+
[`${ROOT}/paperlint.json`]:
|
|
164
|
+
'{"extends":"paperlint:agenticdev","kind":"short","rules":{"pdf/profile":"off"}}',
|
|
165
|
+
[`${PAPER}/paperlint.json`]:
|
|
166
|
+
'{"extends":null,"kind":"research","rules":{"pdf/fonts":"off"}}',
|
|
167
|
+
}),
|
|
168
|
+
).toEqual({
|
|
169
|
+
extends: "paperlint:agenticdev",
|
|
173
170
|
kind: "research",
|
|
174
|
-
pdf:
|
|
175
|
-
|
|
171
|
+
pdf: null,
|
|
172
|
+
// The root's rules are the project's blocks (cli.ts), not this paper's.
|
|
173
|
+
rules: { "pdf/fonts": "off" },
|
|
176
174
|
});
|
|
177
175
|
});
|
|
176
|
+
|
|
177
|
+
it("papersDir in a paper's file: broken, naming the root file as its place", () => {
|
|
178
|
+
const r = readPaperSettings(
|
|
179
|
+
withFiles({ [`${PAPER}/paperlint.json`]: '{"papersDir":"x"}' }),
|
|
180
|
+
PAPER,
|
|
181
|
+
);
|
|
182
|
+
expect(r.ok).toBe(false);
|
|
183
|
+
if (!r.ok) expect(r.error.why).toMatch(/project setting/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("findProjectRoot — where the root paperlint.json is looked for", () => {
|
|
188
|
+
const ROOT = "/work";
|
|
189
|
+
const withFiles = (f: Record<string, string>) =>
|
|
190
|
+
memoryFiles({ [`${ROOT}/package.json`]: "{}", ...f });
|
|
191
|
+
|
|
192
|
+
it("🔴 from INSIDE a paper, the project root is not the paper: its paperlint.json sits beside paper.tex", () => {
|
|
193
|
+
const files = withFiles({
|
|
194
|
+
[`${PAPER}/paper.tex`]: "x",
|
|
195
|
+
[`${PAPER}/paperlint.json`]: '{"kind":"research"}',
|
|
196
|
+
[`${ROOT}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
|
|
197
|
+
});
|
|
198
|
+
const isFile = (p: string) => files.isFile(p as AbsolutePath);
|
|
199
|
+
expect(findProjectRoot(PAPER, isFile)).toBe(ROOT);
|
|
200
|
+
// No root file anywhere: the package.json directory.
|
|
201
|
+
expect(findProjectRoot(PAPER, (p) => p === `${ROOT}/package.json`)).toBe(
|
|
202
|
+
ROOT,
|
|
203
|
+
);
|
|
204
|
+
// Neither: where the walk started.
|
|
205
|
+
expect(findProjectRoot(PAPER, () => false)).toBe(PAPER);
|
|
206
|
+
});
|
|
178
207
|
});
|
|
179
208
|
|
|
180
209
|
describe("paperRules — `rules` in paperlint.json", () => {
|
package/src/paper-settings.ts
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<paper>/paperlint.json` — ONE PAPER'S SETTINGS, parsed at the boundary.
|
|
2
|
+
* `<paper>/paperlint.json` — ONE PAPER'S SETTINGS, parsed at the boundary, merged over the root's.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* One file name at two levels, one schema (lib/paper-config.mjs → SETTINGS_KEYS):
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* paperlint.json the project (optional): papersDir, rules, defaults
|
|
7
7
|
* <paper>/paperlint.json this paper: { extends, kind, pdf, rules }
|
|
8
8
|
* a venue preset `paperlint:<name>` (shipped) or `./x.jsonc` (src/presets.ts)
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The paper's `extends`, `kind` and `pdf` win over the root's; with no own value, the root's is
|
|
11
|
+
* the default. Every reader — the build, the facts writer, the venue rules, `paperlint lint` —
|
|
12
|
+
* goes through `readPaperSettings`, so there is one answer to "what does this paper declare".
|
|
12
13
|
*
|
|
13
14
|
* 🔴 STRICT. An unknown key is an error naming the known ones: `"venu": "aisec"` would otherwise
|
|
14
|
-
* read as "no preset", and every venue check would be silently off.
|
|
15
|
-
*
|
|
16
|
-
* 🔴 THE OLD NAME IS NOT READ. Before 2.1.0 the file was `venue.json`. A fallback would keep it
|
|
17
|
-
* working forever and leave two names for one file; instead a `venue.json` with no `paperlint.json`
|
|
18
|
-
* is an error that names `npx paperlint init`, which moves it (`migrationOf` plans that move).
|
|
15
|
+
* read as "no preset", and every venue check would be silently off. A project-only key
|
|
16
|
+
* (`papersDir`, …) in a paper's file is refused too: it would be read nowhere.
|
|
19
17
|
*/
|
|
20
|
-
import { join } from "node:path";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
21
19
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
CONFIG_FILE,
|
|
21
|
+
ROOT_ONLY_KEYS,
|
|
22
|
+
SETTINGS_KEYS,
|
|
23
|
+
findProjectRoot,
|
|
25
24
|
} from "../lib/paper-config.mjs";
|
|
26
25
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
27
26
|
import { err, ok, type Result } from "./domain/result.ts";
|
|
@@ -40,16 +39,17 @@ export interface PaperSettings {
|
|
|
40
39
|
readonly kind: string | null;
|
|
41
40
|
/** The built PDF relative to the paper, when it is not `paper.pdf`. */
|
|
42
41
|
readonly pdf: string | null;
|
|
43
|
-
/**
|
|
44
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Rule overrides for this paper alone — `{ id: severity }` or ESLint blocks — NOT yet checked
|
|
44
|
+
* against the shipped rules.
|
|
45
|
+
*/
|
|
46
|
+
readonly rules: Readonly<Record<string, unknown>> | readonly unknown[] | null;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
/** Why a paper's settings cannot be read. */
|
|
48
|
-
export type SettingsProblem =
|
|
49
|
-
| { readonly kind: "broken"; readonly why: string }
|
|
50
|
-
| { readonly kind: "legacy" };
|
|
50
|
+
export type SettingsProblem = { readonly kind: "broken"; readonly why: string };
|
|
51
51
|
|
|
52
|
-
const KNOWN = Object.keys(
|
|
52
|
+
const KNOWN = Object.keys(SETTINGS_KEYS);
|
|
53
53
|
const STRING_FIELDS = ["extends", "kind", "pdf"] as const;
|
|
54
54
|
|
|
55
55
|
const isObject = (v: unknown): v is Readonly<Record<string, unknown>> =>
|
|
@@ -68,8 +68,8 @@ function stringField(
|
|
|
68
68
|
: err(`"${k}" must be a non-empty string, got ${JSON.stringify(v)}`);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/** `extends`, `kind` and `pdf`, each null when absent. */
|
|
72
|
-
function stringFields(
|
|
71
|
+
/** `extends`, `kind` and `pdf`, each null when absent. The root file's defaults parse the same way. */
|
|
72
|
+
export function stringFields(
|
|
73
73
|
d: Readonly<Record<string, unknown>>,
|
|
74
74
|
): Result<Pick<PaperSettings, (typeof STRING_FIELDS)[number]>, string> {
|
|
75
75
|
const out: Record<string, string | null> = {};
|
|
@@ -93,11 +93,18 @@ export function parsePaperSettings(
|
|
|
93
93
|
const unknown = Object.keys(json).filter((k) => !KNOWN.includes(k));
|
|
94
94
|
if (unknown.length > 0)
|
|
95
95
|
return err(`unknown key "${unknown[0]}" — known keys: ${KNOWN.join(", ")}`);
|
|
96
|
+
const project = Object.keys(json).filter((k) => ROOT_ONLY_KEYS.includes(k));
|
|
97
|
+
if (project.length > 0)
|
|
98
|
+
return err(
|
|
99
|
+
`"${project[0]}" is a project setting — set it in the root ${CONFIG_FILE}, not in a paper's`,
|
|
100
|
+
);
|
|
96
101
|
const fields = stringFields(json);
|
|
97
102
|
if (!fields.ok) return fields;
|
|
98
103
|
const rules = json["rules"];
|
|
99
|
-
if (rules !== undefined && !isObject(rules))
|
|
100
|
-
return err(
|
|
104
|
+
if (rules !== undefined && !isObject(rules) && !Array.isArray(rules))
|
|
105
|
+
return err(
|
|
106
|
+
`"rules" must be an object of rule id → severity, or a list of ESLint blocks`,
|
|
107
|
+
);
|
|
101
108
|
return ok({
|
|
102
109
|
...fields.value,
|
|
103
110
|
rules: rules ?? null,
|
|
@@ -106,101 +113,96 @@ export function parsePaperSettings(
|
|
|
106
113
|
|
|
107
114
|
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
108
115
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
111
|
-
* or only the pre-2.1.0 `venue.json` is there. Reads through `files` only; never throws.
|
|
112
|
-
*/
|
|
113
|
-
export function readPaperSettings(
|
|
116
|
+
/** A JSON file through `files`: undefined when absent, the problem when it does not parse. */
|
|
117
|
+
function readJson(
|
|
114
118
|
files: Files,
|
|
115
|
-
|
|
116
|
-
): Result<
|
|
117
|
-
const bytes = files.readBytes(at(
|
|
118
|
-
if (bytes === null)
|
|
119
|
-
return files.readBytes(at(join(paperDir, LEGACY_PAPER_SETTINGS_FILE))) ===
|
|
120
|
-
null
|
|
121
|
-
? ok(null)
|
|
122
|
-
: err({ kind: "legacy" });
|
|
123
|
-
let json: unknown;
|
|
119
|
+
path: string,
|
|
120
|
+
): Result<unknown, SettingsProblem> {
|
|
121
|
+
const bytes = files.readBytes(at(path));
|
|
122
|
+
if (bytes === null) return ok(undefined);
|
|
124
123
|
try {
|
|
125
|
-
|
|
124
|
+
return ok(JSON.parse(new TextDecoder().decode(bytes)));
|
|
126
125
|
} catch (e) {
|
|
127
126
|
return err({ kind: "broken", why: `not JSON (${(e as Error).message})` });
|
|
128
127
|
}
|
|
129
|
-
const parsed = parsePaperSettings(json);
|
|
130
|
-
return parsed.ok ? parsed : err({ kind: "broken", why: parsed.error });
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
// ── the move from venue.json ────────────────────────────────────────────────────────────
|
|
134
|
-
|
|
135
|
-
/** What `paperlint init` does with one paper's files. */
|
|
136
|
-
export type Migration =
|
|
137
|
-
/** No `venue.json`: nothing to do. */
|
|
138
|
-
| { readonly kind: "none" }
|
|
139
|
-
/** Only `venue.json`: write `text` as `paperlint.json`, delete `venue.json`. */
|
|
140
|
-
| { readonly kind: "move"; readonly text: string }
|
|
141
|
-
/** Both, and `paperlint.json` already says the same: the old one is a leftover, delete it. */
|
|
142
|
-
| { readonly kind: "drop-legacy" }
|
|
143
|
-
/** Both, and they differ: refuse — there is no way to know which one the author means. */
|
|
144
|
-
| { readonly kind: "conflict" }
|
|
145
|
-
/** `venue.json` is not a JSON object: refuse, naming why. */
|
|
146
|
-
| { readonly kind: "broken"; readonly why: string };
|
|
147
|
-
|
|
148
130
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* is carried as it is — an unknown one is then refused by name, as in any `paperlint.json`. Pure.
|
|
131
|
+
* The root file's defaults for every paper — `extends`, `kind`, `pdf` — found by walking up from
|
|
132
|
+
* the paper's parent. None when there is no root file, or it is not an object; a root file that
|
|
133
|
+
* is wrong in another way is reported by `paperlint lint`, which parses it strictly.
|
|
153
134
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
135
|
+
function rootDefaults(
|
|
136
|
+
files: Files,
|
|
137
|
+
paperDir: string,
|
|
138
|
+
): Result<Defaults | null, SettingsProblem> {
|
|
139
|
+
const root = findProjectRoot(dirname(paperDir), (p) => files.isFile(at(p)));
|
|
140
|
+
const json = readJson(files, join(root, CONFIG_FILE));
|
|
141
|
+
if (!json.ok) return json;
|
|
142
|
+
if (!isObject(json.value)) return ok(null);
|
|
143
|
+
const fields = stringFields(json.value);
|
|
144
|
+
return fields.ok
|
|
145
|
+
? fields
|
|
146
|
+
: err({ kind: "broken", why: `the root ${CONFIG_FILE}: ${fields.error}` });
|
|
165
147
|
}
|
|
166
148
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
149
|
+
/**
|
|
150
|
+
* A paper's settings, its own `paperlint.json` merged over the root's defaults: null when neither
|
|
151
|
+
* says anything; the problem when a file does not parse. Reads through `files` only; never throws.
|
|
152
|
+
*/
|
|
153
|
+
export function readPaperSettings(
|
|
154
|
+
files: Files,
|
|
155
|
+
paperDir: string,
|
|
156
|
+
): Result<PaperSettings | null, SettingsProblem> {
|
|
157
|
+
const own = readJson(files, join(paperDir, CONFIG_FILE));
|
|
158
|
+
if (!own.ok) return own;
|
|
159
|
+
const parsed =
|
|
160
|
+
own.value === undefined ? ok(null) : parsePaperSettings(own.value);
|
|
161
|
+
if (!parsed.ok) return err({ kind: "broken", why: parsed.error });
|
|
162
|
+
const defaults = rootDefaults(files, paperDir);
|
|
163
|
+
if (!defaults.ok) return defaults;
|
|
164
|
+
return ok(merge(defaults.value, parsed.value));
|
|
165
|
+
}
|
|
174
166
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
:
|
|
167
|
+
type Defaults = Pick<PaperSettings, "extends" | "kind" | "pdf">;
|
|
168
|
+
const NO_DEFAULTS: Defaults = { extends: null, kind: null, pdf: null };
|
|
169
|
+
|
|
170
|
+
/** The paper's own values win; each absent one falls back to the root's. */
|
|
171
|
+
function merge(
|
|
172
|
+
root: Defaults | null,
|
|
173
|
+
paper: PaperSettings | null,
|
|
174
|
+
): PaperSettings | null {
|
|
175
|
+
const r = root ?? NO_DEFAULTS;
|
|
176
|
+
const p = paper ?? { ...NO_DEFAULTS, rules: null };
|
|
177
|
+
const merged: PaperSettings = {
|
|
178
|
+
extends: p.extends ?? r.extends,
|
|
179
|
+
kind: p.kind ?? r.kind,
|
|
180
|
+
pdf: p.pdf ?? r.pdf,
|
|
181
|
+
rules: p.rules,
|
|
182
|
+
};
|
|
183
|
+
return paper === null && Object.values(merged).every((v) => v === null)
|
|
184
|
+
? null
|
|
185
|
+
: merged;
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
// ── the paper's rule overrides ──────────────────────────────────────────────────────────
|
|
192
189
|
|
|
193
190
|
/**
|
|
194
191
|
* `rules` → the parsed entries (rule id → severity or `[severity, options]`), or the error naming
|
|
195
|
-
* the file. Null when the paper sets none.
|
|
196
|
-
* (`paperRuleBlocks` in cli.ts)
|
|
192
|
+
* the file. Null when the paper sets none. Blocks (the list form) are parsed by the caller, which
|
|
193
|
+
* knows the file globs they are relative to (`paperRuleBlocks` in cli.ts).
|
|
197
194
|
*/
|
|
198
195
|
export function paperRules(
|
|
199
196
|
paperDir: string,
|
|
200
197
|
settings: PaperSettings,
|
|
201
198
|
shipped: ReadonlySet<string>,
|
|
202
199
|
): Parsed<Record<string, RuleEntry> | null> {
|
|
203
|
-
if (settings.rules === null
|
|
204
|
-
|
|
205
|
-
|
|
200
|
+
if (settings.rules === null || Array.isArray(settings.rules))
|
|
201
|
+
return { ok: true, value: null };
|
|
202
|
+
const where = `${join(paperDir, CONFIG_FILE)} → "rules"`;
|
|
203
|
+
return parseRuleEntries(
|
|
204
|
+
settings.rules as Readonly<Record<string, unknown>>,
|
|
205
|
+
where,
|
|
206
|
+
shipped,
|
|
207
|
+
);
|
|
206
208
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--flag=value` and unknown flags — the command line the CI action actually writes.
|
|
3
|
+
*
|
|
4
|
+
* action.yml runs `npx paperlint lint $PATHS --max-warnings="$MAXWARN" --json`. The parser knew
|
|
5
|
+
* only `--max-warnings <n>`, so `--max-warnings=-1` fell through to the list of PATHS: ESLint was
|
|
6
|
+
* handed a file named `--max-warnings=-1`, found nothing for it, and the run reported "nothing was
|
|
7
|
+
* linted under papers, --max-warnings=-1". Any flag the parser did not know became a path the same
|
|
8
|
+
* way, silently.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { parseArgs, run } from "./cli.ts";
|
|
12
|
+
|
|
13
|
+
describe("parseArgs — `--flag=value` is the same as `--flag value`", () => {
|
|
14
|
+
it("🔴 the action's own command line: `--max-warnings=-1` is a threshold, not a path", () => {
|
|
15
|
+
const a = parseArgs(["lint", "papers", "--max-warnings=-1", "--json"]);
|
|
16
|
+
// Guards: the defect — the flag landed in `paths`.
|
|
17
|
+
expect(a.paths).toEqual(["papers"]);
|
|
18
|
+
expect(a.maxWarnings).toBe(-1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
["--config=o.json", "config", "o.json"],
|
|
23
|
+
["--venue=agenticdev", "venue", "agenticdev"],
|
|
24
|
+
["--kind=short", "kind", "short"],
|
|
25
|
+
["--format=md", "format", "md"],
|
|
26
|
+
["--paper=p", "paper", "p"],
|
|
27
|
+
] as const)("%s", (flag, key, value) => {
|
|
28
|
+
const a = parseArgs(["new", "x", flag]);
|
|
29
|
+
expect(a[key]).toBe(value);
|
|
30
|
+
expect(a.paths).toEqual(["x"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("an empty value after `=` is a missing value, refused like `--config` with none", () => {
|
|
34
|
+
expect(parseArgs(["lint", "--config="]).missingValue).toBe("--config");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("an unknown flag is refused, not read as a path", () => {
|
|
39
|
+
it("exits 2 and names the flag", async () => {
|
|
40
|
+
const err: string[] = [];
|
|
41
|
+
const code = await run(["lint", "--frobnicate"], {
|
|
42
|
+
log: () => {},
|
|
43
|
+
err: (s: string) => err.push(s),
|
|
44
|
+
});
|
|
45
|
+
expect(code).toBe(2);
|
|
46
|
+
expect(err.join("\n")).toMatch(/unknown flag `--frobnicate`/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking a paper's references online — shaped by what the build records, not by what DBLP or
|
|
3
|
+
* Crossref answer. The adapter (`adapters/references/`) runs the package's two checkers; a test
|
|
4
|
+
* passes a function.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** What one bibliography entry came to. */
|
|
8
|
+
export interface EntryVerdict {
|
|
9
|
+
readonly key: string;
|
|
10
|
+
/**
|
|
11
|
+
* Does the cited work exist, and does its title match? `false` only on a positive disproof (an
|
|
12
|
+
* identifier that resolves to nothing or to another work); a work that simply was not found is
|
|
13
|
+
* `unresolvable`, never `false`.
|
|
14
|
+
*/
|
|
15
|
+
readonly exists: "true" | "false" | "unresolvable";
|
|
16
|
+
/** Are the authors those of the version cited (not the preprint's)? */
|
|
17
|
+
readonly authors: "match" | "mismatch" | "skipped" | "unchecked";
|
|
18
|
+
/** Why, in one line, when either is not a plain pass. */
|
|
19
|
+
readonly why?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ReferencesCheck =
|
|
23
|
+
| { readonly kind: "checked"; readonly entries: readonly EntryVerdict[] }
|
|
24
|
+
/** Nothing could be asked — no network, every service down. Never a pass. */
|
|
25
|
+
| { readonly kind: "not-checked"; readonly why: string };
|
|
26
|
+
|
|
27
|
+
/** The bibliography's text (BibTeX) → the verdicts. Never throws: a failure is `not-checked`. */
|
|
28
|
+
export type CheckReferences = (bib: string) => Promise<ReferencesCheck>;
|
package/src/presets.ts
CHANGED
|
@@ -47,11 +47,7 @@ import {
|
|
|
47
47
|
type PaperSettings,
|
|
48
48
|
type SettingsProblem,
|
|
49
49
|
} from "./paper-settings.ts";
|
|
50
|
-
import {
|
|
51
|
-
LEGACY_PAPER_SETTINGS_FILE,
|
|
52
|
-
LEGACY_PAPER_SETTINGS_MESSAGE,
|
|
53
|
-
PAPER_SETTINGS_FILE,
|
|
54
|
-
} from "../lib/paper-config.mjs";
|
|
50
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
55
51
|
|
|
56
52
|
/** The prefix of a shipped preset's spec. */
|
|
57
53
|
export const SHIPPED_PREFIX = "paperlint:";
|
|
@@ -268,7 +264,7 @@ export type PaperPreset =
|
|
|
268
264
|
readonly settings: PaperSettings;
|
|
269
265
|
readonly preset: Preset;
|
|
270
266
|
}
|
|
271
|
-
/** `paperlint.json` does not parse
|
|
267
|
+
/** `paperlint.json` does not parse. */
|
|
272
268
|
| { readonly kind: "settings-problem"; readonly problem: SettingsProblem }
|
|
273
269
|
/** `extends` does not resolve. */
|
|
274
270
|
| {
|
|
@@ -283,11 +279,7 @@ export function paperPreset(paperDir: string, deps: PresetDeps): PaperPreset {
|
|
|
283
279
|
if (!read.ok) return { kind: "settings-problem", problem: read.error };
|
|
284
280
|
const settings = read.value;
|
|
285
281
|
if (settings?.extends == null) return { kind: "none", settings };
|
|
286
|
-
const r = resolvePreset(
|
|
287
|
-
settings.extends,
|
|
288
|
-
join(paperDir, PAPER_SETTINGS_FILE),
|
|
289
|
-
deps,
|
|
290
|
-
);
|
|
282
|
+
const r = resolvePreset(settings.extends, join(paperDir, CONFIG_FILE), deps);
|
|
291
283
|
return r.ok
|
|
292
284
|
? { kind: "resolved", settings, preset: r.value }
|
|
293
285
|
: { kind: "preset-problem", settings, problem: r.error };
|
|
@@ -299,9 +291,7 @@ export function paperPresetProblem(
|
|
|
299
291
|
p: PaperPreset,
|
|
300
292
|
): string | null {
|
|
301
293
|
if (p.kind === "preset-problem")
|
|
302
|
-
return `${join(paperDir,
|
|
294
|
+
return `${join(paperDir, CONFIG_FILE)}: ${presetProblemText(p.problem)}`;
|
|
303
295
|
if (p.kind !== "settings-problem") return null;
|
|
304
|
-
return p.problem.
|
|
305
|
-
? `${join(paperDir, LEGACY_PAPER_SETTINGS_FILE)}: ${LEGACY_PAPER_SETTINGS_MESSAGE}`
|
|
306
|
-
: `${join(paperDir, PAPER_SETTINGS_FILE)}: ${p.problem.why}`;
|
|
296
|
+
return `${join(paperDir, CONFIG_FILE)}: ${p.problem.why}`;
|
|
307
297
|
}
|