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
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status.mjs — the paper's status view, derived from the ledger. Every section it can print is
|
|
3
|
+
* driven by rows written for the purpose: fresh and stale answers, open findings with and without
|
|
4
|
+
* a report path, retired-vocabulary rows (an acquittal and a counted one), an orphan check, a check
|
|
5
|
+
* that has run and never found anything, and the all-answered footer.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { afterEach, test, vi } from "vitest";
|
|
12
|
+
import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
|
|
13
|
+
|
|
14
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const root = useTempDir("status-");
|
|
16
|
+
const consumer = join(root, "consumer");
|
|
17
|
+
const LEDGER = join(root, "runs.jsonl");
|
|
18
|
+
const paper = join(consumer, "papers", "p");
|
|
19
|
+
writeTree(paper, { "paper.md": "# P\n\ntext\n" });
|
|
20
|
+
Object.assign(process.env, {
|
|
21
|
+
CLAUDE_PROJECT_DIR: consumer,
|
|
22
|
+
PIPELINE_LEDGER: LEDGER,
|
|
23
|
+
});
|
|
24
|
+
const { inputHash } = await import("./ledger.mjs");
|
|
25
|
+
const { EXPECTED_GATES, main, unlistedGates } = await import("./status.mjs");
|
|
26
|
+
afterEach(() => rmSync(LEDGER, { force: true }));
|
|
27
|
+
|
|
28
|
+
const NOW = inputHash(paper);
|
|
29
|
+
const TS = "2026-09-01T10:30:00.000Z";
|
|
30
|
+
/** A ledger row for `key` on paper p. No skill is installed here, so its current hash is null. */
|
|
31
|
+
const row = (key, fields) => {
|
|
32
|
+
const [skill, check] = key.split("/");
|
|
33
|
+
return {
|
|
34
|
+
ts: TS,
|
|
35
|
+
skill,
|
|
36
|
+
check: check ?? skill,
|
|
37
|
+
paper: "p",
|
|
38
|
+
inputSha: NOW,
|
|
39
|
+
skillSha: null,
|
|
40
|
+
...fields,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const NO_WITNESS = { kind: "ABSTAINED", reason: "no-witness", findings: 0 };
|
|
44
|
+
|
|
45
|
+
function view(rows) {
|
|
46
|
+
writeFileSync(LEDGER, rows.map((r) => JSON.stringify(r)).join("\n") + "\n");
|
|
47
|
+
const lines = [];
|
|
48
|
+
const spy = vi
|
|
49
|
+
.spyOn(console, "log")
|
|
50
|
+
.mockImplementation((...a) => lines.push(a.join(" ")));
|
|
51
|
+
let code;
|
|
52
|
+
try {
|
|
53
|
+
code = main(["node", "status.mjs", paper]);
|
|
54
|
+
} finally {
|
|
55
|
+
spy.mockRestore();
|
|
56
|
+
}
|
|
57
|
+
return { code, text: lines.join("\n") };
|
|
58
|
+
}
|
|
59
|
+
/** The table line for one check, with its padding collapsed. */
|
|
60
|
+
const tableLine = (text, key) =>
|
|
61
|
+
text
|
|
62
|
+
.split("\n")
|
|
63
|
+
.find((l) => l.includes(` ${key} `))
|
|
64
|
+
?.trim()
|
|
65
|
+
.replace(/\s{2,}/g, " | ");
|
|
66
|
+
/** A section: from the line that starts with `head` to the next blank line. */
|
|
67
|
+
const section = (text, head) => {
|
|
68
|
+
const lines = text.split("\n");
|
|
69
|
+
const i = lines.findIndex((l) => l.trim().startsWith(head));
|
|
70
|
+
if (i < 0) return null;
|
|
71
|
+
const end = lines.findIndex((l, j) => j > i && l.trim() === "");
|
|
72
|
+
return lines.slice(i, end < 0 ? undefined : end).map((l) => l.trim());
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const RAN = [
|
|
76
|
+
"research-ideate",
|
|
77
|
+
"find-venue",
|
|
78
|
+
"map-prior-work",
|
|
79
|
+
"sweep-design-space",
|
|
80
|
+
"plan-paper-timeline",
|
|
81
|
+
"build-benchmark",
|
|
82
|
+
"draft-paper",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
test("every kind of row, and every section it triggers", () => {
|
|
86
|
+
const { code, text } = view([
|
|
87
|
+
row("research-ideate", NO_WITNESS),
|
|
88
|
+
row("find-venue", {
|
|
89
|
+
kind: "FINDING",
|
|
90
|
+
findings: 3,
|
|
91
|
+
blocking: true,
|
|
92
|
+
report: "r.md",
|
|
93
|
+
}),
|
|
94
|
+
row("map-prior-work", { kind: "FINDING", findings: 2, inputSha: "older" }),
|
|
95
|
+
row("sweep-design-space", { ...NO_WITNESS, skillSha: "a-different-skill" }),
|
|
96
|
+
row("plan-paper-timeline", { verdict: "PASS" }),
|
|
97
|
+
row("build-benchmark", { verdict: "FINDINGS", findings: 2 }),
|
|
98
|
+
row("draft-paper", { kind: "FINDING", findings: 0 }),
|
|
99
|
+
row("mystery-check", NO_WITNESS),
|
|
100
|
+
row("paper-status", NO_WITNESS),
|
|
101
|
+
]);
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
{
|
|
104
|
+
code,
|
|
105
|
+
rows: [
|
|
106
|
+
"research-ideate",
|
|
107
|
+
"find-venue",
|
|
108
|
+
"map-prior-work",
|
|
109
|
+
"sweep-design-space",
|
|
110
|
+
"plan-paper-timeline",
|
|
111
|
+
"build-benchmark",
|
|
112
|
+
"draft-paper",
|
|
113
|
+
"camera-ready",
|
|
114
|
+
].map((k) => tableLine(text, k)),
|
|
115
|
+
stale: text
|
|
116
|
+
.split("\n")
|
|
117
|
+
.filter((l) => /^\s+(🟠|🟡) [\w/-]+: /.test(l))
|
|
118
|
+
.map((l) => l.trim()),
|
|
119
|
+
never: section(text, "⬜ never run:"),
|
|
120
|
+
mute: section(text, "🔇"),
|
|
121
|
+
legacy: section(text, "🏷️"),
|
|
122
|
+
orphans: section(text, "❗"),
|
|
123
|
+
open: section(text, "📌"),
|
|
124
|
+
quiet: section(text, "🤍"),
|
|
125
|
+
footer: text.trim().split("\n").pop().trim(),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
code: 0,
|
|
129
|
+
rows: [
|
|
130
|
+
"🟢 research-ideate | FRESH | abstained:no-witness | 1 | 2026-09-01 10:30",
|
|
131
|
+
"🟢 find-venue | FRESH | 🔴 3 found | 1 | 2026-09-01 10:30",
|
|
132
|
+
"🟠 map-prior-work | STALE-PAPER | • 2 found | 1 | 2026-09-01 10:30",
|
|
133
|
+
"🟡 sweep-design-space | STALE-SKILL | abstained:no-witness | 1 | 2026-09-01 10:30",
|
|
134
|
+
"🟢 plan-paper-timeline | FRESH | legacy:PASS | 1 | 2026-09-01 10:30",
|
|
135
|
+
"🟢 build-benchmark | FRESH | • 2 found | 1 | 2026-09-01 10:30",
|
|
136
|
+
"🟢 draft-paper | FRESH | no row | 1 | 2026-09-01 10:30",
|
|
137
|
+
"⬜ camera-ready | NEVER-RUN | — | 0 | —",
|
|
138
|
+
],
|
|
139
|
+
stale: [
|
|
140
|
+
// In EXPECTED_GATES order.
|
|
141
|
+
"🟡 sweep-design-space: the check itself changed since it ran — that answer came from a different tool",
|
|
142
|
+
"🟠 map-prior-work: the paper changed since it ran — its answer is about a document that is gone",
|
|
143
|
+
],
|
|
144
|
+
never: [
|
|
145
|
+
`⬜ never run: ${EXPECTED_GATES.filter((k) => !RAN.includes(k)).join(", ")}`,
|
|
146
|
+
"nobody has asked this question about this paper — an unasked question reads exactly like a clean one in prose.",
|
|
147
|
+
],
|
|
148
|
+
mute: [
|
|
149
|
+
"🔇 has run and has NEVER recorded a finding: research-ideate, sweep-design-space, plan-paper-timeline",
|
|
150
|
+
"Borrowed from mutation testing: a test that kills no mutant is not a test. This is",
|
|
151
|
+
"not proof the check is broken — it is the reason to go plant a defect and watch.",
|
|
152
|
+
"Precedent here: a tightening pass returned KEEP on 80 of 81 sections and a",
|
|
153
|
+
'justification pass never once recommended a deletion. Both "ran".',
|
|
154
|
+
],
|
|
155
|
+
legacy: [
|
|
156
|
+
"🏷️ answered in the RETIRED vocabulary: plan-paper-timeline=PASS, build-benchmark=FINDINGS",
|
|
157
|
+
"Rows written before 2026-08-10 are kept verbatim and are NOT translated into the",
|
|
158
|
+
"current constructors — a silent reinterpretation is the class of defect this",
|
|
159
|
+
"pipeline removed. A retired FINDINGS/FAIL row still shows its count above, because",
|
|
160
|
+
"it carries evidence and discarding evidence is the destructive direction.",
|
|
161
|
+
"plan-paper-timeline=PASS carry NOTHING forward:",
|
|
162
|
+
"they recorded that nothing was wrong, with no evidence attached, which is exactly",
|
|
163
|
+
"the value that was deleted. Re-run those checks for an answer in a live vocabulary.",
|
|
164
|
+
],
|
|
165
|
+
// `paper-status` records too, but is the view itself, so it is never an orphan.
|
|
166
|
+
orphans: [
|
|
167
|
+
"❗ recording but NOT IN THE TABLE: mystery-check",
|
|
168
|
+
"These ran and said something, and this view showed you nothing. Add them to",
|
|
169
|
+
"EXPECTED_GATES — an invisible row is the failure this directory exists to end.",
|
|
170
|
+
],
|
|
171
|
+
open: [
|
|
172
|
+
"📌 open findings: 7 across 3 check(s)",
|
|
173
|
+
"• map-prior-work: 2 (no report path recorded — the count is all there is) [STALE-PAPER: this count describes an older text]",
|
|
174
|
+
"🔴 find-venue: 3 → r.md",
|
|
175
|
+
"• build-benchmark: 2 (no report path recorded — the count is all there is)",
|
|
176
|
+
"Nothing here proves anyone acted on these. The ledger records what a check SAID,",
|
|
177
|
+
"never what was done about it — that lives in the session, not in a file.",
|
|
178
|
+
],
|
|
179
|
+
quiet: [
|
|
180
|
+
"🤍 ran against the current bytes and recorded nothing: 1 check(s)",
|
|
181
|
+
"research-ideate (no-witness)",
|
|
182
|
+
"Read this as the ABSENCE of a finding, which is what it is. `no-witness` means the",
|
|
183
|
+
"check ran and has no witness for the negative answer — unlike a certifying",
|
|
184
|
+
"algorithm (McConnell et al. 2011), which ships one for both answers. Our discipline",
|
|
185
|
+
"is strictly weaker than theirs and this line is where that shows.",
|
|
186
|
+
"no-witness: ran to completion, produced no finding — NOT a claim that nothing is wrong",
|
|
187
|
+
],
|
|
188
|
+
footer: "🔴 29 check(s) not answering for the current text",
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("every expected check answered on the current bytes: the green footer, and nothing to flag", () => {
|
|
194
|
+
const { code, text } = view(
|
|
195
|
+
EXPECTED_GATES.map((k) =>
|
|
196
|
+
row(k, { kind: "FINDING", findings: 1, report: "r.md" }),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
assert.deepEqual(
|
|
200
|
+
{
|
|
201
|
+
code,
|
|
202
|
+
sections: ["🟠", "🟡", "⬜ never", "🔇", "🏷️", "❗", "🤍"].map((h) =>
|
|
203
|
+
section(text, h),
|
|
204
|
+
),
|
|
205
|
+
footer: text.trim().split("\n").pop().trim(),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
code: 0,
|
|
209
|
+
sections: [null, null, null, null, null, null, null],
|
|
210
|
+
footer: "🟢 every expected check answered for the current text",
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("unlistedGates names checks the table does not list, except the view's own rows", () => {
|
|
216
|
+
assert.deepEqual(
|
|
217
|
+
unlistedGates([
|
|
218
|
+
{ skill: "find-venue", check: "find-venue" },
|
|
219
|
+
{ skill: "x", check: "y" },
|
|
220
|
+
{ skill: "paper-status", check: "paper-status" },
|
|
221
|
+
]),
|
|
222
|
+
["x/y"],
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("as a process, with no argument, it reads the current directory and exits 0", () => {
|
|
227
|
+
const r = runNode(join(HERE, "status.mjs"), [], {
|
|
228
|
+
cwd: paper,
|
|
229
|
+
env: { CLAUDE_PROJECT_DIR: consumer, PIPELINE_LEDGER: LEDGER },
|
|
230
|
+
});
|
|
231
|
+
assert.deepEqual([r.status, r.stdout.split("\n")[1]], [0, " p"]);
|
|
232
|
+
});
|
|
@@ -343,8 +343,8 @@ try {
|
|
|
343
343
|
// matters more than it looks. The first version of this guard keyed on `evt_fake_`,
|
|
344
344
|
// a string the stand-in prints only on SUCCESS — so a perfectly correct schema
|
|
345
345
|
// REJECTION arrived with no marker and was reported as "something else served this
|
|
346
|
-
// tool". The guard meant to protect the verdict was hiding it, and
|
|
347
|
-
//
|
|
346
|
+
// tool". The guard meant to protect the verdict was hiding it, and that went
|
|
347
|
+
// unnoticed until 2026-08-17. The log is written on both paths, so a
|
|
348
348
|
// non-empty log means our server handled the call, whatever it decided.
|
|
349
349
|
const logged = existsSync(callLog)
|
|
350
350
|
? readFileSync(callLog, "utf8")
|
|
@@ -8,25 +8,26 @@
|
|
|
8
8
|
|
|
9
9
|
jinja2>=3.0 # {{macro}} substitution in paper_numbers.py
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# 0, `python -m aclpubcheck.formatchecker --help` runs.
|
|
16
|
-
bibtexparser<2
|
|
11
|
+
# bibtexparser is pulled in by rebiber (below), not by aclpubcheck: aclpubcheck's own code never
|
|
12
|
+
# imports it. rebiber 1.4 moved to bibtexparser 2 (`bibtexparser>=2,<3` in its pyproject), and
|
|
13
|
+
# 2.0.1 is a stable release with a prebuilt wheel. The old pin `bibtexparser<2` existed only
|
|
14
|
+
# for rebiber 1.1.3; it would now make the file unsatisfiable, so it is gone.
|
|
17
15
|
|
|
18
|
-
# 🔴 aclpubcheck IS INSTALLED SEPARATELY, WITH `--no-deps`, AND IT IS
|
|
19
|
-
#
|
|
20
|
-
#
|
|
16
|
+
# 🔴 aclpubcheck IS INSTALLED SEPARATELY, WITH `--no-deps`, BY ensure-checkers.sh, AND IT IS
|
|
17
|
+
# DELIBERATELY NOT LISTED HERE (a requirements file cannot say `--no-deps` for one line).
|
|
18
|
+
# It comes from its GitHub source pinned to a commit, not from PyPI: upstream says to install "from
|
|
19
|
+
# the GitHub repository (DIFFERENT from `pypi`)", and PyPI's 0.2.0 is a snapshot of 2022-10-10. The
|
|
20
|
+
# pin is 237bee3 of 2026-09-11, whose own setup.py pins rebiber to the SAME commit as below —
|
|
21
|
+
# measured 2026-09-27: both resolve together, `pip check` names no rebiber conflict.
|
|
22
|
+
# Why `--no-deps`: it still declares `tsv`, which has NO PREBUILT WHEEL AT ALL
|
|
23
|
+
# (`pip download --only-binary :all: tsv` → "from versions: none"), and whose source does not build
|
|
21
24
|
# under a modern setuptools:
|
|
22
25
|
# AttributeError: install_layout. Did you mean: 'install_platlib'?
|
|
23
|
-
# That one line alone made installing the WHOLE file fail, and that's why
|
|
26
|
+
# That one line alone once made installing the WHOLE file fail, and that's why a paper went a month
|
|
24
27
|
# without an official format check.
|
|
25
|
-
# Verified, not assumed: `tsv` is imported NOWHERE in aclpubcheck's source
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# So what follows is aclpubcheck's OWN dependencies, taken verbatim from the package's metadata,
|
|
29
|
-
# minus tsv. A list, rather than `aclpubcheck` on one line, so the next unrepresentable dependency
|
|
28
|
+
# Verified, not assumed, on the pinned commit: `tsv` is imported NOWHERE in aclpubcheck's source,
|
|
29
|
+
# and neither is `setuptools`, which it also declares.
|
|
30
|
+
# So what follows is aclpubcheck's OWN dependencies, from its setup.py, minus tsv and setuptools. A list, rather than `aclpubcheck` on one line, so the next unrepresentable dependency
|
|
30
31
|
# reads as a missing line rather than an unverified assumption.
|
|
31
32
|
# 🔴 THE UPPER BOUNDS ARE REMOVED, AND THAT IS A MEASUREMENT, NOT CARELESSNESS. The first edition
|
|
32
33
|
# of this list copied aclpubcheck's bounds verbatim — and turned red on CI, where Python is newer:
|
|
@@ -48,8 +49,18 @@ pylatexenc>=2.10
|
|
|
48
49
|
termcolor>=2.0.1
|
|
49
50
|
tqdm>=4.64.1
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
# 🔴 rebiber FROM ITS GITHUB SOURCE, PINNED TO A COMMIT — NOT FROM PyPI. The upstream README says
|
|
53
|
+
# so itself: "Install from GitHub only. We do not publish or support PyPI (`pip install rebiber` is
|
|
54
|
+
# a 2021 1.1.3 release)." The `rebiber>=1.1.3` that stood here installed exactly that 2021 release
|
|
55
|
+
# (#114). The pin is 1.4.0, commit b917e36 of 2026-09-08 ("Support bibtexparser 2 parsing and
|
|
56
|
+
# writing APIs"); upstream has no 1.4 tag, so a commit is the only fixed name.
|
|
57
|
+
# Measured 2026-09-27 in a clean Python 3.11 venv, under this file's `--only-binary :all:`: installs
|
|
58
|
+
# in 36 s (pip builds it from source with hatchling, the one allowed build), `rebiber -v` runs,
|
|
59
|
+
# and a `--dry-run` over a three-entry .bib leaves the file untouched. It carries 409 MB of venue
|
|
60
|
+
# data. aclpubcheck declares `rebiber>=1.1.3,<2`, which 1.4.0 satisfies.
|
|
61
|
+
rebiber @ git+https://github.com/yuchenlin/rebiber@b917e36ec94084a84025eb06781b5aac96d1cedd
|
|
52
62
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
63
|
+
# Since the pinned commit the package has a `__main__.py`, so `python3 -m aclpubcheck <pdf>` runs
|
|
64
|
+
# the format checker (measured 2026-09-27 on its example PDF: 10 errors, 0 warnings, exit 0). The
|
|
65
|
+
# PyPI 0.2.0 that stood here had none, and `python3 -m aclpubcheck` failed with "cannot be directly
|
|
66
|
+
# executed"; `python3 -m aclpubcheck.formatchecker` works on both.
|
|
@@ -27,8 +27,10 @@ echo "== python dependencies =="
|
|
|
27
27
|
# bibtexparser looked like the guilty one.
|
|
28
28
|
pip install -q --only-binary :all: -r "$HERE/checkers-requirements.txt"
|
|
29
29
|
# The checker itself — WITHOUT its dependencies: they are listed in requirements.txt by name, minus
|
|
30
|
-
# the unbuildable `tsv`, which it does not import.
|
|
31
|
-
|
|
30
|
+
# the unbuildable `tsv`, which it does not import. From its GitHub source at a fixed commit, not
|
|
31
|
+
# PyPI — both reasons are in the header of requirements.txt.
|
|
32
|
+
ACLPUBCHECK="aclpubcheck @ git+https://github.com/acl-org/aclpubcheck@237bee3a554f2d2fcda69cd0cf1edf4168e3d339"
|
|
33
|
+
pip install -q --no-deps "$ACLPUBCHECK"
|
|
32
34
|
|
|
33
35
|
echo "== TeXtidote (a jar, not a pip package) =="
|
|
34
36
|
if [ -f "$JAR" ]; then
|
|
@@ -55,10 +57,24 @@ fail=0
|
|
|
55
57
|
# "numpy.dtype size changed"). That is, the acceptance check that stood here let through exactly
|
|
56
58
|
# the failure it was written for. `import aclpubcheck.formatchecker` pulls in numpy and pandas on
|
|
57
59
|
# lines 15 and 20 of the checker itself and therefore fails honestly.
|
|
60
|
+
# And the entry point, which the PyPI release did not have: `python3 -m aclpubcheck` must answer.
|
|
58
61
|
python3 -c "import aclpubcheck.formatchecker" >/dev/null 2>&1 \
|
|
62
|
+
&& python3 -m aclpubcheck --help >/dev/null 2>&1 \
|
|
59
63
|
&& echo " ✅ aclpubcheck" || { echo " ❌ aclpubcheck is installed, but does not run"; fail=1; }
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
# 🔴 RUN IT, NOT IMPORT IT. rebiber is used as a command, and an import proves neither the entry
|
|
65
|
+
# point, nor that it loads its bundled venue data, nor that it parses a .bib (bibtexparser 2 since
|
|
66
|
+
# 1.4). So it is given the arXiv entry of a paper its data knows (NeurIPS 2017) with --dry-run
|
|
67
|
+
# (nothing is written) and must report that entry CONVERTED to the official record — measured
|
|
68
|
+
# 2026-09-27 on the pinned commit: `converted (official): 1`, no network.
|
|
69
|
+
reb_tmp="$(mktemp -d)"
|
|
70
|
+
printf '@article{vaswani2017attention,\n title = {Attention Is All You Need},\n author = {Vaswani, Ashish and Shazeer, Noam and Parmar, Niki and Uszkoreit, Jakob and Jones, Llion and Gomez, Aidan N. and Kaiser, Lukasz and Polosukhin, Illia},\n journal = {arXiv preprint arXiv:1706.03762},\n year = {2017}\n}\n' > "$reb_tmp/in.bib"
|
|
71
|
+
reb_out="$(rebiber -i "$reb_tmp/in.bib" --dry-run 2>&1 || true)"
|
|
72
|
+
if grep -q "converted (official): 1" <<<"$reb_out"; then
|
|
73
|
+
echo " ✅ rebiber $(rebiber -v 2>/dev/null)"
|
|
74
|
+
else
|
|
75
|
+
echo " ❌ rebiber does not convert a known entry"; fail=1
|
|
76
|
+
fi
|
|
77
|
+
rm -rf "$reb_tmp"
|
|
62
78
|
python3 -c "import jinja2" >/dev/null 2>&1 \
|
|
63
79
|
&& echo " ✅ jinja2" || { echo " ❌ jinja2 does not import"; fail=1; }
|
|
64
80
|
java -jar "$JAR" --version >/dev/null 2>&1 \
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The shim imports the compiled package (`dist/`), so `npm run build` runs before this.
|
|
12
12
|
*/
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
13
|
import { spawnSync } from "node:child_process";
|
|
15
14
|
import {
|
|
16
15
|
cpSync,
|
|
@@ -25,16 +24,13 @@ import {
|
|
|
25
24
|
import { tmpdir } from "node:os";
|
|
26
25
|
import { dirname, join, resolve } from "node:path";
|
|
27
26
|
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { createChecker } from "../../lib/check.mjs";
|
|
28
28
|
|
|
29
29
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
30
30
|
const SHIM = join(HERE, "extract-pdf-facts.mjs");
|
|
31
31
|
const FIX = resolve(HERE, "..", "..", "fixtures", "pdf-facts");
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
const check = (label, cond, detail = "") => {
|
|
35
|
-
assert.ok(cond, detail ? `${label} — ${detail}` : label);
|
|
36
|
-
n++;
|
|
37
|
-
};
|
|
33
|
+
const check = createChecker();
|
|
38
34
|
|
|
39
35
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-extract-")));
|
|
40
36
|
/**
|
|
@@ -49,6 +45,16 @@ const shim = (args, env = {}) =>
|
|
|
49
45
|
PATH: process.env.PATH,
|
|
50
46
|
HOME: root,
|
|
51
47
|
CLAUDE_PROJECT_DIR: root,
|
|
48
|
+
// Forwarded, not chosen: under `npm run coverage` these carry the coverage directory and the
|
|
49
|
+
// preload that resolves dist/ to src/. Dropping them measured the shim's dist/ imports as a
|
|
50
|
+
// second copy of each module (the function map remapped through source maps, with shifted
|
|
51
|
+
// columns), so every function it did not call counted as uncovered in src/.
|
|
52
|
+
...(process.env.NODE_OPTIONS
|
|
53
|
+
? { NODE_OPTIONS: process.env.NODE_OPTIONS }
|
|
54
|
+
: {}),
|
|
55
|
+
...(process.env.NODE_V8_COVERAGE
|
|
56
|
+
? { NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE }
|
|
57
|
+
: {}),
|
|
52
58
|
...env,
|
|
53
59
|
},
|
|
54
60
|
});
|
|
@@ -157,10 +163,53 @@ try {
|
|
|
157
163
|
missing.status === 3 && /not built/.test(said(missing)),
|
|
158
164
|
said(missing),
|
|
159
165
|
);
|
|
166
|
+
|
|
167
|
+
// ── a PDF named directly, in a directory with no paperlint.json, no project dir set ──
|
|
168
|
+
const loose = join(root, "papers", "loose");
|
|
169
|
+
mkdirSync(loose, { recursive: true });
|
|
170
|
+
cpSync(join(FIX, "t3-mixed.pdf"), join(loose, "draft.pdf"));
|
|
171
|
+
const direct = shim([join(loose, "draft.pdf")], { CLAUDE_PROJECT_DIR: "" });
|
|
172
|
+
const looseFacts = join(loose, "_build", "paper.facts.json");
|
|
173
|
+
check(
|
|
174
|
+
"a PDF path: facts go to its directory's _build/, the path printed relative to the cwd",
|
|
175
|
+
direct.status === 0 &&
|
|
176
|
+
direct.stdout.startsWith(
|
|
177
|
+
"✅ draft.pdf → papers/loose/_build/paper.facts.json (",
|
|
178
|
+
) &&
|
|
179
|
+
JSON.parse(readFileSync(looseFacts, "utf8")).venue === null,
|
|
180
|
+
said(direct),
|
|
181
|
+
);
|
|
182
|
+
// The project directory IS the facts file (nonsense, but an environment can say it): the
|
|
183
|
+
// relative path would be empty, so the absolute one is printed instead of nothing.
|
|
184
|
+
const same = shim([join(loose, "draft.pdf")], {
|
|
185
|
+
CLAUDE_PROJECT_DIR: looseFacts,
|
|
186
|
+
});
|
|
187
|
+
check(
|
|
188
|
+
"a project dir equal to the facts file prints the absolute path, never an empty one",
|
|
189
|
+
same.stdout.startsWith(`✅ draft.pdf → ${looseFacts} (`),
|
|
190
|
+
said(same),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// ── a file that is not a PDF: refused, strict or not, in the words for the mode ──
|
|
194
|
+
writeFileSync(join(loose, "broken.pdf"), "not a pdf at all\n");
|
|
195
|
+
const brokenStrict = shim([join(loose, "broken.pdf"), "--strict"]);
|
|
196
|
+
const brokenLocal = shim([join(loose, "broken.pdf")]);
|
|
197
|
+
check(
|
|
198
|
+
"an unreadable PDF: --strict exits 1 as an environment error; locally exit 0, saying THE PDF IS NOT CHECKED",
|
|
199
|
+
brokenStrict.status === 1 &&
|
|
200
|
+
/🛑 facts not taken: .* — in CI this is an environment error/.test(
|
|
201
|
+
brokenStrict.stderr,
|
|
202
|
+
) &&
|
|
203
|
+
brokenLocal.status === 0 &&
|
|
204
|
+
/⏭️ {2}facts not taken: .*THE PDF IS NOT CHECKED/.test(
|
|
205
|
+
brokenLocal.stderr,
|
|
206
|
+
),
|
|
207
|
+
`${said(brokenStrict)}\n---\n${said(brokenLocal)}`,
|
|
208
|
+
);
|
|
160
209
|
} finally {
|
|
161
210
|
rmSync(root, { recursive: true, force: true });
|
|
162
211
|
}
|
|
163
212
|
|
|
164
213
|
console.log(
|
|
165
|
-
`✓ ${String(
|
|
214
|
+
`✓ ${String(check.count)} assertions passed — extract-pdf-facts: the exit-code contract, schema 2, and the no-no-yes font`,
|
|
166
215
|
);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`--section=1 keeps section 1 and its sub-sections, and nothing before it 1`] = `
|
|
4
|
+
"
|
|
5
|
+
=== full.md — outline, in order ===
|
|
6
|
+
own total share section
|
|
7
|
+
10 16 53.3% 1 Introduction
|
|
8
|
+
↳ [7/10 KEEP] carries: the claim
|
|
9
|
+
4 1.1 Detail
|
|
10
|
+
↳ carries: a sub-point
|
|
11
|
+
2 1.2 Bare sub
|
|
12
|
+
↳ 🔴 no carries: note — nobody has said why this section is here
|
|
13
|
+
|
|
14
|
+
body total 30 words across 4 sections
|
|
15
|
+
|
|
16
|
+
=== outside the page limit ===
|
|
17
|
+
40 Limitations
|
|
18
|
+
↳ 🔴 no carries: note — and no page limit forcing the question
|
|
19
|
+
5 Scope
|
|
20
|
+
↳ carries: why the scope note
|
|
21
|
+
30 Appendix A
|
|
22
|
+
↳ carries: the raw tables
|
|
23
|
+
|
|
24
|
+
free total 75 words = 250% of the body
|
|
25
|
+
🔴 The unbudgeted half is LARGER than the paper. Nothing prices these sections, which
|
|
26
|
+
is exactly why prose settles here. Ask of each: would a reviewer miss it?
|
|
27
|
+
|
|
28
|
+
The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
|
|
29
|
+
heading name a thing, and does each section earn its place. Read the headings as a set — a
|
|
30
|
+
stranger should be able to reconstruct the argument from them alone.
|
|
31
|
+
"
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
exports[`the inventory of body-only.md 1`] = `
|
|
35
|
+
"
|
|
36
|
+
=== body-only.md — outline, in order ===
|
|
37
|
+
own total share section
|
|
38
|
+
8 8 66.7% 1 Intro
|
|
39
|
+
↳ 🔴 no carries: note — nobody has said why this section is here
|
|
40
|
+
4 4 33.3% 2 Rest
|
|
41
|
+
↳ 🔴 no carries: note — nobody has said why this section is here
|
|
42
|
+
0 0 0.0% 3 Empty
|
|
43
|
+
↳ 🔴 no carries: note — nobody has said why this section is here
|
|
44
|
+
|
|
45
|
+
body total 12 words across 3 sections
|
|
46
|
+
|
|
47
|
+
The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
|
|
48
|
+
heading name a thing, and does each section earn its place. Read the headings as a set — a
|
|
49
|
+
stranger should be able to reconstruct the argument from them alone.
|
|
50
|
+
"
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
exports[`the inventory of full.md 1`] = `
|
|
54
|
+
"
|
|
55
|
+
=== full.md — outline, in order ===
|
|
56
|
+
own total share section
|
|
57
|
+
3 Orphan sub
|
|
58
|
+
10 16 53.3% 1 Introduction
|
|
59
|
+
↳ [7/10 KEEP] carries: the claim
|
|
60
|
+
4 1.1 Detail
|
|
61
|
+
↳ carries: a sub-point
|
|
62
|
+
2 1.2 Bare sub
|
|
63
|
+
↳ 🔴 no carries: note — nobody has said why this section is here
|
|
64
|
+
6 6 20.0% 2 Method
|
|
65
|
+
↳ 🔴 no carries: note — nobody has said why this section is here
|
|
66
|
+
5 5 16.7% 3 Results
|
|
67
|
+
↳ [?/10 CUT] carries: only a verdict
|
|
68
|
+
3 3 10.0% Discussion
|
|
69
|
+
↳ [4/10 NO VERDICT] carries: score only
|
|
70
|
+
|
|
71
|
+
body total 30 words across 4 sections
|
|
72
|
+
|
|
73
|
+
=== outside the page limit ===
|
|
74
|
+
40 Limitations
|
|
75
|
+
↳ 🔴 no carries: note — and no page limit forcing the question
|
|
76
|
+
5 Scope
|
|
77
|
+
↳ carries: why the scope note
|
|
78
|
+
30 Appendix A
|
|
79
|
+
↳ carries: the raw tables
|
|
80
|
+
|
|
81
|
+
free total 75 words = 250% of the body
|
|
82
|
+
🔴 The unbudgeted half is LARGER than the paper. Nothing prices these sections, which
|
|
83
|
+
is exactly why prose settles here. Ask of each: would a reviewer miss it?
|
|
84
|
+
|
|
85
|
+
The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
|
|
86
|
+
heading name a thing, and does each section earn its place. Read the headings as a set — a
|
|
87
|
+
stranger should be able to reconstruct the argument from them alone.
|
|
88
|
+
"
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
exports[`the inventory of no-appendix.md 1`] = `
|
|
92
|
+
"
|
|
93
|
+
=== no-appendix.md — outline, in order ===
|
|
94
|
+
own total share section
|
|
95
|
+
8 8 100.0% 1 Intro
|
|
96
|
+
↳ 🔴 no carries: note — nobody has said why this section is here
|
|
97
|
+
|
|
98
|
+
body total 8 words across 1 sections
|
|
99
|
+
|
|
100
|
+
=== outside the page limit ===
|
|
101
|
+
3 Limitations
|
|
102
|
+
↳ 🔴 no carries: note — and no page limit forcing the question
|
|
103
|
+
|
|
104
|
+
free total 3 words = 38% of the body
|
|
105
|
+
|
|
106
|
+
The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
|
|
107
|
+
heading name a thing, and does each section earn its place. Read the headings as a set — a
|
|
108
|
+
stranger should be able to reconstruct the argument from them alone.
|
|
109
|
+
"
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
exports[`the inventory of refs-first.md 1`] = `
|
|
113
|
+
"
|
|
114
|
+
=== refs-first.md — outline, in order ===
|
|
115
|
+
own total share section
|
|
116
|
+
8 8 100.0% 1 Intro
|
|
117
|
+
↳ 🔴 no carries: note — nobody has said why this section is here
|
|
118
|
+
|
|
119
|
+
body total 8 words across 1 sections
|
|
120
|
+
|
|
121
|
+
=== outside the page limit ===
|
|
122
|
+
2 References
|
|
123
|
+
↳ 🔴 no carries: note — and no page limit forcing the question
|
|
124
|
+
2 Ethical note
|
|
125
|
+
↳ 🔴 no carries: note — and no page limit forcing the question
|
|
126
|
+
|
|
127
|
+
free total 4 words = 50% of the body
|
|
128
|
+
|
|
129
|
+
The rest is judgement and stays with the skill: is this the ORDER a stranger needs, does each
|
|
130
|
+
heading name a thing, and does each section earn its place. Read the headings as a set — a
|
|
131
|
+
stranger should be able to reconstruct the argument from them alone.
|
|
132
|
+
"
|
|
133
|
+
`;
|