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
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# What we test with
|
|
1
|
+
# What we test with
|
|
2
2
|
|
|
3
|
-
**Question this file answers:**
|
|
4
|
-
|
|
5
|
-
Is that a gap? Should an npm package's install be tested through a local registry?
|
|
3
|
+
**Question this file answers:** should an npm package's install be tested through a local
|
|
4
|
+
registry, and which tests belong to which tool?
|
|
6
5
|
|
|
7
|
-
**Verdict
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
**Verdict.** Unit tests run on vitest (`*.test.ts` / `*.test.mjs`); agent-facing checks are vigiles
|
|
7
|
+
harnesses; the install and build are end-to-end scripts. The install is tested by packing a
|
|
8
|
+
tarball and installing it by path, not through a local registry: a registry is what
|
|
9
|
+
MULTI-package repositories reach for. That conclusion has named triggers for revisiting, below.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -27,10 +27,6 @@ So the genuinely agent-facing harnesses are three: `hooks/hooks.harness.mjs` (`r
|
|
|
27
27
|
(`runHarnessTest`, which spawns the real `claude` binary against a scripted model), and the skill
|
|
28
28
|
contract checks. Everything else is ordinary Node testing that happens to carry the suffix.
|
|
29
29
|
|
|
30
|
-
⚠️ **The suffix is a CONTRACT, not a style choice.** `lib/mutation-driver.mjs` pairs batteries to
|
|
31
|
-
harnesses by it. Renaming files for a new runner would break that, which has nothing to do with
|
|
32
|
-
running tests.
|
|
33
|
-
|
|
34
30
|
## Why the install e2e stays a script
|
|
35
31
|
|
|
36
32
|
`test/e2e/install.mjs` is one linear scenario per package manager with strictly dependent steps
|
|
@@ -81,25 +77,6 @@ answering. Recording this so the question "would it even run here" never has to
|
|
|
81
77
|
documented install becoming a cold `npx <name>`. Then: verdaccio as a devDependency spawned by the
|
|
82
78
|
script, Angular-style — not a Docker service in CI.
|
|
83
79
|
|
|
84
|
-
## Why no vitest, and no `node:test` either
|
|
85
|
-
|
|
86
|
-
vitest's value is its Vite transform pipeline, jsdom, module mocking and a watch UI. The tests here
|
|
87
|
-
are plain `.mjs`, the sources are compiled by `tsc`, there is no DOM, and the design deliberately
|
|
88
|
-
does not mock — it spawns real processes into temp directories, which is the right shape for a CLI.
|
|
89
|
-
So vitest would add vite and a native esbuild binary to a package whose install weight is itself
|
|
90
|
-
under scrutiny, and nothing breaks without it.
|
|
91
|
-
|
|
92
|
-
`node:test` costs no dependency but creates the two-answers problem verbatim: a second discovery
|
|
93
|
-
mechanism (`--test` globs against the `.harness.mjs` suffix) and a second exit code.
|
|
94
|
-
|
|
95
|
-
**What staying honestly costs:** a top-level `assert` stops at the FIRST failure, so a table-driven
|
|
96
|
-
test over many fixtures reports one row per run. Also no name-level filtering (file-level exists:
|
|
97
|
-
`vigiles test <file>`).
|
|
98
|
-
|
|
99
|
-
**Trigger to revisit:** when continue-after-failure is measurably costing iteration time on a
|
|
100
|
-
table-driven test. The answer then is `node:test`, never vitest, and it must run under the same
|
|
101
|
-
`npm test` line rather than beside it.
|
|
102
|
-
|
|
103
80
|
## One known limit, recorded so it is not claimed
|
|
104
81
|
|
|
105
82
|
**Yarn Berry (PnP) cannot be supported as things stand.** `plugin/hooks/hooks.json` addresses
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# The reference check
|
|
2
|
+
|
|
3
|
+
`paperlint build` checks every entry of a paper's bibliography against the services that know
|
|
4
|
+
about published work, after the PDF is built. `paperlint lint` then reports what the check found,
|
|
5
|
+
offline. This page explains how the check works, the file it keeps in your paper's folder, and
|
|
6
|
+
what happens in CI and without a network.
|
|
7
|
+
|
|
8
|
+
## What is checked, and where
|
|
9
|
+
|
|
10
|
+
For each entry in the bibliography (the `filecontents` block inside `paper.tex`, else `refs.bib`):
|
|
11
|
+
|
|
12
|
+
| question | asked of |
|
|
13
|
+
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
14
|
+
| does the cited work exist, and does its title match? | Crossref, OpenAlex, Semantic Scholar, arXiv |
|
|
15
|
+
| does a DOI that no registry knows really not exist? | doi.org, the DOI authority |
|
|
16
|
+
| for an entry that claims a published venue, are the authors the published version's, not the preprint's? | DBLP |
|
|
17
|
+
|
|
18
|
+
CVE identifiers are not checked by `build`: a bibliography has no CVE field, so none is extracted.
|
|
19
|
+
The verify-citations script checks a `cve` against NVD when it is given citations as JSON (see the
|
|
20
|
+
skill), and that is the only place NVD is asked.
|
|
21
|
+
|
|
22
|
+
A work that is simply not found is `unresolvable`, never a failure: plenty of real work is not
|
|
23
|
+
indexed. Only a positive disproof fails an entry, for example a DOI that resolves to a different
|
|
24
|
+
paper, or one the DOI authority says does not exist.
|
|
25
|
+
|
|
26
|
+
The verdicts land in `<paper>/_build/references.json`. `paperlint lint` reads that file and reports:
|
|
27
|
+
|
|
28
|
+
| rule | when |
|
|
29
|
+
| -------------------- | ----------------------------------------------------------------- |
|
|
30
|
+
| `paper/cite-exists` | an entry's identifier provably does not resolve to the work cited |
|
|
31
|
+
| `paper/author-list` | an entry's authors are the preprint's under a published venue |
|
|
32
|
+
| `paper/refs-fresh` | the bibliography changed since the build checked it — build again |
|
|
33
|
+
| `paper/refs-checked` | no build has checked the references, or the last one could not |
|
|
34
|
+
|
|
35
|
+
The step never fails the build. Without a network the PDF is still built and lint warns that the
|
|
36
|
+
references were not checked.
|
|
37
|
+
|
|
38
|
+
## `repro/references-cache.json` — commit it
|
|
39
|
+
|
|
40
|
+
Asking five services about every entry is slow. DBLP asks clients to pause between requests, and a
|
|
41
|
+
slow service is retried. Before this cache existed, a 27-reference paper spent 217 seconds on the
|
|
42
|
+
check, on every build, and a 51-reference paper did not finish inside a 12-minute CI job
|
|
43
|
+
([#107](https://github.com/zernie/paperlint/issues/107)).
|
|
44
|
+
|
|
45
|
+
So the build keeps every answer it receives in `<paper>/repro/references-cache.json`, and the next
|
|
46
|
+
build asks only what that file cannot answer:
|
|
47
|
+
|
|
48
|
+
- An unchanged bibliography whose answers are under 30 days old asks nothing and makes no network request at all, not even the check
|
|
49
|
+
for whether the services can be reached.
|
|
50
|
+
- Adding or editing an entry asks only about that entry. Answers are stored per identifier (DOI,
|
|
51
|
+
arXiv id, or title), so changing an entry's DOI or title is what makes it be asked again.
|
|
52
|
+
- A request that failed is not an answer and is not stored, so the next build asks it again.
|
|
53
|
+
- "Does not exist" from doi.org is not stored either. A DOI cited before the authority has
|
|
54
|
+
registered it would otherwise stay "does not exist" in the file forever; it is asked again on each
|
|
55
|
+
build until the authority confirms it.
|
|
56
|
+
|
|
57
|
+
## When a service refuses
|
|
58
|
+
|
|
59
|
+
Free services rate-limit. From a CI runner or a shared network, Semantic Scholar answers `429`,
|
|
60
|
+
DBLP answers an HTML page where JSON was asked, a registry stops answering. The check handles this
|
|
61
|
+
within one build:
|
|
62
|
+
|
|
63
|
+
- **A service that refuses stops being asked for the rest of the build.** The first request to
|
|
64
|
+
each service goes alone; once a service refuses (a `429` or `5xx`, HTML instead of JSON, a
|
|
65
|
+
timeout, no connection), no new request goes to it until the next build, and no retries or pauses
|
|
66
|
+
are spent on it. A service that refuses from the start costs one request. One that starts refusing
|
|
67
|
+
later costs at most the requests already in flight at that moment — six, the number of entries
|
|
68
|
+
checked at once.
|
|
69
|
+
- **The entries it would have answered say so.** An entry no registry confirmed carries
|
|
70
|
+
`not asked: <service>: <reason>` in its reason. An entry whose authors DBLP could not check is
|
|
71
|
+
`authors: unchecked`, which `paper/refs-checked` reports and lint never treats as a pass.
|
|
72
|
+
- **Once one registry confirms a work, the others are not asked.** A confirmation settles the
|
|
73
|
+
verdict, so the remaining registries (and doi.org) could only cost requests.
|
|
74
|
+
- **DBLP's pace** — it asks clients to pause between queries — is kept between queries DBLP
|
|
75
|
+
actually answered, and not after a cached answer or once DBLP has refused.
|
|
76
|
+
|
|
77
|
+
Measured 2026-09-27 from a sandboxed runner where Semantic Scholar, OpenAlex and Crossref answered
|
|
78
|
+
`429` and DBLP answered HTML: a 30-reference paper built in 5.6 s cold and 5.4 s warm (the check
|
|
79
|
+
used to take 475 s and 450 s). Its entries were recorded, with the refusals named, and each later
|
|
80
|
+
build fills in more of the cache as the services let it.
|
|
81
|
+
|
|
82
|
+
**Commit this file.** It sits in `repro/`, beside the paper's other reproduction files, and not in
|
|
83
|
+
`_build/`, which is build output. Committed, it makes CI and every other clone start warm: a CI job
|
|
84
|
+
that builds an unchanged paper spends no time on the check.
|
|
85
|
+
|
|
86
|
+
It holds what the services answered, each answer with the day it was fetched, and never a verdict.
|
|
87
|
+
The verdicts are worked out again from those answers on every build, so an improvement to the
|
|
88
|
+
checker reaches every paper at once, instead of being frozen behind a stored "passed".
|
|
89
|
+
|
|
90
|
+
### Refreshing it
|
|
91
|
+
|
|
92
|
+
An answer is used for 30 days after the date in its `fetched` field; after that the next build asks
|
|
93
|
+
again and re-dates it. This is what lets a work that a service had not indexed yet ("no record")
|
|
94
|
+
be found once it is. An answer that could not be refreshed because the service refused stays, and is
|
|
95
|
+
asked again on the next build. To ask sooner:
|
|
96
|
+
|
|
97
|
+
- **one entry**: delete its lines from the file (the keys name the service and the identifier, and
|
|
98
|
+
DBLP answers carry the title they were asked for);
|
|
99
|
+
- **everything**: delete the file.
|
|
100
|
+
|
|
101
|
+
The next build asks again and writes the new answers.
|
|
102
|
+
|
|
103
|
+
### A file that does not read
|
|
104
|
+
|
|
105
|
+
The build refuses a cache file it cannot read (bad JSON, another schema version, a field of the
|
|
106
|
+
wrong type) and says which part is wrong. It does not overwrite the file, and does not run the
|
|
107
|
+
check over half a cache: the references are recorded as not checked, and lint says so. Fix the
|
|
108
|
+
file, or delete it.
|
|
109
|
+
|
|
110
|
+
## CI
|
|
111
|
+
|
|
112
|
+
| the paper has | a CI build |
|
|
113
|
+
| --------------------------------- | ------------------------------------------------------------------ |
|
|
114
|
+
| a committed, up-to-date cache | asks nothing, needs no network for the check |
|
|
115
|
+
| a cache missing a few new entries | asks about those entries only |
|
|
116
|
+
| no cache committed | asks about every entry, every run: commit the file after one build |
|
|
117
|
+
|
|
118
|
+
A CI run writes new answers into its own checkout, which is thrown away. Build locally and commit
|
|
119
|
+
the file to keep the answers.
|
|
120
|
+
|
|
121
|
+
## Running a checker by hand
|
|
122
|
+
|
|
123
|
+
The two checkers are scripts in the `verify-citations` skill and can be run on their own to read
|
|
124
|
+
one verdict in full: `verify-cites.mjs` (existence and title) and `bib-authors.mjs` (authors). Run
|
|
125
|
+
from the command line, `verify-cites.mjs` keeps its own cache in `~/.cache/paperlint/cite-cache.json`
|
|
126
|
+
(`VERIFY_CITES_CACHE` overrides); it never reads or writes the paper's committed cache.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
The one map of the kinds of test in this repository: what each answers, where it lives, what it
|
|
4
|
+
may touch, and how to run it. The rules that apply to every kind are at the end.
|
|
5
|
+
|
|
6
|
+
## Which kind do I need?
|
|
7
|
+
|
|
8
|
+
| you are testing | kind |
|
|
9
|
+
| --------------------------------------------------------------------------------------------- | --------------- |
|
|
10
|
+
| a pure function: a parser, a decision, a formatter | **unit** |
|
|
11
|
+
| a use case or an adapter that reaches the disk, a process, the network or the clock | **integration** |
|
|
12
|
+
| what an agent sees or does: a hook's decision, a skill's contract | **harness** |
|
|
13
|
+
| the package as someone else gets it: the tarball installed elsewhere, a real TeX build, a PDF | **e2e** |
|
|
14
|
+
|
|
15
|
+
When in doubt between unit and integration: if the function takes a port (`Files`,
|
|
16
|
+
`CheckReferences`, `RunProcess`, …) or calls `fetch`, it is integration.
|
|
17
|
+
|
|
18
|
+
## Unit — vitest, pure functions
|
|
19
|
+
|
|
20
|
+
- **Answers:** does this function return the right value for this input?
|
|
21
|
+
- **Lives:** beside the module, `<module>.test.ts` for a `.ts` module, `<module>.test.mjs` for a
|
|
22
|
+
`.mjs` one (TypeScript has no types to check a JS module against, and `allowJs` was measured to
|
|
23
|
+
break a dozen existing `@ts-expect-error` imports).
|
|
24
|
+
- **May touch:** nothing. No disk, no network, no process, no clock.
|
|
25
|
+
- **Example:** `src/domain/lookup-cache.test.ts` — the reference cache's parse, serialize and key
|
|
26
|
+
functions, each refusal compared as a whole value.
|
|
27
|
+
|
|
28
|
+
## Integration — vitest, with fakes through the ports
|
|
29
|
+
|
|
30
|
+
- **Answers:** does this use case or adapter do the right thing with the world it talks to?
|
|
31
|
+
- **Lives:** beside the module, named like a unit test.
|
|
32
|
+
- **May touch:** a temp directory (`useTempDir` and `writeTree` in `test/support.mjs`), a child
|
|
33
|
+
process of this repository's own scripts (`runNode`). Never the real network.
|
|
34
|
+
- **How fakes get in:** through the seams the code already has.
|
|
35
|
+
- a port the use case takes: `memoryFiles` and `scriptedProcess` in `src/adapters/memory/`, a
|
|
36
|
+
`CheckReferences` function, a `MeasureGeometry`;
|
|
37
|
+
- `fetch`, stubbed with `vi.stubGlobal` and routed by URL, which also records every request made;
|
|
38
|
+
- a clock or a pause, passed in (`referencesChecker({ today })`) or run under `vi.useFakeTimers`.
|
|
39
|
+
- **Example:** `src/adapters/references/index.test.ts` — the reference checker over a fake
|
|
40
|
+
`fetch`: a cold run stores every answer, a warm run makes no request at all (the recorded list of
|
|
41
|
+
requests is empty), an edited entry asks only for itself.
|
|
42
|
+
|
|
43
|
+
## Harness — vigiles, the agent-facing surface only
|
|
44
|
+
|
|
45
|
+
- **Answers:** what does an agent see or cause? A hook's allow or deny on a given input, what a
|
|
46
|
+
skill's contract promises.
|
|
47
|
+
- **Lives:** `<surface>.harness.mjs` beside what it tests; it imports `runHook`, `runHarnessTest`
|
|
48
|
+
or `runEval` from vigiles. Harnesses written before this rule that import none of the three are
|
|
49
|
+
frozen in `scripts/harness-api.frozen.json`, which only shrinks (`scripts/harness-api.test.ts`,
|
|
50
|
+
#77). A new test of anything else is a unit or integration test.
|
|
51
|
+
- **Asserts through** `lib/check.mjs`: `const check = createChecker();` then
|
|
52
|
+
`check(label, cond, detail)`. A failure prints the label and the detail, every call is counted.
|
|
53
|
+
Do not define a local `check`.
|
|
54
|
+
- **Example:** `hooks/hooks.harness.mjs`.
|
|
55
|
+
- **Skills are tested through vigiles**, not a bespoke runner: a home-grown runner here once printed
|
|
56
|
+
byte-identical "clean" verdicts for three skills that had never loaded.
|
|
57
|
+
|
|
58
|
+
## End-to-end — `test/e2e/*.mjs`
|
|
59
|
+
|
|
60
|
+
- **Answers:** does the package work once it is somewhere else? The tarball installed into another
|
|
61
|
+
tree, a real `pdflatex` build, the PDF's fonts measured.
|
|
62
|
+
- **May touch:** everything a user's machine has, except the network: a citation service is a
|
|
63
|
+
counting fake `fetch` even here (`test/e2e/build.mjs`).
|
|
64
|
+
- **What each run proves, and when a change owes one:** [`e2e.md`](e2e.md).
|
|
65
|
+
|
|
66
|
+
## Running
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx vitest run <file or folder> # unit and integration tests
|
|
70
|
+
npx vigiles test <file> # one harness
|
|
71
|
+
npm test # everything: vitest, then every harness
|
|
72
|
+
npm run coverage # the same under c8, failing below the thresholds in .c8rc.json
|
|
73
|
+
npm run test:e2e # the end-to-end runs
|
|
74
|
+
npm run check # every gate CI runs, and the list of what it does not reproduce
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
While editing, run the tests you touched, `npx tsc --noEmit`, `npx eslint <files>` and
|
|
78
|
+
`npm run fmt:check`. **Before every push, run `npm run check` and read its exit code** — the
|
|
79
|
+
coverage gate cannot be judged from one file.
|
|
80
|
+
|
|
81
|
+
⚠️ **Not `vigiles test .`**, and never drop `--min=1` from `npm test`. Measured 2026-09-11:
|
|
82
|
+
|
|
83
|
+
| form | what happens | exit |
|
|
84
|
+
| ---------------------- | --------------------------------------------------------------------------- | ----- |
|
|
85
|
+
| `vigiles test .` | `.` is read as a FILE — `ERR_UNSUPPORTED_DIR_IMPORT`, uncaught, runner dies | **0** |
|
|
86
|
+
| `vigiles test` | `No **/*.harness.{mjs,cjs,js,mts,cts,ts} files found.` | **0** |
|
|
87
|
+
| `vigiles test --min=1` | names the empty match and fails | **1** |
|
|
88
|
+
|
|
89
|
+
`--min=1` is the only thing between "every test passed" and "no test ran". vitest exits 1 when no
|
|
90
|
+
file matches, and it transpiles without type-checking, so `npm run check` runs
|
|
91
|
+
`tsc -p tsconfig.test.json` as its own gate.
|
|
92
|
+
|
|
93
|
+
## Rules for every kind
|
|
94
|
+
|
|
95
|
+
1. **Red first.** Run a new test on the code before the change and watch it fail at its own
|
|
96
|
+
assertion. A test that has only ever been green is indistinguishable from one that cannot fail.
|
|
97
|
+
Never add a file that patches source to prove a test can fail.
|
|
98
|
+
2. **Assert the whole value** — `assert.deepEqual`, `expect(x).toEqual(…)` on the entire result,
|
|
99
|
+
not a substring or one field. A substring passes on output that is wrong everywhere else. The
|
|
100
|
+
exception is prose whose wording is not the subject; say so in a comment.
|
|
101
|
+
3. **Test what the code does, never what its source says.** A test that reads a source file (a
|
|
102
|
+
`.mjs`, a `SKILL.md`, a README, a config) and asserts it contains some text restates the file:
|
|
103
|
+
rewording turns it red while a real regression stays green. Assert a return value, a run's output
|
|
104
|
+
and exit code, what landed on disk, the requests made. Text is the subject only when it is the
|
|
105
|
+
output — a message the program prints, a file it generates.
|
|
106
|
+
4. **No test touches the real network.** A service is a fake `fetch` or a fake port. A test that
|
|
107
|
+
passes only while Crossref or DBLP answers is not a test of this package.
|
|
108
|
+
5. **Coverage is 100% on lines, statements, functions and branches**, enforced by `npm run coverage`
|
|
109
|
+
in `npm run check` and CI. c8 reads `NODE_V8_COVERAGE`, so a CLI a test spawns is measured too,
|
|
110
|
+
unless the test hands the child a fresh `env` without it. vitest runs in the `threads` pool with
|
|
111
|
+
native `import` (`vitest.config.ts` says why), and the run preloads `test/coverage-src.mjs`, which
|
|
112
|
+
answers every import of `dist/*.js` with `src/*.ts`, so no module is measured twice.
|
|
113
|
+
6. **No `c8 ignore`, and no guard deleted to reach 100%.** An effect a test cannot reach — a race, a
|
|
114
|
+
permission root is never denied, a broken install, a 270 MB download — is made injectable and the
|
|
115
|
+
test passes a fake. A precondition about an argument becomes the signature. A guard whose input can
|
|
116
|
+
really occur stays, with the test that reaches it. `scripts/coverage-config.test.ts` fails on any
|
|
117
|
+
coverage-ignore comment and pins `.c8rc.json`'s `exclude` list.
|
|
118
|
+
7. **Say what an assertion guards** in a comment directly above it (`// Guards: …`).
|
package/docs/toolchain.md
CHANGED
|
@@ -141,12 +141,14 @@ bash node_modules/paperlint/skills/render-paper/ensure-checkers.sh
|
|
|
141
141
|
|
|
142
142
|
```
|
|
143
143
|
✅ aclpubcheck
|
|
144
|
-
✅ rebiber
|
|
144
|
+
✅ rebiber 1.4.0
|
|
145
145
|
✅ jinja2
|
|
146
146
|
✅ textidote (/opt/textidote/textidote.jar)
|
|
147
147
|
✅ all checkers are installed AND run
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
It checks that the tools RUN, not that pip exited zero
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
It checks that the tools RUN, not that pip exited zero: aclpubcheck's checker module is imported
|
|
151
|
+
(which pulls in numpy and pandas) and its `python3 -m aclpubcheck` entry point must answer, and
|
|
152
|
+
rebiber must convert a known arXiv entry to its official record. Both come from their GitHub
|
|
153
|
+
sources pinned to a commit — each project's own instruction, and why is recorded in
|
|
154
|
+
`checkers-requirements.txt`.
|
|
@@ -118,4 +118,20 @@ const cases = [];
|
|
|
118
118
|
cases.push("js quiet on a module that asks the port");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
// ── 5. A TAGGED TEMPLATE WITH AN INVALID ESCAPE has no cooked value, only the raw one — and the
|
|
122
|
+
// path in it is still a path. (`String.raw` is exactly where such a template is legal.)
|
|
123
|
+
{
|
|
124
|
+
const [res] = await jsLinter.lintText(
|
|
125
|
+
"export const p = String.raw`node_modules/paperlint/\\unicode`;\n",
|
|
126
|
+
{ filePath: join(ROOT, "tagged.probe.mjs") },
|
|
127
|
+
);
|
|
128
|
+
const m = res.messages.filter((x) => x.ruleId === RULE_JS);
|
|
129
|
+
assert.equal(
|
|
130
|
+
m.length,
|
|
131
|
+
1,
|
|
132
|
+
`a raw-only template quasi must be read, got ${JSON.stringify(res.messages)}`,
|
|
133
|
+
);
|
|
134
|
+
cases.push("js fires on a tagged template whose quasi has no cooked value");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
recordCheck(cases.length);
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* clean.tex` goes through the repository's real `eslint.config.mjs` and must be silent.
|
|
12
12
|
* II. THE PROJECTION ACTUALLY DID ITS WORK — headings (including SYNTHESISED ones), caption
|
|
13
13
|
* and footnote prose kept, float markup blanked, opaque macros blanked, math blanked,
|
|
14
|
-
* comments recognised. This is the half
|
|
15
|
-
* language could be gutted and part I would stay green, because a
|
|
16
|
-
* everything is also silent.
|
|
14
|
+
* comments recognised. This is the half a gutted projection could still hide behind;
|
|
15
|
+
* without it the language could be gutted and part I would stay green, because a
|
|
16
|
+
* projection that blanks everything is also silent.
|
|
17
17
|
* III. FIRES ON A PLANTED DEFECT — `fixtures/latex-language/defect.tex`, through the real
|
|
18
18
|
* config, must produce findings, and at the right addresses.
|
|
19
19
|
* IV. THE LANGUAGE'S BOUNDARIES AS ASSERTIONS. A known limitation recorded only in prose
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* arguments of those two macros.
|
|
34
34
|
* 🔴 Removing `table` / `figure` from `CODEISH` was NOT an option — rules would then receive
|
|
35
35
|
* `&`, `\\`, the column specification and tikz nodes, i.e. findings on markup. That is pinned
|
|
36
|
-
* by the harness
|
|
36
|
+
* by the harness.
|
|
37
37
|
* Measured after the fix: nine new findings across three papers, all of them captions and
|
|
38
38
|
* footnotes, reviewed one by one; zero false positives on markup.
|
|
39
39
|
*
|
|
@@ -143,8 +143,13 @@ const argEnd = (node) => {
|
|
|
143
143
|
return end;
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
/**
|
|
147
|
+
* The projection of `src`. `parse` is unified-latex by default; a test passes one returning the
|
|
148
|
+
* node shapes today's parser never emits (a macro without `args`, an argument without `content`, an
|
|
149
|
+
* `env` given as nodes), which the fallbacks below keep reading instead of throwing on.
|
|
150
|
+
*/
|
|
151
|
+
export function texToMdast(src, { parse = (s) => getParser().parse(s) } = {}) {
|
|
152
|
+
const ast = parse(src);
|
|
148
153
|
const chars = src.split("");
|
|
149
154
|
const blank = (from, to) => {
|
|
150
155
|
for (let i = from; i < to && i < chars.length; i++)
|
|
@@ -264,15 +269,13 @@ export function texToMdast(src) {
|
|
|
264
269
|
return;
|
|
265
270
|
}
|
|
266
271
|
if (env === "thebibliography" && pos) {
|
|
267
|
-
|
|
272
|
+
// `\begin{thebibliography}` (23 characters) always holds `## References` (13).
|
|
273
|
+
synthHeading(
|
|
268
274
|
pos.start.offset,
|
|
269
275
|
pos.start.offset + "\\begin{thebibliography}".length,
|
|
270
276
|
"References",
|
|
271
277
|
);
|
|
272
|
-
blank(
|
|
273
|
-
pos.start.offset + (h ? "## References".length : 0),
|
|
274
|
-
pos.end.offset,
|
|
275
|
-
);
|
|
278
|
+
blank(pos.start.offset + "## References".length, pos.end.offset);
|
|
276
279
|
children.push({
|
|
277
280
|
type: "code",
|
|
278
281
|
lang: env,
|
|
@@ -282,7 +285,7 @@ export function texToMdast(src) {
|
|
|
282
285
|
return;
|
|
283
286
|
}
|
|
284
287
|
if (CODEISH.test(env) && pos) {
|
|
285
|
-
// 🔴 THERE WERE TWO LOCKS, AND LIFTING ONE DID NOT HELP
|
|
288
|
+
// 🔴 THERE WERE TWO LOCKS, AND LIFTING ONE DID NOT HELP: a
|
|
286
289
|
// caption was blanked by `OPAQUE` as a macro, and inside `table` / `figure` it was
|
|
287
290
|
// blanked AGAIN by the whole environment. So what is blanked here is not the float's
|
|
288
291
|
// borders but the float MINUS the arguments of `\caption{}` / `\footnote{}`.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* texToMdast / texLanguage on the shapes the harness does not carry: a `thebibliography`
|
|
3
|
+
* environment, a float on the file's last line with no newline after it, a heading whose title
|
|
4
|
+
* holds a macro, an unknown macro in prose, and a parse that fails (nesting deep enough to exhaust the parser).
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { test } from "vitest";
|
|
8
|
+
import { texLanguage, texToMdast } from "./latex-language.mjs";
|
|
9
|
+
|
|
10
|
+
/** The projection plus each node's type, lang/title and start–end offsets. */
|
|
11
|
+
const project = (src) => {
|
|
12
|
+
const { root, text } = texToMdast(src);
|
|
13
|
+
return {
|
|
14
|
+
text,
|
|
15
|
+
nodes: root.children.map((c) => [
|
|
16
|
+
c.type,
|
|
17
|
+
c.lang ?? c.title,
|
|
18
|
+
c.position.start.offset,
|
|
19
|
+
c.position.end.offset,
|
|
20
|
+
]),
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
test("thebibliography: a References heading over `\\begin`, the rest one blanked code node", () => {
|
|
25
|
+
assert.deepEqual(
|
|
26
|
+
project(
|
|
27
|
+
"\\begin{thebibliography}{9}\n\\bibitem{a} A.\n\\end{thebibliography}\n",
|
|
28
|
+
),
|
|
29
|
+
{
|
|
30
|
+
text: "## References \n \n \n",
|
|
31
|
+
nodes: [
|
|
32
|
+
["heading", "References", 0, 13],
|
|
33
|
+
["code", "thebibliography", 14, 63],
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("a float on the last line with no newline after it ends at the end of the file", () => {
|
|
40
|
+
assert.deepEqual(project("x\n\\begin{table}y\\end{table}"), {
|
|
41
|
+
text: "x\n ",
|
|
42
|
+
nodes: [["code", "table", 2, 27]],
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("a heading's title drops an argument-less macro inside it; an unknown macro in prose loses only its name", () => {
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
project("\\section{A \\LaTeX{} b}\nsome \\foo{bar} text\n"),
|
|
49
|
+
{
|
|
50
|
+
text: "## A b \nsome {bar} text\n",
|
|
51
|
+
nodes: [["heading", "A b", 0, 22]],
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a heading's title keeps the text of a macro with an argument", () => {
|
|
57
|
+
assert.deepEqual(project("\\section{A \\emph{b} c}\n"), {
|
|
58
|
+
text: "## A b c \n",
|
|
59
|
+
nodes: [["heading", "A b c", 0, 22]],
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a body the parser cannot take (nesting deep enough to exhaust its stack) is a parse failure, not a throw", () => {
|
|
64
|
+
const r = texLanguage.parse({ body: "{".repeat(20_000) });
|
|
65
|
+
assert.deepEqual(
|
|
66
|
+
[r.ok, r.errors.map((e) => e.constructor.name)],
|
|
67
|
+
[false, ["RangeError"]],
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** A node position on line 1, from offset `a` to offset `b`. */
|
|
72
|
+
const at = (a, b) => ({
|
|
73
|
+
start: { offset: a, line: 1, column: a + 1 },
|
|
74
|
+
end: { offset: b, line: 1, column: b + 1 },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("node shapes today's parser never emits are read, not thrown on", () => {
|
|
78
|
+
// Each node lacks what unified-latex always gives: `args`, an argument's `content`, a string `env`.
|
|
79
|
+
const src = " ".repeat(200);
|
|
80
|
+
const ast = {
|
|
81
|
+
type: "root",
|
|
82
|
+
content: [
|
|
83
|
+
// a heading macro with no args at all: an empty title
|
|
84
|
+
{ type: "macro", content: "section", position: at(0, 8) },
|
|
85
|
+
// a heading whose title holds a macro whose argument has no content
|
|
86
|
+
{
|
|
87
|
+
type: "macro",
|
|
88
|
+
content: "subsection",
|
|
89
|
+
position: at(10, 40),
|
|
90
|
+
args: [
|
|
91
|
+
{
|
|
92
|
+
type: "argument",
|
|
93
|
+
openMark: "{",
|
|
94
|
+
closeMark: "}",
|
|
95
|
+
content: [
|
|
96
|
+
{ type: "string", content: "T", position: at(22, 23) },
|
|
97
|
+
{ type: "macro", content: "emph", args: [{ type: "argument" }] },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
// an environment named by nodes rather than a string
|
|
103
|
+
{
|
|
104
|
+
type: "environment",
|
|
105
|
+
env: [{ type: "string", content: "abstract" }],
|
|
106
|
+
content: [],
|
|
107
|
+
position: at(50, 80),
|
|
108
|
+
},
|
|
109
|
+
// a float whose caption has no args, and whose footnote's argument has no content
|
|
110
|
+
{
|
|
111
|
+
type: "environment",
|
|
112
|
+
env: "table",
|
|
113
|
+
position: at(90, 120),
|
|
114
|
+
content: [
|
|
115
|
+
{ type: "macro", content: "caption", position: at(95, 100) },
|
|
116
|
+
{
|
|
117
|
+
type: "macro",
|
|
118
|
+
content: "footnote",
|
|
119
|
+
position: at(101, 110),
|
|
120
|
+
args: [{ type: "argument" }],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
// texttt and a first-column textbf with no args; an ordinary macro whose argument is empty
|
|
125
|
+
{ type: "macro", content: "texttt", position: at(125, 132) },
|
|
126
|
+
{
|
|
127
|
+
type: "macro",
|
|
128
|
+
content: "textbf",
|
|
129
|
+
position: {
|
|
130
|
+
start: { offset: 133, line: 2, column: 1 },
|
|
131
|
+
end: { offset: 140, line: 2, column: 8 },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: "macro",
|
|
136
|
+
content: "foo",
|
|
137
|
+
position: at(141, 150),
|
|
138
|
+
args: [{ type: "argument" }],
|
|
139
|
+
},
|
|
140
|
+
// the same macros with an argument that has no content
|
|
141
|
+
{
|
|
142
|
+
type: "macro",
|
|
143
|
+
content: "subsubsection",
|
|
144
|
+
position: at(170, 180),
|
|
145
|
+
args: [{ type: "argument", openMark: "{", closeMark: "}" }],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: "macro",
|
|
149
|
+
content: "texttt",
|
|
150
|
+
position: at(181, 185),
|
|
151
|
+
args: [{ type: "argument" }],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
type: "macro",
|
|
155
|
+
content: "emph",
|
|
156
|
+
position: {
|
|
157
|
+
start: { offset: 186, line: 3, column: 1 },
|
|
158
|
+
end: { offset: 190, line: 3, column: 5 },
|
|
159
|
+
},
|
|
160
|
+
args: [{ type: "argument" }],
|
|
161
|
+
},
|
|
162
|
+
// `\bibliography` squeezed into one character: too short for `## References`, so no heading
|
|
163
|
+
{ type: "macro", content: "bibliography", position: at(160, 160) },
|
|
164
|
+
// a node the walk meets without a position
|
|
165
|
+
{ type: "environment", env: "abstract", content: [] },
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
const { root } = texToMdast(src, { parse: () => ast });
|
|
169
|
+
assert.deepEqual(
|
|
170
|
+
root.children.map((c) => [c.type, c.title ?? c.lang ?? ""]),
|
|
171
|
+
// Sorted by offset: the float's code node starts at its (single) line's start.
|
|
172
|
+
[
|
|
173
|
+
["heading", ""],
|
|
174
|
+
["code", "table"],
|
|
175
|
+
["heading", "T"],
|
|
176
|
+
["heading", "Abstract"],
|
|
177
|
+
["strong", ""],
|
|
178
|
+
["heading", ""],
|
|
179
|
+
["strong", ""],
|
|
180
|
+
],
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("a source code made from a parse result without the raw text reads the projection; parents are recorded", () => {
|
|
185
|
+
const body = "\\section{A}\n";
|
|
186
|
+
const parsed = texLanguage.parse({ body });
|
|
187
|
+
const code = texLanguage.createSourceCode(
|
|
188
|
+
{ body },
|
|
189
|
+
{ ast: parsed.ast, projected: parsed.projected },
|
|
190
|
+
);
|
|
191
|
+
const [heading] = parsed.ast.children;
|
|
192
|
+
assert.deepEqual(
|
|
193
|
+
[
|
|
194
|
+
code.raw === parsed.projected,
|
|
195
|
+
code.getParent(parsed.ast),
|
|
196
|
+
code.getParent(heading) === parsed.ast,
|
|
197
|
+
],
|
|
198
|
+
[true, undefined, true],
|
|
199
|
+
);
|
|
200
|
+
});
|