paperlint 3.0.0 → 3.1.1
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 +51 -56
- package/CONTRIBUTING.md +30 -18
- package/README.md +179 -180
- 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/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- 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-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 +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- 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/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/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/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 +4 -4
- 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/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- 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/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.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.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 +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- 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 +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- 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 +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -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.schema.json +1 -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/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 +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/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/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 +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -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 +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -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/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- 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-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +77 -7
- package/src/engine.ts +35 -0
- 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 +4 -4
- 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/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -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/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- 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 -79
- package/src/structure.mutations.mjs +0 -83
|
@@ -16,6 +16,7 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import { Linter } from "eslint";
|
|
17
17
|
import markdown from "@eslint/markdown";
|
|
18
18
|
import stages from "./paper-stages.mjs";
|
|
19
|
+
import { createChecker } from "../lib/check.mjs";
|
|
19
20
|
|
|
20
21
|
const FIX = join(
|
|
21
22
|
dirname(fileURLToPath(import.meta.url)),
|
|
@@ -25,11 +26,7 @@ const FIX = join(
|
|
|
25
26
|
);
|
|
26
27
|
const linter = new Linter();
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
const check = (label, cond) => {
|
|
30
|
-
assert.ok(cond, label);
|
|
31
|
-
n++;
|
|
32
|
-
};
|
|
29
|
+
const check = createChecker();
|
|
33
30
|
|
|
34
31
|
// 🔴 The `n` counter is RUNNING TOTAL across the whole file, so printing it under one rule's
|
|
35
32
|
// heading would credit that rule with someone else's asserts. That's exactly what happened: the
|
|
@@ -38,8 +35,8 @@ const check = (label, cond) => {
|
|
|
38
35
|
// stands next to it again.
|
|
39
36
|
let mark = 0;
|
|
40
37
|
const since = () => {
|
|
41
|
-
const d =
|
|
42
|
-
mark =
|
|
38
|
+
const d = check.count - mark;
|
|
39
|
+
mark = check.count;
|
|
43
40
|
return d;
|
|
44
41
|
};
|
|
45
42
|
|
|
@@ -141,12 +138,10 @@ console.log(
|
|
|
141
138
|
const { mkdtempSync, writeFileSync, mkdirSync, rmSync } =
|
|
142
139
|
await import("node:fs");
|
|
143
140
|
const root = mkdtempSync(join(FIX, "..", ".tmp-stages-src-"));
|
|
144
|
-
// 🔴 try/finally, NOT cleanup at the end of the block.
|
|
145
|
-
//
|
|
146
|
-
// the
|
|
147
|
-
// in the repository
|
|
148
|
-
// cosmetic: the mutation driver refuses to run on a dirty tree, so the harness would break
|
|
149
|
-
// the next run.
|
|
141
|
+
// 🔴 try/finally, NOT cleanup at the end of the block. Any `check()` above can throw on
|
|
142
|
+
// failure — that is precisely when the harness is doing its job — so cleanup written after
|
|
143
|
+
// the checks would never run on that path and would leave a `.tmp-stages-src-*` directory
|
|
144
|
+
// behind in the repository.
|
|
150
145
|
try {
|
|
151
146
|
const paper = join(root, "one");
|
|
152
147
|
mkdirSync(join(paper, "versions"), { recursive: true });
|
|
@@ -237,4 +232,4 @@ console.log(
|
|
|
237
232
|
`✓ ${String(since())} assertions passed — paper/source, frozen bytes instead of a sha`,
|
|
238
233
|
);
|
|
239
234
|
|
|
240
|
-
console.log(`✓ ${String(
|
|
235
|
+
console.log(`✓ ${String(check.count)} assertions passed in total`);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paper/stages` and `paper/source` on the malformed declarations the fixture papers do not carry:
|
|
3
|
+
* YAML that does not parse, `stages` that is not a list, an unknown or missing stage, a missing
|
|
4
|
+
* field, a date that is not a date — and, in `versions/`, the files the rule must NOT count.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import markdown from "@eslint/markdown";
|
|
9
|
+
import { Linter } from "eslint";
|
|
10
|
+
import { test } from "vitest";
|
|
11
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
12
|
+
import stages from "./paper-stages.mjs";
|
|
13
|
+
|
|
14
|
+
const root = useTempDir("paper-stages-");
|
|
15
|
+
// Flat config matches `files` against paths under the linter's cwd; the temp papers live outside
|
|
16
|
+
// the repository, so the cwd is the temp root (otherwise: "No matching configuration found").
|
|
17
|
+
const linter = new Linter({ cwd: root });
|
|
18
|
+
|
|
19
|
+
/** The messages of `rule` on a PIPELINE-STATUS.md with `frontmatter`, in a paper holding `files`. */
|
|
20
|
+
function lint(name, frontmatter, { rule = "stages", files = {} } = {}) {
|
|
21
|
+
const dir = writeTree(join(root, name), files);
|
|
22
|
+
const file = join(dir, "PIPELINE-STATUS.md");
|
|
23
|
+
const msgs = linter.verify(
|
|
24
|
+
`---\n${frontmatter}\n---\n\n# Status\n`,
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
files: ["**/*.md"],
|
|
28
|
+
plugins: { markdown, paper: stages },
|
|
29
|
+
language: "markdown/gfm",
|
|
30
|
+
languageOptions: { frontmatter: "yaml" },
|
|
31
|
+
rules: { [`paper/${rule}`]: "error" },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
file,
|
|
35
|
+
);
|
|
36
|
+
assert.deepEqual(
|
|
37
|
+
msgs.filter((m) => m.fatal),
|
|
38
|
+
[],
|
|
39
|
+
`${name}: the rule threw`,
|
|
40
|
+
);
|
|
41
|
+
return msgs.map((m) => m.message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("YAML that does not parse is named; paper/source leaves it to paper/stages", () => {
|
|
45
|
+
const [msg] = lint("bad-yaml", "stages: [");
|
|
46
|
+
assert.match(msg, /^the frontmatter does not parse as YAML: /);
|
|
47
|
+
assert.deepEqual(lint("bad-yaml-src", "stages: [", { rule: "source" }), []);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("`stages` must be a list: empty and a scalar are each named", () => {
|
|
51
|
+
assert.deepEqual(lint("empty", "stages:"), [
|
|
52
|
+
"`stages` must be a LIST of entries, not empty — a paper can reach the same stage twice",
|
|
53
|
+
]);
|
|
54
|
+
assert.deepEqual(lint("scalar", "stages: submitted"), [
|
|
55
|
+
"`stages` must be a LIST of entries, not string — a paper can reach the same stage twice",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("each entry: an unknown or missing stage, missing fields, a date that is not a date", () => {
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
lint(
|
|
62
|
+
"entries",
|
|
63
|
+
[
|
|
64
|
+
"stages:",
|
|
65
|
+
" - stage: reviewed",
|
|
66
|
+
" - date: 2026-08-01",
|
|
67
|
+
" - stage: submitted",
|
|
68
|
+
" date: 2026-08-01",
|
|
69
|
+
" - stage: arxiv",
|
|
70
|
+
" date: soon",
|
|
71
|
+
" pdf: versions/x.pdf",
|
|
72
|
+
" bytes: 1",
|
|
73
|
+
" - stage: camera-ready",
|
|
74
|
+
" date: 20260801",
|
|
75
|
+
" pdf: versions/x.pdf",
|
|
76
|
+
" bytes: 1",
|
|
77
|
+
" - stage: arxiv",
|
|
78
|
+
' date: "2026-08-03"',
|
|
79
|
+
" pdf: versions/x.pdf",
|
|
80
|
+
" bytes: 1",
|
|
81
|
+
].join("\n"),
|
|
82
|
+
),
|
|
83
|
+
[
|
|
84
|
+
"unknown stage «reviewed» — the vocabulary is: submitted · camera-ready · arxiv",
|
|
85
|
+
"unknown stage «» — the vocabulary is: submitted · camera-ready · arxiv",
|
|
86
|
+
"the «submitted» entry has no `pdf` field",
|
|
87
|
+
"the «submitted» entry has no `bytes` field",
|
|
88
|
+
"the date «soon» in the «arxiv» entry is not YYYY-MM-DD",
|
|
89
|
+
"the date «20260801» in the «camera-ready» entry is not YYYY-MM-DD",
|
|
90
|
+
// A quoted date stays a string and is read the same as an unquoted one.
|
|
91
|
+
"stage «arxiv» (2026-08-03) is declared, but `versions/x.pdf` is not on disk",
|
|
92
|
+
],
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("paper/source skips a `stages` that is not a list, and entries whose stage is unknown or missing", () => {
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
lint("src-scalar", "stages: submitted", { rule: "source" }),
|
|
99
|
+
[],
|
|
100
|
+
);
|
|
101
|
+
assert.deepEqual(
|
|
102
|
+
lint("src-skip", "stages:\n - stage: reviewed\n - date: 2026-08-01", {
|
|
103
|
+
rule: "source",
|
|
104
|
+
}),
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("versions/: only dated stage pdfs count — not other files, STALE ones, odd names or other stages", () => {
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
lint("versions", "title: x", {
|
|
112
|
+
files: {
|
|
113
|
+
"versions/README.md": "x",
|
|
114
|
+
"versions/2026-08-01-submitted.STALE-wrong.pdf": "x",
|
|
115
|
+
"versions/draft.pdf": "x",
|
|
116
|
+
"versions/2026-08-01-review.pdf": "x",
|
|
117
|
+
"versions/2026-08-02-arxiv.pdf": "x",
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
[
|
|
121
|
+
"`versions/2026-08-02-arxiv.pdf` is frozen, but no «arxiv» stage on 2026-08-02 is declared in `stages` — the artefact ran ahead of the declaration",
|
|
122
|
+
],
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a frontmatter node without a value (another markdown language) reads as empty YAML", () => {
|
|
127
|
+
const reports = [];
|
|
128
|
+
const context = {
|
|
129
|
+
filename: join(root, "valueless", "PIPELINE-STATUS.md"),
|
|
130
|
+
report: (r) => reports.push(r),
|
|
131
|
+
};
|
|
132
|
+
for (const rule of ["stages", "source"])
|
|
133
|
+
stages.rules[rule].create(context).yaml({ type: "yaml" });
|
|
134
|
+
assert.deepEqual(reports, []);
|
|
135
|
+
});
|
|
@@ -50,6 +50,14 @@
|
|
|
50
50
|
* inserts the zero there. The arXiv lookbehind stays: in `2310.05736` the dot follows a digit.
|
|
51
51
|
* A text macro outside the known set (`\hl{.05}`) is read as a parameter and NOT reported — a
|
|
52
52
|
* miss costs less than a finding on markup.
|
|
53
|
+
*
|
|
54
|
+
* 🔴 A NUMBER AFTER `¶` OR `§` IS A DESIGNATOR, NOT A DECIMAL (#102). An auditing standard is cited
|
|
55
|
+
* by paragraph as `¶¶.42`, and `--fix` rewrote it to `¶¶0.42` — a fix that changed a reference.
|
|
56
|
+
* So a lexeme right after the sign (`¶`, `§`, `\P`, `\S`, spaces or `~` between) is silent. The
|
|
57
|
+
* same sentence then goes on `and .44`: a paragraph number continuing the list, or a decimal —
|
|
58
|
+
* the text does not say which. In a paragraph that already cites `¶.nn` this way, a bare decimal
|
|
59
|
+
* is REPORTED WITHOUT A FIX and the zero is offered as a suggestion: an autofix must never be the
|
|
60
|
+
* one to decide what a number means. A real `.05` in any other paragraph is fixed as before.
|
|
53
61
|
*/
|
|
54
62
|
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
55
63
|
|
|
@@ -193,8 +201,15 @@ function texRuns(nodes, math, src, out) {
|
|
|
193
201
|
return out;
|
|
194
202
|
}
|
|
195
203
|
|
|
196
|
-
|
|
197
|
-
|
|
204
|
+
/**
|
|
205
|
+
* The visible runs of a LaTeX source. `parse` is unified-latex's parser; a test hands in a tree
|
|
206
|
+
* with the fields the walk tolerates being absent.
|
|
207
|
+
*/
|
|
208
|
+
export function texVisibleRuns(
|
|
209
|
+
raw,
|
|
210
|
+
{ parse = (s) => getParser().parse(s) } = {},
|
|
211
|
+
) {
|
|
212
|
+
const root = parse(raw).content;
|
|
198
213
|
// Only the document body is typeset. A fragment with no `document` environment is all body.
|
|
199
214
|
const document = root.find(
|
|
200
215
|
(n) => n.type === "environment" && n.env === "document",
|
|
@@ -312,16 +327,43 @@ function sectionWord(context) {
|
|
|
312
327
|
// (2310.05736) out: there the dot follows a digit.
|
|
313
328
|
const BARE_DECIMAL = /(?<![\d.\w])\.\d{2,}\b/g;
|
|
314
329
|
|
|
330
|
+
// A paragraph or section sign — the glyph, or `\P`/`\S` not followed by a letter — and what may
|
|
331
|
+
// sit between it and the number. Both read the SOURCE, because in LaTeX `\P\P.42` reaches the
|
|
332
|
+
// visible runs as `.42` alone: the macros end the run.
|
|
333
|
+
const DESIGNATOR_BEFORE = /(?:[¶§]|\\[PS](?![A-Za-z]))[ \t~]*$/;
|
|
334
|
+
const DESIGNATED = /(?:[¶§]|\\[PS](?![A-Za-z]))[ \t~]*\.\d/;
|
|
335
|
+
|
|
336
|
+
/** The source of the paragraph holding `i`, up to `i`: from the last blank line before it. */
|
|
337
|
+
const paragraphBefore = (src, i) => {
|
|
338
|
+
const head = src.slice(0, i);
|
|
339
|
+
const blank = [...head.matchAll(/\n[ \t]*\n/g)].at(-1);
|
|
340
|
+
return head.slice(blank ? blank.index + blank[0].length : 0);
|
|
341
|
+
};
|
|
342
|
+
|
|
315
343
|
function leadingZero(context) {
|
|
316
|
-
|
|
344
|
+
const sc = context.sourceCode;
|
|
345
|
+
const src = isTex(sc) ? sc.raw : sc.text;
|
|
346
|
+
for (const r of visibleRuns(sc))
|
|
317
347
|
for (const m of r.text.matchAll(BARE_DECIMAL)) {
|
|
318
348
|
const dot = r.offs[m.index];
|
|
319
349
|
if (dot === null || dot === undefined) continue;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
350
|
+
const before = paragraphBefore(src, dot);
|
|
351
|
+
if (DESIGNATOR_BEFORE.test(before)) continue;
|
|
352
|
+
const zero = (f) => f.insertTextBeforeRange([dot, dot], "0");
|
|
353
|
+
at(
|
|
354
|
+
context,
|
|
355
|
+
dot,
|
|
356
|
+
dot + 1,
|
|
357
|
+
DESIGNATED.test(before)
|
|
358
|
+
? {
|
|
359
|
+
messageId: "ambiguous",
|
|
360
|
+
data: { n: m[0] },
|
|
361
|
+
suggest: [
|
|
362
|
+
{ messageId: "insertZero", data: { n: m[0] }, fix: zero },
|
|
363
|
+
],
|
|
364
|
+
}
|
|
365
|
+
: { messageId: "bare", data: { n: m[0] }, fix: zero },
|
|
366
|
+
);
|
|
325
367
|
}
|
|
326
368
|
}
|
|
327
369
|
|
|
@@ -349,13 +391,14 @@ function figureRefStyle(context) {
|
|
|
349
391
|
}
|
|
350
392
|
}
|
|
351
393
|
|
|
352
|
-
const rule = (description, messages, check) => ({
|
|
394
|
+
const rule = (description, messages, check, meta = {}) => ({
|
|
353
395
|
meta: {
|
|
354
396
|
type: "suggestion",
|
|
355
397
|
fixable: "code",
|
|
356
398
|
docs: { description },
|
|
357
399
|
schema: [],
|
|
358
400
|
messages,
|
|
401
|
+
...meta,
|
|
359
402
|
},
|
|
360
403
|
create: (context) => ({ "root:exit": () => check(context) }),
|
|
361
404
|
});
|
|
@@ -373,8 +416,12 @@ export default {
|
|
|
373
416
|
"a decimal below 1 written without its leading zero (IEEE / ISO 80000-1); fixable",
|
|
374
417
|
{
|
|
375
418
|
bare: "`{{n}}` has no leading zero — write `0{{n}}` (IEEE / ISO 80000-1); `paperlint lint --fix` inserts it",
|
|
419
|
+
ambiguous:
|
|
420
|
+
"`{{n}}` in a paragraph that cites `¶`/`§` numbers: a decimal without its leading zero, or another paragraph number? Not fixed automatically — write `0{{n}}` for a decimal, or put the sign before a paragraph number",
|
|
421
|
+
insertZero: "It is a decimal: write `0{{n}}`",
|
|
376
422
|
},
|
|
377
423
|
leadingZero,
|
|
424
|
+
{ hasSuggestions: true },
|
|
378
425
|
),
|
|
379
426
|
"figure-ref-style": rule(
|
|
380
427
|
"`Fig.~\\ref` and `Figure~\\ref` mixed in one document; fixable to the majority form",
|
|
@@ -16,7 +16,7 @@ import { readFileSync } from "node:fs";
|
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { texLanguage } from "./latex-language.mjs";
|
|
19
|
-
import typography from "./paper-typography.mjs";
|
|
19
|
+
import typography, { texVisibleRuns } from "./paper-typography.mjs";
|
|
20
20
|
import bib from "./bib-reachable-entry.mjs";
|
|
21
21
|
|
|
22
22
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -203,6 +203,46 @@ describe("paper/leading-zero", () => {
|
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
describe("paper/leading-zero — a number after ¶ or § is not a decimal (#102)", () => {
|
|
207
|
+
// From a consumer's bibliography: `--fix` rewrote the paragraph number `¶¶.42` to `¶¶0.42`.
|
|
208
|
+
const CITE =
|
|
209
|
+
"PCAOB. \\emph{Audit of Financial Statements}, ¶¶.42 (design\neffectiveness) and .44 (operating effectiveness).";
|
|
210
|
+
|
|
211
|
+
it.each([
|
|
212
|
+
["¶¶.42", "Standard 5, ¶¶.42 applies."],
|
|
213
|
+
["¶ .42", "Standard 5, ¶ .42 applies."],
|
|
214
|
+
["\\P\\P.42", "Standard 5, \\P\\P.42 applies."],
|
|
215
|
+
["§.12", "Rule 10b-5, §.12 applies."],
|
|
216
|
+
])("SILENT: %s is a designator", async (_, body) => {
|
|
217
|
+
// Guards: the defect — the lexeme after a paragraph/section sign was read as a decimal.
|
|
218
|
+
expect(ids(await lint(doc(body)), "paper/leading-zero")).toEqual([]);
|
|
219
|
+
expect(await fixed(doc(body))).toBe(doc(body));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("🔴 the list continuation `and .44` is reported WITHOUT a fix, with the zero as a suggestion", async () => {
|
|
223
|
+
const res = await lint(doc(CITE));
|
|
224
|
+
const found = ids(res, "paper/leading-zero");
|
|
225
|
+
expect(found.map((m) => [m.line, m.message.slice(0, 5)])).toEqual([
|
|
226
|
+
[4, "`.44`"],
|
|
227
|
+
]);
|
|
228
|
+
expect(found[0].fix).toBeUndefined();
|
|
229
|
+
expect(found[0].suggestions).toHaveLength(1);
|
|
230
|
+
// Guards: --fix changes nothing in an ambiguous paragraph — neither number.
|
|
231
|
+
expect(await fixed(doc(CITE))).toBe(doc(CITE));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("…while a real decimal still fixes, in markdown too, even beside a cited paragraph", async () => {
|
|
235
|
+
const body = `${CITE}\n\nWe use a threshold of .05 throughout.`;
|
|
236
|
+
expect(await fixed(doc(body))).toBe(
|
|
237
|
+
doc(`${CITE}\n\nWe use a threshold of 0.05 throughout.`),
|
|
238
|
+
);
|
|
239
|
+
const md = "See ¶¶.42 and .44.\n\nSignificant at p<.05.\n";
|
|
240
|
+
expect(await fixed(md, "paper.md")).toBe(
|
|
241
|
+
"See ¶¶.42 and .44.\n\nSignificant at p<0.05.\n",
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
206
246
|
describe("paper/figure-ref-style", () => {
|
|
207
247
|
it("the minority form is reported and rewritten to the majority", async () => {
|
|
208
248
|
const src = doc("Figure~\\ref{a}, Figure~\\ref{b} and Fig.~\\ref{c}.");
|
|
@@ -253,6 +293,32 @@ describe("bib/reachable-entry", () => {
|
|
|
253
293
|
expect(m.column).toBe(1);
|
|
254
294
|
expect(m.message).toMatch(/`lost`/);
|
|
255
295
|
});
|
|
296
|
+
|
|
297
|
+
it("an entry with no key is reported as `?`, not skipped", async () => {
|
|
298
|
+
const [m] = ids(
|
|
299
|
+
await lint(bibDoc("@article{\n title = {x}}")),
|
|
300
|
+
"bib/reachable-entry",
|
|
301
|
+
);
|
|
302
|
+
expect(m.message).toMatch(/`\?`/);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("a source code with only `text`, or with neither text nor raw", () => {
|
|
306
|
+
const run = (sourceCode) => {
|
|
307
|
+
const out = [];
|
|
308
|
+
bib.rules["reachable-entry"]
|
|
309
|
+
.create({
|
|
310
|
+
sourceCode: {
|
|
311
|
+
getLocFromIndex: (i) => ({ line: 1, column: i }),
|
|
312
|
+
...sourceCode,
|
|
313
|
+
},
|
|
314
|
+
report: (r) => out.push(r.data.key),
|
|
315
|
+
})
|
|
316
|
+
["root:exit"]();
|
|
317
|
+
return out;
|
|
318
|
+
};
|
|
319
|
+
expect(run({ text: bibDoc("@article{t, title = {x}}") })).toEqual(["t"]);
|
|
320
|
+
expect(run({})).toEqual([]);
|
|
321
|
+
});
|
|
256
322
|
});
|
|
257
323
|
|
|
258
324
|
describe("the escape hatch — a disable directive in a `%` comment", () => {
|
|
@@ -295,3 +361,122 @@ describe("the escape hatch — a disable directive in a `%` comment", () => {
|
|
|
295
361
|
).toHaveLength(1);
|
|
296
362
|
});
|
|
297
363
|
});
|
|
364
|
+
|
|
365
|
+
describe("where visible text comes from — the walk's other paths", () => {
|
|
366
|
+
it("a decimal in a braced group, in a typeset math argument, in a table's last argument", async () => {
|
|
367
|
+
const src = doc(
|
|
368
|
+
"In a group {.05}.\n\n\\multicolumn{2}{c}{.35}\n\n$x_{.75}$",
|
|
369
|
+
);
|
|
370
|
+
expect(
|
|
371
|
+
ids(await lint(src), "paper/leading-zero").map((m) =>
|
|
372
|
+
m.message.slice(0, 5),
|
|
373
|
+
),
|
|
374
|
+
).toEqual(["`.05`", "`.35`", "`.75`"]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("🔴 inside `\\frac{}{}` unified-latex gives offsets relative to the argument: the decimal is NOT reported", async () => {
|
|
378
|
+
// Guards: a character whose file offset is unknown gets no report and, above all, no fix at
|
|
379
|
+
// a guessed place. Known false negative, recorded rather than hidden.
|
|
380
|
+
const src = doc("$\\frac{1}{.25}$");
|
|
381
|
+
expect(texVisibleRuns(src).map((r) => [r.text, r.offs])).toContainEqual([
|
|
382
|
+
".25",
|
|
383
|
+
[null, null, null],
|
|
384
|
+
]);
|
|
385
|
+
expect(ids(await lint(src), "paper/leading-zero")).toEqual([]);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("a last-argument macro written without its argument ends the run and reports nothing", async () => {
|
|
389
|
+
expect(ids(await lint(doc("See \\href")), "paper/leading-zero")).toEqual(
|
|
390
|
+
[],
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("a tree with no content, no args and a non-string environment name is walked, not thrown on", () => {
|
|
395
|
+
const tree = {
|
|
396
|
+
content: [
|
|
397
|
+
{ type: "string", content: "a" },
|
|
398
|
+
{ type: "group" },
|
|
399
|
+
{ type: "environment", env: { weird: true } },
|
|
400
|
+
{ type: "inlinemath" },
|
|
401
|
+
{ type: "string", content: "b" },
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
expect(texVisibleRuns("ab", { parse: () => tree })).toEqual([
|
|
405
|
+
{ text: "a", offs: [null] },
|
|
406
|
+
{ text: "b", offs: [null] },
|
|
407
|
+
]);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("markdown: offsets that cannot be followed, and rules that are LaTeX-only", () => {
|
|
412
|
+
const mdLint = async (text, rules) => {
|
|
413
|
+
const [res] = await new ESLint({
|
|
414
|
+
cwd: FIX,
|
|
415
|
+
overrideConfigFile: true,
|
|
416
|
+
overrideConfig: [
|
|
417
|
+
{
|
|
418
|
+
files: ["**/*.md"],
|
|
419
|
+
plugins: { markdown, paper: typography },
|
|
420
|
+
language: "markdown/gfm",
|
|
421
|
+
rules,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
}).lintText(text, { filePath: join(FIX, "inline", "paper.md") });
|
|
425
|
+
expect(res.messages.filter((m) => m.fatal)).toEqual([]);
|
|
426
|
+
return res;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
it("`§` before prose is reported without a fix", async () => {
|
|
430
|
+
const src = "As § above.\n";
|
|
431
|
+
const res = await mdLint(src, { "paper/section-word": "warn" });
|
|
432
|
+
expect(res.messages.map((m) => [m.ruleId, m.column, m.fix])).toEqual([
|
|
433
|
+
["paper/section-word", 4, undefined],
|
|
434
|
+
]);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("a sign or a dot written as a character reference has no source offset: nothing is reported", async () => {
|
|
438
|
+
const res = await mdLint("See § 5 and .25.\n", {
|
|
439
|
+
"paper/section-word": "warn",
|
|
440
|
+
"paper/leading-zero": "warn",
|
|
441
|
+
});
|
|
442
|
+
expect(res.messages).toEqual([]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("figure-ref-style is LaTeX-only: silent on markdown", async () => {
|
|
446
|
+
const res = await mdLint("Figure~\\ref{a} and Fig.~\\ref{b}.\n", {
|
|
447
|
+
"paper/figure-ref-style": "warn",
|
|
448
|
+
});
|
|
449
|
+
expect(res.messages).toEqual([]);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe("a source code the rules did not expect — the defaults hold", () => {
|
|
454
|
+
const reports = (ruleId, sourceCode) => {
|
|
455
|
+
const out = [];
|
|
456
|
+
const context = {
|
|
457
|
+
sourceCode: {
|
|
458
|
+
getLocFromIndex: (i) => ({ line: 1, column: i }),
|
|
459
|
+
...sourceCode,
|
|
460
|
+
},
|
|
461
|
+
report: (r) => out.push(r),
|
|
462
|
+
};
|
|
463
|
+
typography.rules[ruleId].create(context)["root:exit"]();
|
|
464
|
+
return out;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
it("LaTeX source without an AST: no comment ranges, the sign is still found", () => {
|
|
468
|
+
const found = reports("section-word", { raw: "See §5." });
|
|
469
|
+
expect(found.map((r) => [r.messageId, r.loc.start.column])).toEqual([
|
|
470
|
+
["sign", 4],
|
|
471
|
+
]);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("markdown text nodes without positions: seen, never reported at a guessed place", () => {
|
|
475
|
+
const ast = {
|
|
476
|
+
type: "root",
|
|
477
|
+
children: [{ type: "text", value: "§5 and .25" }],
|
|
478
|
+
};
|
|
479
|
+
expect(reports("section-word", { text: "§5 and .25", ast })).toEqual([]);
|
|
480
|
+
expect(reports("leading-zero", { text: "§5 and .25", ast })).toEqual([]);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* foreign schema, facts about another PDF), and it is QUIET on a balanced page, a stub, a review
|
|
12
12
|
* build, a file that is not paper.tex — and when nobody turned it on.
|
|
13
13
|
*/
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
14
|
import { createHash } from "node:crypto";
|
|
16
15
|
import {
|
|
17
16
|
mkdirSync,
|
|
@@ -25,6 +24,7 @@ import { dirname, join } from "node:path";
|
|
|
25
24
|
import { fileURLToPath } from "node:url";
|
|
26
25
|
import { ESLint } from "eslint";
|
|
27
26
|
import { texLanguage } from "./latex-language.mjs";
|
|
27
|
+
import { createChecker } from "../lib/check.mjs";
|
|
28
28
|
|
|
29
29
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
30
30
|
const { parseFacts, judgeColumns, DEFAULT_TOLERANCE_PT } = await import(
|
|
@@ -32,11 +32,7 @@ const { parseFacts, judgeColumns, DEFAULT_TOLERANCE_PT } = await import(
|
|
|
32
32
|
);
|
|
33
33
|
const { buildConfig } = await import(join(HERE, "..", "src", "cli.ts"));
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const check = (label, cond, detail = "") => {
|
|
37
|
-
assert.ok(cond, detail ? `${label} — ${detail}` : label);
|
|
38
|
-
n++;
|
|
39
|
-
};
|
|
35
|
+
const check = createChecker();
|
|
40
36
|
|
|
41
37
|
const TEX =
|
|
42
38
|
"% a paper\n\\documentclass[sigconf]{acmart}\n\\begin{document}x\\end{document}\n";
|
|
@@ -179,6 +175,66 @@ try {
|
|
|
179
175
|
only(await lint([join(old, "paper.tex")]))[0]?.messageId === "schema",
|
|
180
176
|
);
|
|
181
177
|
|
|
178
|
+
// ── the facts' optional spellings, and a paper.tex the rule cannot place the finding in ──
|
|
179
|
+
check(
|
|
180
|
+
"parseFacts: no schema at all, or JSON null, is schema `null` — named, not crashed on",
|
|
181
|
+
parseFacts("{}").data?.got === "null" &&
|
|
182
|
+
parseFacts("null").data?.got === "null",
|
|
183
|
+
);
|
|
184
|
+
check(
|
|
185
|
+
"parseFacts: facts without a pdf_sha256 are `factsBroken`",
|
|
186
|
+
parseFacts(JSON.stringify(facts(MEASURED(1, 1), { pdf_sha256: undefined })))
|
|
187
|
+
.messageId === "factsBroken",
|
|
188
|
+
);
|
|
189
|
+
const abs = paper("absolute-pdf", null);
|
|
190
|
+
writeFileSync(
|
|
191
|
+
join(abs, "_build", "paper.facts.json"),
|
|
192
|
+
JSON.stringify(
|
|
193
|
+
facts(MEASURED(621.5, 264.8), { pdf: join(abs, "paper.pdf") }),
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
const absMsgs = only(await lint([join(abs, "paper.tex")]));
|
|
197
|
+
check(
|
|
198
|
+
"facts naming the PDF by an absolute path are read from there, and judged",
|
|
199
|
+
absMsgs.length === 1 && absMsgs[0].messageId === "unbalanced",
|
|
200
|
+
JSON.stringify(absMsgs),
|
|
201
|
+
);
|
|
202
|
+
const noClass = paper("no-documentclass", facts(MEASURED(621.5, 264.8)));
|
|
203
|
+
writeFileSync(
|
|
204
|
+
join(noClass, "paper.tex"),
|
|
205
|
+
"\\begin{document}x\\end{document}\n",
|
|
206
|
+
);
|
|
207
|
+
check(
|
|
208
|
+
"a paper.tex with no \\documentclass line: the finding goes on line 1",
|
|
209
|
+
only(await lint([join(noClass, "paper.tex")]))[0]?.line === 1,
|
|
210
|
+
);
|
|
211
|
+
// The text is linted from memory and paper.tex is not on disk: the rule cannot read it back
|
|
212
|
+
// to find the class line, and still reports — on line 1.
|
|
213
|
+
const unsaved = paper("unsaved", facts(MEASURED(621.5, 264.8)));
|
|
214
|
+
rmSync(join(unsaved, "paper.tex"));
|
|
215
|
+
const [fromMemory] = await new ESLint({
|
|
216
|
+
cwd: root,
|
|
217
|
+
overrideConfigFile: true,
|
|
218
|
+
overrideConfig: buildConfig(
|
|
219
|
+
{
|
|
220
|
+
rules: [
|
|
221
|
+
{
|
|
222
|
+
basePath: root,
|
|
223
|
+
files: ["papers/**"],
|
|
224
|
+
rules: { "pdf/last-page-balance": "error" },
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
texLanguage,
|
|
229
|
+
),
|
|
230
|
+
}).lintText(TEX, { filePath: join(unsaved, "paper.tex") });
|
|
231
|
+
const mem = only(fromMemory.messages);
|
|
232
|
+
check(
|
|
233
|
+
"a paper.tex linted from memory, not on disk: reported on line 1, not crashed on",
|
|
234
|
+
mem.length === 1 && mem[0].line === 1 && mem[0].messageId === "unbalanced",
|
|
235
|
+
JSON.stringify(fromMemory.messages),
|
|
236
|
+
);
|
|
237
|
+
|
|
182
238
|
// ── quiet ───────────────────────────────────────────────────────────────────────────
|
|
183
239
|
const bal = paper("balanced", facts(MEASURED(464.2, 461.5)));
|
|
184
240
|
const stub = paper("stub", facts({ kind: "stub", words: 12 }));
|
|
@@ -204,5 +260,5 @@ try {
|
|
|
204
260
|
}
|
|
205
261
|
|
|
206
262
|
console.log(
|
|
207
|
-
`✓ ${String(
|
|
263
|
+
`✓ ${String(check.count)} assertions passed — pdf/last-page-balance: fires on unbalanced and unjudgeable, quiet on balanced/stub/review/off`,
|
|
208
264
|
);
|
|
@@ -53,7 +53,8 @@ function describe(e) {
|
|
|
53
53
|
return `${where} ${e.message}`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/** The schema's verdict on `data` as messages. Ajv types `errors` as possibly null; that is tolerated. */
|
|
57
|
+
export const errorsOf = (validate, data) =>
|
|
57
58
|
validate(data)
|
|
58
59
|
? []
|
|
59
60
|
: (validate.errors ?? []).filter((e) => e.keyword !== "if").map(describe);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "urn:paperlint:review-frontmatter",
|
|
4
4
|
"title": "paperlint review frontmatter",
|
|
5
|
-
"description": "The YAML frontmatter of a review report under reviews/. A review with no `findings` key is not a findings record and is not checked for findings.
|
|
5
|
+
"description": "The YAML frontmatter of a review report under reviews/. A review with no `findings` key is not a findings record and is not checked for findings.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"findings": {
|