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,713 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* prose-lint.mjs — countable checks for "reads machine-written" expository prose.
|
|
4
|
+
* Usage: node prose-lint.mjs <file.md|file.txt> [--flags-only|--headings]
|
|
5
|
+
*
|
|
6
|
+
* Every check counts something. Thresholds and their provenance are in THRESHOLDS.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 WHAT LEFT THIS FILE ON 2026-08-26 AND WHERE IT WENT. Seven of the twelve gating metrics were
|
|
9
|
+
* moved into `@eslint/markdown` rules — `eslint-rules/paper-craft.mjs`:
|
|
10
|
+
* citation-density · unexplained-jargon · multi-claim-sentence · conceits ·
|
|
11
|
+
* hedge-density · discourse-subject · undefined-coinage.
|
|
12
|
+
* Parity was proven on a real paper BEFORE the deletion (the sets of findings matched, the sentence
|
|
13
|
+
* heads byte for byte); the measurement and the classification of all twelve are in
|
|
14
|
+
* `papers/research/2026-08-26-klassifikatsiya-prose-lint.md`.
|
|
15
|
+
* Do NOT add them back: a fact has one home, and `paper-craft.harness.mjs` fails if the number of
|
|
16
|
+
* gating metrics here grows.
|
|
17
|
+
*
|
|
18
|
+
* 🔴 THE SECOND BATCH, 2026-09-06: TWO MORE LEFT, AND THE RECEIVING RULES WERE EXTENDED FIRST.
|
|
19
|
+
* frameMarkersPer1000 → `paper/metadiscourse`, the `density` finding. The dictionary here was wider
|
|
20
|
+
* (17 patterns against 9), so the dictionaries were merged first and the threshold removed after.
|
|
21
|
+
* ⚠️ NOT all seventeen were merged: the patterns `\bfirst(ly)?\b(?=[ ,])` / `second` / `third`
|
|
22
|
+
* gave 23 matches out of 27 on `compile-rules-2026/paper.md`, and ZERO of them were REAL
|
|
23
|
+
* ("Only the second one runs", "The first two rows", "A third group ignores"). Those are
|
|
24
|
+
* ordinary adjectives. What went into the rule is Hyland's form — "First," at the start of a
|
|
25
|
+
* clause; over four papers it gives 9 matches, all real;
|
|
26
|
+
* epanorthosisPer10k → `paper/ai-tells`, together with all four forms and with Boggia's baseline.
|
|
27
|
+
* The receiving rule knew only one form, and that regex caught "is not X," WITHOUT the
|
|
28
|
+
* substituted replacement — five of its matches on a real paper were not the construction at
|
|
29
|
+
* all. Parity: the rule prints the same `17.2/10k` and the same 10 sentences.
|
|
30
|
+
*
|
|
31
|
+
* 🔴 RETRACTED 2026-09-06: "A SECOND ENTITY ON THE FILESYSTEM ⇒ UNAVAILABLE TO AN ESLINT RULE" IS
|
|
32
|
+
* WRONG. The paragraph below is kept as a record of what used to be written here, but IT MUST NOT BE
|
|
33
|
+
* ACTED ON. Measured: `eslint-rules/paper-registry.mjs` makes 8 disk accesses and reads neighbouring
|
|
34
|
+
* files of the paper's directory; `pdf-facts.mjs` opens a PDF and computes sha256; `paper-texcount.mjs`
|
|
35
|
+
* RUNS an external program through `execFileSync`. An ESLint rule executes in node, and `readFileSync`
|
|
36
|
+
* is available to it exactly as it is to this script. The real "one file at a time" model is about
|
|
37
|
+
* WHERE a rule reports, not about what it reads.
|
|
38
|
+
* ⇒ All three metrics were reclassified as MOVABLE and are queued as work:
|
|
39
|
+
* `node .claude/lib/refactor-state.mjs --index`, the "MOVABLE" section.
|
|
40
|
+
* `captionSentenceWords` / `captionWords` — after one edit of `files:` in `eslint.config.mjs`
|
|
41
|
+
* (today only the paper's own `paper.tex` is listed there, while the captions live in `figures/`).
|
|
42
|
+
*
|
|
43
|
+
* ✅ `abstractVsVenueMedian` WAS MOVED 2026-09-07 → `eslint-rules/paper-craft.mjs`, the rule
|
|
44
|
+
* `paper/abstract-length` (`warn`). It was the one "without a single condition": the median is read
|
|
45
|
+
* from the neighbouring `CLAUDE.md` exactly as `pdf/profile` reads a venue profile, and the address
|
|
46
|
+
* of the finding is the `## Abstract` line in the paper itself. Parity was taken BEFORE the deletion
|
|
47
|
+
* on `compile-rules-2026/paper.md`: 306 words against a ceiling of 305, one finding in both places,
|
|
48
|
+
* the same four numbers. The test and eight mutations are in
|
|
49
|
+
* `eslint-rules/paper-craft.{harness,mutations}.mjs`.
|
|
50
|
+
*
|
|
51
|
+
* WHAT IS LEFT AND WHY — TWO. The paragraph below was written when there were three, and is kept as
|
|
52
|
+
* a record: they read a SECOND ENTITY on the filesystem, while an ESLint rule sees one linted file:
|
|
53
|
+
* ← RETRACTED, see above
|
|
54
|
+
* captionSentenceWords, captionWords → the input is `figures/*.tex` NEXT TO the paper.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ `.tex` IS NO LONGER ACCEPTED, AND THAT IS FIXING A LIE, NOT NARROWING THE SCOPE. The header
|
|
57
|
+
* declared `<file.md|file.tex|->` from the very beginning, while there is not one line of LaTeX
|
|
58
|
+
* stripping in the file. Measured 2026-08-26 on `agenticdev-2026/paper.tex`: it prints `8127 words`
|
|
59
|
+
* against ~4636 real ones — 43% of that is the preamble, `%` comments and `filecontents`
|
|
60
|
+
* bibliography entries, and the first items in the list of long sentences are `.bib` lines. Every
|
|
61
|
+
* metric in this file is a fraction of the word count, so on `.tex` they were all understated by
|
|
62
|
+
* roughly half and printed `ok`. Nobody ever called it with `.tex` anyway (CI explicitly prints
|
|
63
|
+
* `NOT COVERED` for .tex papers), so the cost of refusing is zero, while the cost of a confident
|
|
64
|
+
* wrong number is not. A LaTeX parser is deliberately not built here: the occupancy of the tools was
|
|
65
|
+
* measured separately, see `papers/research/2026-08-26-proza-v-latex-zanyatost.md`.
|
|
66
|
+
*
|
|
67
|
+
* Advisory: it prints findings; `--flags-only` exits with code 1 if anything was found.
|
|
68
|
+
*/
|
|
69
|
+
import {
|
|
70
|
+
headings as mdHeadings,
|
|
71
|
+
stripFences,
|
|
72
|
+
requireMarkdown,
|
|
73
|
+
stripFrontmatter,
|
|
74
|
+
} from "../../lib/markdown.mjs";
|
|
75
|
+
|
|
76
|
+
// Markup is parsed with a PARSER (`CLAUDE.md`, 2026-08-11). We fail rather than degrade, even
|
|
77
|
+
// though this file is advisory: without a parser the paper's body would collapse to empty, and from
|
|
78
|
+
// empty this linter derives zero for EVERY metric — that is, a report saying "the prose is perfect".
|
|
79
|
+
// `paper-lint.mjs` runs us as a subprocess and prints our stderr, so the reason is visible, and its
|
|
80
|
+
// own gate does not suffer: it swallows our exit code deliberately.
|
|
81
|
+
requireMarkdown();
|
|
82
|
+
|
|
83
|
+
const THRESHOLDS = {
|
|
84
|
+
// frameMarkersPer1000 — moved 2026-09-06 into `eslint-rules/paper-prose.mjs`, into the rule
|
|
85
|
+
// `paper/metadiscourse` (the `density` finding), together with Hyland's provenance and the
|
|
86
|
+
// threshold of 10. The FRAME_MARKERS dictionary below STAYS: it is a term in
|
|
87
|
+
// metadiscourseTotalPer1000.
|
|
88
|
+
metadiscourseTotalPer1000: {
|
|
89
|
+
warn: 90,
|
|
90
|
+
src: "Hyland 2005 Table 5.1 total 64.9/1000; Table 7.1 totals 60.0-73.6/1000. Warn ~1.4x.",
|
|
91
|
+
},
|
|
92
|
+
// pureFrameSentencePct / propositionsPerSentence — moved into `eslint-rules/paper-craft.mjs`
|
|
93
|
+
// as `discourse-subject` and `multi-claim-sentence` (2026-08-26).
|
|
94
|
+
firstMentionDefinites: {
|
|
95
|
+
warn: 3,
|
|
96
|
+
src: 'No published rate. Operationalises Pinker\'s curse of knowledge: "the X" on first mention presupposes shared knowledge.',
|
|
97
|
+
},
|
|
98
|
+
negatedMainClausePct: {
|
|
99
|
+
warn: 50,
|
|
100
|
+
src: 'No published rate for negation. Related: Boggia 2026 (arXiv 2607.21498) measures the "not X, but Y" family per 10,000 words; human academic abstracts 3.6, LLM 7.8 (p=0.28, n.s.).',
|
|
101
|
+
},
|
|
102
|
+
topicChainBreaksPct: {
|
|
103
|
+
warn: 75,
|
|
104
|
+
src: 'Gopen & Swan 1990 principles 3-4: "We cannot tell whose story the passage is" when the topic position changes every sentence.',
|
|
105
|
+
},
|
|
106
|
+
stressPositionWaste: {
|
|
107
|
+
warn: 0,
|
|
108
|
+
src: "Gopen & Swan 1990 principle 2: put the new information you want emphasised in the stress position. A trailing citation/cross-reference/hedge wastes it.",
|
|
109
|
+
},
|
|
110
|
+
// hedgeWordPct — moved into `eslint-rules/paper-craft.mjs` as `hedge-density` (2026-08-26).
|
|
111
|
+
// The HEDGES lexicon STAYS here: it is a term in metadiscourseTotalPer1000 and in the wasted
|
|
112
|
+
// stress-position detector. Deleting it would silently understate both metrics.
|
|
113
|
+
// epanorthosisPer10k — moved 2026-09-06 into `eslint-rules/paper-prose.mjs`, into the rule
|
|
114
|
+
// `paper/ai-tells`, together with the ONLY published baseline in the whole set
|
|
115
|
+
// (Boggia 2026, arXiv 2607.21498: humans 3.6, models 7.8 per 10 000 words; threshold 15.6) and
|
|
116
|
+
// together with the EPANORTHOSIS lexicon. The metric's history — "it was counted and PRINTED
|
|
117
|
+
// WITHOUT A THRESHOLD from the day the linter was written, sat at 29.8 through five editing
|
|
118
|
+
// passes, and the corpus owner found the tic by eye before the tool did, because measuring is not
|
|
119
|
+
// checking" — moved into the rule's header: it explains why a metric must have a threshold and not
|
|
120
|
+
// only a number.
|
|
121
|
+
|
|
122
|
+
contrastivePer10k: {
|
|
123
|
+
warn: 25,
|
|
124
|
+
src: 'ours. "rather than" is an ordinary connective; a pile of them is a register problem, but it is NOT the construction Boggia measured and must not borrow that baseline.',
|
|
125
|
+
},
|
|
126
|
+
// conceitsPer10k — moved into `eslint-rules/paper-craft.mjs` as `conceits` (2026-08-26).
|
|
127
|
+
|
|
128
|
+
// Abstract length, as a multiple of the TARGET VENUE's own median. The corpus owner, 2026-08-05:
|
|
129
|
+
// "abstract should be closer to median. no longer than 50% [more than] median". For REALM the measured
|
|
130
|
+
// median over all 34 archival papers is 203 words, so the ceiling is ~305. The previous guidance
|
|
131
|
+
// in this project pointed at the venue MAXIMUM (518) and was wrong in the way maxima always are:
|
|
132
|
+
// it licensed 654 words, longer than any abstract that workshop has ever published, and read as
|
|
133
|
+
// an introduction rather than an abstract. Measure the venue, take the median, allow half again.
|
|
134
|
+
|
|
135
|
+
// citationsPerParagraph / jargonPerSentence — moved into `eslint-rules/paper-craft.mjs` as
|
|
136
|
+
// `citation-density` and `unexplained-jargon` (2026-08-26). The provenance of both thresholds,
|
|
137
|
+
// including the history of "count GROUPS, not numbers", moved with them.
|
|
138
|
+
// 🔴 FIGURE CAPTIONS ARE PROSE THAT NOTHING WAS READING. They live in figures/*.tex, not in
|
|
139
|
+
// paper.md, so every writing pass, every persona read and every threshold in paper-lint's pre-edit
|
|
140
|
+
// gate was blind to them by construction. Found 2026-08-05 when a reader hit a 136-word caption
|
|
141
|
+
// containing a 54-word sentence — one word under the limit that would have blocked the same
|
|
142
|
+
// sentence had it been typed into the paper. An entire text surface, unchecked.
|
|
143
|
+
captionSentenceWords: {
|
|
144
|
+
warn: 40,
|
|
145
|
+
src: "ours, from this paper's own four captions: longest sentences 21, 31, 41 and 54 words. A caption is read in one pass with the figure, so it tolerates less than body prose, where the limit is 55.",
|
|
146
|
+
},
|
|
147
|
+
captionWords: {
|
|
148
|
+
warn: 100,
|
|
149
|
+
src: "ours, same four captions: 67, 97, 135, 148. ACL captions in the venue corpus run far shorter; past ~100 words a caption is a section that happens to sit under a picture.",
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// CONCEITS — moved into `eslint-rules/paper-craft.mjs` (the `conceits` rule, 2026-08-26)
|
|
154
|
+
// together with its whole tuning history.
|
|
155
|
+
|
|
156
|
+
// --- lexicons ---
|
|
157
|
+
// 🔴 FRAME_MARKERS STAYS EVEN THOUGH ITS THRESHOLD LEFT (2026-09-06) — for exactly the same reason
|
|
158
|
+
// as HEDGES above: it is a term in `metadiscourseTotalPer1000`, and deleting it would silently
|
|
159
|
+
// understate that metric. ⚠️ AND HERE IS A KNOWN DEFECT, measured the same day and NOT fixed: the
|
|
160
|
+
// three ordinal-numeral patterns give 23 matches out of 27 on a real paper, and none of them are
|
|
161
|
+
// real, while `comes? first` is written TWICE (the second time as `come(s)? (first|before)`), so
|
|
162
|
+
// every "comes first" is counted as two. That means `metadiscourse/1000w` in the report below is
|
|
163
|
+
// OVERSTATED. Deliberately untouched: narrowing the dictionary is an edit to the BEHAVIOUR of a live
|
|
164
|
+
// metric that has no test here, and making it under the guise of a move would change a number
|
|
165
|
+
// without showing that it changed.
|
|
166
|
+
const FRAME_MARKERS = [
|
|
167
|
+
/\bcomes? first\b/gi,
|
|
168
|
+
/\bfirst(ly)?\b(?=[ ,])/gi,
|
|
169
|
+
/\bsecond(ly)?\b(?=[ ,])/gi,
|
|
170
|
+
/\bthird(ly)?\b(?=[ ,])/gi,
|
|
171
|
+
/\bfinally\b/gi,
|
|
172
|
+
/\bto (conclude|summari[sz]e|begin( with)?|start)\b/gi,
|
|
173
|
+
/\bin (conclusion|summary|what follows|this (paper|section|article))\b/gi,
|
|
174
|
+
/\bwe (begin|start|turn|now turn|conclude|proceed)\b/gi,
|
|
175
|
+
/\b(my|our) (purpose|aim|goal) is\b/gi,
|
|
176
|
+
/\bthe (rest|remainder) of (this|the)\b/gi,
|
|
177
|
+
/\bbefore (we|turning|proceeding)\b/gi,
|
|
178
|
+
/\bhere we (show|present|report|argue)\b/gi,
|
|
179
|
+
/\bwe (report|present) (it|them|this) as\b/gi,
|
|
180
|
+
/\bcome(s)? (first|before)\b/gi,
|
|
181
|
+
/\bthis (paper|section) (is organi[sz]ed|proceeds)\b/gi,
|
|
182
|
+
/\bwhat follows\b/gi,
|
|
183
|
+
/\bthe (first|second|two|three) (finding|result|point|thing)s?\b/gi,
|
|
184
|
+
];
|
|
185
|
+
const TRANSITIONS = [
|
|
186
|
+
/\bhowever\b/gi,
|
|
187
|
+
/\bmoreover\b/gi,
|
|
188
|
+
/\bfurthermore\b/gi,
|
|
189
|
+
/\btherefore\b/gi,
|
|
190
|
+
/\bthus\b/gi,
|
|
191
|
+
/\bhence\b/gi,
|
|
192
|
+
/\bin addition\b/gi,
|
|
193
|
+
/\bnevertheless\b/gi,
|
|
194
|
+
/\bconsequently\b/gi,
|
|
195
|
+
/\bwhile\b/gi,
|
|
196
|
+
/\bwhereas\b/gi,
|
|
197
|
+
/\balthough\b/gi,
|
|
198
|
+
/\bthough\b/gi,
|
|
199
|
+
/\bbut\b/gi,
|
|
200
|
+
/\bso\b/gi,
|
|
201
|
+
/\byet\b/gi,
|
|
202
|
+
/\beither\b/gi,
|
|
203
|
+
/\bas well as\b/gi,
|
|
204
|
+
/\bin contrast\b/gi,
|
|
205
|
+
/\bon the other hand\b/gi,
|
|
206
|
+
];
|
|
207
|
+
const ENDOPHORIC = [
|
|
208
|
+
/\b(see|cf\.?) (Fig|Table|Section|Appendix|§)/gi,
|
|
209
|
+
/\((Appendix|Fig\.?|Table|Section|§)[^)]*\)/gi,
|
|
210
|
+
/\b(noted|discussed|shown|described) (above|below|earlier|previously)\b/gi,
|
|
211
|
+
/\bin (Section|Table|Figure|Appendix) \S+/gi,
|
|
212
|
+
];
|
|
213
|
+
const CODE_GLOSSES = [
|
|
214
|
+
/\bnamely\b/gi,
|
|
215
|
+
/\be\.g\.,?/gi,
|
|
216
|
+
/\bi\.e\.,?/gi,
|
|
217
|
+
/\bsuch as\b/gi,
|
|
218
|
+
/\bin other words\b/gi,
|
|
219
|
+
/\bthat is\b/gi,
|
|
220
|
+
/\bwhich means\b/gi,
|
|
221
|
+
];
|
|
222
|
+
const HEDGES = [
|
|
223
|
+
/\bmight\b/gi,
|
|
224
|
+
/\bmay\b/gi,
|
|
225
|
+
/\bcould\b/gi,
|
|
226
|
+
/\bperhaps\b/gi,
|
|
227
|
+
/\bpossibl[ey]\b/gi,
|
|
228
|
+
/\bprobabl[ey]\b/gi,
|
|
229
|
+
/\bsuggest(s|ive|ed)?\b/gi,
|
|
230
|
+
/\bappear(s|ed)?\b/gi,
|
|
231
|
+
/\bseem(s|ed)?\b/gi,
|
|
232
|
+
/\bindicate(s|d)?\b/gi,
|
|
233
|
+
/\brelatively\b/gi,
|
|
234
|
+
/\bsomewhat\b/gi,
|
|
235
|
+
/\bapparently\b/gi,
|
|
236
|
+
/\bnearly\b/gi,
|
|
237
|
+
/\btend(s|ed)? to\b/gi,
|
|
238
|
+
/\blargely\b/gi,
|
|
239
|
+
/\bgenerally\b/gi,
|
|
240
|
+
/\btypically\b/gi,
|
|
241
|
+
/\bin part\b/gi,
|
|
242
|
+
/\bto some extent\b/gi,
|
|
243
|
+
/\bassume(s|d)?\b/gi,
|
|
244
|
+
];
|
|
245
|
+
const BOOSTERS = [
|
|
246
|
+
/\bclearly\b/gi,
|
|
247
|
+
/\bobviously\b/gi,
|
|
248
|
+
/\bdefinitely\b/gi,
|
|
249
|
+
/\bin fact\b/gi,
|
|
250
|
+
/\bit is clear that\b/gi,
|
|
251
|
+
/\bof course\b/gi,
|
|
252
|
+
/\bmust\b/gi,
|
|
253
|
+
/\bcannot\b/gi,
|
|
254
|
+
/\bno\b(?= \w+ (can|could|will|would|is|are))/gi,
|
|
255
|
+
/\bnever\b/gi,
|
|
256
|
+
/\balways\b/gi,
|
|
257
|
+
/\bshow(s|ed)? that\b/gi,
|
|
258
|
+
/\bdemonstrate(s|d)?\b/gi,
|
|
259
|
+
];
|
|
260
|
+
const SELF_MENTION = [
|
|
261
|
+
/\bwe\b/gi,
|
|
262
|
+
/\bour\b/gi,
|
|
263
|
+
/\bus\b/gi,
|
|
264
|
+
/\bI\b/g,
|
|
265
|
+
/\bmy\b/gi,
|
|
266
|
+
];
|
|
267
|
+
const ENGAGEMENT = [
|
|
268
|
+
/\byou(r)?\b/gi,
|
|
269
|
+
/\bconsider\b/gi,
|
|
270
|
+
/\bnote that\b/gi,
|
|
271
|
+
/\bimagine\b/gi,
|
|
272
|
+
/\blet us\b/gi,
|
|
273
|
+
/\brecall\b/gi,
|
|
274
|
+
];
|
|
275
|
+
const NEGATORS = [
|
|
276
|
+
/\bnot\b/gi,
|
|
277
|
+
/\bno\b/gi,
|
|
278
|
+
/\bnone\b/gi,
|
|
279
|
+
/\bnever\b/gi,
|
|
280
|
+
/\bcannot\b/gi,
|
|
281
|
+
/\bn't\b/gi,
|
|
282
|
+
/\bneither\b/gi,
|
|
283
|
+
/\bnor\b/gi,
|
|
284
|
+
/\bwithout\b/gi,
|
|
285
|
+
/\bfail(s|ed)? to\b/gi,
|
|
286
|
+
/\bunable\b/gi,
|
|
287
|
+
];
|
|
288
|
+
// EPANORTHOSIS — moved 2026-09-06 into `eslint-rules/paper-prose.mjs` (the `paper/ai-tells` rule)
|
|
289
|
+
// together with all four forms and with their history, including the one that turned the list into a
|
|
290
|
+
// list: the appositive "Admission, not translation", which reaches as far as HEADINGS.
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* `rather than` — moved OUT of EPANORTHOSIS on 2026-08-05, and the reason is arithmetic, not taste.
|
|
294
|
+
*
|
|
295
|
+
* The threshold above compares our count against Boggia's measured baselines (human 3.6, LLM 7.8 per
|
|
296
|
+
* 10k). Those were measured on "not X, but Y". `rather than` is a different construction and a
|
|
297
|
+
* mostly innocent English connective — "we report the direction rather than restate corpus shares"
|
|
298
|
+
* is fine writing. Counting it in the same numerator while comparing against that denominator makes
|
|
299
|
+
* the headline number incomparable to the baseline it is printed next to, which is a measurement bug
|
|
300
|
+
* of exactly the kind this paper is about.
|
|
301
|
+
*
|
|
302
|
+
* It still gets reported, on its own line, because a pile of them is a real register problem — it
|
|
303
|
+
* just is not epanorthosis and must not borrow epanorthosis's baseline. No published baseline exists
|
|
304
|
+
* for it, so the threshold is ours and generous.
|
|
305
|
+
*/
|
|
306
|
+
const CONTRASTIVE = [
|
|
307
|
+
/\brather than\b/gi,
|
|
308
|
+
/\binstead of\b/gi,
|
|
309
|
+
/\bas opposed to\b/gi,
|
|
310
|
+
];
|
|
311
|
+
// nominalisations: -tion/-sion/-ment/-ance/-ence/-ity/-ness/-ing used as head noun (approximate)
|
|
312
|
+
const NOMINAL = /\b\w{4,}(tion|sion|ment|ance|ence|ity|ness)s?\b/gi;
|
|
313
|
+
|
|
314
|
+
// JARGON — moved into `eslint-rules/paper-craft.mjs` (the `unexplained-jargon` rule, 2026-08-26).
|
|
315
|
+
|
|
316
|
+
const count = (t, pats) =>
|
|
317
|
+
(Array.isArray(pats) ? pats : [pats]).reduce(
|
|
318
|
+
(n, p) => n + (t.match(p) || []).length,
|
|
319
|
+
0,
|
|
320
|
+
);
|
|
321
|
+
// 🔴 Next to this lived `listHits(t, pats)` — the same walk, but returning THE MATCHES THEMSELVES
|
|
322
|
+
// rather than their count. Nobody called it (`no-unused-vars`, 2026-08-28), and that is not a
|
|
323
|
+
// trifle: the paragraph below explains that a metric was ignored precisely because it could not NAME
|
|
324
|
+
// the offender ("A miscounted metric does not get argued with; it gets ignored"). `listHits` was the
|
|
325
|
+
// remedy, and it was not wired into the report. Deleted; the hole — "the report prints counters
|
|
326
|
+
// without the offending lines" — is recorded here.
|
|
327
|
+
|
|
328
|
+
// 🔴 Paragraph and heading boundaries END a sentence. The first version collapsed all whitespace
|
|
329
|
+
// first, so a heading ran into the paragraph under it and the last sentence of one paragraph ran
|
|
330
|
+
// into the first of the next. Measured 2026-08-05: that inflated "sentences carrying >2
|
|
331
|
+
// propositions" to 55, and the top offenders printed by the report were not sentences at all —
|
|
332
|
+
// they were a title glued to an abstract. The metric therefore looked like noise, which is exactly
|
|
333
|
+
// why nobody ever wired it into the flags that hooks and pre-commit consume. A miscounted metric
|
|
334
|
+
// does not get argued with; it gets ignored.
|
|
335
|
+
// 2026-08-11: heading lines are struck out BEFORE splitting, and the parser is what strikes them
|
|
336
|
+
// out. This used to be done by the filter `!/^#{1,6}\s/.test(l)` inside `flatMap`, and it erred in
|
|
337
|
+
// both directions silently: it missed a heading indented by 1-3 spaces (CommonMark counts it as a
|
|
338
|
+
// heading) and struck out a `# …` line inside a ``` block — that is, CODE quoted by the paper fell
|
|
339
|
+
// soundlessly out of the sentence count, and this file counts nothing but sentences.
|
|
340
|
+
// The order is preserved: a line is struck out, not a paragraph, so the paragraph boundaries are
|
|
341
|
+
// the same.
|
|
342
|
+
function stripHeadingLines(t) {
|
|
343
|
+
const drop = new Set(mdHeadings(t).map((h) => h.line));
|
|
344
|
+
return drop.size === 0
|
|
345
|
+
? t
|
|
346
|
+
: t
|
|
347
|
+
.split("\n")
|
|
348
|
+
.filter((_, i) => !drop.has(i))
|
|
349
|
+
.join("\n");
|
|
350
|
+
}
|
|
351
|
+
function splitSentences(t) {
|
|
352
|
+
return (
|
|
353
|
+
stripHeadingLines(t)
|
|
354
|
+
.split(/\n\s*\n/) // paragraphs never run together
|
|
355
|
+
// A list item is its own unit. Without this the last sentence of one bullet ran into the first
|
|
356
|
+
// of the next and the claim counter blamed the pair for the merge.
|
|
357
|
+
.flatMap((p) => p.split(/\n(?=\s*(?:[-*]\s|\d+\.\s))/))
|
|
358
|
+
.flatMap((p) =>
|
|
359
|
+
p
|
|
360
|
+
.split("\n")
|
|
361
|
+
.join(" ") // headings are already struck out, see above
|
|
362
|
+
.replace(/\s+/g, " ")
|
|
363
|
+
.split(/(?<=[.!?])\s+(?=[A-Z“"(*`])/),
|
|
364
|
+
)
|
|
365
|
+
.map((s) => s.trim())
|
|
366
|
+
.filter(Boolean)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const words = (t) => (t.match(/[A-Za-z0-9%.'’-]+/g) || []).length;
|
|
370
|
+
|
|
371
|
+
function analyse(text) {
|
|
372
|
+
const W = words(text),
|
|
373
|
+
per1k = (n) => +((n / W) * 1000).toFixed(1);
|
|
374
|
+
const sents = splitSentences(text);
|
|
375
|
+
const lens = sents.map(words);
|
|
376
|
+
const mean = lens.reduce((a, b) => a + b, 0) / (lens.length || 1);
|
|
377
|
+
const sd = Math.sqrt(
|
|
378
|
+
lens.reduce((a, b) => a + (b - mean) ** 2, 0) / (lens.length || 1),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const cats = {
|
|
382
|
+
frameMarkers: count(text, FRAME_MARKERS),
|
|
383
|
+
transitions: count(text, TRANSITIONS),
|
|
384
|
+
endophoric: count(text, ENDOPHORIC),
|
|
385
|
+
codeGlosses: count(text, CODE_GLOSSES),
|
|
386
|
+
hedges: count(text, HEDGES),
|
|
387
|
+
boosters: count(text, BOOSTERS),
|
|
388
|
+
selfMention: count(text, SELF_MENTION),
|
|
389
|
+
engagement: count(text, ENGAGEMENT),
|
|
390
|
+
};
|
|
391
|
+
const mdTotal = Object.values(cats).reduce((a, b) => a + b, 0);
|
|
392
|
+
|
|
393
|
+
// per-sentence structure.
|
|
394
|
+
// `props` (>2 propositions) and the "discourse subject" moved into
|
|
395
|
+
// `eslint-rules/paper-craft.mjs` — the rules `multi-claim-sentence` and `discourse-subject`
|
|
396
|
+
// (2026-08-26). What stayed here is the wasted stress position, negation and the topic position:
|
|
397
|
+
// they had no thresholds in the gate and still have none, they are printed by the report.
|
|
398
|
+
const perSent = sents.map((s) => {
|
|
399
|
+
const sw = words(s);
|
|
400
|
+
const frame = count(s, FRAME_MARKERS);
|
|
401
|
+
// stress position: last ~8 words
|
|
402
|
+
const tail = s.split(/\s+/).slice(-8).join(" ");
|
|
403
|
+
const wastedStress =
|
|
404
|
+
/\((Appendix|Fig|Table|Section|§)[^)]*\)\.?$/i.test(s.trim()) ||
|
|
405
|
+
/\[\d+([,–-]\s*\d+)*\]\.?$/.test(s.trim()) ||
|
|
406
|
+
// a RESULT number = %, or a digit that is not part of a section/appendix/figure pointer
|
|
407
|
+
(count(tail, HEDGES) > 0 &&
|
|
408
|
+
/%|\b\d+(\.\d+)?\b/.test(
|
|
409
|
+
s.replace(
|
|
410
|
+
/\b(Appendix|Fig\.?|Table|Section|§)\s*[A-Z]?\.?\d+(\.\d+)*/gi,
|
|
411
|
+
"",
|
|
412
|
+
),
|
|
413
|
+
) &&
|
|
414
|
+
!/%|\b\d+(\.\d+)?\b/.test(
|
|
415
|
+
tail.replace(
|
|
416
|
+
/\b(Appendix|Fig\.?|Table|Section|§)\s*[A-Z]?\.?\d+(\.\d+)*/gi,
|
|
417
|
+
"",
|
|
418
|
+
),
|
|
419
|
+
));
|
|
420
|
+
const negMain = count(s, NEGATORS) > 0;
|
|
421
|
+
// topic position = first 5 words
|
|
422
|
+
const topic = s.split(/\s+/).slice(0, 5).join(" ").toLowerCase();
|
|
423
|
+
return {
|
|
424
|
+
s,
|
|
425
|
+
sw,
|
|
426
|
+
frame,
|
|
427
|
+
wastedStress,
|
|
428
|
+
negMain,
|
|
429
|
+
topic,
|
|
430
|
+
hedges: count(s, HEDGES),
|
|
431
|
+
definites: s.match(/\bthe [a-z][a-z-]*(\s+[a-z][a-z-]*)?\b/g) || [],
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const negPct = Math.round(
|
|
436
|
+
(perSent.filter((p) => p.negMain).length / sents.length) * 100,
|
|
437
|
+
);
|
|
438
|
+
return {
|
|
439
|
+
words: W,
|
|
440
|
+
sentences: sents.length,
|
|
441
|
+
sentenceLens: lens,
|
|
442
|
+
meanLen: +mean.toFixed(1),
|
|
443
|
+
sdLen: +sd.toFixed(1),
|
|
444
|
+
cv: +(sd / mean).toFixed(2),
|
|
445
|
+
per1k: Object.fromEntries(
|
|
446
|
+
Object.entries(cats).map(([k, v]) => [k, per1k(v)]),
|
|
447
|
+
),
|
|
448
|
+
raw: cats,
|
|
449
|
+
metadiscoursePer1000: per1k(mdTotal),
|
|
450
|
+
nominalisationsPer1000: per1k((text.match(NOMINAL) || []).length),
|
|
451
|
+
contrastivePer10k: +((count(text, CONTRASTIVE) / W) * 10000).toFixed(1),
|
|
452
|
+
wastedStress: perSent.filter((p) => p.wastedStress).map((p) => p.s),
|
|
453
|
+
negatedSentencePct: negPct,
|
|
454
|
+
topics: perSent.map((p) => p.topic),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// coinages() + DEFINING_CUE + STOPWORDS — moved into `eslint-rules/paper-craft.mjs`
|
|
459
|
+
// (the `undefined-coinage` rule, 2026-08-26) together with the whole justification of the heuristic.
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* An inventory of headings — all that is left of `structure()`.
|
|
463
|
+
*
|
|
464
|
+
* 🔴 The two checks that lived here (citation density in a paragraph and a pile-up of jargon in a
|
|
465
|
+
* sentence) moved on 2026-08-26 into `eslint-rules/paper-craft.mjs` — `citation-density` and
|
|
466
|
+
* `unexplained-jargon`. The reason they were SEPARATE from `analyse()` has not gone anywhere and is
|
|
467
|
+
* now expressed by construction: an ESLint rule has the document tree, so the question "how many
|
|
468
|
+
* stops are in THIS paragraph" is asked directly rather than reconstructed from a collapsed stream
|
|
469
|
+
* of words.
|
|
470
|
+
*/
|
|
471
|
+
function structure(body) {
|
|
472
|
+
// Not a gate — an INVENTORY. "A heading names a concrete noun" is a judgement call and pretending
|
|
473
|
+
// otherwise would be the failure this file exists to stop. What is mechanical, and what was
|
|
474
|
+
// missing, is that nobody ever saw all the headings AT ONCE: the whole-document question. Two
|
|
475
|
+
// headings a reader called "VAGUE AF" and "why not mention the linter??" survived six passes
|
|
476
|
+
// because each pass looked at the section it was editing.
|
|
477
|
+
// 2026-08-11: the heading inventory comes from the parser. The previous `/^#{2,4} .+$/gm` also
|
|
478
|
+
// printed lines from ``` blocks in this list: an inventory read BY EYE as "all the paper's
|
|
479
|
+
// headings" silently mixed them with quotations of someone else's markup. The parser hands back
|
|
480
|
+
// the heading text already trimmed at the edges — the previous `.replace(/^#+\s*/, '')` left
|
|
481
|
+
// trailing spaces.
|
|
482
|
+
const headings = mdHeadings(body)
|
|
483
|
+
.filter((h) => h.depth >= 2 && h.depth <= 4)
|
|
484
|
+
.map((h) => h.text);
|
|
485
|
+
|
|
486
|
+
// 🔴 The document's SHAPE — outline, order, section weights — deliberately does NOT live here.
|
|
487
|
+
// It was written here first and the author drew the line correctly: "I thought prose was about
|
|
488
|
+
// prose, and structure is something else entirely" (said in Russian). Prose-lint judges sentences.
|
|
489
|
+
// Structure belongs to tighten-paper, which had no mechanical leg at all, so the inventory now
|
|
490
|
+
// lives there:
|
|
491
|
+
// node .claude/skills/tighten-paper/structure.mjs <paper.md> [--section=N]
|
|
492
|
+
return { headings };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Captions from figures/*.tex beside the paper — the text surface nothing was reading. */
|
|
496
|
+
function captions(paperPath, fs, path) {
|
|
497
|
+
const dir = path.join(path.dirname(paperPath), "figures");
|
|
498
|
+
let files; // no initialiser: the catch branch returns, only try assigns (2026-08-28)
|
|
499
|
+
try {
|
|
500
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".tex"));
|
|
501
|
+
} catch {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
const out = [];
|
|
505
|
+
for (const f of files) {
|
|
506
|
+
const s = fs.readFileSync(path.join(dir, f), "utf8");
|
|
507
|
+
for (const m of s.matchAll(/\\caption\{/g)) {
|
|
508
|
+
let i = m.index + m[0].length,
|
|
509
|
+
depth = 1,
|
|
510
|
+
j = i;
|
|
511
|
+
while (j < s.length && depth) {
|
|
512
|
+
if (s[j] === "{") depth++;
|
|
513
|
+
else if (s[j] === "}") depth--;
|
|
514
|
+
j++;
|
|
515
|
+
}
|
|
516
|
+
const text = s
|
|
517
|
+
.slice(i, j - 1)
|
|
518
|
+
.replace(/\\[a-zA-Z]+\s*/g, " ")
|
|
519
|
+
.replace(/[{}]/g, " ")
|
|
520
|
+
.replace(/---/g, " ")
|
|
521
|
+
.replace(/\s+/g, " ")
|
|
522
|
+
.trim();
|
|
523
|
+
if (!text) continue;
|
|
524
|
+
const sents = splitSentences(text);
|
|
525
|
+
out.push({
|
|
526
|
+
file: f,
|
|
527
|
+
words: words(text),
|
|
528
|
+
longest: Math.max(0, ...sents.map(words)),
|
|
529
|
+
longestSentence:
|
|
530
|
+
sents.slice().sort((a, b) => words(b) - words(a))[0] ?? "",
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function report(name, r) {
|
|
538
|
+
const flag = (ok) => (ok ? "ok " : "FLAG");
|
|
539
|
+
console.log(`\n=== ${name} — ${r.words} words, ${r.sentences} sentences ===`);
|
|
540
|
+
console.log(
|
|
541
|
+
`sentence lengths ${JSON.stringify(r.sentenceLens)} mean ${r.meanLen} sd ${r.sdLen} CV ${r.cv}`,
|
|
542
|
+
);
|
|
543
|
+
console.log(
|
|
544
|
+
`${flag(r.metadiscoursePer1000 <= THRESHOLDS.metadiscourseTotalPer1000.warn)} all metadiscourse/1000w ${r.metadiscoursePer1000} (Hyland RA baseline 64.9, warn >${THRESHOLDS.metadiscourseTotalPer1000.warn})`,
|
|
545
|
+
);
|
|
546
|
+
console.log(` nominalisations/1000w ${r.nominalisationsPer1000}`);
|
|
547
|
+
// "not X, but Y" and the frame-marker density were printed from here until 2026-09-06. Both moved
|
|
548
|
+
// into `eslint-rules/paper-prose.mjs`, where a finding has a `file:line:col`; printing them here a
|
|
549
|
+
// second time would set up a second source of truth:
|
|
550
|
+
// npx eslint papers/*/paper.md papers/*/paper.tex
|
|
551
|
+
console.log(
|
|
552
|
+
`${flag(r.contrastivePer10k <= THRESHOLDS.contrastivePer10k.warn)} "rather than"/instead-of/10,000w ${r.contrastivePer10k} (warn >${THRESHOLDS.contrastivePer10k.warn}; not epanorthosis, no published baseline)`,
|
|
553
|
+
);
|
|
554
|
+
// Conceits, coined terms, sentences about the text and multi-proposition sentences were printed
|
|
555
|
+
// from here until 2026-08-26. They moved into `eslint-rules/paper-craft.mjs`, where every finding
|
|
556
|
+
// has a `file:line:col` — whereas this report printed their heads and made you hunt for the
|
|
557
|
+
// sentence by eye. Printing them here a second time would set up a second source of truth:
|
|
558
|
+
// `npx eslint papers/*/paper.md`.
|
|
559
|
+
console.log(
|
|
560
|
+
`${flag(r.wastedStress.length === 0)} sentences ending on a cross-ref/citation/hedge (wasted stress position): ${r.wastedStress.length}`,
|
|
561
|
+
);
|
|
562
|
+
r.wastedStress.forEach((s) => console.log(` > …${s.slice(-70)}`));
|
|
563
|
+
console.log(
|
|
564
|
+
`${flag(r.negatedSentencePct <= THRESHOLDS.negatedMainClausePct.warn)} sentences whose main claim is a negation: ${r.negatedSentencePct}%`,
|
|
565
|
+
);
|
|
566
|
+
console.log(
|
|
567
|
+
` topic positions (whose story is this?): ${r.topics.map((t) => `"${t}"`).join(" | ")}`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const args = process.argv.slice(2);
|
|
572
|
+
if (args.length === 0) {
|
|
573
|
+
console.error("usage: node prose-lint.mjs <file.md|file.txt> [more files]");
|
|
574
|
+
process.exit(0);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// 🔴 REFUSE RATHER THAN PRINT A CONFIDENT WRONG NUMBER. The analysis is in the file header: there is
|
|
578
|
+
// not one line of LaTeX stripping here, so on `.tex` the preamble, the `%` comments and the
|
|
579
|
+
// `filecontents` bibliography entries all landed in the denominator of every metric (measured: 8127
|
|
580
|
+
// "words" against ~4636 real ones), and every metric printed `ok`. Silent degradation here reads as
|
|
581
|
+
// "the prose is clean" — exactly the class of failure for which this file calls `requireMarkdown()`
|
|
582
|
+
// above.
|
|
583
|
+
const tex = args.filter((a) => !a.startsWith("--") && a.endsWith(".tex"));
|
|
584
|
+
if (tex.length) {
|
|
585
|
+
console.error(`prose-lint reads markdown, not LaTeX: ${tex.join(", ")}`);
|
|
586
|
+
console.error(
|
|
587
|
+
"There is no .tex stripping here — the word count would include the preamble and the .bib,",
|
|
588
|
+
);
|
|
589
|
+
console.error(
|
|
590
|
+
"and every metric (they are all fractions of the word count) would be understated by roughly half.",
|
|
591
|
+
);
|
|
592
|
+
console.error(
|
|
593
|
+
"What to cover prose in .tex with — papers/research/2026-08-26-proza-v-latex-zanyatost.md",
|
|
594
|
+
);
|
|
595
|
+
process.exit(2);
|
|
596
|
+
}
|
|
597
|
+
const fs = await import("node:fs");
|
|
598
|
+
const path = await import("node:path");
|
|
599
|
+
|
|
600
|
+
// 🔴 The paper's own working notes are not the paper. Without this, the linter scored TIGHTEN
|
|
601
|
+
// comments, the YAML frontmatter and the keyword block as prose -- and its loudest complaints were
|
|
602
|
+
// about text no reviewer will ever see. Found 2026-08-04 on its first real run, where it reported
|
|
603
|
+
// the frontmatter's `title:` line as a six-proposition sentence. Strip what the converter strips.
|
|
604
|
+
// (It also caught something real that way: the frontmatter title was two revisions stale.)
|
|
605
|
+
// 🔴 The RENDERED page, not the markdown. Added 2026-08-05 after the author asked why the render
|
|
606
|
+
// step had not caught a stall he found by opening the PDF. It had not because nothing reads the
|
|
607
|
+
// rendered text: build-submission.sh checks FORM — page count, overfull boxes, unresolved
|
|
608
|
+
// references, unset characters, the anonymity grep — and never looks at a sentence, while every
|
|
609
|
+
// prose check reads paper.md and never sees a typeset page. Between those two layers sat every
|
|
610
|
+
// defect he found by eye, and he was the only reader of the artifact a reviewer actually gets.
|
|
611
|
+
function pdfTextOnly(t) {
|
|
612
|
+
const cut = t.search(/^\s*References\s*$/m);
|
|
613
|
+
if (cut > 0) t = t.slice(0, cut);
|
|
614
|
+
return t
|
|
615
|
+
.replace(/^\s*\d+\s*$/gm, "") // page numbers
|
|
616
|
+
.replace(/(\w)-\n(\w)/g, "$1$2") // hyphenation introduced by the two-column set
|
|
617
|
+
.replace(/\f/g, "\n\n");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 2026-08-11: code blocks are cut out by `stripFences()`, and the parser finds the body boundary.
|
|
621
|
+
// The previous `/^```[\s\S]*?^```/gm`, with an ODD number of fences, glued the end of one block to
|
|
622
|
+
// the beginning of the next and ate the prose between them — and eaten prose here means an
|
|
623
|
+
// understated density for every metric, that is, a clean verdict over a dirty paper. And
|
|
624
|
+
// `/^## (References|Limitations)/m` cut the body at hashes INSIDE such a block.
|
|
625
|
+
function bodyOnly(t) {
|
|
626
|
+
t = stripFrontmatter(t); // YAML frontmatter
|
|
627
|
+
t = t.replace(/<!--[\s\S]*?-->/g, ""); // working comments, incl. TIGHTEN letters
|
|
628
|
+
// `blank: true` — the block's lines become empty rather than disappear. Otherwise the paragraph
|
|
629
|
+
// before the block gets glued to the paragraph after it, and this file is precisely about
|
|
630
|
+
// "Paragraph and heading boundaries END a sentence" (see the comment at `splitSentences`). The
|
|
631
|
+
// measurement is in the `stripFences` docstring: on `aisec-2026/README.md` removing the lines gave
|
|
632
|
+
// 81 sentences instead of 82, merging the lead paragraph with the text after the block.
|
|
633
|
+
t = stripFences(t, { blank: true }); // code blocks
|
|
634
|
+
t = t.replace(/^\|.*\|$/gm, ""); // table rows: data, not prose
|
|
635
|
+
const freeH = mdHeadings(t).find(
|
|
636
|
+
(h) => h.depth === 2 && /^(References|Limitations)/u.test(h.text),
|
|
637
|
+
);
|
|
638
|
+
const cut = freeH ? freeH.offset : -1; // the `-1` convention and the `> 0` comparison — as before
|
|
639
|
+
return cut > 0 ? t.slice(0, cut) : t; // body only; free sections hold a different bar
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 🔴 `--flags-only` and a NON-ZERO EXIT, added 2026-08-05, are what let anything downstream act.
|
|
643
|
+
// Until today this file printed a wall of numbers and always exited 0, and it was wired to no hook
|
|
644
|
+
// at all — it ran only if a human or a skill remembered it existed. Its epanorthosis metric sat at
|
|
645
|
+
// 50.8 per 10,000 words (the LLM baseline is 7.8) through five consecutive writing passes and never
|
|
646
|
+
// once flagged, because it had no threshold. The corpus owner found the tic by eye in a single reading and
|
|
647
|
+
// asked why the tooling had not. It had not because it was measuring, and measuring is not checking
|
|
648
|
+
// — which is, word for word, this paper's own thesis running loose inside its own toolchain.
|
|
649
|
+
const flagsOnly = args.includes("--flags-only");
|
|
650
|
+
let flagged = 0;
|
|
651
|
+
for (const f of args.filter((a) => !a.startsWith("--"))) {
|
|
652
|
+
const raw = fs.readFileSync(f, "utf8");
|
|
653
|
+
const isPdfText = f.endsWith(".txt");
|
|
654
|
+
const prep = isPdfText ? pdfTextOnly : bodyOnly;
|
|
655
|
+
const r = analyse(prep(raw));
|
|
656
|
+
if (flagsOnly) {
|
|
657
|
+
const lines = [];
|
|
658
|
+
// Citation density in a paragraph and a pile-up of jargon in a sentence moved on 2026-08-26 into
|
|
659
|
+
// `eslint-rules/paper-craft.mjs` (`citation-density`, `unexplained-jargon`).
|
|
660
|
+
for (const c of captions(f, fs, path)) {
|
|
661
|
+
if (c.longest > THRESHOLDS.captionSentenceWords.warn)
|
|
662
|
+
lines.push(
|
|
663
|
+
` figures/${c.file}: caption sentence of ${c.longest} words (warn >${THRESHOLDS.captionSentenceWords.warn}) — "${c.longestSentence.slice(0, 110)}…"`,
|
|
664
|
+
);
|
|
665
|
+
if (c.words > THRESHOLDS.captionWords.warn)
|
|
666
|
+
lines.push(
|
|
667
|
+
` figures/${c.file}: caption is ${c.words} words (warn >${THRESHOLDS.captionWords.warn}) — a caption this long is a section under a picture`,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
// Sentences carrying more than two claims moved to the same place — the rule
|
|
671
|
+
// `multi-claim-sentence`, together with the "five worst" slice (now the `maxReported` option).
|
|
672
|
+
// ✅ Abstract length against the venue's median moved on 2026-09-07 into
|
|
673
|
+
// `eslint-rules/paper-craft.mjs` — the rule `paper/abstract-length`. Parity was taken BEFORE the
|
|
674
|
+
// deletion on a real paper: `compile-rules-2026/paper.md` — 306 words against a ceiling of 305
|
|
675
|
+
// (median 203 × 1.5), one finding here and one there, the same four numbers; the rule
|
|
676
|
+
// additionally carries the address `paper.md:126:1`, which this line never had.
|
|
677
|
+
// Hedges, sentences about the text and coined terms are there too:
|
|
678
|
+
// `hedge-density`, `discourse-subject`, `undefined-coinage`.
|
|
679
|
+
if (lines.length) {
|
|
680
|
+
flagged += lines.length;
|
|
681
|
+
console.error(`✍️ prose-lint — ${f.split("/").pop()}:`);
|
|
682
|
+
lines.forEach((l) => console.error(l));
|
|
683
|
+
console.error(
|
|
684
|
+
" run `node .claude/skills/grade-paper-writing/prose-lint.mjs <file>` for the sentences",
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
} else if (args.includes("--headings")) {
|
|
688
|
+
// The whole-document question, printed as one list. Not a gate: "does this heading name a
|
|
689
|
+
// concrete noun" is judgement and pretending otherwise would be the exact failure this file
|
|
690
|
+
// exists to stop. What was missing is cheaper and was the real cause — nobody had ever seen the
|
|
691
|
+
// headings AS A SET. Two that a reader called "VAGUE AF" and "why not mention the linter??"
|
|
692
|
+
// survived six passes because every pass looked only at the section it was editing.
|
|
693
|
+
console.log(`\n=== ${f.split("/").pop()} — every heading, in order ===`);
|
|
694
|
+
structure(prep(raw)).headings.forEach((h, i) =>
|
|
695
|
+
console.log(`${String(i + 1).padStart(3)}. ${h}`),
|
|
696
|
+
);
|
|
697
|
+
console.log(
|
|
698
|
+
"\nRead these as a stranger who will read nothing else. Each should name a thing:",
|
|
699
|
+
);
|
|
700
|
+
console.log(
|
|
701
|
+
'a linter, a configuration, a repository, a rule — not "answers", "kinds", "moves".',
|
|
702
|
+
);
|
|
703
|
+
} else {
|
|
704
|
+
report(f.split("/").pop(), r);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (!flagsOnly)
|
|
708
|
+
console.log(
|
|
709
|
+
"\n(thresholds and their sources are in THRESHOLDS at the top of this file)",
|
|
710
|
+
);
|
|
711
|
+
// Exit 1 on a flag so a caller can branch. Hooks stay advisory by swallowing it themselves; the
|
|
712
|
+
// point is that the information now EXISTS at the exit code, where it was previously unreachable.
|
|
713
|
+
process.exit(flagged ? 1 : 0);
|