paperlint 3.1.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/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.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -8
- 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/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/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/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 +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- 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/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.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +13 -9
- 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/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/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/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
|
@@ -13,10 +13,19 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
15
|
import { join, dirname } from "node:path";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
mkdirSync,
|
|
18
|
+
mkdtempSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
realpathSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
17
25
|
import { fileURLToPath } from "node:url";
|
|
18
26
|
import { Linter } from "eslint";
|
|
19
27
|
import markdown from "@eslint/markdown";
|
|
28
|
+
import { createChecker } from "../lib/check.mjs";
|
|
20
29
|
|
|
21
30
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
22
31
|
const FIX = join(HERE, "..", "fixtures", "paper-research-question");
|
|
@@ -24,11 +33,7 @@ const FIX = join(HERE, "..", "fixtures", "paper-research-question");
|
|
|
24
33
|
const { texLanguage } = await import(join(HERE, "latex-language.mjs"));
|
|
25
34
|
const rq = (await import(join(HERE, "paper-research-question.mjs"))).default;
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
const check = (label, cond) => {
|
|
29
|
-
assert.ok(cond, label);
|
|
30
|
-
n++;
|
|
31
|
-
};
|
|
36
|
+
const check = createChecker();
|
|
32
37
|
|
|
33
38
|
const linter = new Linter();
|
|
34
39
|
|
|
@@ -141,6 +146,67 @@ check(
|
|
|
141
146
|
.length === 1,
|
|
142
147
|
);
|
|
143
148
|
|
|
149
|
+
// ── a scorecard that cannot be read is no scorecard: silence, never a crash ────────────
|
|
150
|
+
// A directory where the file should be (EISDIR), and front matter that is not YAML — which
|
|
151
|
+
// `paper/stages` reports on the scorecard itself, so this rule does not report it twice.
|
|
152
|
+
{
|
|
153
|
+
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-rq-")));
|
|
154
|
+
try {
|
|
155
|
+
const paperIn = (name, status) => {
|
|
156
|
+
const dir = join(work, name);
|
|
157
|
+
mkdirSync(dir);
|
|
158
|
+
status(join(dir, "PIPELINE-STATUS.md"));
|
|
159
|
+
const path = join(dir, "paper.md");
|
|
160
|
+
writeFileSync(path, "# A paper\n");
|
|
161
|
+
// A linter rooted at the scratch directory: files outside `cwd` match no config.
|
|
162
|
+
return new Linter({ cwd: work }).verify(
|
|
163
|
+
"# A paper\n",
|
|
164
|
+
[
|
|
165
|
+
{
|
|
166
|
+
files: ["**/*.md"],
|
|
167
|
+
plugins: { markdown, paper: rq },
|
|
168
|
+
language: "markdown/gfm",
|
|
169
|
+
rules: { "paper/research-question": "error" },
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
path,
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
check(
|
|
176
|
+
"a scorecard path that is a directory: no finding, no crash",
|
|
177
|
+
JSON.stringify(paperIn("dir", (p) => mkdirSync(p))) === "[]",
|
|
178
|
+
() => paperIn("dir2", (p) => mkdirSync(p)),
|
|
179
|
+
);
|
|
180
|
+
check(
|
|
181
|
+
"a scorecard whose front matter is not YAML: no finding, no crash",
|
|
182
|
+
JSON.stringify(
|
|
183
|
+
paperIn("yaml", (p) =>
|
|
184
|
+
writeFileSync(p, "---\nstages: [submitted\n---\n"),
|
|
185
|
+
),
|
|
186
|
+
) === "[]",
|
|
187
|
+
);
|
|
188
|
+
} finally {
|
|
189
|
+
rmSync(work, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── a language whose source code carries no text at all: nothing to compare, silence ───
|
|
194
|
+
{
|
|
195
|
+
const reports = [];
|
|
196
|
+
rq.rules["research-question"]
|
|
197
|
+
.create({
|
|
198
|
+
options: [],
|
|
199
|
+
filename: join(FIX, "shipped-no-rq", "paper.tex"),
|
|
200
|
+
sourceCode: {},
|
|
201
|
+
report: (r) => reports.push(r),
|
|
202
|
+
})
|
|
203
|
+
["root:exit"]({});
|
|
204
|
+
check(
|
|
205
|
+
"a source code with neither `raw` nor `text` is skipped, even for a shipped paper",
|
|
206
|
+
reports.length === 0,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
144
210
|
console.log(
|
|
145
|
-
`✓ ${String(
|
|
211
|
+
`✓ ${String(check.count)} assertions passed — paper/research-question, the debt of a shipped paper`,
|
|
146
212
|
);
|
|
@@ -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
|
+
});
|
|
@@ -201,8 +201,15 @@ function texRuns(nodes, math, src, out) {
|
|
|
201
201
|
return out;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
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;
|
|
206
213
|
// Only the document body is typeset. A fragment with no `document` environment is all body.
|
|
207
214
|
const document = root.find(
|
|
208
215
|
(n) => n.type === "environment" && n.env === "document",
|
|
@@ -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));
|
|
@@ -293,6 +293,32 @@ describe("bib/reachable-entry", () => {
|
|
|
293
293
|
expect(m.column).toBe(1);
|
|
294
294
|
expect(m.message).toMatch(/`lost`/);
|
|
295
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
|
+
});
|
|
296
322
|
});
|
|
297
323
|
|
|
298
324
|
describe("the escape hatch — a disable directive in a `%` comment", () => {
|
|
@@ -335,3 +361,122 @@ describe("the escape hatch — a disable directive in a `%` comment", () => {
|
|
|
335
361
|
).toHaveLength(1);
|
|
336
362
|
});
|
|
337
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);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import { ESLint } from "eslint";
|
|
7
7
|
import markdown from "@eslint/markdown";
|
|
8
|
-
import review from "./review-frontmatter.mjs";
|
|
8
|
+
import review, { errorsOf } from "./review-frontmatter.mjs";
|
|
9
9
|
import sibling from "./sibling-frontmatter.mjs";
|
|
10
10
|
|
|
11
11
|
async function lint(text) {
|
|
@@ -81,6 +81,42 @@ describe("review/frontmatter — the findings record", () => {
|
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
describe("review/frontmatter — what the parser and the validator may hand back", () => {
|
|
85
|
+
/** The rule's visitors over a fake context; the reports it makes. */
|
|
86
|
+
const visit = (value) => {
|
|
87
|
+
const reports = [];
|
|
88
|
+
const v = review.rules.frontmatter.create({
|
|
89
|
+
report: (r) => reports.push(r),
|
|
90
|
+
});
|
|
91
|
+
v.yaml({ value });
|
|
92
|
+
v["root:exit"]({});
|
|
93
|
+
return reports.map(
|
|
94
|
+
(r) => r.messageId + (r.data?.why ? `: ${r.data.why}` : ""),
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
it("an empty frontmatter block is validated as `{}` — silent", async () => {
|
|
99
|
+
expect(await lint("---\n---\n# Review\n")).toEqual([]);
|
|
100
|
+
expect(visit(undefined)).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("a parser failure without a YAML reason is reported by its message, or as unparseable", () => {
|
|
104
|
+
const throwing = (thrown) => ({
|
|
105
|
+
toString() {
|
|
106
|
+
throw thrown;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
expect(visit(throwing(new Error("boom")))).toEqual(["malformed: boom"]);
|
|
110
|
+
expect(visit(throwing("not an Error"))).toEqual(["malformed: unparseable"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("a failed validation with no errors attached names nothing", () => {
|
|
114
|
+
const validate = () => false;
|
|
115
|
+
validate.errors = null;
|
|
116
|
+
expect(errorsOf(validate, {})).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
84
120
|
describe("sibling/frontmatter — the same mechanism, the package's own sibling-card schema", () => {
|
|
85
121
|
async function card(text) {
|
|
86
122
|
const eslint = new ESLint({
|