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/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,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
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference rules — `paper/author-list`, `paper/cite-exists`, `paper/refs-checked`,
|
|
3
|
+
* `paper/refs-fresh` — judge `_build/references.json` (`references.ts`), offline. `paperlint build`
|
|
4
|
+
* does the online work; lint never touches the network.
|
|
5
|
+
*
|
|
6
|
+
* entry whose authors are the preprint's paper/author-list (error), on the entry
|
|
7
|
+
* entry whose identifier provably fails paper/cite-exists (error), on the entry
|
|
8
|
+
* no record, or recorded "not checked" paper/refs-checked (warn) — like pdf/measured
|
|
9
|
+
* bibliography edited after the build paper/refs-fresh (error) — like pdf/fresh
|
|
10
|
+
*
|
|
11
|
+
* While the record is stale, the per-entry rules are silent: they would judge a bibliography
|
|
12
|
+
* that no longer exists, and refs-fresh already says so once. A paper with no bibliography gets
|
|
13
|
+
* nothing.
|
|
14
|
+
*
|
|
15
|
+
* `paper/author-list` used to live on PIPELINE-STATUS.md and ask whether the scorecard mentioned
|
|
16
|
+
* a run. It lives here now because its subject is the bibliography, and the record of the run is
|
|
17
|
+
* the run's own output.
|
|
18
|
+
*/
|
|
19
|
+
import { basename, dirname } from "node:path";
|
|
20
|
+
import type { Files } from "./ports/files.ts";
|
|
21
|
+
import {
|
|
22
|
+
bibHash,
|
|
23
|
+
bibliographyOf,
|
|
24
|
+
readReferences,
|
|
25
|
+
REFERENCES_FILE,
|
|
26
|
+
type Bibliography,
|
|
27
|
+
} from "./references.ts";
|
|
28
|
+
|
|
29
|
+
interface Loc {
|
|
30
|
+
readonly line: number;
|
|
31
|
+
readonly column: number;
|
|
32
|
+
}
|
|
33
|
+
interface RuleContext {
|
|
34
|
+
readonly filename: string;
|
|
35
|
+
readonly cwd: string;
|
|
36
|
+
readonly sourceCode: {
|
|
37
|
+
getLocFromIndex(i: number): Loc;
|
|
38
|
+
};
|
|
39
|
+
report(d: {
|
|
40
|
+
loc: { start: Loc; end: Loc };
|
|
41
|
+
messageId: string;
|
|
42
|
+
data?: Record<string, string>;
|
|
43
|
+
}): void;
|
|
44
|
+
}
|
|
45
|
+
export interface ReferenceRuleModule {
|
|
46
|
+
readonly meta: {
|
|
47
|
+
readonly type: "problem" | "suggestion";
|
|
48
|
+
readonly docs: { readonly description: string };
|
|
49
|
+
readonly schema: readonly object[];
|
|
50
|
+
readonly messages: Readonly<Record<string, string>>;
|
|
51
|
+
};
|
|
52
|
+
create(context: RuleContext): { "root:exit"?: () => void };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type Name = "author-list" | "cite-exists" | "refs-checked" | "refs-fresh";
|
|
56
|
+
|
|
57
|
+
/** What one lint run knows about a paper's references. */
|
|
58
|
+
type Assessment =
|
|
59
|
+
| { readonly kind: "no-bibliography" }
|
|
60
|
+
| { readonly kind: "unrecorded" }
|
|
61
|
+
| { readonly kind: "not-checked"; readonly why: string }
|
|
62
|
+
| { readonly kind: "stale" }
|
|
63
|
+
| {
|
|
64
|
+
readonly kind: "ready";
|
|
65
|
+
readonly bib: Bibliography;
|
|
66
|
+
readonly failing: readonly {
|
|
67
|
+
readonly key: string;
|
|
68
|
+
readonly rule: "author-list" | "cite-exists";
|
|
69
|
+
readonly why: string;
|
|
70
|
+
}[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function assess(files: Files, paperDir: string): Assessment {
|
|
74
|
+
const bib = bibliographyOf(files, paperDir);
|
|
75
|
+
if (bib === null) return { kind: "no-bibliography" };
|
|
76
|
+
const doc = readReferences(files, paperDir);
|
|
77
|
+
if (doc === null) return { kind: "unrecorded" };
|
|
78
|
+
if (doc.bib.sha256 !== bibHash(bib)) return { kind: "stale" };
|
|
79
|
+
if (doc.status === "not-checked")
|
|
80
|
+
return { kind: "not-checked", why: doc.why ?? "no reason recorded" };
|
|
81
|
+
const failing = doc.entries.flatMap((e) => [
|
|
82
|
+
...(e.authors === "mismatch"
|
|
83
|
+
? [{ key: e.key, rule: "author-list" as const, why: e.why ?? "" }]
|
|
84
|
+
: []),
|
|
85
|
+
...(e.exists === "false"
|
|
86
|
+
? [{ key: e.key, rule: "cite-exists" as const, why: e.why ?? "" }]
|
|
87
|
+
: []),
|
|
88
|
+
]);
|
|
89
|
+
return { kind: "ready", bib, failing };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Where entry `key` starts in the source file: its `@type{key,` line, or the file's start. */
|
|
93
|
+
function entryOffset(bib: Bibliography, key: string): number {
|
|
94
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
95
|
+
const m = new RegExp(`^@\\w+\\s*\\{\\s*${escaped}\\s*,`, "m").exec(bib.text);
|
|
96
|
+
return m ? bib.offset + m.index : 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const BUILD = "`npx paperlint build`";
|
|
100
|
+
|
|
101
|
+
const META: Readonly<Record<Name, ReferenceRuleModule["meta"]>> = {
|
|
102
|
+
"author-list": {
|
|
103
|
+
type: "problem",
|
|
104
|
+
docs: {
|
|
105
|
+
description:
|
|
106
|
+
"each entry's authors are those of the version it cites, not the preprint's (recorded by paperlint build)",
|
|
107
|
+
},
|
|
108
|
+
schema: [],
|
|
109
|
+
messages: {
|
|
110
|
+
mismatch:
|
|
111
|
+
"`{{key}}`: the authors are not those of the version cited — {{why}}. The citation resolves and the id resolves, and the list is the PREPRINT's under a published venue",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
"cite-exists": {
|
|
115
|
+
type: "problem",
|
|
116
|
+
docs: {
|
|
117
|
+
description:
|
|
118
|
+
"each entry's identifier resolves to the work cited (recorded by paperlint build)",
|
|
119
|
+
},
|
|
120
|
+
schema: [],
|
|
121
|
+
messages: {
|
|
122
|
+
missing: "`{{key}}` does not resolve to the work cited — {{why}}",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
"refs-checked": {
|
|
126
|
+
type: "suggestion",
|
|
127
|
+
docs: {
|
|
128
|
+
description:
|
|
129
|
+
"the build checked the references online (existence, title, authors); a warning when it could not",
|
|
130
|
+
},
|
|
131
|
+
schema: [],
|
|
132
|
+
messages: {
|
|
133
|
+
unrecorded: `the references have not been checked — ${BUILD} checks them online and records the result in _build/${REFERENCES_FILE}`,
|
|
134
|
+
notChecked: `the last build could not check the references ({{why}}) — run ${BUILD} with network`,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"refs-fresh": {
|
|
138
|
+
type: "problem",
|
|
139
|
+
docs: {
|
|
140
|
+
description:
|
|
141
|
+
"the recorded reference verdicts are about the bibliography on disk, not an earlier one",
|
|
142
|
+
},
|
|
143
|
+
schema: [],
|
|
144
|
+
messages: {
|
|
145
|
+
stale: `the references changed since the last build checked them — run ${BUILD}`,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type Report = { at: number; messageId: string; data?: Record<string, string> };
|
|
151
|
+
|
|
152
|
+
const JUDGES: Readonly<Record<Name, (a: Assessment) => Report[]>> = {
|
|
153
|
+
"author-list": (a) =>
|
|
154
|
+
a.kind === "ready"
|
|
155
|
+
? a.failing
|
|
156
|
+
.filter((f) => f.rule === "author-list")
|
|
157
|
+
.map((f) => ({
|
|
158
|
+
at: entryOffset(a.bib, f.key),
|
|
159
|
+
messageId: "mismatch",
|
|
160
|
+
data: { key: f.key, why: f.why },
|
|
161
|
+
}))
|
|
162
|
+
: [],
|
|
163
|
+
"cite-exists": (a) =>
|
|
164
|
+
a.kind === "ready"
|
|
165
|
+
? a.failing
|
|
166
|
+
.filter((f) => f.rule === "cite-exists")
|
|
167
|
+
.map((f) => ({
|
|
168
|
+
at: entryOffset(a.bib, f.key),
|
|
169
|
+
messageId: "missing",
|
|
170
|
+
data: { key: f.key, why: f.why },
|
|
171
|
+
}))
|
|
172
|
+
: [],
|
|
173
|
+
"refs-checked": (a) =>
|
|
174
|
+
a.kind === "unrecorded"
|
|
175
|
+
? [{ at: 0, messageId: "unrecorded" }]
|
|
176
|
+
: a.kind === "not-checked"
|
|
177
|
+
? [{ at: 0, messageId: "notChecked", data: { why: a.why } }]
|
|
178
|
+
: [],
|
|
179
|
+
"refs-fresh": (a) =>
|
|
180
|
+
a.kind === "stale" ? [{ at: 0, messageId: "stale" }] : [],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** The four rules, reading through `files`. They act on `paper.tex` only. */
|
|
184
|
+
export function referenceRules({
|
|
185
|
+
files,
|
|
186
|
+
}: {
|
|
187
|
+
files: Files;
|
|
188
|
+
}): Record<Name, ReferenceRuleModule> {
|
|
189
|
+
const make = (name: Name): ReferenceRuleModule => ({
|
|
190
|
+
meta: META[name],
|
|
191
|
+
create: (context) =>
|
|
192
|
+
basename(context.filename) !== "paper.tex"
|
|
193
|
+
? {}
|
|
194
|
+
: {
|
|
195
|
+
"root:exit": () => {
|
|
196
|
+
const paperDir = dirname(context.filename);
|
|
197
|
+
const a = assess(files, paperDir);
|
|
198
|
+
// An external refs.bib: the entry is not in this file, so the finding sits at its
|
|
199
|
+
// start and names the key.
|
|
200
|
+
const external =
|
|
201
|
+
a.kind === "ready" && a.bib.source !== "paper.tex";
|
|
202
|
+
for (const r of JUDGES[name](a)) {
|
|
203
|
+
const loc = context.sourceCode.getLocFromIndex(
|
|
204
|
+
external ? 0 : r.at,
|
|
205
|
+
);
|
|
206
|
+
context.report({
|
|
207
|
+
loc: { start: loc, end: loc },
|
|
208
|
+
messageId: r.messageId,
|
|
209
|
+
...(r.data ? { data: r.data } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
"author-list": make("author-list"),
|
|
217
|
+
"cite-exists": make("cite-exists"),
|
|
218
|
+
"refs-checked": make("refs-checked"),
|
|
219
|
+
"refs-fresh": make("refs-fresh"),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The level each is on at for every `paper.tex` in paperlint's own config. */
|
|
224
|
+
export const REFERENCE_RULE_LEVELS = {
|
|
225
|
+
"paper/author-list": "error",
|
|
226
|
+
"paper/cite-exists": "error",
|
|
227
|
+
"paper/refs-checked": "warn",
|
|
228
|
+
"paper/refs-fresh": "error",
|
|
229
|
+
} as const;
|