paperlint 7.2.0 → 9.0.0
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/CLAUDE.md +9 -25
- package/CONTRIBUTING.md +5 -4
- package/README.md +4 -3
- package/dist/adapters/bibtex/index.d.ts +3 -0
- package/dist/adapters/bibtex/index.d.ts.map +1 -0
- package/dist/adapters/bibtex/index.js +103 -0
- package/dist/adapters/bibtex/index.js.map +1 -0
- package/dist/adapters/latex/filecontents.d.ts +33 -0
- package/dist/adapters/latex/filecontents.d.ts.map +1 -0
- package/dist/adapters/latex/filecontents.js +65 -0
- package/dist/adapters/latex/filecontents.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +3 -3
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +18 -10
- package/dist/adapters/latex/includes.js.map +1 -1
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +2 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/nodes.d.ts +12 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +39 -0
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +7 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +55 -6
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +9 -0
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +27 -24
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/sources-record/index.d.ts +9 -0
- package/dist/adapters/sources-record/index.d.ts.map +1 -0
- package/dist/adapters/sources-record/index.js +68 -0
- package/dist/adapters/sources-record/index.js.map +1 -0
- package/dist/adapters/tex-output/blg.d.ts +9 -0
- package/dist/adapters/tex-output/blg.d.ts.map +1 -0
- package/dist/adapters/tex-output/blg.js +20 -0
- package/dist/adapters/tex-output/blg.js.map +1 -0
- package/dist/adapters/tex-output/fls.d.ts +8 -0
- package/dist/adapters/tex-output/fls.d.ts.map +1 -0
- package/dist/adapters/tex-output/fls.js +16 -0
- package/dist/adapters/tex-output/fls.js.map +1 -0
- package/dist/adapters/tex-output/index.d.ts +7 -0
- package/dist/adapters/tex-output/index.d.ts.map +1 -0
- package/dist/adapters/tex-output/index.js +7 -0
- package/dist/adapters/tex-output/index.js.map +1 -0
- package/dist/bib-rules.d.ts +47 -0
- package/dist/bib-rules.d.ts.map +1 -0
- package/dist/bib-rules.js +127 -0
- package/dist/bib-rules.js.map +1 -0
- package/dist/build.d.ts +29 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +41 -30
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +2 -2
- package/dist/cli.d.ts +9 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -68
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +1 -10
- package/dist/domain/paper-source.d.ts.map +1 -1
- package/dist/domain/paper-source.js +34 -24
- package/dist/domain/paper-source.js.map +1 -1
- package/dist/domain/paper-sources.d.ts +72 -0
- package/dist/domain/paper-sources.d.ts.map +1 -0
- package/dist/domain/paper-sources.js +18 -0
- package/dist/domain/paper-sources.js.map +1 -0
- package/dist/domain/sources-record.d.ts +102 -0
- package/dist/domain/sources-record.d.ts.map +1 -0
- package/dist/domain/sources-record.js +38 -0
- package/dist/domain/sources-record.js.map +1 -0
- package/dist/domain/tex-run.d.ts +25 -0
- package/dist/domain/tex-run.d.ts.map +1 -0
- package/dist/domain/tex-run.js +79 -0
- package/dist/domain/tex-run.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts.map +1 -1
- package/dist/eslint-rules/paper-context.js +6 -5
- package/dist/eslint-rules/paper-context.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +0 -9
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +34 -96
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +10 -0
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +10 -0
- package/dist/latex-log.js.map +1 -1
- package/dist/latex-loop.d.ts +2 -0
- package/dist/latex-loop.d.ts.map +1 -1
- package/dist/latex-loop.js.map +1 -1
- package/dist/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +0 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +3 -2
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-includes.d.ts +4 -7
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +29 -21
- package/dist/paper-includes.js.map +1 -1
- package/dist/paper-prose.d.ts +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- package/dist/paper-record.d.ts +25 -0
- package/dist/paper-record.d.ts.map +1 -0
- package/dist/paper-record.js +39 -0
- package/dist/paper-record.js.map +1 -0
- package/dist/paper-sources.d.ts +66 -0
- package/dist/paper-sources.d.ts.map +1 -0
- package/dist/paper-sources.js +92 -0
- package/dist/paper-sources.js.map +1 -0
- package/dist/ports/bib-reader.d.ts +18 -0
- package/dist/ports/bib-reader.d.ts.map +1 -0
- package/dist/ports/bib-reader.js +2 -0
- package/dist/ports/bib-reader.js.map +1 -0
- package/dist/ports/check-references.d.ts +9 -4
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/latex.d.ts +15 -0
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/sources-codec.d.ts +11 -0
- package/dist/ports/sources-codec.d.ts.map +1 -0
- package/dist/ports/sources-codec.js +2 -0
- package/dist/ports/sources-codec.js.map +1 -0
- package/dist/ports/tex-output.d.ts +17 -0
- package/dist/ports/tex-output.d.ts.map +1 -0
- package/dist/ports/tex-output.js +2 -0
- package/dist/ports/tex-output.js.map +1 -0
- package/dist/recorded-bibliography.d.ts +89 -0
- package/dist/recorded-bibliography.d.ts.map +1 -0
- package/dist/recorded-bibliography.js +136 -0
- package/dist/recorded-bibliography.js.map +1 -0
- package/dist/reference-rules.d.ts +5 -5
- package/dist/reference-rules.d.ts.map +1 -1
- package/dist/reference-rules.js +150 -77
- package/dist/reference-rules.js.map +1 -1
- package/dist/references.d.ts +71 -16
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +110 -26
- package/dist/references.js.map +1 -1
- package/dist/register-bands.d.ts +3 -3
- package/dist/register-bands.d.ts.map +1 -1
- package/dist/register-bands.js.map +1 -1
- package/dist/register.d.ts +2 -2
- package/dist/sources-record.d.ts +38 -0
- package/dist/sources-record.d.ts.map +1 -0
- package/dist/sources-record.js +123 -0
- package/dist/sources-record.js.map +1 -0
- package/dist/sources-rules.d.ts +44 -0
- package/dist/sources-rules.d.ts.map +1 -0
- package/dist/sources-rules.js +76 -0
- package/dist/sources-rules.js.map +1 -0
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-paper.d.ts +10 -15
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +24 -40
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +3 -3
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +18 -12
- package/docs/design/paper-sources.md +197 -0
- package/docs/e2e.md +4 -6
- package/docs/package-shape-options.md +1 -1
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- package/docs/references.md +34 -8
- package/docs/rules/bib/reachable-entry.md +64 -0
- package/docs/rules/paper/sources-fresh.md +81 -0
- package/docs/rules/tex/claim-provenance.md +0 -1
- package/docs/rules.md +62 -61
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +6 -5
- package/eslint-rules/paper-typography.test.mjs +29 -157
- package/eslint-rules/paper-typography.ts +35 -104
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +22 -1
- package/fixtures/build-e2e/aidc/paper.tex +1 -1
- package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
- package/fixtures/build-e2e/anon-author/paper.tex +1 -1
- package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
- package/fixtures/build-e2e/anon-href/paper.tex +1 -1
- package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
- package/fixtures/paper-sources/p1/paper.tex +30 -0
- package/fixtures/paper-sources/p1/refs.bib +6 -0
- package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
- package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
- package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
- package/fixtures/paper-sources/p1/tex-truth.json +37 -0
- package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
- package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
- package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
- package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
- package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
- package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
- package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
- package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
- package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
- package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
- package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
- package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
- package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
- package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
- package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
- package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
- package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
- package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
- package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
- package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
- package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
- package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
- package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
- package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
- package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
- package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
- package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
- package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
- package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
- package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
- package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
- package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
- package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
- package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
- package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
- package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
- package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
- package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
- package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
- package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
- package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
- package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
- package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
- package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
- package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
- package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
- package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
- package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
- package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
- package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
- package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
- package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
- package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
- package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
- package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
- package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
- package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
- package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
- package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
- package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
- package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
- package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
- package/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +0 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +2 -12
- package/presets/realm.jsonc +1 -1
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +19 -10
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +15 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +117 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- package/skills/verify-citations/SKILL.md +7 -4
- package/skills/verify-citations/SKILL.md.spec.ts +6 -3
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -32
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
- package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
- package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
- package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
- package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
- package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
- package/src/adapters/bibtex/index.test.ts +163 -0
- package/src/adapters/bibtex/index.ts +155 -0
- package/src/adapters/latex/filecontents.ts +114 -0
- package/src/adapters/latex/includes.ts +18 -10
- package/src/adapters/latex/index.test.ts +73 -0
- package/src/adapters/latex/index.ts +2 -0
- package/src/adapters/latex/nodes.ts +64 -0
- package/src/adapters/latex/parse.test.ts +38 -0
- package/src/adapters/latex/parse.ts +67 -6
- package/src/adapters/memory/index.ts +17 -0
- package/src/adapters/references/index.test.ts +75 -21
- package/src/adapters/references/index.ts +37 -32
- package/src/adapters/sources-record/index.test.ts +87 -0
- package/src/adapters/sources-record/index.ts +82 -0
- package/src/adapters/tex-output/blg.ts +27 -0
- package/src/adapters/tex-output/fls.ts +24 -0
- package/src/adapters/tex-output/index.test.ts +171 -0
- package/src/adapters/tex-output/index.ts +12 -0
- package/src/bib-rules.test.ts +356 -0
- package/src/bib-rules.ts +228 -0
- package/src/bibliography-at.test.ts +286 -0
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +211 -1
- package/src/build.ts +101 -41
- package/src/claim-provenance.test.ts +33 -7
- package/src/claim-provenance.ts +2 -2
- package/src/cli.harness.mjs +46 -56
- package/src/cli.test.ts +44 -48
- package/src/cli.ts +51 -82
- package/src/domain/paper-source.test.ts +58 -20
- package/src/domain/paper-source.ts +38 -42
- package/src/domain/paper-sources.ts +94 -0
- package/src/domain/sources-record.test.ts +71 -0
- package/src/domain/sources-record.ts +136 -0
- package/src/domain/tex-run.test.ts +228 -0
- package/src/domain/tex-run.ts +139 -0
- package/src/folder-venue-rule.test.ts +1 -1
- package/src/heading-case.test.ts +24 -10
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/latex-log.ts +20 -0
- package/src/latex-loop.harness.ts +1 -0
- package/src/latex-loop.ts +2 -0
- package/src/lint-fix.test.ts +10 -4
- package/src/linted-set.test.ts +9 -1
- package/src/new-paper.harness.mjs +12 -30
- package/src/new-paper.test.ts +15 -11
- package/src/new-paper.ts +11 -30
- package/src/package-dirs.ts +3 -2
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-includes.test.ts +157 -38
- package/src/paper-includes.ts +39 -35
- package/src/paper-prose.test.ts +78 -0
- package/src/paper-prose.ts +67 -0
- package/src/paper-record.test.ts +173 -0
- package/src/paper-record.ts +66 -0
- package/src/paper-settings-commands.test.ts +5 -2
- package/src/paper-sources.ts +151 -0
- package/src/parse-args.test.ts +0 -1
- package/src/ports/bib-reader.ts +27 -0
- package/src/ports/check-references.ts +7 -3
- package/src/ports/latex.ts +19 -0
- package/src/ports/sources-codec.ts +11 -0
- package/src/ports/tex-output.ts +17 -0
- package/src/recorded-bibliography.ts +297 -0
- package/src/reference-rules.ts +217 -104
- package/src/references.test.ts +520 -177
- package/src/references.ts +185 -37
- package/src/register-bands.test.ts +5 -1
- package/src/register-bands.ts +3 -5
- package/src/register.test.ts +6 -1
- package/src/register.ts +2 -2
- package/src/rule-cases.test.ts +68 -12
- package/src/rule-docs.test.ts +0 -1
- package/src/sources-record.test.ts +308 -0
- package/src/sources-record.ts +231 -0
- package/src/sources-rules.test.ts +152 -0
- package/src/sources-rules.ts +117 -0
- package/src/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- package/src/tex-paper.test.ts +148 -78
- package/src/tex-paper.ts +31 -61
- package/src/tex-venue-rules.test.ts +36 -8
- package/src/tex-venue-rules.ts +3 -5
- package/src/types.ts +0 -2
- package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
- package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
- package/dist/eslint-rules/bib-reachable-entry.js +0 -75
- package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
- package/eslint-rules/bib-reachable-entry.ts +0 -76
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
package/hooks/hooks.harness.ts
CHANGED
|
@@ -206,7 +206,7 @@ try {
|
|
|
206
206
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
207
|
{
|
|
208
208
|
const dir = fixture({ [PAPERS_DIR_FIELD]: "docs/papers" });
|
|
209
|
-
const P = "docs/papers/alpha/paper.
|
|
209
|
+
const P = "docs/papers/alpha/paper.tex";
|
|
210
210
|
const T = "docs/papers/alpha/paper.tex";
|
|
211
211
|
const deny = (label: string, cmd: string) => {
|
|
212
212
|
const r = at(dir, "paper-edit-guard", onBash(cmd));
|
|
@@ -228,7 +228,7 @@ try {
|
|
|
228
228
|
};
|
|
229
229
|
|
|
230
230
|
// The two cases `touches` alone MISSES: a redirection target is not an argv token.
|
|
231
|
-
deny("a redirect write to paper.
|
|
231
|
+
deny("a redirect write to paper.tex", `echo x ${GT} ${P}`);
|
|
232
232
|
deny("an append write to a .tex", `cat /tmp/a ${GT}${GT} ${T}`);
|
|
233
233
|
deny("sed -i on a paper", `sed -i s/a/b/ ${P}`);
|
|
234
234
|
deny("cp onto a paper", `cp /tmp/a ${P}`);
|
|
@@ -358,8 +358,8 @@ try {
|
|
|
358
358
|
// for a gate that had never read the declaration. Caught by its own negative control,
|
|
359
359
|
// 2026-09-12. A declared root only demonstrates anything when the default would MISS it.
|
|
360
360
|
const DECLARED = "writing/drafts";
|
|
361
|
-
const declaredWrite = onBash(`echo x ${GT} ${DECLARED}/alpha/paper.
|
|
362
|
-
const defaultWrite = onBash(`echo x ${GT} papers/alpha/paper.
|
|
361
|
+
const declaredWrite = onBash(`echo x ${GT} ${DECLARED}/alpha/paper.tex`);
|
|
362
|
+
const defaultWrite = onBash(`echo x ${GT} papers/alpha/paper.tex`);
|
|
363
363
|
|
|
364
364
|
// (a) a declared root is used AS DECLARED — and the default is NOT.
|
|
365
365
|
{
|
|
@@ -479,11 +479,11 @@ try {
|
|
|
479
479
|
r.stdout.length === 0 && r.stderr.length === 0 && r.exitCode === 0,
|
|
480
480
|
);
|
|
481
481
|
};
|
|
482
|
-
lands("a relative paper.
|
|
482
|
+
lands("a relative paper.tex", "docs/papers/alpha/paper.tex");
|
|
483
483
|
lands("a .tex", "docs/papers/alpha/paper.tex");
|
|
484
484
|
// 🔴 THE ABSOLUTE SPELLING. The live harness sends absolute paths; a helper that only builds
|
|
485
485
|
// relative ones let an anchored predecessor pass every test while being dead in production.
|
|
486
|
-
lands("an ABSOLUTE paper.
|
|
486
|
+
lands("an ABSOLUTE paper.tex", join(dir, "docs/papers/alpha/paper.tex"));
|
|
487
487
|
silent("a README under the papers root", "docs/papers/README.md");
|
|
488
488
|
silent("a file outside the papers root", "README.md");
|
|
489
489
|
// The advisory's own asymmetry: it goes quiet where the gate refuses.
|
|
@@ -495,7 +495,7 @@ try {
|
|
|
495
495
|
const r = at(
|
|
496
496
|
broken,
|
|
497
497
|
"paper-skills-nudge",
|
|
498
|
-
onEdit("docs/papers/alpha/paper.
|
|
498
|
+
onEdit("docs/papers/alpha/paper.tex"),
|
|
499
499
|
);
|
|
500
500
|
check(
|
|
501
501
|
"nudge is SILENT (not blocking) on an unusable declaration",
|
|
@@ -508,7 +508,7 @@ try {
|
|
|
508
508
|
const d = at(
|
|
509
509
|
broken,
|
|
510
510
|
"paper-skills-nudge",
|
|
511
|
-
onEdit("papers/alpha/paper.
|
|
511
|
+
onEdit("papers/alpha/paper.tex"),
|
|
512
512
|
);
|
|
513
513
|
check(
|
|
514
514
|
"nudge: an explicit null is NOT read as an absence (no fallback to the default root)",
|
|
@@ -537,15 +537,15 @@ try {
|
|
|
537
537
|
r.stdout.length === 0 && r.stderr.length === 0 && r.exitCode === 0,
|
|
538
538
|
);
|
|
539
539
|
};
|
|
540
|
-
fires("a relative paper.
|
|
541
|
-
fires("an ABSOLUTE paper.
|
|
540
|
+
fires("a relative paper.tex", "docs/papers/alpha/paper.tex");
|
|
541
|
+
fires("an ABSOLUTE paper.tex", join(dir, "docs/papers/alpha/paper.tex"));
|
|
542
542
|
silent("a README under the papers root", "docs/papers/README.md");
|
|
543
543
|
silent("a file outside the papers root", "README.md");
|
|
544
544
|
// 🔴 THE BOUNDARY IN `(?:^|/)`: a directory merely ENDING in the root's last segment must
|
|
545
545
|
// not smuggle a match. Without the boundary this is a hit.
|
|
546
546
|
silent(
|
|
547
547
|
"a look-alike root (`notdocs/papers/…`)",
|
|
548
|
-
"notdocs/papers/alpha/paper.
|
|
548
|
+
"notdocs/papers/alpha/paper.tex",
|
|
549
549
|
);
|
|
550
550
|
// A `.tex` whose directory name carries a shell metacharacter cannot reach a command.
|
|
551
551
|
silent(
|
|
@@ -560,8 +560,8 @@ try {
|
|
|
560
560
|
);
|
|
561
561
|
check(
|
|
562
562
|
"gates: an explicit null is NOT read as an absence (no fallback to the default root)",
|
|
563
|
-
at(broken, "paper-status-gates", onEdit("papers/alpha/paper.
|
|
564
|
-
.length === 0,
|
|
563
|
+
at(broken, "paper-status-gates", onEdit("papers/alpha/paper.tex"))
|
|
564
|
+
.stderr.length === 0,
|
|
565
565
|
);
|
|
566
566
|
}
|
|
567
567
|
// The declared root is regex-ESCAPED: `.` in a root must not act as a wildcard.
|
|
@@ -576,7 +576,7 @@ try {
|
|
|
576
576
|
at(
|
|
577
577
|
dotted,
|
|
578
578
|
"paper-status-gates",
|
|
579
|
-
onEdit("docs.v2/papers/alpha/paper.
|
|
579
|
+
onEdit("docs.v2/papers/alpha/paper.tex"),
|
|
580
580
|
).stderr,
|
|
581
581
|
),
|
|
582
582
|
);
|
|
@@ -585,7 +585,7 @@ try {
|
|
|
585
585
|
at(
|
|
586
586
|
dotted,
|
|
587
587
|
"paper-status-gates",
|
|
588
|
-
onEdit("docsXv2/papers/alpha/paper.
|
|
588
|
+
onEdit("docsXv2/papers/alpha/paper.tex"),
|
|
589
589
|
).stderr.length === 0,
|
|
590
590
|
);
|
|
591
591
|
}
|
|
@@ -186,7 +186,7 @@ export const papersRoot = (rawConfig) => {
|
|
|
186
186
|
/**
|
|
187
187
|
* Redirection targets (`> f`, `>> f`, `1> f`, `&> f`), which `touches` cannot see.
|
|
188
188
|
*
|
|
189
|
-
* MEASURED, not assumed: `leafCommandsNormalized("echo x > p/x/paper.
|
|
189
|
+
* MEASURED, not assumed: `leafCommandsNormalized("echo x > p/x/paper.tex")` returns
|
|
190
190
|
* `[{head:"echo", argv:["echo","x"], args:["x"], …}]` — the redirection and its target are
|
|
191
191
|
* DROPPED by the parser, so they appear in no field of any leaf. The closed vocabulary
|
|
192
192
|
* (`runs` / `touches` / `pipesToShell` / `isSideEffecting`) has no way to ask what a command
|
|
@@ -196,9 +196,9 @@ export const papersRoot = (rawConfig) => {
|
|
|
196
196
|
* 🔴 THE SCAN IS QUOTE-AWARE, and the version before it was not. It stripped quoted spans
|
|
197
197
|
* wholesale, so a quoted redirection target vanished with them. MEASURED on this guard:
|
|
198
198
|
*
|
|
199
|
-
* echo x > <root>/x/paper.
|
|
200
|
-
* echo x > "<root>/x/paper.
|
|
201
|
-
* echo x > '<root>/x/paper.
|
|
199
|
+
* echo x > <root>/x/paper.tex → blocked
|
|
200
|
+
* echo x > "<root>/x/paper.tex" → ALLOWED
|
|
201
|
+
* echo x > '<root>/x/paper.tex' → ALLOWED
|
|
202
202
|
*
|
|
203
203
|
* Stripping was not merely careless: a `>` INSIDE quotes is not a redirection at all, so the
|
|
204
204
|
* guard has to know about quoting or it fires on any command that merely QUOTES a paper write —
|
|
@@ -376,10 +376,7 @@ const runsMutator = (cmd) => MUTATORS.some((m) => cmd.runs(m));
|
|
|
376
376
|
*/
|
|
377
377
|
const isPaperSource = (p) => !isFrozenSnapshot(p) && hasSourceExtension(p);
|
|
378
378
|
|
|
379
|
-
const hasSourceExtension = (p) =>
|
|
380
|
-
p.endsWith(".tex") ||
|
|
381
|
-
/\/(paper|draft)\.md$/.test(p) ||
|
|
382
|
-
/^(paper|draft)\.md$/.test(p);
|
|
379
|
+
const hasSourceExtension = (p) => p.endsWith(".tex");
|
|
383
380
|
|
|
384
381
|
/**
|
|
385
382
|
* A FROZEN SNAPSHOT — a file under a paper's `versions/`, which is the same carve-out as the
|
|
@@ -15,7 +15,7 @@ const dir = hookConsumer(useTempDir("paper-edit-guard-"), {
|
|
|
15
15
|
});
|
|
16
16
|
/** `>` built rather than typed: a literal one here would be a redirection to the guard's own scan. */
|
|
17
17
|
const GT = String.fromCharCode(62);
|
|
18
|
-
const P = "papers/alpha/paper.
|
|
18
|
+
const P = "papers/alpha/paper.tex";
|
|
19
19
|
|
|
20
20
|
/** The guard's exit code for one Bash command: 2 denies, 0 lets it through. */
|
|
21
21
|
const guard = (command: string): number =>
|
|
@@ -35,8 +35,8 @@ test("a `>` inside quotes, after an escaped quote, or escaped itself is not a re
|
|
|
35
35
|
test("a redirection target is read through backslashes, quoted or not, and ends at a delimiter", () => {
|
|
36
36
|
assert.deepEqual(
|
|
37
37
|
[
|
|
38
|
-
guard(`echo x ${GT} "papers/alpha/pa\\per.
|
|
39
|
-
guard(`echo x ${GT} papers/alpha/pa\\per.
|
|
38
|
+
guard(`echo x ${GT} "papers/alpha/pa\\per.tex"`),
|
|
39
|
+
guard(`echo x ${GT} papers/alpha/pa\\per.tex`),
|
|
40
40
|
guard(`echo x ${GT}${P}; true`),
|
|
41
41
|
],
|
|
42
42
|
[2, 2, 2],
|
|
@@ -55,3 +55,12 @@ test("a command cut off right after a backslash still names its target, quoted o
|
|
|
55
55
|
[2, 2],
|
|
56
56
|
);
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
test("only paper.tex is a paper source: copying over another file of the paper is let through", () => {
|
|
60
|
+
// Guards: the extension filter of the argv leg. (A redirection is refused for ANY file under the
|
|
61
|
+
// papers root, so the argv leg is the one that says which files are sources.)
|
|
62
|
+
assert.deepEqual(
|
|
63
|
+
[guard("cp /tmp/x papers/alpha/notes.txt"), guard(`cp /tmp/x ${P}`)],
|
|
64
|
+
[0, 2],
|
|
65
|
+
);
|
|
66
|
+
});
|
|
@@ -96,9 +96,8 @@ const papersRoot = (rawConfig) => {
|
|
|
96
96
|
return root;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
/** A paper SOURCE — a `.tex
|
|
100
|
-
const isPaperSource = (p) =>
|
|
101
|
-
p.endsWith(".tex") || /\/(paper|draft)\.md$/.test(p);
|
|
99
|
+
/** A paper SOURCE — a `.tex`. Not every note or README. */
|
|
100
|
+
const isPaperSource = (p) => p.endsWith(".tex");
|
|
102
101
|
|
|
103
102
|
const CHECKLIST = `📄 Editing a paper — drive it with the \`paper-pipeline\` skill, don't wing the review.
|
|
104
103
|
→ Update <paper-dir>/PIPELINE-STATUS.md: mark the row for any stage you (re)ran, and read its
|
|
@@ -10,11 +10,11 @@ import { useTempDir } from "../test/support.ts";
|
|
|
10
10
|
|
|
11
11
|
const root = useTempDir("paper-skills-nudge-");
|
|
12
12
|
|
|
13
|
-
test("no paperlint.json: an edit to papers/<p>/paper.
|
|
13
|
+
test("no paperlint.json: an edit to papers/<p>/paper.tex gets the checklist", () => {
|
|
14
14
|
const dir = hookConsumer(join(root, "default"));
|
|
15
15
|
const r = runShippedHook(
|
|
16
16
|
"paper-skills-nudge",
|
|
17
|
-
onEdit("papers/alpha/paper.
|
|
17
|
+
onEdit("papers/alpha/paper.tex"),
|
|
18
18
|
dir,
|
|
19
19
|
);
|
|
20
20
|
assert.equal(r.exitCode, 0);
|
|
@@ -27,7 +27,17 @@ test("a paperlint.json that does not parse: silent, exit 0", () => {
|
|
|
27
27
|
});
|
|
28
28
|
const r = runShippedHook(
|
|
29
29
|
"paper-skills-nudge",
|
|
30
|
-
onEdit("papers/alpha/paper.
|
|
30
|
+
onEdit("papers/alpha/paper.tex"),
|
|
31
|
+
dir,
|
|
32
|
+
);
|
|
33
|
+
assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("an edit to a file of the paper other than its source: silent", () => {
|
|
37
|
+
const dir = hookConsumer(join(root, "not-a-source"));
|
|
38
|
+
const r = runShippedHook(
|
|
39
|
+
"paper-skills-nudge",
|
|
40
|
+
onEdit("papers/alpha/notes.txt"),
|
|
31
41
|
dir,
|
|
32
42
|
);
|
|
33
43
|
assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* harness's own edit helper builds a RELATIVE path. The live harness sends an ABSOLUTE one.
|
|
32
32
|
* Measured against the real runtime:
|
|
33
33
|
*
|
|
34
|
-
* file_path "<root>/<paper>/paper.
|
|
34
|
+
* file_path "<root>/<paper>/paper.tex" → fires
|
|
35
35
|
* file_path "/abs/path/to/repo/<root>/<paper>/…" → SILENT
|
|
36
36
|
*
|
|
37
37
|
* So the hook would have been dead in production and green in the tests: precisely the
|
|
@@ -120,7 +120,7 @@ const papersRoot = (rawConfig) => {
|
|
|
120
120
|
*/
|
|
121
121
|
const paperSourceRe = (root) =>
|
|
122
122
|
new RegExp(
|
|
123
|
-
`(?:^|/)${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([A-Za-z0-9._-]+)/
|
|
123
|
+
`(?:^|/)${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/([A-Za-z0-9._-]+)/[^/]*\\.tex$`,
|
|
124
124
|
);
|
|
125
125
|
|
|
126
126
|
/**
|
|
@@ -17,7 +17,7 @@ test("no paperlint.json: an edit under papers/ runs the gates for that paper", (
|
|
|
17
17
|
});
|
|
18
18
|
const r = runShippedHook(
|
|
19
19
|
"paper-status-gates",
|
|
20
|
-
onEdit("papers/alpha/paper.
|
|
20
|
+
onEdit("papers/alpha/paper.tex"),
|
|
21
21
|
dir,
|
|
22
22
|
);
|
|
23
23
|
assert.equal(r.exitCode, 0);
|
|
@@ -31,7 +31,19 @@ test("a paperlint.json that does not parse: silent, exit 0", () => {
|
|
|
31
31
|
});
|
|
32
32
|
const r = runShippedHook(
|
|
33
33
|
"paper-status-gates",
|
|
34
|
-
onEdit("papers/alpha/paper.
|
|
34
|
+
onEdit("papers/alpha/paper.tex"),
|
|
35
|
+
dir,
|
|
36
|
+
);
|
|
37
|
+
assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("an edit to a file of the paper other than its source: no gates run", () => {
|
|
41
|
+
const dir = hookConsumer(join(root, "not-a-source"), {
|
|
42
|
+
"papers/alpha/PIPELINE-STATUS.md": STATUS,
|
|
43
|
+
});
|
|
44
|
+
const r = runShippedHook(
|
|
45
|
+
"paper-status-gates",
|
|
46
|
+
onEdit("papers/alpha/notes.txt"),
|
|
35
47
|
dir,
|
|
36
48
|
);
|
|
37
49
|
assert.deepEqual([r.exitCode, r.stdout, r.stderr], [0, "", ""]);
|
package/lib/markdown.test.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* markdown.mjs — the markup readers every script shares, on the shapes their callers rely on:
|
|
3
3
|
* frontmatter blanked (offsets kept) or cut, headings from the parser (not from a `#` inside a
|
|
4
|
-
* fence),
|
|
4
|
+
* fence), and fences. And the one thing a test run never
|
|
5
5
|
* sees by accident: markdown-it failing to resolve, where every reader must degrade to its
|
|
6
6
|
* documented empty value and `requireMarkdown` must refuse with exit 2.
|
|
7
7
|
*/
|
|
@@ -17,11 +17,8 @@ import {
|
|
|
17
17
|
frontmatterBlock,
|
|
18
18
|
headings,
|
|
19
19
|
requireMarkdown,
|
|
20
|
-
paragraphs,
|
|
21
|
-
splitSections,
|
|
22
20
|
stripFences,
|
|
23
21
|
stripFrontmatter,
|
|
24
|
-
tables,
|
|
25
22
|
} from "./markdown.ts";
|
|
26
23
|
|
|
27
24
|
const DOC = [
|
|
@@ -93,27 +90,6 @@ test("an empty ATX heading has empty text", () => {
|
|
|
93
90
|
);
|
|
94
91
|
});
|
|
95
92
|
|
|
96
|
-
test("splitSections: a preamble always first, then each heading of the level range", () => {
|
|
97
|
-
assert.deepEqual(
|
|
98
|
-
splitSections(DOC, { min: 2, max: 2 }).map((s) => [
|
|
99
|
-
s.heading?.text ?? null,
|
|
100
|
-
s.body.split("\n")[0],
|
|
101
|
-
]),
|
|
102
|
-
[
|
|
103
|
-
[null, "---"],
|
|
104
|
-
["One", "Body one."],
|
|
105
|
-
["Two", "last"],
|
|
106
|
-
],
|
|
107
|
-
);
|
|
108
|
-
assert.deepEqual(
|
|
109
|
-
splitSections("## A").map((s) => [s.heading?.text ?? null, s.body]),
|
|
110
|
-
[
|
|
111
|
-
[null, ""],
|
|
112
|
-
["A", ""],
|
|
113
|
-
],
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
93
|
test("fences and indented code, as content; stripped as lines, or blanked in place", () => {
|
|
118
94
|
const src = "a\n```\nx\n```\n\n y\nb";
|
|
119
95
|
assert.deepEqual(
|
|
@@ -126,30 +102,6 @@ test("fences and indented code, as content; stripped as lines, or blanked in pla
|
|
|
126
102
|
);
|
|
127
103
|
});
|
|
128
104
|
|
|
129
|
-
test("paragraphs are line ranges; tables are rows of cells, the alignment row skipped", () => {
|
|
130
|
-
assert.deepEqual(
|
|
131
|
-
{ paragraphs: paragraphs(DOC), tables: tables(DOC) },
|
|
132
|
-
{
|
|
133
|
-
paragraphs: [
|
|
134
|
-
{ line: 6, endLine: 7 },
|
|
135
|
-
{ line: 9, endLine: 10 },
|
|
136
|
-
{ line: 13, endLine: 14 },
|
|
137
|
-
{ line: 27, endLine: 28 },
|
|
138
|
-
],
|
|
139
|
-
tables: [
|
|
140
|
-
{
|
|
141
|
-
line: 22,
|
|
142
|
-
endLine: 25,
|
|
143
|
-
rows: [
|
|
144
|
-
["a", "b"],
|
|
145
|
-
["1", "2"],
|
|
146
|
-
],
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
105
|
const root = useTempDir("markdown-no-parser-");
|
|
154
106
|
writeTree(root, {
|
|
155
107
|
// Registered with --import: resolving `markdown-it` fails, as it does with no node_modules.
|
|
@@ -159,7 +111,7 @@ writeTree(root, {
|
|
|
159
111
|
"probe.mjs":
|
|
160
112
|
`const m = await import(${JSON.stringify(join(dirname(fileURLToPath(import.meta.url)), "markdown.ts"))});\n` +
|
|
161
113
|
"const t = '# H\\n\\n```\\nx\\n```\\n';\n" +
|
|
162
|
-
"console.log(JSON.stringify([m.MD_AVAILABLE(), m.headings(t), m.fences(t), m.stripFences(t)
|
|
114
|
+
"console.log(JSON.stringify([m.MD_AVAILABLE(), m.headings(t), m.fences(t), m.stripFences(t)]));\n" +
|
|
163
115
|
"m.requireMarkdown();\n" +
|
|
164
116
|
"console.log('not reached');\n",
|
|
165
117
|
"probe-plugin.mjs":
|
|
@@ -188,22 +140,15 @@ test("with no markdown-it, every reader degrades to its empty value and requireM
|
|
|
188
140
|
});
|
|
189
141
|
assert.deepEqual(r, {
|
|
190
142
|
status: 2,
|
|
191
|
-
stdout:
|
|
192
|
-
JSON.stringify([false, [], [], "# H\n\n```\nx\n```\n", [], []]) + "\n",
|
|
143
|
+
stdout: JSON.stringify([false, [], [], "# H\n\n```\nx\n```\n"]) + "\n",
|
|
193
144
|
stderr:
|
|
194
145
|
"markdown-it does not resolve — this script parses markup with a parser and without it would produce a confident zero instead of an error.\n" +
|
|
195
146
|
" Cured by `npm i` at the repo root.\n",
|
|
196
147
|
});
|
|
197
148
|
});
|
|
198
149
|
|
|
199
|
-
test("
|
|
200
|
-
assert.
|
|
201
|
-
[
|
|
202
|
-
splitSections("just text").map((s) => [s.heading, s.body]),
|
|
203
|
-
requireMarkdown(),
|
|
204
|
-
],
|
|
205
|
-
[[[null, "just text"]], undefined],
|
|
206
|
-
);
|
|
150
|
+
test("requireMarkdown is silent with a parser", () => {
|
|
151
|
+
assert.equal(requireMarkdown(), undefined);
|
|
207
152
|
});
|
|
208
153
|
|
|
209
154
|
test("headings read a token stream that breaks markdown-it's shape as empty text and offset 0", () => {
|
package/lib/markdown.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* markdown.mjs — markup parsing for EVERYTHING in this repo that reads markdown.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* would be a lie about the direction of the dependency.
|
|
4
|
+
* Its consumers read the package's own markdown — skills' `SKILL.md` frontmatter
|
|
5
|
+
* (`skill-corpus.mjs`), fenced commands — and a consumer's linters of its own files.
|
|
6
|
+
* Hence the home is here and not in a linter core: importing "the linter core" from a
|
|
7
|
+
* skill script would be a lie about the direction of the dependency.
|
|
9
8
|
*
|
|
10
9
|
* CHECKS NOTHING AND PRINTS NOTHING: text in, data out. What to do with a missing
|
|
11
10
|
* parser is the caller's decision, and the decisions DIFFER:
|
|
@@ -43,13 +42,6 @@ export interface Heading {
|
|
|
43
42
|
readonly offset: number;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
/** A chunk of the document: the heading it starts with (`null` for the preamble), with and without it. */
|
|
47
|
-
export interface Section {
|
|
48
|
-
readonly heading: Heading | null;
|
|
49
|
-
readonly raw: string;
|
|
50
|
-
readonly body: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
45
|
/** A block tokenizer with markdown-it's shape — the default, or one a test passes. */
|
|
54
46
|
export type Parse = (text: string) => readonly Token[];
|
|
55
47
|
|
|
@@ -169,56 +161,6 @@ export function headings(
|
|
|
169
161
|
return out;
|
|
170
162
|
}
|
|
171
163
|
|
|
172
|
-
/**
|
|
173
|
-
* Slicing the document into chunks "a heading + everything up to the next heading FROM
|
|
174
|
-
* THE SAME RANGE of levels". → `[{ heading, raw, body }]`.
|
|
175
|
-
*
|
|
176
|
-
* This is exactly what four scripts did as `split(/^(?=## )/m)` and
|
|
177
|
-
* `split(/^(?=#{2,3} )/m)`. The shape of the expression lied about the intent: "two or
|
|
178
|
-
* three hashes" is about CHARACTERS, while what is needed is the LEVEL, and `#{2,3}`
|
|
179
|
-
* also caught a hash inside a ```-block, that is, it cut a quotation from someone
|
|
180
|
-
* else's paper as a section of its own.
|
|
181
|
-
*
|
|
182
|
-
* `heading` — `null` on the zeroth element (the preamble before the first heading). It
|
|
183
|
-
* is ALWAYS there, including empty: the former `split` also always returned the chunk
|
|
184
|
-
* before the first separator as its zeroth element, and callers rely on that
|
|
185
|
-
* (`sub === chunk` in `artifact-coverage` meant "there are no subsections").
|
|
186
|
-
*
|
|
187
|
-
* `raw` — WITH the heading line (it is used to count `§` references and bold numbers,
|
|
188
|
-
* which occur in the heading itself too), `body` — without it (it is used to count words).
|
|
189
|
-
*
|
|
190
|
-
* 2026-08-11: moved here rather than copied into every script, because the chunk
|
|
191
|
-
* boundary is the one place where an off-by-one is easy, and such a mistake has already
|
|
192
|
-
* happened (`recordBlock` in `skill-corpus.mjs` returned a single "#" character for all
|
|
193
|
-
* 21 skills).
|
|
194
|
-
*/
|
|
195
|
-
export function splitSections(
|
|
196
|
-
text: string,
|
|
197
|
-
{ min = 2, max = 6 }: { min?: number; max?: number } = {},
|
|
198
|
-
): Section[] {
|
|
199
|
-
const hs = headings(text).filter((h) => h.depth >= min && h.depth <= max);
|
|
200
|
-
const out: Section[] = [];
|
|
201
|
-
const [first] = hs;
|
|
202
|
-
const preEnd = first ? first.offset : text.length;
|
|
203
|
-
out.push({
|
|
204
|
-
heading: null,
|
|
205
|
-
raw: text.slice(0, preEnd),
|
|
206
|
-
body: text.slice(0, preEnd),
|
|
207
|
-
});
|
|
208
|
-
for (const [i, h] of hs.entries()) {
|
|
209
|
-
const next = hs[i + 1];
|
|
210
|
-
const end = next ? next.offset : text.length;
|
|
211
|
-
const nl = text.indexOf("\n", h.offset);
|
|
212
|
-
const bodyStart = nl === -1 || nl + 1 > end ? end : nl + 1;
|
|
213
|
-
out.push({
|
|
214
|
-
heading: h,
|
|
215
|
-
raw: text.slice(h.offset, end),
|
|
216
|
-
body: text.slice(bodyStart, end),
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
return out;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
164
|
/**
|
|
223
165
|
* The text without the contents of ```-blocks (the fence lines themselves go too).
|
|
224
166
|
*
|
|
@@ -229,14 +171,11 @@ export function splitSections(
|
|
|
229
171
|
* `blank: true` — the block's lines are NOT deleted but REPLACED with empty ones. The
|
|
230
172
|
* difference is not cosmetic: deleting the lines GLUES the paragraph before the block to
|
|
231
173
|
* the paragraph after it, and a code block is a block boundary — on the other side a
|
|
232
|
-
* different paragraph starts. Measured 2026-08-11 on
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* claims-per-sentence counter — the threshold at which the file emits findings. The
|
|
238
|
-
* former regex preserved the boundary (it left one newline on each side), so `blank` is
|
|
239
|
-
* also a preservation of the former behaviour where that behaviour was right.
|
|
174
|
+
* different paragraph starts. Measured 2026-08-11 on a README: a sentence counter counted
|
|
175
|
+
* 82 sentences there, and after the lines were deleted it counted 81 — the lead paragraph
|
|
176
|
+
* "**Reproduction commands:**" merged with the text AFTER the block into one sentence.
|
|
177
|
+
* The former regex preserved the boundary (it left one newline on each side), so `blank`
|
|
178
|
+
* is also a preservation of the former behaviour where that behaviour was right.
|
|
240
179
|
*/
|
|
241
180
|
/**
|
|
242
181
|
* The CONTENTS of fenced blocks — the other side of `stripFences`.
|
|
@@ -304,69 +243,3 @@ export function requireMarkdown({
|
|
|
304
243
|
);
|
|
305
244
|
process.exit(2);
|
|
306
245
|
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* GFM tables — rows, cells and the line range, WITH THE PARSER.
|
|
310
|
-
*
|
|
311
|
-
* The only consumer as of 2026-08-20 is `crosspost-article`, which needs to re-lay a
|
|
312
|
-
* table into a monospace block for a platform without tables (Medium). It lives here
|
|
313
|
-
* and not in the skill, by the rule "markup is parsed with a parser, and in one place":
|
|
314
|
-
* with a regex a table is told apart from a line of text with pipes only by its
|
|
315
|
-
* neighbours, and that is exactly the class that has already cost this repo 27 places.
|
|
316
|
-
*
|
|
317
|
-
* Returns `{ line, endLine, rows }`, where `rows[0]` is the header; the alignment row
|
|
318
|
-
* (`| --- |`) is thrown away, because in monospace output it has nothing to express.
|
|
319
|
-
*/
|
|
320
|
-
/**
|
|
321
|
-
* The document's paragraphs — `paragraph_open` line ranges, WITH THE PARSER.
|
|
322
|
-
* `[{ line, endLine }]`.
|
|
323
|
-
*
|
|
324
|
-
* The only consumer as of 2026-08-21 is `crosspost-article`: the blog's sources are
|
|
325
|
-
* hard-wrapped at a column (an editorial habit, not markdown), and the platforms render
|
|
326
|
-
* a single `\n` inside a paragraph DIFFERENTLY — dev.to (Redcarpet) keeps it as a
|
|
327
|
-
* literal line break, and the paragraph arrives torn into short lines exactly at the
|
|
328
|
-
* source file's boundaries. Medium has no such defect not because the platform is more
|
|
329
|
-
* honest, but because the paste goes through "Import a story" — the importer takes the
|
|
330
|
-
* ALREADY RENDERED page of the site (where the breaks were collapsed by the browser long
|
|
331
|
-
* ago), not the raw markdown.
|
|
332
|
-
*
|
|
333
|
-
* The range is EXACTLY the one `t.map` gives on `paragraph_open` — that is, a code
|
|
334
|
-
* fence, a heading, a table, a list (in a TIGHT list the item is not wrapped in a
|
|
335
|
-
* `paragraph_open` at all, it has no paragraph of its own) fall outside this function
|
|
336
|
-
* automatically, by the construction of the parser, and not by a separate line-type
|
|
337
|
-
* check.
|
|
338
|
-
*/
|
|
339
|
-
export function paragraphs(text: string): { line: number; endLine: number }[] {
|
|
340
|
-
if (md === null) return [];
|
|
341
|
-
return md
|
|
342
|
-
.parse(blankFrontmatter(text), {})
|
|
343
|
-
.flatMap((t) =>
|
|
344
|
-
t.type === "paragraph_open" && t.map
|
|
345
|
-
? [{ line: t.map[0], endLine: t.map[1] }]
|
|
346
|
-
: [],
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
export function tables(
|
|
351
|
-
text: string,
|
|
352
|
-
): { line: number; endLine: number; rows: string[][] }[] {
|
|
353
|
-
if (md === null) return [];
|
|
354
|
-
const lines = blankFrontmatter(text).split("\n");
|
|
355
|
-
const out: { line: number; endLine: number; rows: string[][] }[] = [];
|
|
356
|
-
for (const t of md.parse(blankFrontmatter(text), {})) {
|
|
357
|
-
if (t.type !== "table_open" || !t.map) continue;
|
|
358
|
-
const rows: string[][] = [];
|
|
359
|
-
for (const raw of lines.slice(t.map[0], t.map[1])) {
|
|
360
|
-
if (/^\s*\|?[\s:|-]+\|?\s*$/.test(raw) && raw.includes("-")) continue; // alignment
|
|
361
|
-
rows.push(
|
|
362
|
-
raw
|
|
363
|
-
.replace(/^\s*\|/, "")
|
|
364
|
-
.replace(/\|\s*$/, "")
|
|
365
|
-
.split("|")
|
|
366
|
-
.map((c) => c.trim()),
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
out.push({ line: t.map[0], endLine: t.map[1], rows });
|
|
370
|
-
}
|
|
371
|
-
return out;
|
|
372
|
-
}
|
package/lib/paper-config.ts
CHANGED
|
@@ -236,7 +236,10 @@ test("a Bash entry narrowed to the instructed script permits it, and one narrowe
|
|
|
236
236
|
await brokenBy("verify-citations", narrowTo("extract-ref-facts.mjs")),
|
|
237
237
|
null,
|
|
238
238
|
);
|
|
239
|
-
const lines = await brokenBy(
|
|
239
|
+
const lines = await brokenBy(
|
|
240
|
+
"verify-citations",
|
|
241
|
+
narrowTo("generated-code.mjs"),
|
|
242
|
+
);
|
|
240
243
|
assert.equal(
|
|
241
244
|
lines?.[0],
|
|
242
245
|
'2 assertion(s) failed for skill "verify-citations":',
|