paperlint 2.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/.github/dependabot.yml +72 -0
- package/.github/workflows/ci.yml +297 -0
- package/.github/workflows/dependabot-automerge.yml +70 -0
- package/.github/workflows/pr-title.yml +59 -0
- package/.github/workflows/release.yml +54 -0
- package/CLAUDE.md +598 -0
- package/CONTRIBUTING.md +159 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/action.harness.mjs +287 -0
- package/action.mutations.mjs +162 -0
- package/action.yml +138 -0
- package/bin/rpp.mjs +43 -0
- package/dist/action-ref.d.ts +12 -0
- package/dist/action-ref.d.ts.map +1 -0
- package/dist/action-ref.js +16 -0
- package/dist/action-ref.js.map +1 -0
- package/dist/adapters/banal/failure.d.ts +73 -0
- package/dist/adapters/banal/failure.d.ts.map +1 -0
- package/dist/adapters/banal/failure.js +58 -0
- package/dist/adapters/banal/failure.js.map +1 -0
- package/dist/adapters/banal/index.d.ts +17 -0
- package/dist/adapters/banal/index.d.ts.map +1 -0
- package/dist/adapters/banal/index.js +56 -0
- package/dist/adapters/banal/index.js.map +1 -0
- package/dist/adapters/banal/install.d.ts +26 -0
- package/dist/adapters/banal/install.d.ts.map +1 -0
- package/dist/adapters/banal/install.js +15 -0
- package/dist/adapters/banal/install.js.map +1 -0
- package/dist/adapters/banal/invocation.d.ts +48 -0
- package/dist/adapters/banal/invocation.d.ts.map +1 -0
- package/dist/adapters/banal/invocation.js +43 -0
- package/dist/adapters/banal/invocation.js.map +1 -0
- package/dist/adapters/banal/locate.d.ts +50 -0
- package/dist/adapters/banal/locate.d.ts.map +1 -0
- package/dist/adapters/banal/locate.js +34 -0
- package/dist/adapters/banal/locate.js.map +1 -0
- package/dist/adapters/banal/output.d.ts +27 -0
- package/dist/adapters/banal/output.d.ts.map +1 -0
- package/dist/adapters/banal/output.js +112 -0
- package/dist/adapters/banal/output.js.map +1 -0
- package/dist/adapters/banal/pin.d.ts +19 -0
- package/dist/adapters/banal/pin.d.ts.map +1 -0
- package/dist/adapters/banal/pin.js +15 -0
- package/dist/adapters/banal/pin.js.map +1 -0
- package/dist/adapters/banal/probe.d.ts +12 -0
- package/dist/adapters/banal/probe.d.ts.map +1 -0
- package/dist/adapters/banal/probe.js +27 -0
- package/dist/adapters/banal/probe.js.map +1 -0
- package/dist/adapters/banal/run.d.ts +89 -0
- package/dist/adapters/banal/run.d.ts.map +1 -0
- package/dist/adapters/banal/run.js +104 -0
- package/dist/adapters/banal/run.js.map +1 -0
- package/dist/adapters/banal/settings.d.ts +18 -0
- package/dist/adapters/banal/settings.d.ts.map +1 -0
- package/dist/adapters/banal/settings.js +29 -0
- package/dist/adapters/banal/settings.js.map +1 -0
- package/dist/adapters/banal/xml.d.ts +48 -0
- package/dist/adapters/banal/xml.d.ts.map +1 -0
- package/dist/adapters/banal/xml.js +67 -0
- package/dist/adapters/banal/xml.js.map +1 -0
- package/dist/adapters/curl/download.io.d.ts +14 -0
- package/dist/adapters/curl/download.io.d.ts.map +1 -0
- package/dist/adapters/curl/download.io.js +69 -0
- package/dist/adapters/curl/download.io.js.map +1 -0
- package/dist/adapters/curl/index.d.ts +6 -0
- package/dist/adapters/curl/index.d.ts.map +1 -0
- package/dist/adapters/curl/index.js +6 -0
- package/dist/adapters/curl/index.js.map +1 -0
- package/dist/adapters/memory/index.d.ts +43 -0
- package/dist/adapters/memory/index.d.ts.map +1 -0
- package/dist/adapters/memory/index.js +79 -0
- package/dist/adapters/memory/index.js.map +1 -0
- package/dist/adapters/node/files.io.d.ts +3 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -0
- package/dist/adapters/node/files.io.js +31 -0
- package/dist/adapters/node/files.io.js.map +1 -0
- package/dist/adapters/node/host.io.d.ts +3 -0
- package/dist/adapters/node/host.io.d.ts.map +1 -0
- package/dist/adapters/node/host.io.js +14 -0
- package/dist/adapters/node/host.io.js.map +1 -0
- package/dist/adapters/node/index.d.ts +25 -0
- package/dist/adapters/node/index.d.ts.map +1 -0
- package/dist/adapters/node/index.js +14 -0
- package/dist/adapters/node/index.js.map +1 -0
- package/dist/adapters/node/process.io.d.ts +14 -0
- package/dist/adapters/node/process.io.d.ts.map +1 -0
- package/dist/adapters/node/process.io.js +41 -0
- package/dist/adapters/node/process.io.js.map +1 -0
- package/dist/adapters/node/workspace.io.d.ts +4 -0
- package/dist/adapters/node/workspace.io.d.ts.map +1 -0
- package/dist/adapters/node/workspace.io.js +33 -0
- package/dist/adapters/node/workspace.io.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts +42 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -0
- package/dist/adapters/pdfjs/fill.js +91 -0
- package/dist/adapters/pdfjs/fill.js.map +1 -0
- package/dist/build-engine.d.ts +48 -0
- package/dist/build-engine.d.ts.map +1 -0
- package/dist/build-engine.js +148 -0
- package/dist/build-engine.js.map +1 -0
- package/dist/build.d.ts +163 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +575 -0
- package/dist/build.js.map +1 -0
- package/dist/cli.d.ts +151 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +951 -0
- package/dist/cli.js.map +1 -0
- package/dist/doctor.d.ts +42 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +280 -0
- package/dist/doctor.js.map +1 -0
- package/dist/domain/geometry.d.ts +71 -0
- package/dist/domain/geometry.d.ts.map +1 -0
- package/dist/domain/geometry.js +35 -0
- package/dist/domain/geometry.js.map +1 -0
- package/dist/domain/host.d.ts +16 -0
- package/dist/domain/host.d.ts.map +1 -0
- package/dist/domain/host.js +8 -0
- package/dist/domain/host.js.map +1 -0
- package/dist/domain/page-layout.d.ts +34 -0
- package/dist/domain/page-layout.d.ts.map +1 -0
- package/dist/domain/page-layout.js +8 -0
- package/dist/domain/page-layout.js.map +1 -0
- package/dist/domain/paths.d.ts +5 -0
- package/dist/domain/paths.d.ts.map +1 -0
- package/dist/domain/paths.js +2 -0
- package/dist/domain/paths.js.map +1 -0
- package/dist/domain/result.d.ts +23 -0
- package/dist/domain/result.d.ts.map +1 -0
- package/dist/domain/result.js +10 -0
- package/dist/domain/result.js.map +1 -0
- package/dist/domain/sha256.d.ts +7 -0
- package/dist/domain/sha256.d.ts.map +1 -0
- package/dist/domain/sha256.js +14 -0
- package/dist/domain/sha256.js.map +1 -0
- package/dist/domain/text.d.ts +6 -0
- package/dist/domain/text.d.ts.map +1 -0
- package/dist/domain/text.js +7 -0
- package/dist/domain/text.js.map +1 -0
- package/dist/engine.d.ts +93 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +119 -0
- package/dist/engine.js.map +1 -0
- package/dist/exit-code.d.ts +22 -0
- package/dist/exit-code.d.ts.map +1 -0
- package/dist/exit-code.js +10 -0
- package/dist/exit-code.js.map +1 -0
- package/dist/facts-file.d.ts +96 -0
- package/dist/facts-file.d.ts.map +1 -0
- package/dist/facts-file.js +134 -0
- package/dist/facts-file.js.map +1 -0
- package/dist/hooks-settings.d.ts +141 -0
- package/dist/hooks-settings.d.ts.map +1 -0
- package/dist/hooks-settings.js +306 -0
- package/dist/hooks-settings.js.map +1 -0
- package/dist/init.d.ts +201 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +579 -0
- package/dist/init.js.map +1 -0
- package/dist/latex-log.d.ts +80 -0
- package/dist/latex-log.d.ts.map +1 -0
- package/dist/latex-log.js +187 -0
- package/dist/latex-log.js.map +1 -0
- package/dist/latex-loop.d.ts +129 -0
- package/dist/latex-loop.d.ts.map +1 -0
- package/dist/latex-loop.js +113 -0
- package/dist/latex-loop.js.map +1 -0
- package/dist/link-skills.d.ts +51 -0
- package/dist/link-skills.d.ts.map +1 -0
- package/dist/link-skills.js +199 -0
- package/dist/link-skills.js.map +1 -0
- package/dist/new-paper.d.ts +48 -0
- package/dist/new-paper.d.ts.map +1 -0
- package/dist/new-paper.js +110 -0
- package/dist/new-paper.js.map +1 -0
- package/dist/pdf-facts.d.ts +44 -0
- package/dist/pdf-facts.d.ts.map +1 -0
- package/dist/pdf-facts.js +239 -0
- package/dist/pdf-facts.js.map +1 -0
- package/dist/pdf-geometry.d.ts +170 -0
- package/dist/pdf-geometry.d.ts.map +1 -0
- package/dist/pdf-geometry.js +158 -0
- package/dist/pdf-geometry.js.map +1 -0
- package/dist/ports/download.d.ts +9 -0
- package/dist/ports/download.d.ts.map +1 -0
- package/dist/ports/download.js +2 -0
- package/dist/ports/download.js.map +1 -0
- package/dist/ports/files.d.ts +11 -0
- package/dist/ports/files.d.ts.map +1 -0
- package/dist/ports/files.js +2 -0
- package/dist/ports/files.js.map +1 -0
- package/dist/ports/measure-geometry.d.ts +8 -0
- package/dist/ports/measure-geometry.d.ts.map +1 -0
- package/dist/ports/measure-geometry.js +2 -0
- package/dist/ports/measure-geometry.js.map +1 -0
- package/dist/ports/process.d.ts +45 -0
- package/dist/ports/process.d.ts.map +1 -0
- package/dist/ports/process.js +2 -0
- package/dist/ports/process.js.map +1 -0
- package/dist/ports/tool-installer.d.ts +29 -0
- package/dist/ports/tool-installer.d.ts.map +1 -0
- package/dist/ports/tool-installer.js +2 -0
- package/dist/ports/tool-installer.js.map +1 -0
- package/dist/ports/workspace.d.ts +18 -0
- package/dist/ports/workspace.d.ts.map +1 -0
- package/dist/ports/workspace.js +2 -0
- package/dist/ports/workspace.js.map +1 -0
- package/dist/rules-config.d.ts +34 -0
- package/dist/rules-config.d.ts.map +1 -0
- package/dist/rules-config.js +132 -0
- package/dist/rules-config.js.map +1 -0
- package/dist/structure.d.ts +34 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +149 -0
- package/dist/structure.js.map +1 -0
- package/dist/tex-requirements.d.ts +43 -0
- package/dist/tex-requirements.d.ts.map +1 -0
- package/dist/tex-requirements.js +127 -0
- package/dist/tex-requirements.js.map +1 -0
- package/dist/toolchain.d.ts +159 -0
- package/dist/toolchain.d.ts.map +1 -0
- package/dist/toolchain.js +542 -0
- package/dist/toolchain.js.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/configuration.md +235 -0
- package/docs/e2e.md +152 -0
- package/docs/incidents.md +59 -0
- package/docs/install.md +170 -0
- package/docs/optional-rules.md +107 -0
- package/docs/package-shape-options.md +262 -0
- package/docs/prior-art/README.md +76 -0
- package/docs/prior-art/blocking-vs-advisory.md +83 -0
- package/docs/prior-art/content-delivery.md +124 -0
- package/docs/prior-art/multi-mode-tools.md +106 -0
- package/docs/prior-art/nondeterministic-checks.md +99 -0
- package/docs/prior-art/package-location.md +422 -0
- package/docs/prior-art/paper-folder-scaffolding.md +538 -0
- package/docs/prior-art/readme-structure.md +69 -0
- package/docs/prior-art/repro/README.md +92 -0
- package/docs/prior-art/repro/claim1-allowedtools.mjs +66 -0
- package/docs/prior-art/repro/claim1-at2.mjs +40 -0
- package/docs/prior-art/repro/claim1-crosschannel.mjs +54 -0
- package/docs/prior-art/repro/claim1-frontmatter.mjs +76 -0
- package/docs/prior-art/repro/claim1-hook-payload-reporter.mjs +10 -0
- package/docs/prior-art/repro/claim1-plugin-frontmatter.mjs +27 -0
- package/docs/prior-art/repro/claim1-plugin-skill.mjs +52 -0
- package/docs/prior-art/repro/claim1-project-skill.mjs +81 -0
- package/docs/prior-art/repro/claim2-marketplace-flat-asclaimed.json +1 -0
- package/docs/prior-art/repro/claim2-marketplace-negative-control.json +1 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-exact.json +9 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-noversion.json +9 -0
- package/docs/prior-art/repro/claim2-marketplace-nested-range.json +1 -0
- package/docs/prior-art/repro/claim3-imports.mjs +50 -0
- package/docs/prior-art/repro/claim4-find-package-json.mjs +8 -0
- package/docs/prior-art/repro/claim4-package-dir.mjs +39 -0
- package/docs/prior-art/repro/claim4-parent-arg.mjs +17 -0
- package/docs/prior-art/repro/claim4-resolve-apis.mjs +21 -0
- package/docs/prior-art/repro/claim4-setup-consumers.mjs +45 -0
- package/docs/prior-art/repro/claim4-yarn-pnp.mjs +70 -0
- package/docs/prior-art/repro/claim5-bin-launch.mjs +39 -0
- package/docs/prior-art/repro/claim5-exports-mutation.mjs +57 -0
- package/docs/prior-art/repro/claim5-resolved-location-and-bin.mjs +33 -0
- package/docs/prior-art/repro/claim6-candidate-ambiguity.mjs +17 -0
- package/docs/prior-art/repro/claim6-doc-path-candidates.mjs +27 -0
- package/docs/prior-art/test-tooling.md +131 -0
- package/docs/rules.md +58 -0
- package/docs/texlive-install-decision.md +230 -0
- package/docs/toolchain.md +152 -0
- package/eslint-rules/doc-fields.harness.mjs +336 -0
- package/eslint-rules/doc-fields.mjs +186 -0
- package/eslint-rules/doc-fields.mutations.mjs +96 -0
- package/eslint-rules/install-path-literals.harness.mjs +121 -0
- package/eslint-rules/install-path-literals.mjs +108 -0
- package/eslint-rules/install-path-literals.mutations.mjs +62 -0
- package/eslint-rules/latex-language.harness.mjs +599 -0
- package/eslint-rules/latex-language.mjs +591 -0
- package/eslint-rules/latex-language.mutations.mjs +196 -0
- package/eslint-rules/paper-research-question.harness.mjs +146 -0
- package/eslint-rules/paper-research-question.mjs +180 -0
- package/eslint-rules/paper-research-question.mutations.mjs +127 -0
- package/eslint-rules/paper-stages.harness.mjs +356 -0
- package/eslint-rules/paper-stages.mjs +455 -0
- package/eslint-rules/paper-stages.mutations.mjs +157 -0
- package/eslint-rules/paper-typography.harness.mjs +291 -0
- package/eslint-rules/paper-typography.mjs +313 -0
- package/eslint-rules/paper-typography.mutations.mjs +131 -0
- package/eslint-rules/papers.harness.mjs +259 -0
- package/eslint-rules/papers.mjs +166 -0
- package/eslint-rules/papers.mutations.mjs +186 -0
- package/eslint-rules/pdf-last-page-balance.harness.mjs +206 -0
- package/eslint-rules/pdf-last-page-balance.mjs +208 -0
- package/eslint-rules/review-findings-cause.harness.mjs +228 -0
- package/eslint-rules/review-findings-cause.mjs +135 -0
- package/eslint-rules/review-findings-cause.mutations.mjs +72 -0
- package/eslint-rules/temp-root-realpath.harness.mjs +176 -0
- package/eslint-rules/temp-root-realpath.mjs +129 -0
- package/eslint-rules/temp-root-realpath.mutations.mjs +99 -0
- package/eslint-rules/tex-build.harness.mjs +753 -0
- package/eslint-rules/tex-build.mjs +322 -0
- package/eslint-rules/tex-build.mutations.mjs +258 -0
- package/eslint.config.mjs +521 -0
- package/fixtures/build-e2e/acmart/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/acmart/paper.tex +11 -0
- package/fixtures/build-e2e/acmart/venue.json +1 -0
- package/fixtures/build-e2e/broken/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/broken/paper.tex +7 -0
- package/fixtures/build-e2e/cite/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/cite/build.sh +5 -0
- package/fixtures/build-e2e/cite/paper.tex +10 -0
- package/fixtures/build-e2e/cite/refs.bib +9 -0
- package/fixtures/build-e2e/empty/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/empty/paper.tex +6 -0
- package/fixtures/build-e2e/fallback/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/fallback/paper.tex +11 -0
- package/fixtures/build-e2e/guards/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/guards/paper.tex +10 -0
- package/fixtures/build-e2e/no-source/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/unbalanced/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/unbalanced/paper.tex +28 -0
- package/fixtures/build-e2e/unbalanced/refs.bib +269 -0
- package/fixtures/install-path-literals/clean.fixture.mjs +3 -0
- package/fixtures/install-path-literals/clean.md +15 -0
- package/fixtures/install-path-literals/defect.fixture.mjs +3 -0
- package/fixtures/install-path-literals/defect.md +14 -0
- package/fixtures/latex-language/clean.tex +50 -0
- package/fixtures/latex-language/defect.tex +52 -0
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-research-question/comment-only/paper.tex +7 -0
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +10 -0
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +6 -0
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +6 -0
- package/fixtures/paper-research-question/draft/paper.tex +2 -0
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +4 -0
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +12 -0
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +3 -0
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +10 -0
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +2 -0
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +16 -0
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +17 -0
- package/fixtures/paper-stages/nofile/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/noheader/PIPELINE-STATUS.md +1 -0
- package/fixtures/paper-stages/noheader/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/nothing/PIPELINE-STATUS.md +3 -0
- package/fixtures/paper-stages/ok/PIPELINE-STATUS.md +9 -0
- package/fixtures/paper-stages/ok/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/stale/PIPELINE-STATUS.md +1 -0
- package/fixtures/paper-stages/stale/versions/2026-07-22-submitted.STALE-WRONG-FILE.pdf +0 -0
- package/fixtures/paper-stages/twice/PIPELINE-STATUS.md +14 -0
- package/fixtures/paper-stages/twice/versions/2026-08-06-submitted.pdf +0 -0
- package/fixtures/paper-stages/twice/versions/2026-10-24-submitted.pdf +0 -0
- package/fixtures/paper-stages/undeclared/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/undeclared/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-stages/undeclared/versions/2026-08-29-camera-ready.pdf +0 -0
- package/fixtures/paper-stages/wrongsize/PIPELINE-STATUS.md +8 -0
- package/fixtures/paper-stages/wrongsize/versions/2026-07-22-submitted.pdf +0 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +29 -0
- package/fixtures/paper-typography/messy-paper/paper.tex +27 -0
- package/fixtures/pdf-facts/README.md +22 -0
- package/fixtures/pdf-facts/corrupt-font.pdf +0 -0
- package/fixtures/pdf-facts/encrypted.pdf +0 -0
- package/fixtures/pdf-facts/hidden-text.pdf +0 -0
- package/fixtures/pdf-facts/hidden-text.tex +28 -0
- package/fixtures/pdf-facts/t3-all.pdf +0 -0
- package/fixtures/pdf-facts/t3-all.tex +8 -0
- package/fixtures/pdf-facts/t3-mixed.pdf +0 -0
- package/fixtures/pdf-facts/t3-mixed.tex +9 -0
- package/fixtures/pdf-facts/ttf.pdf +2240 -1
- package/fixtures/pdf-facts/ttf.tex +6 -0
- package/fixtures/real-markdown-paper/baseline.json +24 -0
- package/fixtures/real-markdown-paper/baseline.mjs +48 -0
- package/fixtures/render-paper/build-clean.sh +25 -0
- package/fixtures/render-paper/build-defect.sh +15 -0
- package/fixtures/review-findings-cause/clean.md +17 -0
- package/fixtures/review-findings-cause/defect.md +14 -0
- package/fixtures/review-findings-cause/old-debt.md +14 -0
- package/fixtures/review-findings-cause/quiet-in-fence.md +16 -0
- package/fixtures/tex-build/clean.tex +21 -0
- package/fixtures/tex-build/defect.tex +24 -0
- package/fixtures/tex-build/frontmatter-clean.tex +25 -0
- package/fixtures/tex-build/frontmatter-defect.tex +23 -0
- package/fixtures/toolchain-mirror/catalog.txt +5 -0
- package/fixtures/toolchain-mirror/install-tl +27 -0
- package/fixtures/toolchain-mirror/release-texlive.txt +3 -0
- package/fixtures/toolchain-mirror/release-year +1 -0
- package/fixtures/toolchain-mirror/stub-kpsewhich +8 -0
- package/fixtures/toolchain-mirror/stub-pdflatex +3 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +44 -0
- package/hooks/hooks.harness.mjs +713 -0
- package/hooks/hooks.mutations.mjs +337 -0
- package/hooks/paper-edit-guard.hook.d.mts +13 -0
- package/hooks/paper-edit-guard.hook.mjs +457 -0
- package/hooks/paper-skills-nudge.hook.mjs +136 -0
- package/hooks/paper-status-gates.hook.mjs +156 -0
- package/hooks/paper-status-gates.sh +91 -0
- package/lib/agent-cli-version.harness.mjs +165 -0
- package/lib/agent-cli-version.mjs +106 -0
- package/lib/agent-cli-version.mutations.mjs +109 -0
- package/lib/markdown.mjs +386 -0
- package/lib/mutation-driver.harness.mjs +227 -0
- package/lib/mutation-driver.mjs +397 -0
- package/lib/mutation-driver.mutations.mjs +68 -0
- package/lib/paper-config.d.mts +34 -0
- package/lib/paper-config.harness.mjs +286 -0
- package/lib/paper-config.mjs +142 -0
- package/lib/paper-config.mutations.mjs +143 -0
- package/lib/skill-checks.mjs +701 -0
- package/lib/skill-corpus.mjs +403 -0
- package/lib/skill-eval-fixture.mjs +63 -0
- package/lib/skill-eval-kit.mjs +257 -0
- package/lib/skill-trigger-cases.harness.mjs +170 -0
- package/lib/skill-trigger-cases.mjs +446 -0
- package/lib/skill-trigger-cases.mutations.mjs +65 -0
- package/lib/trigger-ledger.mjs +215 -0
- package/package.json +97 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/hooks/hooks.json +30 -0
- package/scripts/check.harness.mjs +177 -0
- package/scripts/check.mjs +239 -0
- package/scripts/check.mutations.mjs +110 -0
- package/scripts/eslint-report-guard.mjs +82 -0
- package/scripts/exclusive.mjs +138 -0
- package/scripts/harness-api.frozen.json +76 -0
- package/scripts/harness-api.test.ts +175 -0
- package/scripts/layer-legacy-frozen.d.mts +28 -0
- package/scripts/layer-legacy-frozen.mjs +152 -0
- package/scripts/layer-legacy-frozen.test.ts +115 -0
- package/scripts/layer-legacy.frozen.json +50 -0
- package/scripts/mutation-batteries-frozen.harness.mjs +204 -0
- package/scripts/mutation-batteries-frozen.mjs +238 -0
- package/scripts/mutation-batteries.frozen.json +117 -0
- package/scripts/release-config.test.ts +90 -0
- package/scripts/rules-are-content-only.harness.mjs +113 -0
- package/scripts/rules-are-content-only.mjs +138 -0
- package/scripts/rules-are-content-only.mutations.mjs +81 -0
- package/scripts/rules-see-files.harness.mjs +115 -0
- package/scripts/rules-see-files.mjs +99 -0
- package/scripts/rules-see-files.mutations.mjs +131 -0
- package/scripts/run-mutations.mjs +100 -0
- package/scripts/semantic-release-plugins.d.ts +16 -0
- package/skills/README.md +15 -0
- package/skills/analyze-sibling-paper/SKILL.md +170 -0
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +186 -0
- package/skills/analyze-sibling-paper/analyze-sibling-paper.eval.mjs +19 -0
- package/skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs +23 -0
- package/skills/argument-arc/SKILL.md +177 -0
- package/skills/argument-arc/SKILL.md.spec.ts +192 -0
- package/skills/argument-arc/argument-arc.eval.mjs +19 -0
- package/skills/argument-arc/argument-arc.harness.mjs +23 -0
- package/skills/build-benchmark/SKILL.md +213 -0
- package/skills/build-benchmark/SKILL.md.spec.ts +220 -0
- package/skills/build-benchmark/build-benchmark.eval.mjs +19 -0
- package/skills/build-benchmark/build-benchmark.harness.mjs +23 -0
- package/skills/build-benchmark/references/adversarial-cold-repro.md +68 -0
- package/skills/camera-ready/SKILL.md +148 -0
- package/skills/camera-ready/SKILL.md.spec.ts +164 -0
- package/skills/camera-ready/camera-ready.eval.mjs +19 -0
- package/skills/camera-ready/camera-ready.harness.mjs +23 -0
- package/skills/cold-read-diff/SKILL.md +160 -0
- package/skills/cold-read-diff/SKILL.md.spec.ts +166 -0
- package/skills/cold-read-diff/cold-read-diff.eval.mjs +19 -0
- package/skills/cold-read-diff/cold-read-diff.harness.mjs +23 -0
- package/skills/draft-paper/SKILL.md +152 -0
- package/skills/draft-paper/SKILL.md.spec.ts +169 -0
- package/skills/draft-paper/draft-paper.eval.mjs +19 -0
- package/skills/draft-paper/draft-paper.harness.mjs +23 -0
- package/skills/extend-paper/SKILL.md +99 -0
- package/skills/extend-paper/SKILL.md.spec.ts +116 -0
- package/skills/extend-paper/extend-paper.eval.mjs +19 -0
- package/skills/extend-paper/extend-paper.harness.mjs +23 -0
- package/skills/find-venue/SKILL.md +128 -0
- package/skills/find-venue/SKILL.md.spec.ts +145 -0
- package/skills/find-venue/find-venue.eval.mjs +19 -0
- package/skills/find-venue/find-venue.harness.mjs +23 -0
- package/skills/grade-paper-writing/SKILL.md +436 -0
- package/skills/grade-paper-writing/SKILL.md.spec.ts +453 -0
- package/skills/grade-paper-writing/fixtures/control_gopen.txt +1 -0
- package/skills/grade-paper-writing/fixtures/control_human_paper.txt +1 -0
- package/skills/grade-paper-writing/fixtures/rewrite.txt +1 -0
- package/skills/grade-paper-writing/fixtures/specimen.txt +1 -0
- package/skills/grade-paper-writing/fixtures/structure-checks.md +22 -0
- package/skills/grade-paper-writing/grade-paper-writing.eval.mjs +19 -0
- package/skills/grade-paper-writing/grade-paper-writing.harness.mjs +23 -0
- package/skills/grade-paper-writing/prose-lint.mjs +713 -0
- package/skills/harden-paper/SKILL.md +318 -0
- package/skills/harden-paper/SKILL.md.spec.ts +336 -0
- package/skills/harden-paper/check-numbers.sh +33 -0
- package/skills/harden-paper/check-release-claims.sh +35 -0
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +43 -0
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +77 -0
- package/skills/harden-paper/harden-paper.eval.mjs +19 -0
- package/skills/harden-paper/harden-paper.harness.mjs +23 -0
- package/skills/map-prior-work/SKILL.md +211 -0
- package/skills/map-prior-work/SKILL.md.spec.ts +227 -0
- package/skills/map-prior-work/map-prior-work.eval.mjs +19 -0
- package/skills/map-prior-work/map-prior-work.harness.mjs +23 -0
- package/skills/osf-artifact-upload/SKILL.md +52 -0
- package/skills/osf-artifact-upload/SKILL.md.spec.ts +59 -0
- package/skills/osf-artifact-upload/osf-artifact-upload.eval.mjs +22 -0
- package/skills/osf-artifact-upload/osf-artifact-upload.harness.mjs +103 -0
- package/skills/paper-adversarial-review/SKILL.md +126 -0
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +142 -0
- package/skills/paper-adversarial-review/paper-adversarial-review.eval.mjs +19 -0
- package/skills/paper-adversarial-review/paper-adversarial-review.harness.mjs +23 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +371 -0
- package/skills/paper-pipeline/SKILL.md +499 -0
- package/skills/paper-pipeline/SKILL.md.spec.ts +517 -0
- package/skills/paper-pipeline/description-language.eval.mjs +347 -0
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +891 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1254 -0
- package/skills/paper-pipeline/paper-pipeline.eval.mjs +22 -0
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +143 -0
- package/skills/paper-pipeline/pipeline-firing.baseline.json +270 -0
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +664 -0
- package/skills/paper-pipeline/pipeline-language.eval.mjs +672 -0
- package/skills/paper-pipeline/references/acceptance-gate.md +329 -0
- package/skills/paper-pipeline/references/acl-venue-rules.md +142 -0
- package/skills/paper-pipeline/references/anonymization.md +68 -0
- package/skills/paper-pipeline/references/artifact-checklist.md +93 -0
- package/skills/paper-pipeline/references/body-vs-appendix.md +97 -0
- package/skills/paper-pipeline/references/credit-criteria.md +69 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/README.md +35 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/run_retext.mjs +24 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/sentences.txt +11 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/test_sentences.py +25 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-prose-checkers.md +538 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-reproducible-tooling.md +431 -0
- package/skills/paper-pipeline/references/occupancy-2026-08-06-staleness-and-orchestration.md +592 -0
- package/skills/paper-pipeline/references/pipeline-status-template.md +162 -0
- package/skills/paper-pipeline/references/review-ratchet.md +36 -0
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +585 -0
- package/skills/paper-pipeline/references/writing-craft.md +448 -0
- package/skills/paper-pipeline/repro/2026-08-07-description-language-control.log +63 -0
- package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +52 -0
- package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +33 -0
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +33 -0
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +166 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.json +338 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +118 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-raw.log +245 -0
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary.json +776 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +53 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log +89 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.json +450 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +136 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-raw.log +242 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log +59 -0
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation.json +1032 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.json +139 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +98 -0
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +92 -0
- package/skills/paper-pipeline/repro/README.md +129 -0
- package/skills/paper-pipeline/repro/analyze-language-eval.py +116 -0
- package/skills/paper-pipeline/scripts/README.md +344 -0
- package/skills/paper-pipeline/scripts/announce.mjs +67 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +496 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +397 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +218 -0
- package/skills/paper-pipeline/scripts/check-provenance.mjs +184 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +32 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +562 -0
- package/skills/paper-pipeline/scripts/consumer.mjs +535 -0
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +190 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +457 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +656 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +54 -0
- package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +51 -0
- package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +52 -0
- package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +6 -0
- package/skills/paper-pipeline/scripts/fixtures/real-bib/refs.bib +153 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +466 -0
- package/skills/paper-pipeline/scripts/generated-code.mjs +338 -0
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +254 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +623 -0
- package/skills/paper-pipeline/scripts/ledger.selftest.mjs +286 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +389 -0
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +737 -0
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +54 -0
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +169 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +178 -0
- package/skills/paper-pipeline/scripts/population-map.mjs +181 -0
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +65 -0
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +122 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +240 -0
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +59 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +881 -0
- package/skills/paper-pipeline/scripts/round-diff.mjs +576 -0
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +276 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +633 -0
- package/skills/paper-pipeline/scripts/status.mjs +295 -0
- package/skills/paper-status/SKILL.md +183 -0
- package/skills/paper-status/SKILL.md.spec.ts +190 -0
- package/skills/paper-status/paper-status.eval.mjs +22 -0
- package/skills/paper-status/paper-status.harness.mjs +25 -0
- package/skills/pc-panel-review/SKILL.md +263 -0
- package/skills/pc-panel-review/SKILL.md.spec.ts +280 -0
- package/skills/pc-panel-review/pc-panel-review.eval.mjs +19 -0
- package/skills/pc-panel-review/pc-panel-review.harness.mjs +23 -0
- package/skills/plan-paper-timeline/SKILL.md +182 -0
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +200 -0
- package/skills/plan-paper-timeline/fixtures/fake-google-calendar.mjs +239 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +431 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +65 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.eval.mjs +19 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.harness.mjs +23 -0
- package/skills/render-paper/SKILL.md +159 -0
- package/skills/render-paper/SKILL.md.spec.ts +166 -0
- package/skills/render-paper/check-render.sh +419 -0
- package/skills/render-paper/checkers-requirements.txt +55 -0
- package/skills/render-paper/ensure-checkers.sh +69 -0
- package/skills/render-paper/extract-pdf-facts.harness.mjs +166 -0
- package/skills/render-paper/extract-pdf-facts.mjs +144 -0
- package/skills/render-paper/render-paper.eval.mjs +19 -0
- package/skills/render-paper/render-paper.harness.mjs +339 -0
- package/skills/research-ideate/SKILL.md +136 -0
- package/skills/research-ideate/SKILL.md.spec.ts +152 -0
- package/skills/research-ideate/research-ideate.eval.mjs +19 -0
- package/skills/research-ideate/research-ideate.harness.mjs +23 -0
- package/skills/skill-contract.mutations.mjs +179 -0
- package/skills/study-accepted-papers/SKILL.md +206 -0
- package/skills/study-accepted-papers/SKILL.md.spec.ts +223 -0
- package/skills/study-accepted-papers/study-accepted-papers.eval.mjs +19 -0
- package/skills/study-accepted-papers/study-accepted-papers.harness.mjs +23 -0
- package/skills/submit-paper/SKILL.md +182 -0
- package/skills/submit-paper/SKILL.md.spec.ts +199 -0
- package/skills/submit-paper/check-deanon.sh +149 -0
- package/skills/submit-paper/references/publishers/acm.md +92 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +108 -0
- package/skills/submit-paper/references/venues/agenticdev.md +139 -0
- package/skills/submit-paper/references/venues/agenticdev.tex +19 -0
- package/skills/submit-paper/references/venues/aisec.jsonc +101 -0
- package/skills/submit-paper/references/venues/aisec.md +105 -0
- package/skills/submit-paper/references/venues/paper-guards.tex +41 -0
- package/skills/submit-paper/references/venues/realm.jsonc +81 -0
- package/skills/submit-paper/references/venues/realm.md +155 -0
- package/skills/submit-paper/references/venues/tex-base.jsonc +50 -0
- package/skills/submit-paper/references/venues/venue-profile.schema.json +74 -0
- package/skills/submit-paper/submit-paper.eval.mjs +19 -0
- package/skills/submit-paper/submit-paper.harness.mjs +23 -0
- package/skills/sweep-design-space/SKILL.md +269 -0
- package/skills/sweep-design-space/SKILL.md.spec.ts +285 -0
- package/skills/sweep-design-space/sweep-design-space.eval.mjs +19 -0
- package/skills/sweep-design-space/sweep-design-space.harness.mjs +23 -0
- package/skills/tighten-paper/SKILL.md +368 -0
- package/skills/tighten-paper/SKILL.md.spec.ts +384 -0
- package/skills/tighten-paper/structure.mjs +371 -0
- package/skills/tighten-paper/tighten-paper.eval.mjs +19 -0
- package/skills/tighten-paper/tighten-paper.harness.mjs +23 -0
- package/skills/verify-citations/SKILL.md +328 -0
- package/skills/verify-citations/SKILL.md.spec.ts +345 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +479 -0
- package/skills/verify-citations/scripts/bib-authors.test.mjs +175 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +1108 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +735 -0
- package/skills/verify-citations/verify-citations.eval.mjs +19 -0
- package/skills/verify-citations/verify-citations.harness.mjs +23 -0
- package/src/CLAUDE.md +51 -0
- package/src/action-ref.test.ts +26 -0
- package/src/action-ref.ts +15 -0
- package/src/adapters/banal/failure.test.ts +63 -0
- package/src/adapters/banal/failure.ts +118 -0
- package/src/adapters/banal/index.test.ts +119 -0
- package/src/adapters/banal/index.ts +100 -0
- package/src/adapters/banal/install.test.ts +20 -0
- package/src/adapters/banal/install.ts +41 -0
- package/src/adapters/banal/invocation.test.ts +74 -0
- package/src/adapters/banal/invocation.ts +95 -0
- package/src/adapters/banal/locate.test.ts +52 -0
- package/src/adapters/banal/locate.ts +84 -0
- package/src/adapters/banal/output.test.ts +140 -0
- package/src/adapters/banal/output.ts +141 -0
- package/src/adapters/banal/pin.ts +30 -0
- package/src/adapters/banal/probe.ts +35 -0
- package/src/adapters/banal/run.test.ts +191 -0
- package/src/adapters/banal/run.ts +244 -0
- package/src/adapters/banal/settings.test.ts +31 -0
- package/src/adapters/banal/settings.ts +55 -0
- package/src/adapters/banal/xml.test.ts +111 -0
- package/src/adapters/banal/xml.ts +112 -0
- package/src/adapters/curl/download.io.ts +73 -0
- package/src/adapters/curl/download.test.ts +55 -0
- package/src/adapters/curl/index.ts +5 -0
- package/src/adapters/memory/index.ts +131 -0
- package/src/adapters/node/files.io.ts +39 -0
- package/src/adapters/node/files.test.ts +28 -0
- package/src/adapters/node/host.io.ts +15 -0
- package/src/adapters/node/index.ts +36 -0
- package/src/adapters/node/process.io.ts +49 -0
- package/src/adapters/node/process.test.ts +46 -0
- package/src/adapters/node/workspace.io.ts +40 -0
- package/src/adapters/node/workspace.test.ts +58 -0
- package/src/adapters/pdfjs/fill.test.ts +111 -0
- package/src/adapters/pdfjs/fill.ts +141 -0
- package/src/build-engine.harness.mjs +314 -0
- package/src/build-engine.ts +219 -0
- package/src/build.harness.mjs +631 -0
- package/src/build.mutations.mjs +195 -0
- package/src/build.ts +793 -0
- package/src/cli.harness.mjs +2007 -0
- package/src/cli.mutations.mjs +448 -0
- package/src/cli.ts +1189 -0
- package/src/doctor.harness.mjs +396 -0
- package/src/doctor.mutations.mjs +175 -0
- package/src/doctor.ts +356 -0
- package/src/domain/geometry.ts +108 -0
- package/src/domain/host.ts +23 -0
- package/src/domain/page-layout.ts +32 -0
- package/src/domain/paths.ts +5 -0
- package/src/domain/result.test.ts +26 -0
- package/src/domain/result.ts +29 -0
- package/src/domain/sha256.test.ts +12 -0
- package/src/domain/sha256.ts +21 -0
- package/src/domain/text.ts +11 -0
- package/src/engine.harness.mjs +252 -0
- package/src/engine.ts +176 -0
- package/src/exit-code.test.ts +21 -0
- package/src/exit-code.ts +38 -0
- package/src/facts-file.test.ts +240 -0
- package/src/facts-file.ts +241 -0
- package/src/hooks-settings.harness.mjs +386 -0
- package/src/hooks-settings.mutations.mjs +116 -0
- package/src/hooks-settings.ts +434 -0
- package/src/init.ts +900 -0
- package/src/latex-log.harness.mjs +226 -0
- package/src/latex-log.ts +234 -0
- package/src/latex-loop.harness.mjs +449 -0
- package/src/latex-loop.ts +211 -0
- package/src/link-skills.harness.mjs +273 -0
- package/src/link-skills.mutations.mjs +136 -0
- package/src/link-skills.ts +258 -0
- package/src/new-paper.harness.mjs +216 -0
- package/src/new-paper.mutations.mjs +79 -0
- package/src/new-paper.ts +158 -0
- package/src/pdf-facts.harness.mjs +188 -0
- package/src/pdf-facts.ts +327 -0
- package/src/pdf-geometry.harness.mjs +254 -0
- package/src/pdf-geometry.ts +300 -0
- package/src/ports/download.ts +10 -0
- package/src/ports/files.ts +11 -0
- package/src/ports/measure-geometry.ts +8 -0
- package/src/ports/process.ts +46 -0
- package/src/ports/tool-installer.ts +33 -0
- package/src/ports/workspace.ts +20 -0
- package/src/rules-config.harness.mjs +114 -0
- package/src/rules-config.ts +178 -0
- package/src/structure.harness.mjs +179 -0
- package/src/structure.mutations.mjs +83 -0
- package/src/structure.ts +166 -0
- package/src/tex-requirements.harness.mjs +238 -0
- package/src/tex-requirements.ts +181 -0
- package/src/toolchain.harness.mjs +651 -0
- package/src/toolchain.ts +755 -0
- package/src/types.ts +106 -0
- package/templates/paper/PIPELINE-STATUS.md +72 -0
- package/templates/paper/paper.md +4 -0
- package/templates/paper/paper.tex +8 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* extract-ref-facts.mjs — parse a paper's bibliography, ask the registries and write FACTS to JSON.
|
|
4
|
+
* It judges nothing.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node extract-ref-facts.mjs <paper dir|paper.md|refs.bib> [--offline] [--refresh]
|
|
7
|
+
* [--cache=PATH] [--out=PATH] [--quiet]
|
|
8
|
+
* Exit: 0 — facts written · 1 — nothing to write (no source, no entries).
|
|
9
|
+
*
|
|
10
|
+
* WHY THE SPLIT. Before 2026-08-26 measurement and judgement lived in one script
|
|
11
|
+
* (`verify-refs.mjs`, 20 emit sites, 8 kinds of findings) — that is, on the fifth rung of the
|
|
12
|
+
* ladder in `the consumer's papers CLAUDE.md`. The judgement moved into ESLint rules
|
|
13
|
+
* (`eslint-rules/ref-facts.mjs`): a registry of rules, severity from config, `eslint-disable` with a
|
|
14
|
+
* reason, `file:line:col` positions. What stayed here is the plumbing: parsing markup, parsing
|
|
15
|
+
* `.bib`, the network, parsing the registries' responses.
|
|
16
|
+
*
|
|
17
|
+
* The precedent in this same knowledge base is `render-paper/extract-pdf-facts.mjs` +
|
|
18
|
+
* `eslint-rules/pdf-facts.mjs`. The boundary is exactly the same: the script goes out into the
|
|
19
|
+
* world, the rule passes the judgement.
|
|
20
|
+
*
|
|
21
|
+
* 🔴 WHY THE RULE CANNOT LIVE OVER THE PAPER ITSELF. Of the three papers in the knowledge base one
|
|
22
|
+
* has a `paper.md` (`<paper-a>`); in `<paper-b>` and `<paper-c>` the bibliography is `refs.bib`, and
|
|
23
|
+
* no ESLint language plugin parses `.bib`. The common input that can do both is the facts JSON.
|
|
24
|
+
* The price is known and written down: a finding is addressed into the facts, not into a line of
|
|
25
|
+
* `paper.md`; that is why every entry in the facts carries the `line` of its source, and the rule
|
|
26
|
+
* prints it in the message.
|
|
27
|
+
*
|
|
28
|
+
* 🔴 WHAT THIS MOVE FIXED (both legs were DEAD, measured 2026-08-26):
|
|
29
|
+
*
|
|
30
|
+
* 1. `.bib` WAS NEVER OPENED. `resolveSource` walked `paper.md` → `draft.md` →
|
|
31
|
+
* `build/custom.bib`; `refs.bib` was not on the list, although the error text promised
|
|
32
|
+
* "or .bib". The run: `verify-refs.mjs <papers-root>/<paper-b>` →
|
|
33
|
+
* "no paper.md, draft.md or .bib … nothing to check".
|
|
34
|
+
* 2. THE `.bib` PARSER DID NOT TAKE THE REAL FILES. The regex `/@\w+\{([^,]+),([\s\S]*?)\n\}/g`
|
|
35
|
+
* required a `}` on a line of its own, while both of our `refs.bib` close an entry on the line
|
|
36
|
+
* of the last field (`note={arXiv:2310.06770}}`). A run straight at the file: "parsed 0
|
|
37
|
+
* references out of …/<paper-b>/refs.bib". That is, 40 lines of `parseBib` never ran once.
|
|
38
|
+
* 3. AND A THIRD, hidden behind the first two: in `.bib` an author is written "Surname, First
|
|
39
|
+
* name", while the comparison took the LAST token of the string — for `Jimenez, Carlos E.` that
|
|
40
|
+
* is `e`. Even if the regex had worked, checking authors against `.bib` would have compared
|
|
41
|
+
* initials with surnames.
|
|
42
|
+
*
|
|
43
|
+
* None of this is cured by being careful, but by having `.bib` parsed by a REAL PARSER
|
|
44
|
+
* (`@retorquere/bibtex-parser`), which returns `lastName` as a separate field — class #3 becomes
|
|
45
|
+
* inexpressible. Occupancy was checked by a run over both real files BEFORE the choice:
|
|
46
|
+
* `bibtex-parse`, `@retorquere/bibtex-parser`, `citation-js` and `astrocite-bibtex` — all four give
|
|
47
|
+
* the correct 27 and 51 entries against zero from our regex. retorquere was taken because it is the
|
|
48
|
+
* only one that returns a STRUCTURED name; with any other the surname would have to be cut out by
|
|
49
|
+
* hand, that is, we would be writing ourselves the very half that was broken.
|
|
50
|
+
*
|
|
51
|
+
* 🔴 THE CACHE IS EVIDENCE, NOT A SECOND DECLARATION. `<paper>/repro/refs-cache.json` keeps the RAW
|
|
52
|
+
* BODIES of the registries' responses and is committed (for `<paper-a>` it has been in git since
|
|
53
|
+
* 16.08). It cannot be made up — only refreshed from the registry. The facts in `_build/` are
|
|
54
|
+
* derived from it and from the bibliography, which is why `_build/` is in `.gitignore` while the
|
|
55
|
+
* facts carry `source_sha256`: the rule `refs/fresh` compares it with the file on disk, and stale
|
|
56
|
+
* facts become a finding rather than silence. Exactly the failure that made `api:check` in vigiles
|
|
57
|
+
* print "no drift" while reading a `dist/` from the previous build.
|
|
58
|
+
*
|
|
59
|
+
* 🔴 PARSING THE RESPONSES LIVES HERE, NOT IN THE RULE, AND THAT IS LOAD-BEARING. The cache keeps
|
|
60
|
+
* raw bodies precisely so that the CrossRef-JSON and arXiv-Atom readers are exercised on real
|
|
61
|
+
* responses: every fabricated title this knowledge base has ever shipped was found by reading the
|
|
62
|
+
* registry's response, not by a comparison. If the cache kept parsed entries, those readers would
|
|
63
|
+
* stay uncovered while looking covered.
|
|
64
|
+
*/
|
|
65
|
+
import {
|
|
66
|
+
readFileSync,
|
|
67
|
+
writeFileSync,
|
|
68
|
+
existsSync,
|
|
69
|
+
mkdirSync,
|
|
70
|
+
statSync,
|
|
71
|
+
} from "node:fs";
|
|
72
|
+
import { createHash } from "node:crypto";
|
|
73
|
+
import { join, resolve, dirname, basename } from "node:path";
|
|
74
|
+
import {
|
|
75
|
+
headings as mdHeadings,
|
|
76
|
+
requireMarkdown,
|
|
77
|
+
} from "../../../lib/markdown.mjs";
|
|
78
|
+
import { isMain } from "./consumer.mjs";
|
|
79
|
+
|
|
80
|
+
// Markup is parsed with a parser (`CLAUDE.md`, 2026-08-11). We fail rather than degrade: without
|
|
81
|
+
// the parser the reference list would not be found at all, and the facts would come out empty —
|
|
82
|
+
// that is, a clean verdict about a paper that in fact has forty references.
|
|
83
|
+
requireMarkdown();
|
|
84
|
+
|
|
85
|
+
const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
86
|
+
const CROSSREF = "https://api.crossref.org/works/";
|
|
87
|
+
const ARXIV = "https://export.arxiv.org/api/query?id_list=";
|
|
88
|
+
// CrossRef asks for a contact in the User-Agent and gives the polite pool in return. No account
|
|
89
|
+
// and no key — it is a courtesy, the anonymous pool is rate-limited.
|
|
90
|
+
const UA =
|
|
91
|
+
"extract-ref-facts/1.0 (https://github.com/; paper-pipeline citation facts)";
|
|
92
|
+
|
|
93
|
+
export const SCHEMA = 1;
|
|
94
|
+
|
|
95
|
+
// ── parsing: the reference list in markdown ──────────────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// The same shape that `repro/md2submission.py` reads, deliberately: that script turns the list into
|
|
98
|
+
// the .bib actually uploaded to the venue, so whatever it counts as an entry is an entry.
|
|
99
|
+
// `^\d{1,2}. ` opens an entry, an indent continues it, a blank line closes it.
|
|
100
|
+
|
|
101
|
+
const REF_OPEN = /^(\d{1,2})\.\s+(.*)$/;
|
|
102
|
+
/** Working notes that never reach the PDF; md2submission strips them, so we do too. */
|
|
103
|
+
const BRACKET_NOTE = /\[(VERIFY|ANONYMIZ)[^\]]*\]/g;
|
|
104
|
+
|
|
105
|
+
export function parseMarkdownRefs(text) {
|
|
106
|
+
// The section is cut by the parser. The former `text.split(/^## References\s*$/m)[1]` opened the
|
|
107
|
+
// reference list on a `## References` quoted inside a ```-block — and in this repo papers quote
|
|
108
|
+
// their own markup in chunks.
|
|
109
|
+
const refsHs = mdHeadings(text).filter(
|
|
110
|
+
(h) => h.depth === 2 && /^References\s*$/u.test(h.text),
|
|
111
|
+
);
|
|
112
|
+
if (refsHs.length === 0) return [];
|
|
113
|
+
const nl = text.indexOf("\n", refsHs[0].offset);
|
|
114
|
+
const body = text.slice(
|
|
115
|
+
nl === -1 ? text.length : nl,
|
|
116
|
+
refsHs[1] ? refsHs[1].offset : text.length,
|
|
117
|
+
);
|
|
118
|
+
// The line where the body starts in the source file — so that an entry has an ADDRESS in
|
|
119
|
+
// `paper.md` and not just a number in a list. The rule prints it in the message: the facts file
|
|
120
|
+
// lives in `_build/`, and without the source line a finding would have to be hunted for by eye.
|
|
121
|
+
const bodyLine = text
|
|
122
|
+
.slice(0, nl === -1 ? text.length : nl)
|
|
123
|
+
.split("\n").length;
|
|
124
|
+
// A heading closes the list — appendices come AFTER the bibliography, and without this their
|
|
125
|
+
// numbered prose would be parsed as references. The line numbers come from the parser:
|
|
126
|
+
// `/^#{1,6}\s/` also counted a hash inside a ```-block as a heading.
|
|
127
|
+
const headingLines = new Set(mdHeadings(body).map((h) => h.line));
|
|
128
|
+
const entries = [];
|
|
129
|
+
let cur = null;
|
|
130
|
+
let buf = [];
|
|
131
|
+
let curLine = 0;
|
|
132
|
+
const flush = () => {
|
|
133
|
+
if (cur !== null)
|
|
134
|
+
entries.push({
|
|
135
|
+
n: cur,
|
|
136
|
+
line: curLine,
|
|
137
|
+
raw: buf.join(" ").replace(BRACKET_NOTE, "").trim(),
|
|
138
|
+
});
|
|
139
|
+
cur = null;
|
|
140
|
+
buf = [];
|
|
141
|
+
};
|
|
142
|
+
const lns = body.split("\n");
|
|
143
|
+
for (let i = 0; i < lns.length; i++) {
|
|
144
|
+
const ln = lns[i];
|
|
145
|
+
if (headingLines.has(i)) break;
|
|
146
|
+
const m = REF_OPEN.exec(ln);
|
|
147
|
+
if (m) {
|
|
148
|
+
flush();
|
|
149
|
+
cur = Number(m[1]);
|
|
150
|
+
curLine = bodyLine + i;
|
|
151
|
+
buf = [m[2].trim()];
|
|
152
|
+
} else if (cur !== null && ln.trim()) buf.push(ln.trim());
|
|
153
|
+
else if (cur !== null) flush();
|
|
154
|
+
}
|
|
155
|
+
flush();
|
|
156
|
+
return entries.map(splitEntry).filter(Boolean);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** `A, B, C. *Title.* rest` → authors / title / remainder — the shape md2submission produces. */
|
|
160
|
+
function splitEntry(e) {
|
|
161
|
+
const base = { n: e.n, line: e.line, key: null, raw: e.raw };
|
|
162
|
+
const mt = /\*(.+?)\*/.exec(e.raw);
|
|
163
|
+
if (!mt)
|
|
164
|
+
return {
|
|
165
|
+
...base,
|
|
166
|
+
authors: [],
|
|
167
|
+
truncated: false,
|
|
168
|
+
title: null,
|
|
169
|
+
year: yearIn(e.raw),
|
|
170
|
+
venue_text: e.raw,
|
|
171
|
+
};
|
|
172
|
+
const title = mt[1].trim().replace(/\.$/, "");
|
|
173
|
+
const rawAuthors = splitAuthors(e.raw.slice(0, mt.index));
|
|
174
|
+
const note = e.raw
|
|
175
|
+
.slice(mt.index + mt[0].length)
|
|
176
|
+
.trim()
|
|
177
|
+
.replace(/^,\s*/, "");
|
|
178
|
+
const { authors, truncated } = dropEtAl(rawAuthors);
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
authors,
|
|
182
|
+
truncated,
|
|
183
|
+
title,
|
|
184
|
+
year: yearIn(note) ?? yearIn(e.raw),
|
|
185
|
+
venue_text: note,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function splitAuthors(a) {
|
|
190
|
+
const s = a
|
|
191
|
+
.trim()
|
|
192
|
+
.replace(/\.\s*$/, "")
|
|
193
|
+
.trim();
|
|
194
|
+
if (!s) return [];
|
|
195
|
+
return s
|
|
196
|
+
.split(",")
|
|
197
|
+
.map((p) => p.trim())
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `Z. Xiang et al.` — the entry deliberately gives a PREFIX of the author list. That is a fact about
|
|
203
|
+
* the entry, not a judgement, so the truncation is removed here and the rule receives an honest
|
|
204
|
+
* prefix plus a flag.
|
|
205
|
+
*/
|
|
206
|
+
const ET_AL = /\bet\s+al\.?$/i;
|
|
207
|
+
function dropEtAl(authors) {
|
|
208
|
+
if (!authors.length || !ET_AL.test(authors[authors.length - 1].trim()))
|
|
209
|
+
return { authors, truncated: false };
|
|
210
|
+
const out = authors.slice();
|
|
211
|
+
out[out.length - 1] = out[out.length - 1].replace(ET_AL, "").trim();
|
|
212
|
+
return { authors: out.filter(Boolean), truncated: true };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const yearIn = (s) => (/\b(19|20)\d{2}\b/.exec(s ?? "") ?? [null])[0];
|
|
216
|
+
|
|
217
|
+
// ── parsing: .bib WITH A REAL PARSER ─────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The name order is normalised to "First name Surname" — the same shape in which authors are written
|
|
221
|
+
* in markdown. Then the rule has ONE `surname()` function (the last alphabetic token) for both
|
|
222
|
+
* sources instead of two branches. Normalising the format is the parser's job; comparing is the
|
|
223
|
+
* rule's job.
|
|
224
|
+
*
|
|
225
|
+
* 🔴 TWO SHAPES, AND BOTH ARE LISTED EXPLICITLY. The parser returns an institution
|
|
226
|
+
* (`author={{Adversa AI}}` — the double brace means "one name as a whole, do not split") as
|
|
227
|
+
* `{name}`, WITHOUT `lastName`: an organisation has no surname, and that is correct. The first
|
|
228
|
+
* revision of this function did not know the `name` shape, so all four fields were undefined, the
|
|
229
|
+
* string came out empty, and the caller dropped it with its own `.filter(Boolean)` — the author
|
|
230
|
+
* disappeared silently.
|
|
231
|
+
*
|
|
232
|
+
* Measured 2026-09-17 on the real aisec-2026: ELEVEN entries out of fifty-one arrived with
|
|
233
|
+
* `authors = []`, that is, the check over them found nothing and looked like it had passed. Exactly
|
|
234
|
+
* the failure toward silence against which a real parser was taken instead of a regex.
|
|
235
|
+
*
|
|
236
|
+
* ⚠️ An enumeration, not "the first non-empty field": with a list the next shape reads as a missing
|
|
237
|
+
* line, whereas with a `??` chain over arbitrary fields it would dissolve again.
|
|
238
|
+
*/
|
|
239
|
+
const joinName = (a) =>
|
|
240
|
+
a.name ??
|
|
241
|
+
[a.firstName, a.prefix, a.lastName, a.suffix]
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.join(" ")
|
|
244
|
+
.trim();
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 🔴 THE .bib PARSER IS AN OPTIONAL DEPENDENCY, AND THE FAILURE MUST BE LOUD AND CARRY THE CURE.
|
|
248
|
+
* It weighs 15 MB out of the consumer's 56 MB install (the package itself 9 MB plus the English
|
|
249
|
+
* model `wink-eng-lite-web-model` 4 MB and `unicode2latex` 2 MB) — 27% of the weight for a single
|
|
250
|
+
* call that only whoever extracts bibliography facts needs. The import here is dynamic anyway, so
|
|
251
|
+
* the laziness was already there; the manifest merely stopped lying about it being required.
|
|
252
|
+
*
|
|
253
|
+
* ⚠️ A silent skip here would be the worst of the options: a missing checker and a passing one look
|
|
254
|
+
* the same, and "the bibliography was not checked" reads as "the bibliography is fine". That is why
|
|
255
|
+
* the message names the command, not the fact.
|
|
256
|
+
*/
|
|
257
|
+
export async function parseBib(text) {
|
|
258
|
+
let parse;
|
|
259
|
+
try {
|
|
260
|
+
({ parse } = await import("@retorquere/bibtex-parser"));
|
|
261
|
+
} catch (e) {
|
|
262
|
+
if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e;
|
|
263
|
+
throw new Error(
|
|
264
|
+
"parsing .bib requires @retorquere/bibtex-parser — it is declared OPTIONAL because it " +
|
|
265
|
+
"weighs 15 MB and is needed only for bibliography facts.\n" +
|
|
266
|
+
" Install: npm i -D @retorquere/bibtex-parser\n" +
|
|
267
|
+
" Why not our own regex: measured 26.08 — the regex gave 0 entries on both real " +
|
|
268
|
+
"files, four libraries gave the correct 27 and 51.",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
// `sentenceCase: false` — the title is needed as written. Our normalisation folds the case
|
|
272
|
+
// anyway, but a fact has to be a fact: a paraphrased title cannot be shown to a human.
|
|
273
|
+
const res = parse(text, { sentenceCase: false, verbatimFields: [] });
|
|
274
|
+
// The entry's line in the file — found by its key. The parser gives no positions, and a finding
|
|
275
|
+
// needs an address; a key in `.bib` is unique by definition of the format, so the search is
|
|
276
|
+
// unambiguous.
|
|
277
|
+
const lines = text.split("\n");
|
|
278
|
+
const lineOfKey = (key) => {
|
|
279
|
+
const i = lines.findIndex((l) => l.includes(`{${key},`));
|
|
280
|
+
return i === -1 ? 0 : i + 1;
|
|
281
|
+
};
|
|
282
|
+
return res.entries.map((e, i) => {
|
|
283
|
+
const f = e.fields ?? {};
|
|
284
|
+
const names = Array.isArray(f.author) ? f.author : [];
|
|
285
|
+
// `and others` is BibTeX's `et al.`. The parser returns it as an author with no first name.
|
|
286
|
+
const isOthers = (a) => !a.firstName && /^others$/i.test(a.lastName ?? "");
|
|
287
|
+
const truncated = names.some(isOthers);
|
|
288
|
+
const authors = names
|
|
289
|
+
.filter((a) => !isOthers(a))
|
|
290
|
+
.map(joinName)
|
|
291
|
+
.filter(Boolean);
|
|
292
|
+
const str = (v) =>
|
|
293
|
+
Array.isArray(v)
|
|
294
|
+
? v.join(" ")
|
|
295
|
+
: typeof v === "string"
|
|
296
|
+
? v
|
|
297
|
+
: v == null
|
|
298
|
+
? ""
|
|
299
|
+
: String(v);
|
|
300
|
+
const venueText = [f.booktitle, f.journal, f.note, f.howpublished]
|
|
301
|
+
.map(str)
|
|
302
|
+
.filter(Boolean)
|
|
303
|
+
.join(" ");
|
|
304
|
+
return {
|
|
305
|
+
n: i + 1,
|
|
306
|
+
line: lineOfKey(e.key),
|
|
307
|
+
key: e.key,
|
|
308
|
+
// `raw` is where identifiers are looked for: they hide in `note`, in `journal`
|
|
309
|
+
// (`arXiv preprint arXiv:2107.03374`), and in a separate `doi` field.
|
|
310
|
+
raw: [
|
|
311
|
+
str(f.author && authors.join(" and ")),
|
|
312
|
+
str(f.title),
|
|
313
|
+
venueText,
|
|
314
|
+
str(f.doi),
|
|
315
|
+
str(f.url),
|
|
316
|
+
str(f.year),
|
|
317
|
+
]
|
|
318
|
+
.filter(Boolean)
|
|
319
|
+
.join(" "),
|
|
320
|
+
authors,
|
|
321
|
+
truncated,
|
|
322
|
+
title: str(f.title).trim() || null,
|
|
323
|
+
year: str(f.year).trim() || null,
|
|
324
|
+
venue_text: [venueText, str(f.doi)].filter(Boolean).join(" "),
|
|
325
|
+
// In BibTeX a DOI is declared as a field rather than hidden in prose — take it directly.
|
|
326
|
+
doi_field: str(f.doi).trim() || null,
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── identifiers ──────────────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
const ARXIV_RE = /arxiv[:\s]\s*(\d{4}\.\d{4,5})(v\d+)?/i;
|
|
334
|
+
const DOI_RE = /(?:doi[:\s]\s*|doi\.org\/)(10\.\d{4,9}\/[^\s,;)}]+)/i;
|
|
335
|
+
const ARXIV_RE_G = new RegExp(ARXIV_RE.source, "gi");
|
|
336
|
+
const DOI_RE_G = new RegExp(DOI_RE.source, "gi");
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* ALL identifiers of an entry, not the first one.
|
|
340
|
+
*
|
|
341
|
+
* 🔴 Entry [16] of the paper this was written against is one numbered entry carrying TWO works
|
|
342
|
+
* (TOGA and its later re-analysis), each with its own arXiv id. A parser that stops at the first
|
|
343
|
+
* checks half the entry and stays silent about the second — exactly the silence all of this exists
|
|
344
|
+
* to remove. There is nothing to compare the extras against (the entry has one title and one author
|
|
345
|
+
* list, and they are about the first work), so they are RESOLVED while the rule says it did not
|
|
346
|
+
* compare them.
|
|
347
|
+
*/
|
|
348
|
+
export function extractIds(entry) {
|
|
349
|
+
const hay = `${entry.venue_text ?? ""} ${entry.raw ?? ""}`;
|
|
350
|
+
const uniq = (xs) => [...new Set(xs)];
|
|
351
|
+
const doi = uniq([
|
|
352
|
+
...(entry.doi_field ? [entry.doi_field] : []),
|
|
353
|
+
// A trailing period belongs to the sentence, not to the DOI. Suffixes legitimately contain
|
|
354
|
+
// periods (10.18653/v1/2020.acl-main.168), so only the LAST one is stripped.
|
|
355
|
+
...[...hay.matchAll(DOI_RE_G)].map((m) => m[1].replace(/[.,]$/, "")),
|
|
356
|
+
]);
|
|
357
|
+
return { arxiv: uniq([...hay.matchAll(ARXIV_RE_G)].map((m) => m[1])), doi };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 🔴 `10.48550/arXiv.NNNN.NNNNN` IS A DOI REGISTERED WITH DATACITE, NOT WITH CROSSREF.
|
|
362
|
+
* Measured 2026-08-26: `api.crossref.org/works/10.48550%2FarXiv.2107.03374` → **HTTP 404**,
|
|
363
|
+
* "Resource not found", while the DOI is perfectly real and sits in `<paper-b>/refs.bib`.
|
|
364
|
+
* Asking CrossRef about it would have produced `unresolvable-id` on a CORRECT entry — that is, an
|
|
365
|
+
* `error`-level rule failing on valid input, which gets switched off the same day.
|
|
366
|
+
* Such a DOI resolves at arXiv by its own number.
|
|
367
|
+
*/
|
|
368
|
+
export const ARXIV_DOI = /^10\.48550\/arxiv\.(\d{4}\.\d{4,5})(v\d+)?$/i;
|
|
369
|
+
|
|
370
|
+
/** The record the title/authors/year are judged against: the DOI, if there is one. */
|
|
371
|
+
export const primaryOf = (ids) =>
|
|
372
|
+
ids.doi.length
|
|
373
|
+
? `doi:${ids.doi[0]}`
|
|
374
|
+
: ids.arxiv.length
|
|
375
|
+
? `arxiv:${ids.arxiv[0]}`
|
|
376
|
+
: null;
|
|
377
|
+
export const allKeys = (ids) => [
|
|
378
|
+
...ids.doi.map((d) => `doi:${d}`),
|
|
379
|
+
...ids.arxiv.map((a) => `arxiv:${a}`),
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
/** Where to go for a key and with which id. Decided BEFORE the network — parsing depends on it. */
|
|
383
|
+
export function routeOf(key) {
|
|
384
|
+
const kind = key.slice(0, key.indexOf(":"));
|
|
385
|
+
const id = key.slice(key.indexOf(":") + 1);
|
|
386
|
+
if (kind === "arxiv")
|
|
387
|
+
return { registry: "arxiv", url: ARXIV + encodeURIComponent(id) };
|
|
388
|
+
const m = ARXIV_DOI.exec(id);
|
|
389
|
+
if (m) return { registry: "arxiv", url: ARXIV + encodeURIComponent(m[1]) };
|
|
390
|
+
return { registry: "crossref", url: CROSSREF + encodeURIComponent(id) };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── registry readers ─────────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
export function readCrossref(body) {
|
|
396
|
+
const j = JSON.parse(body);
|
|
397
|
+
const m = j.message;
|
|
398
|
+
if (!m) return null;
|
|
399
|
+
const dp =
|
|
400
|
+
m.issued?.["date-parts"]?.[0]?.[0] ??
|
|
401
|
+
m["published-print"]?.["date-parts"]?.[0]?.[0] ??
|
|
402
|
+
m["published-online"]?.["date-parts"]?.[0]?.[0] ??
|
|
403
|
+
null;
|
|
404
|
+
// 🔴 CrossRef puts the subtitle after the colon into a SEPARATE field: for TOSEM's
|
|
405
|
+
// "Variability-Aware Static Analysis at Scale: An Empirical Study" it returns
|
|
406
|
+
// title=["Variability-Aware Static Analysis at Scale"], subtitle=["An Empirical Study"].
|
|
407
|
+
// Comparing against `title[0]` alone gave a correct entry 0.69 and declared the title made up.
|
|
408
|
+
// A checker that fires on correct entries gets muted — and then it is not a checker.
|
|
409
|
+
const parts = [
|
|
410
|
+
...(Array.isArray(m.title) ? m.title : [m.title]),
|
|
411
|
+
...(Array.isArray(m.subtitle)
|
|
412
|
+
? m.subtitle
|
|
413
|
+
: m.subtitle
|
|
414
|
+
? [m.subtitle]
|
|
415
|
+
: []),
|
|
416
|
+
];
|
|
417
|
+
return {
|
|
418
|
+
title: parts.filter(Boolean).join(": "),
|
|
419
|
+
authors: (m.author ?? []).map((a) => a.family ?? a.name ?? ""),
|
|
420
|
+
year: dp ? String(dp) : null,
|
|
421
|
+
venue: Array.isArray(m["container-title"])
|
|
422
|
+
? m["container-title"][0]
|
|
423
|
+
: m["container-title"],
|
|
424
|
+
journalRef: null,
|
|
425
|
+
doi: null,
|
|
426
|
+
error: false,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** arXiv answers in Atom. One `<entry>` per id; zero entries means the id does not resolve. */
|
|
431
|
+
export function readArxiv(body) {
|
|
432
|
+
const entry = /<entry>([\s\S]*?)<\/entry>/.exec(body);
|
|
433
|
+
if (!entry) return null;
|
|
434
|
+
const e = entry[1];
|
|
435
|
+
const tag = (name) => {
|
|
436
|
+
const m = new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`).exec(e);
|
|
437
|
+
return m ? unescapeXml(m[1]).replace(/\s+/g, " ").trim() : null;
|
|
438
|
+
};
|
|
439
|
+
const authors = [
|
|
440
|
+
...e.matchAll(
|
|
441
|
+
/<author>[\s\S]*?<name>([\s\S]*?)<\/name>[\s\S]*?<\/author>/g,
|
|
442
|
+
),
|
|
443
|
+
].map((m) => unescapeXml(m[1]).replace(/\s+/g, " ").trim());
|
|
444
|
+
const published = tag("published");
|
|
445
|
+
return {
|
|
446
|
+
title: tag("title"),
|
|
447
|
+
authors,
|
|
448
|
+
year: published ? published.slice(0, 4) : null,
|
|
449
|
+
venue: null,
|
|
450
|
+
// arXiv's own "this came out somewhere" fields. Either of them means a published version
|
|
451
|
+
// exists.
|
|
452
|
+
journalRef: tag("arxiv:journal_ref"),
|
|
453
|
+
doi: tag("arxiv:doi"),
|
|
454
|
+
// A search for a withdrawn or invented id still returns an <entry> whose id echoes the query;
|
|
455
|
+
// a real entry's title is never "Error".
|
|
456
|
+
error: /^Error$/i.test(tag("title") ?? ""),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const unescapeXml = (s) =>
|
|
461
|
+
s
|
|
462
|
+
.replace(/</g, "<")
|
|
463
|
+
.replace(/>/g, ">")
|
|
464
|
+
.replace(/"/g, '"')
|
|
465
|
+
.replace(/'/g, "'")
|
|
466
|
+
.replace(/&/g, "&");
|
|
467
|
+
|
|
468
|
+
// ── cache and network ────────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
export function loadCache(path) {
|
|
471
|
+
if (!path || !existsSync(path)) return {};
|
|
472
|
+
try {
|
|
473
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
474
|
+
} catch {
|
|
475
|
+
return {};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function saveCache(path, cache) {
|
|
480
|
+
if (!path) return;
|
|
481
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
482
|
+
writeFileSync(path, `${JSON.stringify(cache, null, 1)}\n`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function fetchKey(key) {
|
|
486
|
+
const { url } = routeOf(key);
|
|
487
|
+
const r = await fetch(url, { headers: { "User-Agent": UA } });
|
|
488
|
+
return {
|
|
489
|
+
httpStatus: r.status,
|
|
490
|
+
body: await r.text(),
|
|
491
|
+
fetched: new Date().toISOString().slice(0, 10),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* A raw response → a facts record. Everything that can throw lives HERE: a rule that throws brings
|
|
497
|
+
* down the whole ESLint run, not one finding.
|
|
498
|
+
*/
|
|
499
|
+
export function recordFrom(key, cached) {
|
|
500
|
+
if (!cached) return { cached: false };
|
|
501
|
+
const { registry } = routeOf(key);
|
|
502
|
+
const base = {
|
|
503
|
+
cached: true,
|
|
504
|
+
registry,
|
|
505
|
+
httpStatus: cached.httpStatus ?? null,
|
|
506
|
+
fetched: cached.fetched ?? null,
|
|
507
|
+
};
|
|
508
|
+
if (cached.httpStatus !== 200) return base;
|
|
509
|
+
let rec; // no initializer: try assigns it, catch returns (2026-08-28)
|
|
510
|
+
try {
|
|
511
|
+
rec =
|
|
512
|
+
registry === "crossref"
|
|
513
|
+
? readCrossref(cached.body)
|
|
514
|
+
: readArxiv(cached.body);
|
|
515
|
+
} catch (e) {
|
|
516
|
+
return {
|
|
517
|
+
...base,
|
|
518
|
+
found: false,
|
|
519
|
+
parse_error: String(e.message).split("\n")[0],
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (!rec) return { ...base, found: false };
|
|
523
|
+
return { ...base, found: !rec.error, ...rec };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ── assembling the facts ─────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
/** `paper.md` → `draft.md` → `refs.bib` → `build/custom.bib`. `refs.bib` ADDED 26.08 (defect #1). */
|
|
529
|
+
export const SOURCE_ORDER = [
|
|
530
|
+
"paper.md",
|
|
531
|
+
"draft.md",
|
|
532
|
+
"refs.bib",
|
|
533
|
+
"build/custom.bib",
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
export function resolveSource(target) {
|
|
537
|
+
const t = resolve(target);
|
|
538
|
+
if (existsSync(t) && statSync(t).isFile()) return t;
|
|
539
|
+
for (const c of SOURCE_ORDER) {
|
|
540
|
+
const p = join(t, c);
|
|
541
|
+
if (existsSync(p)) return p;
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export async function loadEntries(path) {
|
|
547
|
+
const text = readFileSync(path, "utf8");
|
|
548
|
+
return path.endsWith(".bib") ? parseBib(text) : parseMarkdownRefs(text);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Every registry key a list of entries needs. Computed once so the requests go in one batch. */
|
|
552
|
+
export function keysFor(entries) {
|
|
553
|
+
const keys = new Set();
|
|
554
|
+
for (const e of entries) for (const k of allKeys(extractIds(e))) keys.add(k);
|
|
555
|
+
return [...keys];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const rel = (p) => (p.startsWith(ROOT) ? p.slice(ROOT.length + 1) : p);
|
|
559
|
+
|
|
560
|
+
export function buildFacts({ source, text, entries, cache, cachePath }) {
|
|
561
|
+
const withIds = entries.map((e) => {
|
|
562
|
+
const ids = extractIds(e);
|
|
563
|
+
return { ...e, ids, primary: primaryOf(ids), keys: allKeys(ids) };
|
|
564
|
+
});
|
|
565
|
+
const records = {};
|
|
566
|
+
for (const e of withIds)
|
|
567
|
+
for (const k of e.keys)
|
|
568
|
+
if (!(k in records)) records[k] = recordFrom(k, cache[k]);
|
|
569
|
+
return {
|
|
570
|
+
schema: SCHEMA,
|
|
571
|
+
source: rel(source),
|
|
572
|
+
source_kind: source.endsWith(".bib") ? "bibtex" : "markdown",
|
|
573
|
+
source_sha256: createHash("sha256").update(text).digest("hex"),
|
|
574
|
+
cache: cachePath ? rel(cachePath) : null,
|
|
575
|
+
generated: new Date().toISOString().slice(0, 10),
|
|
576
|
+
entries: withIds,
|
|
577
|
+
records,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ── cli ──────────────────────────────────────────────────────────────────────
|
|
582
|
+
|
|
583
|
+
async function main(argv) {
|
|
584
|
+
const args = argv.slice(2);
|
|
585
|
+
const target = args.find((a) => !a.startsWith("--")) ?? ".";
|
|
586
|
+
const offline = args.includes("--offline");
|
|
587
|
+
const refresh = args.includes("--refresh");
|
|
588
|
+
const quiet = args.includes("--quiet");
|
|
589
|
+
const opt = (name) =>
|
|
590
|
+
(args.find((a) => a.startsWith(`--${name}=`)) ?? "")
|
|
591
|
+
.split("=")
|
|
592
|
+
.slice(1)
|
|
593
|
+
.join("=");
|
|
594
|
+
|
|
595
|
+
const src = resolveSource(target);
|
|
596
|
+
if (!src) {
|
|
597
|
+
console.error(
|
|
598
|
+
`🛑 no ${SOURCE_ORDER.join(", ")} under ${resolve(target)} — nowhere to take a bibliography from.`,
|
|
599
|
+
);
|
|
600
|
+
return 1;
|
|
601
|
+
}
|
|
602
|
+
const paperDir = statSync(resolve(target)).isFile()
|
|
603
|
+
? dirname(src)
|
|
604
|
+
: resolve(target);
|
|
605
|
+
const cachePath = opt("cache")
|
|
606
|
+
? resolve(opt("cache"))
|
|
607
|
+
: join(paperDir, "repro", "refs-cache.json");
|
|
608
|
+
const out = opt("out")
|
|
609
|
+
? resolve(opt("out"))
|
|
610
|
+
: join(paperDir, "_build", "refs.facts.json");
|
|
611
|
+
|
|
612
|
+
const text = readFileSync(src, "utf8");
|
|
613
|
+
const entries = await loadEntries(src);
|
|
614
|
+
if (!entries.length) {
|
|
615
|
+
// 🔴 Zero entries is a suspect, not a success. Facts with an empty list would look like a
|
|
616
|
+
// clean bibliography, so we do not write them at all and exit with a non-zero code.
|
|
617
|
+
console.error(
|
|
618
|
+
`🛑 parsed 0 entries out of ${rel(src)}. Silence here would look like a clean bibliography.`,
|
|
619
|
+
);
|
|
620
|
+
return 1;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const cache = loadCache(cachePath);
|
|
624
|
+
let fetched = 0;
|
|
625
|
+
if (!offline) {
|
|
626
|
+
for (const k of keysFor(entries)) {
|
|
627
|
+
if (cache[k] && !refresh) continue;
|
|
628
|
+
try {
|
|
629
|
+
cache[k] = await fetchKey(k);
|
|
630
|
+
fetched++;
|
|
631
|
+
} catch (err) {
|
|
632
|
+
// A network failure is NOT a clean result: the key stays missing, and the rule will say
|
|
633
|
+
// the entry was not checked instead of declaring it correct.
|
|
634
|
+
if (!quiet) console.error(` … ${k}: ${err.message}`);
|
|
635
|
+
}
|
|
636
|
+
// A braced body: a concise arrow returns the Timeout out of the promise executor, where
|
|
637
|
+
// nobody reads it (`no-promise-executor-return`, 2026-08-28). The behaviour is the same.
|
|
638
|
+
await new Promise((r) => {
|
|
639
|
+
setTimeout(r, 250);
|
|
640
|
+
}); // both registries ask for a polite pace
|
|
641
|
+
}
|
|
642
|
+
if (fetched) saveCache(cachePath, cache);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const facts = buildFacts({ source: src, text, entries, cache, cachePath });
|
|
646
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
647
|
+
writeFileSync(out, `${JSON.stringify(facts, null, 2)}\n`);
|
|
648
|
+
const resolved = Object.values(facts.records).filter((r) => r.found).length;
|
|
649
|
+
console.log(
|
|
650
|
+
`📚 ${basename(src)} → ${rel(out)} (${entries.length} entries, ${Object.keys(facts.records).length} identifiers, ` +
|
|
651
|
+
`${resolved} resolved${offline ? ", offline" : fetched ? `, +${fetched} fetched` : ", all from cache"})`,
|
|
652
|
+
);
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (isMain(import.meta.url)) main(process.argv).then((c) => process.exit(c));
|