paperlint 3.1.0 → 3.1.2
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 +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +21 -105
- package/CONTRIBUTING.md +23 -25
- package/README.md +11 -7
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/clock.io.d.ts +5 -0
- package/dist/adapters/references/clock.io.d.ts.map +1 -0
- package/dist/adapters/references/clock.io.js +7 -0
- package/dist/adapters/references/clock.io.js.map +1 -0
- package/dist/adapters/references/index.d.ts +24 -2
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +131 -12
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build.d.ts +14 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +17 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/lookup-cache.d.ts +75 -0
- package/dist/domain/lookup-cache.d.ts.map +1 -0
- package/dist/domain/lookup-cache.js +137 -0
- package/dist/domain/lookup-cache.js.map +1 -0
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/check-references.d.ts +12 -2
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/check-references.js +0 -5
- package/dist/ports/check-references.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts +11 -3
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +63 -15
- package/dist/references.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.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/prior-art/README.md +11 -10
- package/docs/prior-art/citation-checkers.md +174 -0
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/references.md +126 -0
- package/docs/testing.md +118 -0
- package/docs/toolchain.md +6 -4
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/checkers-requirements.txt +30 -19
- package/skills/render-paper/ensure-checkers.sh +20 -4
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +30 -14
- package/skills/verify-citations/SKILL.md.spec.ts +29 -13
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +253 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +6 -6
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +360 -215
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +588 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +151 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/clock.io.ts +8 -0
- package/src/adapters/references/index.test.ts +560 -0
- package/src/adapters/references/index.ts +203 -19
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +23 -12
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/lookup-cache.test.ts +178 -0
- package/src/domain/lookup-cache.ts +202 -0
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +27 -7
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +3 -3
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/ports/check-references.ts +16 -2
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +257 -10
- package/src/references.ts +93 -17
- package/src/rules-config.harness.mjs +10 -8
- package/src/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -65
- package/src/structure.mutations.mjs +0 -83
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* 2. the schema REJECTS what it must — each bad profile names the offending path;
|
|
8
8
|
* 3. what a paper gets: its venue plus the base set, or the base set with the reason said.
|
|
9
9
|
*/
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
10
|
import {
|
|
12
11
|
copyFileSync,
|
|
13
12
|
mkdtempSync,
|
|
@@ -19,6 +18,7 @@ import { tmpdir } from "node:os";
|
|
|
19
18
|
import { dirname, join } from "node:path";
|
|
20
19
|
import { fileURLToPath } from "node:url";
|
|
21
20
|
import ts from "typescript";
|
|
21
|
+
import { createChecker } from "../lib/check.mjs";
|
|
22
22
|
|
|
23
23
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
const VENUES = join(
|
|
@@ -31,11 +31,7 @@ const VENUES = join(
|
|
|
31
31
|
);
|
|
32
32
|
const R = await import(join(HERE, "tex-requirements.ts"));
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
const check = (label, cond, detail = "") => {
|
|
36
|
-
assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
|
|
37
|
-
n++;
|
|
38
|
-
};
|
|
34
|
+
const check = createChecker();
|
|
39
35
|
const throws = (fn) => {
|
|
40
36
|
try {
|
|
41
37
|
fn();
|
|
@@ -229,4 +225,35 @@ rmSync(tmp, { recursive: true, force: true });
|
|
|
229
225
|
);
|
|
230
226
|
}
|
|
231
227
|
|
|
232
|
-
|
|
228
|
+
// ── 5. the parser's tolerances: an optional block left out, and Ajv's optional fields ───
|
|
229
|
+
{
|
|
230
|
+
const bare = R.parsePreset(
|
|
231
|
+
'{ "extends": "paperlint:aisec" }',
|
|
232
|
+
"bare.jsonc",
|
|
233
|
+
VENUES,
|
|
234
|
+
);
|
|
235
|
+
check(
|
|
236
|
+
"a preset that only extends another: no tex block, `rules` an empty object",
|
|
237
|
+
JSON.stringify([bare.extends, bare.tex, bare.rules]) ===
|
|
238
|
+
'["paperlint:aisec",null,{}]',
|
|
239
|
+
bare,
|
|
240
|
+
);
|
|
241
|
+
// Guards: `packages` is read without a fallback because the schema refuses a tex block
|
|
242
|
+
// without it — this is the refusal the type relies on.
|
|
243
|
+
check(
|
|
244
|
+
"a tex block without `packages` is refused by the schema, before anything reads it",
|
|
245
|
+
/tex\.jsonc: \.tex should have required property 'packages'/.test(
|
|
246
|
+
throws(() => R.parsePreset('{ "tex": {} }', "tex.jsonc", VENUES)),
|
|
247
|
+
),
|
|
248
|
+
throws(() => R.parsePreset('{ "tex": {} }', "tex.jsonc", VENUES)),
|
|
249
|
+
);
|
|
250
|
+
check(
|
|
251
|
+
"violations: Ajv's null `errors` is no violation; a message Ajv left out is an empty one",
|
|
252
|
+
R.violations("f.jsonc", { errors: null }).length === 0 &&
|
|
253
|
+
R.violations("f.jsonc", {
|
|
254
|
+
errors: [{ dataPath: "", message: undefined }],
|
|
255
|
+
})[0] === "f.jsonc: (top level) ",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(`tex-requirements: ${check.count} checks passed`);
|
package/src/tex-requirements.ts
CHANGED
|
@@ -72,8 +72,14 @@ function validatorFor(dir: string): Validate {
|
|
|
72
72
|
return v;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Every violation as `<file>: <path> <message>` — one line each, so all of them are fixed at once.
|
|
77
|
+
* Ajv types `errors` as possibly null and a message as optional; both are tolerated, not assumed.
|
|
78
|
+
*/
|
|
79
|
+
export function violations(
|
|
80
|
+
file: string,
|
|
81
|
+
v: Pick<Validate, "errors">,
|
|
82
|
+
): string[] {
|
|
77
83
|
return (v.errors ?? []).map(
|
|
78
84
|
(e) => `${file}: ${e.dataPath || "(top level)"} ${e.message ?? ""}`,
|
|
79
85
|
);
|
|
@@ -158,7 +164,9 @@ interface PresetJson {
|
|
|
158
164
|
readonly extends?: string;
|
|
159
165
|
readonly name?: string;
|
|
160
166
|
readonly template?: string;
|
|
161
|
-
|
|
167
|
+
/** The schema requires `packages` in a `tex` block; `tools` is optional. */
|
|
168
|
+
readonly tex?: Pick<TexRequirements, "packages"> &
|
|
169
|
+
Partial<Pick<TexRequirements, "tools">>;
|
|
162
170
|
readonly format?: FormatJson;
|
|
163
171
|
readonly rules?: Readonly<Record<string, unknown>>;
|
|
164
172
|
}
|
|
@@ -221,9 +229,7 @@ export function parsePreset(
|
|
|
221
229
|
extends: orNull(j.extends),
|
|
222
230
|
name: orNull(j.name),
|
|
223
231
|
template: orNull(j.template),
|
|
224
|
-
tex: j.tex
|
|
225
|
-
? { packages: j.tex.packages ?? {}, tools: j.tex.tools ?? {} }
|
|
226
|
-
: null,
|
|
232
|
+
tex: j.tex ? { packages: j.tex.packages, tools: j.tex.tools ?? {} } : null,
|
|
227
233
|
format: formatOf(j.format),
|
|
228
234
|
rules: j.rules ?? {},
|
|
229
235
|
};
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* 4. banal, the command's second half: a stand-in banal served from `file://`, never HotCRP —
|
|
17
17
|
* the installer's own tests are `banal.harness.mjs`; here only the wiring into the command.
|
|
18
18
|
*/
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
19
|
import { spawnSync } from "node:child_process";
|
|
21
20
|
import { createHash } from "node:crypto";
|
|
22
21
|
import {
|
|
@@ -33,6 +32,7 @@ import {
|
|
|
33
32
|
import { tmpdir } from "node:os";
|
|
34
33
|
import { dirname, join } from "node:path";
|
|
35
34
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
35
|
+
import { createChecker } from "../lib/check.mjs";
|
|
36
36
|
|
|
37
37
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
38
38
|
const FIXTURE = join(HERE, "..", "fixtures", "toolchain-mirror");
|
|
@@ -43,11 +43,7 @@ const B = await import(join(HERE, "adapters", "banal", "index.ts"));
|
|
|
43
43
|
const N = await import(join(HERE, "adapters", "node", "index.ts"));
|
|
44
44
|
const C = await import(join(HERE, "adapters", "curl", "index.ts"));
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
const check = (label, cond, detail = "") => {
|
|
48
|
-
assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
|
|
49
|
-
n++;
|
|
50
|
-
};
|
|
46
|
+
const check = createChecker();
|
|
51
47
|
|
|
52
48
|
// ── 1. pure pieces ──────────────────────────────────────────────────────────────────────
|
|
53
49
|
check(
|
|
@@ -692,4 +688,4 @@ check(
|
|
|
692
688
|
}
|
|
693
689
|
|
|
694
690
|
rmSync(work, { recursive: true, force: true });
|
|
695
|
-
console.log(`toolchain: ${
|
|
691
|
+
console.log(`toolchain: ${check.count} checks passed`);
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* toolchain.ts edges the harness (a fixture mirror) does not reach, with a scripted runner: curl
|
|
3
|
+
* that fails with and without output, a mirror serving something that is not a tarball, an archive
|
|
4
|
+
* without install-tl or without its release file, install-tl that cannot start, tlmgr with nothing
|
|
5
|
+
* to install; and the pure readers on inputs their happy paths never see.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { test } from "vitest";
|
|
12
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
13
|
+
import {
|
|
14
|
+
cachedTrees,
|
|
15
|
+
CACHE_ENV,
|
|
16
|
+
describeGaps,
|
|
17
|
+
downloadInstaller,
|
|
18
|
+
ensureTexLive,
|
|
19
|
+
installBase,
|
|
20
|
+
installPackages,
|
|
21
|
+
releaseGap,
|
|
22
|
+
runToolchain,
|
|
23
|
+
tlYear,
|
|
24
|
+
UNSUPPORTED,
|
|
25
|
+
type ToolchainIO,
|
|
26
|
+
} from "./toolchain.ts";
|
|
27
|
+
|
|
28
|
+
const root = useTempDir("paperlint-toolchain-test-");
|
|
29
|
+
type Answer = {
|
|
30
|
+
status: number | null;
|
|
31
|
+
stdout?: string;
|
|
32
|
+
stderr?: string;
|
|
33
|
+
error?: Error;
|
|
34
|
+
};
|
|
35
|
+
/** A runner answering by program name (the last path segment); `onRun` may write files first. */
|
|
36
|
+
function io(
|
|
37
|
+
answers: Record<string, (args: string[]) => Answer>,
|
|
38
|
+
): ToolchainIO & { lines: string[] } {
|
|
39
|
+
const lines: string[] = [];
|
|
40
|
+
return {
|
|
41
|
+
lines,
|
|
42
|
+
log: (l) => lines.push(l),
|
|
43
|
+
env: {},
|
|
44
|
+
run: ((cmd: string, args: string[]) => {
|
|
45
|
+
const name = cmd.split("/").pop() ?? cmd;
|
|
46
|
+
const answer = answers[`${name} ${args[0] ?? ""}`] ?? answers[name];
|
|
47
|
+
if (!answer) throw new Error(`unexpected ${name} ${args.join(" ")}`);
|
|
48
|
+
return answer(args);
|
|
49
|
+
}) as never,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const ok = (stdout = ""): Answer => ({ status: 0, stdout, stderr: "" });
|
|
53
|
+
|
|
54
|
+
test("pure readers: a release file with no year, a `local:` line with no year, a gap with no proof file", () => {
|
|
55
|
+
assert.equal(tlYear(""), null);
|
|
56
|
+
assert.equal(releaseGap("local: soon\nrepository: 2026"), null);
|
|
57
|
+
assert.equal(
|
|
58
|
+
describeGaps(
|
|
59
|
+
{ packages: ["acmart"], tools: [], dependencies: [] },
|
|
60
|
+
{ packages: {}, tools: {} },
|
|
61
|
+
),
|
|
62
|
+
"acmart ()",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the cache: a year whose bin directory holds no runnable pdflatex is not a tree", () => {
|
|
67
|
+
const cache = join(root, "cache");
|
|
68
|
+
mkdirSync(join(cache, "2025", "bin", "x86_64-linux"), { recursive: true });
|
|
69
|
+
writeFileSync(join(cache, "2025", "bin", "x86_64-linux", "pdflatex"), "");
|
|
70
|
+
mkdirSync(join(cache, "2026", "bin", "x86_64-linux"), { recursive: true });
|
|
71
|
+
const exe = join(cache, "2026", "bin", "x86_64-linux", "pdflatex");
|
|
72
|
+
writeFileSync(exe, "#!/bin/sh\n");
|
|
73
|
+
chmodSync(exe, 0o755);
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
cachedTrees(cache).map((t) => t.year),
|
|
76
|
+
["2026"],
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("download: curl failing with its own line, with only an error, with only a code; then a non-tarball", () => {
|
|
81
|
+
const t = io({
|
|
82
|
+
curl: (args) => {
|
|
83
|
+
const url = args[args.length - 1] ?? "";
|
|
84
|
+
if (url.startsWith("m1"))
|
|
85
|
+
return { status: 22, stdout: "", stderr: "curl: (22) 404\n" };
|
|
86
|
+
if (url.startsWith("m2"))
|
|
87
|
+
return { status: null, error: new Error("spawn curl ENOENT") };
|
|
88
|
+
if (url.startsWith("m3")) return { status: 7 };
|
|
89
|
+
return ok();
|
|
90
|
+
},
|
|
91
|
+
"tar tzf": () => ({ status: 2 }),
|
|
92
|
+
});
|
|
93
|
+
assert.equal(
|
|
94
|
+
downloadInstaller(t, ["m1", "m2", "m3", "m4"], join(root, "x.tgz")),
|
|
95
|
+
null,
|
|
96
|
+
);
|
|
97
|
+
assert.deepEqual(t.lines, [
|
|
98
|
+
" ⚠ m1/install-tl-unx.tar.gz: curl: (22) 404",
|
|
99
|
+
" ⚠ m2/install-tl-unx.tar.gz: spawn curl ENOENT",
|
|
100
|
+
" ⚠ m3/install-tl-unx.tar.gz: curl exited 7",
|
|
101
|
+
" ⚠ m4/install-tl-unx.tar.gz returned something that is not a .tar.gz — trying the next mirror",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/** A runner whose download succeeds and whose `tar xzf` lays out `files` in the work directory. */
|
|
106
|
+
const unpacking = (
|
|
107
|
+
files: Record<string, string> | null,
|
|
108
|
+
more: Record<string, (args: string[]) => Answer> = {},
|
|
109
|
+
) =>
|
|
110
|
+
io({
|
|
111
|
+
curl: () => ok(),
|
|
112
|
+
"tar tzf": () => ok(),
|
|
113
|
+
"tar xzf": (args) => {
|
|
114
|
+
if (files === null) return { status: 2 };
|
|
115
|
+
writeTree(args[args.length - 1] ?? "", files);
|
|
116
|
+
return ok();
|
|
117
|
+
},
|
|
118
|
+
...more,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("install: an archive that does not unpack, holds no install-tl, or no release file", () => {
|
|
122
|
+
const bad = [
|
|
123
|
+
unpacking(null),
|
|
124
|
+
unpacking({ "other/x": "" }),
|
|
125
|
+
unpacking({ "install-tl-20260101/install-tl": "" }),
|
|
126
|
+
].map((t) => installBase(t, join(root, "tl"), ["m"]));
|
|
127
|
+
for (const r of bad)
|
|
128
|
+
assert.deepEqual(r, {
|
|
129
|
+
ok: false,
|
|
130
|
+
lines: [
|
|
131
|
+
"the archive from m does not hold an install-tl with a release-texlive.txt",
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("install: install-tl that cannot start names the error and its last lines", () => {
|
|
137
|
+
const t = unpacking(
|
|
138
|
+
{
|
|
139
|
+
"install-tl-20260101/install-tl": "",
|
|
140
|
+
"install-tl-20260101/release-texlive.txt":
|
|
141
|
+
"TeX Live (https://tug.org/texlive) version 2026\n",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"install-tl": () => ({ status: null, error: new Error("spawn EACCES") }),
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
assert.deepEqual(installBase(t, join(root, "tl2"), ["m"]), {
|
|
148
|
+
ok: false,
|
|
149
|
+
lines: ["install-tl failed (spawn EACCES) — its last lines:"],
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("tlmgr: nothing missing, nothing installed", () => {
|
|
154
|
+
const t = io({ kpsewhich: () => ok(), tlmgr: () => ok() });
|
|
155
|
+
const tree = { dir: "/t/2026", year: "2026", bin: "/t/2026/bin/x" };
|
|
156
|
+
assert.deepEqual(
|
|
157
|
+
installPackages(t, tree, { packages: {}, tools: {} }, ["m"]),
|
|
158
|
+
{
|
|
159
|
+
ok: true,
|
|
160
|
+
value: { gaps: { packages: [], tools: [], dependencies: [] }, tail: [] },
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("tlmgr: a run that prints nothing still leaves the gap to verify", () => {
|
|
166
|
+
let kpse = 0;
|
|
167
|
+
const t = io({
|
|
168
|
+
kpsewhich: () => ok(kpse++ ? "/t/acmart.cls\n" : ""),
|
|
169
|
+
"tlmgr check": () => ok(),
|
|
170
|
+
"tlmgr --repository": () => ({ status: 0 }),
|
|
171
|
+
});
|
|
172
|
+
const tree = { dir: "/t/2026", year: "2026", bin: "/t/2026/bin/x" };
|
|
173
|
+
const r = installPackages(
|
|
174
|
+
t,
|
|
175
|
+
tree,
|
|
176
|
+
{ packages: { acmart: ["acmart.cls"] }, tools: {} },
|
|
177
|
+
["m"],
|
|
178
|
+
);
|
|
179
|
+
assert.deepEqual(r, {
|
|
180
|
+
ok: true,
|
|
181
|
+
value: { gaps: { packages: [], tools: [], dependencies: [] }, tail: [] },
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/** A cache holding one tree for `year` with a runnable pdflatex; returns the cache root. */
|
|
186
|
+
function cacheWith(name: string, year: string): string {
|
|
187
|
+
const cache = join(root, name);
|
|
188
|
+
const exe = join(cache, year, "bin", "x86_64-linux", "pdflatex");
|
|
189
|
+
mkdirSync(dirname(exe), { recursive: true });
|
|
190
|
+
writeFileSync(exe, "#!/bin/sh\n");
|
|
191
|
+
chmodSync(exe, 0o755);
|
|
192
|
+
return cache;
|
|
193
|
+
}
|
|
194
|
+
const ACMART = { packages: { acmart: ["acmart.cls"] }, tools: {} };
|
|
195
|
+
function ensure(
|
|
196
|
+
cache: string,
|
|
197
|
+
answers: Record<string, (args: string[]) => Answer>,
|
|
198
|
+
) {
|
|
199
|
+
const t = io(answers);
|
|
200
|
+
const errs: string[] = [];
|
|
201
|
+
const r = ensureTexLive(ACMART, {
|
|
202
|
+
run: t.run,
|
|
203
|
+
log: t.log,
|
|
204
|
+
err: (l) => errs.push(l),
|
|
205
|
+
env: { [CACHE_ENV]: cache },
|
|
206
|
+
home: root,
|
|
207
|
+
now: () => 0,
|
|
208
|
+
});
|
|
209
|
+
return { r, errs };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
test("ensure: tlmgr leaves the package missing — the verdict quotes tlmgr's last lines", () => {
|
|
213
|
+
const cache = cacheWith("lacks", "2026");
|
|
214
|
+
const { r, errs } = ensure(cache, {
|
|
215
|
+
kpsewhich: () => ok(),
|
|
216
|
+
"tlmgr check": () => ok(),
|
|
217
|
+
"tlmgr --repository": () =>
|
|
218
|
+
ok("tlmgr: package acmart installed, but nothing changed\n"),
|
|
219
|
+
});
|
|
220
|
+
assert.deepEqual(
|
|
221
|
+
[r, errs.slice(-2)],
|
|
222
|
+
[
|
|
223
|
+
{ ok: false },
|
|
224
|
+
[
|
|
225
|
+
" tlmgr's last lines:",
|
|
226
|
+
" tlmgr: package acmart installed, but nothing changed",
|
|
227
|
+
],
|
|
228
|
+
],
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("ensure: tlmgr refuses across releases, the new tree is installed, and tlmgr still refuses", () => {
|
|
233
|
+
const cache = cacheWith("newer", "2025");
|
|
234
|
+
const refusal = ok(
|
|
235
|
+
"tlmgr: Local TeX Live (2025) is older than remote repository (2026).\n",
|
|
236
|
+
);
|
|
237
|
+
const { r, errs } = ensure(cache, {
|
|
238
|
+
kpsewhich: () => ok(),
|
|
239
|
+
"tlmgr check": () => ok(),
|
|
240
|
+
"tlmgr --repository": () => refusal,
|
|
241
|
+
curl: () => ok(),
|
|
242
|
+
"tar tzf": () => ok(),
|
|
243
|
+
"tar xzf": (args) => {
|
|
244
|
+
writeTree(args[args.length - 1] ?? "", {
|
|
245
|
+
"install-tl-20260101/install-tl": "",
|
|
246
|
+
"install-tl-20260101/release-texlive.txt":
|
|
247
|
+
"TeX Live (https://tug.org/texlive) version 2026\n",
|
|
248
|
+
});
|
|
249
|
+
return ok();
|
|
250
|
+
},
|
|
251
|
+
"install-tl": () => {
|
|
252
|
+
const exe = join(cache, "2026", "bin", "x86_64-linux", "pdflatex");
|
|
253
|
+
mkdirSync(dirname(exe), { recursive: true });
|
|
254
|
+
writeFileSync(exe, "#!/bin/sh\n");
|
|
255
|
+
chmodSync(exe, 0o755);
|
|
256
|
+
return ok();
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
assert.deepEqual(
|
|
260
|
+
[r, errs],
|
|
261
|
+
[
|
|
262
|
+
{ ok: false },
|
|
263
|
+
[
|
|
264
|
+
`✗ paperlint toolchain: tlmgr still refuses: the repository is TeX Live 2026 and the new tree in ${join(cache, "2026")} is 2025`,
|
|
265
|
+
],
|
|
266
|
+
],
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("runToolchain with only its installer: the console is the default output", () => {
|
|
271
|
+
const seen: unknown[] = [];
|
|
272
|
+
const saved = console.error;
|
|
273
|
+
console.error = (...a: unknown[]) => void seen.push(a.join(" "));
|
|
274
|
+
try {
|
|
275
|
+
assert.equal(runToolchain({ platform: "win32", banal: {} as never }), 1);
|
|
276
|
+
} finally {
|
|
277
|
+
console.error = saved;
|
|
278
|
+
}
|
|
279
|
+
assert.deepEqual(seen, [`✗ paperlint toolchain: ${UNSUPPORTED}`]);
|
|
280
|
+
});
|
package/src/toolchain.ts
CHANGED
|
@@ -162,8 +162,8 @@ export function mirrorsFrom(env: NodeJS.ProcessEnv): readonly string[] {
|
|
|
162
162
|
|
|
163
163
|
/** `release-texlive.txt`'s first line: "TeX Live (https://tug.org/texlive) version 2026". */
|
|
164
164
|
export function tlYear(release: string): string | null {
|
|
165
|
-
|
|
166
|
-
return
|
|
165
|
+
// `^[^\n]*`: the first line only — a year on a later line is not the release.
|
|
166
|
+
return /^[^\n]*\bversion (\d{4})\b/.exec(release)?.[1] ?? null;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/** The install-tl profile: scheme-basic, no docs or sources, every tree variable inside `dir`. */
|
|
@@ -359,7 +359,9 @@ function unpack(io: ToolchainIO, archive: string, work: string): string | null {
|
|
|
359
359
|
return dir ? join(work, dir) : null;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
/** A failed step always says why: its first line is the headline, the rest detail. */
|
|
363
|
+
export type Step<T> =
|
|
364
|
+
{ ok: true; value: T } | { ok: false; lines: [string, ...string[]] };
|
|
363
365
|
|
|
364
366
|
/** Download, unpack and run install-tl: a fresh scheme-basic tree in `<root>/<year>`. */
|
|
365
367
|
export function installBase(
|
|
@@ -458,7 +460,7 @@ function runInstallTl(
|
|
|
458
460
|
*/
|
|
459
461
|
export type AddStep =
|
|
460
462
|
| { readonly ok: true; readonly value: { gaps: Gaps; tail: string[] } }
|
|
461
|
-
| { readonly ok: false; readonly lines: string[] }
|
|
463
|
+
| { readonly ok: false; readonly lines: [string, ...string[]] }
|
|
462
464
|
| { readonly ok: false; readonly newer: ReleaseGap };
|
|
463
465
|
|
|
464
466
|
export function installPackages(
|
|
@@ -654,10 +656,9 @@ export const binLine = (tree: CachedTree): string => ` bin: ${tree.bin}`;
|
|
|
654
656
|
|
|
655
657
|
function fail(
|
|
656
658
|
err: (line: string) => void,
|
|
657
|
-
|
|
659
|
+
[head, ...rest]: readonly [string, ...string[]],
|
|
658
660
|
): InstallResult {
|
|
659
|
-
|
|
660
|
-
err(`✗ paperlint toolchain: ${head ?? "failed"}`);
|
|
661
|
+
err(`✗ paperlint toolchain: ${head}`);
|
|
661
662
|
for (const l of rest) err(` ${l}`);
|
|
662
663
|
return { ok: false };
|
|
663
664
|
}
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -113,6 +113,7 @@ function lint(
|
|
|
113
113
|
p: Paper,
|
|
114
114
|
filename = `${PAPER}/paper.tex`,
|
|
115
115
|
options: Json = {},
|
|
116
|
+
cwd?: string,
|
|
116
117
|
): Finding[] {
|
|
117
118
|
const files = memoryFiles(paperFiles(p));
|
|
118
119
|
const rules = venueRules({ files, venuesDir: VENUES });
|
|
@@ -123,6 +124,7 @@ function lint(
|
|
|
123
124
|
][]) {
|
|
124
125
|
const visitor = rule.create({
|
|
125
126
|
filename,
|
|
127
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
126
128
|
sourceCode: { text: TEX },
|
|
127
129
|
options:
|
|
128
130
|
name === "geometry" && Object.keys(options).length ? [options] : [],
|
|
@@ -555,3 +557,61 @@ describe("paperlint's own config turns the venue rules on for every paper.tex",
|
|
|
555
557
|
expect(OPTIONAL_RULES.has("pdf/last-page-balance")).toBe(true);
|
|
556
558
|
});
|
|
557
559
|
});
|
|
560
|
+
|
|
561
|
+
describe("a project's own preset, and the facts' other spellings", () => {
|
|
562
|
+
// A standalone preset (no `extends`, so it carries a `tex` block) that sets a body size with no
|
|
563
|
+
// tolerance, a reference range, two columns — and no page size and no font families.
|
|
564
|
+
const HOUSE = {
|
|
565
|
+
[`${PAPER}/house.jsonc`]: JSON.stringify({
|
|
566
|
+
tex: { packages: { acmart: ["acmart.cls"] } },
|
|
567
|
+
format: { columns: 2, body_pt: 9, ref_pt_min: 7, ref_pt_max: 8 },
|
|
568
|
+
}),
|
|
569
|
+
};
|
|
570
|
+
const house = (patch: (f: Json) => void = () => {}) =>
|
|
571
|
+
lint({
|
|
572
|
+
venue: { extends: "./house.jsonc" },
|
|
573
|
+
facts: withFacts(patch),
|
|
574
|
+
extra: HOUSE,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("no page size in the preset: nothing to compare, whatever the PDF measures", () => {
|
|
578
|
+
expect(ids(house((f) => ((f.page_w_in = 5), (f.page_h_in = 5))))).toEqual(
|
|
579
|
+
[],
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("a body size with no tolerance is not judged; a reference range with none is judged exactly", () => {
|
|
584
|
+
expect(ids(house((f) => (f.body_pt = 12)))).toEqual([]);
|
|
585
|
+
// 6.9 pt passes agenticdev (its body_pt_tol widens the range) and fails here, where nothing does.
|
|
586
|
+
expect(ids(house((f) => (f.ref_pt = 6.9)))).toEqual(["pdf/limits:refPt"]);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("facts naming the PDF by an absolute path are checked against that file", () => {
|
|
590
|
+
expect(
|
|
591
|
+
lint({
|
|
592
|
+
venue: DECL,
|
|
593
|
+
facts: withFacts((f) => (f.pdf = `${PAPER}/paper.pdf`)),
|
|
594
|
+
}),
|
|
595
|
+
).toEqual([]);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("a PDF with no fonts at all: each missing family says so", () => {
|
|
599
|
+
const fs = lint({ venue: DECL, facts: withFacts((f) => (f.fonts = [])) });
|
|
600
|
+
expect(ids(fs)).toEqual(["pdf/fonts:noFamily", "pdf/fonts:noFamily"]);
|
|
601
|
+
expect(fs[0]?.message).toMatch(/\(no fonts\)/);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("file names in messages are relative to ESLint's cwd — and never empty", () => {
|
|
605
|
+
const at = (cwd: string) =>
|
|
606
|
+
lint(
|
|
607
|
+
{ venue: { kind: "short" }, facts: null, pdf: null },
|
|
608
|
+
undefined,
|
|
609
|
+
{},
|
|
610
|
+
cwd,
|
|
611
|
+
)[0]?.message;
|
|
612
|
+
expect(at("/work")).toMatch(/set "extends" in papers\/p\/paperlint\.json/);
|
|
613
|
+
expect(at(`${PAPER}/paperlint.json`)).toMatch(
|
|
614
|
+
/set "extends" in \/work\/papers\/p\/paperlint\.json/,
|
|
615
|
+
);
|
|
616
|
+
});
|
|
617
|
+
});
|
package/src/venue-rules.ts
CHANGED
|
@@ -253,17 +253,19 @@ function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
|
|
|
253
253
|
[kind.limits.bodyPagesMax, g.body_pages, "body pages"],
|
|
254
254
|
[kind.limits.refPagesMax, g.ref_pages, "reference pages"],
|
|
255
255
|
];
|
|
256
|
-
return pages
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
256
|
+
return pages.flatMap(([max, got, what]) =>
|
|
257
|
+
max !== null && got > max
|
|
258
|
+
? [
|
|
259
|
+
finding("pages", {
|
|
260
|
+
what,
|
|
261
|
+
got,
|
|
262
|
+
max,
|
|
263
|
+
venue: resolved.venue,
|
|
264
|
+
kind: kind.name,
|
|
265
|
+
}),
|
|
266
|
+
]
|
|
267
|
+
: [],
|
|
268
|
+
);
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
function judgeRefPt(g: FlatGeometry, resolved: Resolved): Finding[] {
|