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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both halves for `link-skills.ts` — the symlinks that make the shipped skills visible to Claude
|
|
3
|
+
* Code at `.claude/skills/<name>`.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 WHY THE PACKAGE IS BUILT ON DISK HERE, NOT FAKED. What is under test is a property of a tree:
|
|
6
|
+
* whether Node resolves the package from the project, which spelling of it the link uses, and
|
|
7
|
+
* whether `SKILL.md` is reachable THROUGH the link. A stubbed locator would test the stub. So each
|
|
8
|
+
* case lays out a real `node_modules` — the npm shape and the pnpm shape, where
|
|
9
|
+
* `node_modules/<package>` is itself a symlink into a version-stamped store
|
|
10
|
+
* directory — and asks the filesystem.
|
|
11
|
+
*
|
|
12
|
+
* The fixture package keeps its skills where the real one does, under `SHIPPED_SKILLS_DIR` from
|
|
13
|
+
* consumer.mjs — the same constant the linker and the install e2e read.
|
|
14
|
+
*
|
|
15
|
+
* Run: node src/link-skills.harness.mjs
|
|
16
|
+
* Killed by: src/link-skills.mutations.mjs
|
|
17
|
+
*/
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import {
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
mkdtempSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
readlinkSync,
|
|
25
|
+
realpathSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
symlinkSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { tmpdir } from "node:os";
|
|
31
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
|
|
34
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const { linkSkills, locatePackage, shippedSkills } = await import(
|
|
36
|
+
join(HERE, "link-skills.ts")
|
|
37
|
+
);
|
|
38
|
+
const {
|
|
39
|
+
SHIPPED_SKILLS_DIR: SHIPS,
|
|
40
|
+
PACKAGE_NAME: PKG,
|
|
41
|
+
LEGACY_PACKAGE_NAME,
|
|
42
|
+
} = await import(
|
|
43
|
+
join(HERE, "..", "skills", "paper-pipeline", "scripts", "consumer.mjs")
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
let n = 0;
|
|
47
|
+
const check = (label, cond) => {
|
|
48
|
+
n++;
|
|
49
|
+
assert.ok(cond, label);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const SKILLS = ["alpha", "beta", "gamma"];
|
|
53
|
+
const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-link-skills-")));
|
|
54
|
+
|
|
55
|
+
/** A package as a manager would unpack it: manifest, skills, one non-skill. */
|
|
56
|
+
function writePackage(dir) {
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(dir, "package.json"),
|
|
60
|
+
JSON.stringify({ name: PKG, version: "1.0.0" }),
|
|
61
|
+
);
|
|
62
|
+
for (const s of SKILLS) {
|
|
63
|
+
mkdirSync(join(dir, SHIPS, s, "scripts"), { recursive: true });
|
|
64
|
+
writeFileSync(join(dir, SHIPS, s, "SKILL.md"), `---\nname: ${s}\n---\n`);
|
|
65
|
+
writeFileSync(join(dir, SHIPS, s, "scripts", "run.mjs"), "");
|
|
66
|
+
}
|
|
67
|
+
// A directory without SKILL.md and a loose file: neither is a skill.
|
|
68
|
+
mkdirSync(join(dir, SHIPS, "shared"), { recursive: true });
|
|
69
|
+
writeFileSync(join(dir, SHIPS, "README.md"), "# skills\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A consumer project with the package installed the npm way or the pnpm way. */
|
|
73
|
+
function consumer(name, manager = "npm") {
|
|
74
|
+
const dir = join(work, name);
|
|
75
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(dir, "package.json"),
|
|
78
|
+
JSON.stringify({ name: "c", version: "1.0.0" }),
|
|
79
|
+
);
|
|
80
|
+
if (manager === "npm") writePackage(join(dir, "node_modules", PKG));
|
|
81
|
+
else if (manager === "pnpm") {
|
|
82
|
+
const store = join(
|
|
83
|
+
"node_modules",
|
|
84
|
+
".pnpm",
|
|
85
|
+
`${PKG}@1.0.0`,
|
|
86
|
+
"node_modules",
|
|
87
|
+
PKG,
|
|
88
|
+
);
|
|
89
|
+
writePackage(join(dir, store));
|
|
90
|
+
symlinkSync(
|
|
91
|
+
join(".pnpm", `${PKG}@1.0.0`, "node_modules", PKG),
|
|
92
|
+
join(dir, "node_modules", PKG),
|
|
93
|
+
"dir",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return dir;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const home = (dir) => join(dir, ".claude", "skills");
|
|
100
|
+
const status = (report, name) => report.links.find((l) => l.name === name);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// ── I. A FRESH npm CONSUMER: EVERY DECLARED SKILL IS LINKED, AND ONLY SKILLS ───────────
|
|
104
|
+
{
|
|
105
|
+
const dir = consumer("npm");
|
|
106
|
+
const r = linkSkills(dir);
|
|
107
|
+
check(
|
|
108
|
+
"🔴 it links every skill the package ships",
|
|
109
|
+
r.ok === true && SKILLS.every((s) => status(r, s)?.status === "created"),
|
|
110
|
+
);
|
|
111
|
+
check(
|
|
112
|
+
"and nothing that is not a skill — a directory without SKILL.md is not one",
|
|
113
|
+
r.links.length === SKILLS.length &&
|
|
114
|
+
!existsSync(join(home(dir), "shared")),
|
|
115
|
+
);
|
|
116
|
+
check(
|
|
117
|
+
"🔴 SKILL.md is reachable THROUGH the link, where Claude Code looks",
|
|
118
|
+
SKILLS.every((s) => existsSync(join(home(dir), s, "SKILL.md"))),
|
|
119
|
+
);
|
|
120
|
+
check(
|
|
121
|
+
"and a project-root-relative script path resolves from the project root",
|
|
122
|
+
existsSync(join(dir, ".claude", "skills", "beta", "scripts", "run.mjs")),
|
|
123
|
+
);
|
|
124
|
+
const target = readlinkSync(join(home(dir), "alpha"));
|
|
125
|
+
check(
|
|
126
|
+
"🔴 the link is RELATIVE — an absolute one breaks the moment the checkout moves",
|
|
127
|
+
!isAbsolute(target) &&
|
|
128
|
+
target === join("..", "..", "node_modules", PKG, SHIPS, "alpha"),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── II. A SECOND RUN CHANGES NOTHING ──────────────────────────────────────────────────
|
|
133
|
+
{
|
|
134
|
+
const dir = join(work, "npm");
|
|
135
|
+
const before = SKILLS.map((s) => readlinkSync(join(home(dir), s)));
|
|
136
|
+
const r = linkSkills(dir);
|
|
137
|
+
check(
|
|
138
|
+
"🔴 a second run reports every link as ALREADY present, not as a clash",
|
|
139
|
+
r.ok && SKILLS.every((s) => status(r, s)?.status === "present"),
|
|
140
|
+
);
|
|
141
|
+
check(
|
|
142
|
+
"and leaves the links byte for byte as they were",
|
|
143
|
+
SKILLS.every((s, i) => readlinkSync(join(home(dir), s)) === before[i]),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── III. A NAME THAT IS ALREADY TAKEN IS SOMEONE ELSE'S ───────────────────────────────
|
|
148
|
+
{
|
|
149
|
+
const dir = consumer("foreign");
|
|
150
|
+
mkdirSync(join(home(dir), "alpha"), { recursive: true });
|
|
151
|
+
writeFileSync(join(home(dir), "alpha", "SKILL.md"), "mine\n");
|
|
152
|
+
mkdirSync(join(dir, "elsewhere"), { recursive: true });
|
|
153
|
+
symlinkSync(join("..", "..", "elsewhere"), join(home(dir), "beta"), "dir");
|
|
154
|
+
symlinkSync(join("..", "..", "gone"), join(home(dir), "gamma"), "dir");
|
|
155
|
+
const r = linkSkills(dir);
|
|
156
|
+
check(
|
|
157
|
+
"🔴 a foreign DIRECTORY is left untouched, content and all",
|
|
158
|
+
readFileSync(join(home(dir), "alpha", "SKILL.md"), "utf8") === "mine\n",
|
|
159
|
+
);
|
|
160
|
+
check(
|
|
161
|
+
"a link that points ELSEWHERE is left pointing there",
|
|
162
|
+
readlinkSync(join(home(dir), "beta")) === join("..", "..", "elsewhere"),
|
|
163
|
+
);
|
|
164
|
+
check(
|
|
165
|
+
"a DANGLING link is not silently replaced either — it may be the consumer's",
|
|
166
|
+
readlinkSync(join(home(dir), "gamma")) === join("..", "..", "gone"),
|
|
167
|
+
);
|
|
168
|
+
check(
|
|
169
|
+
"and each one is reported by name, with WHAT is there",
|
|
170
|
+
status(r, "alpha")?.status === "foreign" &&
|
|
171
|
+
status(r, "alpha")?.reason === "a directory" &&
|
|
172
|
+
/^a link to /.test(status(r, "beta")?.reason ?? "") &&
|
|
173
|
+
/^a dangling link to /.test(status(r, "gamma")?.reason ?? ""),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── IV. pnpm: THE LINK GOES THROUGH THE PROJECT'S OWN SPELLING ─────────────────────────
|
|
178
|
+
{
|
|
179
|
+
const dir = consumer("pnpm", "pnpm");
|
|
180
|
+
const located = locatePackage(dir);
|
|
181
|
+
check(
|
|
182
|
+
"Node resolves the package to the version-stamped store directory",
|
|
183
|
+
!("error" in located) &&
|
|
184
|
+
located.dir.includes(join(".pnpm", `${PKG}@1.0.0`)),
|
|
185
|
+
);
|
|
186
|
+
const r = linkSkills(dir);
|
|
187
|
+
check(
|
|
188
|
+
"🔴 under pnpm the link goes through node_modules/<package>, not the .pnpm store — that one dangles on the next upgrade",
|
|
189
|
+
r.ok &&
|
|
190
|
+
readlinkSync(join(home(dir), "alpha")) ===
|
|
191
|
+
join("..", "..", "node_modules", PKG, SHIPS, "alpha"),
|
|
192
|
+
);
|
|
193
|
+
check(
|
|
194
|
+
"and SKILL.md is reachable through both hops",
|
|
195
|
+
SKILLS.every((s) => existsSync(join(home(dir), s, "SKILL.md"))),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── V. NOT INSTALLED IN THIS PROJECT — NOTHING IS LINKED, AND IT SAYS WHY ──────────────
|
|
200
|
+
{
|
|
201
|
+
const dir = consumer("bare", "none");
|
|
202
|
+
const r = linkSkills(dir);
|
|
203
|
+
check(
|
|
204
|
+
"a project that does not have the package gets an error, not links into somebody's cache",
|
|
205
|
+
r.ok === false && /does not resolve from/.test(r.error),
|
|
206
|
+
);
|
|
207
|
+
check("and nothing is created", !existsSync(join(dir, ".claude")));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── VI. doctor's READ WRITES NOTHING ───────────────────────────────────────────────────
|
|
211
|
+
{
|
|
212
|
+
const dir = consumer("readonly");
|
|
213
|
+
const r = linkSkills(dir, { write: false });
|
|
214
|
+
check(
|
|
215
|
+
"🔴 write:false reports every skill as missing and creates nothing — doctor only looks",
|
|
216
|
+
r.ok &&
|
|
217
|
+
SKILLS.every((s) => status(r, s)?.status === "missing") &&
|
|
218
|
+
!existsSync(home(dir)),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── VI-bis. LINKS LEFT BY AN INSTALL UNDER THE OLD NAME ARE REPLACED, NOT SKIPPED ─────
|
|
223
|
+
// After `npm rm research-paper-pipeline && npm i -D paperlint` every old link dangles. They are
|
|
224
|
+
// ours, spelled the way an older `init` wrote them, so the next `init` replaces them.
|
|
225
|
+
{
|
|
226
|
+
const dir = consumer("renamed");
|
|
227
|
+
mkdirSync(home(dir), { recursive: true });
|
|
228
|
+
const old = join(
|
|
229
|
+
"..",
|
|
230
|
+
"..",
|
|
231
|
+
"node_modules",
|
|
232
|
+
LEGACY_PACKAGE_NAME,
|
|
233
|
+
SHIPS,
|
|
234
|
+
"alpha",
|
|
235
|
+
);
|
|
236
|
+
symlinkSync(old, join(home(dir), "alpha"), "dir");
|
|
237
|
+
const seen = linkSkills(dir, { write: false });
|
|
238
|
+
check(
|
|
239
|
+
"doctor's read names a link into the old package name, with the command that fixes it",
|
|
240
|
+
status(seen, "alpha")?.status === "foreign" &&
|
|
241
|
+
/old name — `npx paperlint init` replaces it/.test(
|
|
242
|
+
status(seen, "alpha")?.reason ?? "",
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
const r = linkSkills(dir);
|
|
246
|
+
check(
|
|
247
|
+
"🔴 init REPLACES it: status `replaced`, and SKILL.md is reachable through the new link",
|
|
248
|
+
status(r, "alpha")?.status === "replaced" &&
|
|
249
|
+
existsSync(join(home(dir), "alpha", "SKILL.md")) &&
|
|
250
|
+
readlinkSync(join(home(dir), "alpha")).includes(
|
|
251
|
+
join("node_modules", PKG),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── VII. A PACKAGE WITHOUT ITS SKILLS DIRECTORY IS AN ERROR, NOT ZERO SKILLS ─────────
|
|
257
|
+
{
|
|
258
|
+
const pkg = join(work, "no-skills");
|
|
259
|
+
writePackage(pkg);
|
|
260
|
+
rmSync(join(pkg, SHIPS), { recursive: true, force: true });
|
|
261
|
+
const s = shippedSkills(pkg);
|
|
262
|
+
check(
|
|
263
|
+
"no skills directory — an error that names it, not an empty list",
|
|
264
|
+
"error" in s && s.error.includes(join(pkg, SHIPS)),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
rmSync(work, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(
|
|
272
|
+
`✓ ${n} assertions passed — link-skills: the skills are where Claude Code looks`,
|
|
273
|
+
);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battery for `link-skills.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Every case below reintroduces a way the skills stop being where Claude Code looks — or, worse,
|
|
5
|
+
* a way the linker destroys something the consumer made. Both fail silently in real life: a
|
|
6
|
+
* missing skill is simply absent from the listing, and a replaced directory is simply gone.
|
|
7
|
+
*/
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { runMutations } from "../lib/mutation-driver.mjs";
|
|
11
|
+
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const ROOT = resolve(HERE, "..");
|
|
14
|
+
const SRC = join(HERE, "link-skills.ts");
|
|
15
|
+
const HARNESS = join(HERE, "link-skills.harness.mjs");
|
|
16
|
+
const CONSUMER = join(
|
|
17
|
+
ROOT,
|
|
18
|
+
"skills",
|
|
19
|
+
"paper-pipeline",
|
|
20
|
+
"scripts",
|
|
21
|
+
"consumer.mjs",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
process.exit(
|
|
25
|
+
runMutations({
|
|
26
|
+
root: ROOT,
|
|
27
|
+
runner: "node",
|
|
28
|
+
cases: [
|
|
29
|
+
{
|
|
30
|
+
// The definition lives in the port since rpp#62 (`installedSkills` in consumer.mjs), so
|
|
31
|
+
// the defect is planted THERE and must still die in THIS harness — the evidence that the
|
|
32
|
+
// linker reads it through the shared function rather than through a copy of its own.
|
|
33
|
+
name: "any directory counts as a skill",
|
|
34
|
+
harness: HARNESS,
|
|
35
|
+
expect: "a directory without SKILL.md is not one",
|
|
36
|
+
disables:
|
|
37
|
+
"the definition of a skill. A helper directory beside the skills gets a link, and Claude " +
|
|
38
|
+
"Code lists an entry with no SKILL.md behind it",
|
|
39
|
+
edits: [
|
|
40
|
+
[
|
|
41
|
+
CONSUMER,
|
|
42
|
+
' if (st.isDirectory() && existsSync(join(entry, "SKILL.md")))\n names.push(name);',
|
|
43
|
+
" if (st.isDirectory())\n names.push(name);",
|
|
44
|
+
],
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "the link is written absolute",
|
|
49
|
+
harness: HARNESS,
|
|
50
|
+
expect: "the link is RELATIVE",
|
|
51
|
+
disables:
|
|
52
|
+
"portability of the checkout. An absolute link works on the machine that ran init and " +
|
|
53
|
+
"dangles in every other clone, container and CI runner",
|
|
54
|
+
edits: [
|
|
55
|
+
[
|
|
56
|
+
SRC,
|
|
57
|
+
' symlinkSync(target, entry, "dir");',
|
|
58
|
+
' symlinkSync(resolve(dirname(entry), target), entry, "dir");',
|
|
59
|
+
],
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "an existing correct link is not recognised",
|
|
64
|
+
harness: HARNESS,
|
|
65
|
+
expect: "a second run reports every link as ALREADY present",
|
|
66
|
+
disables:
|
|
67
|
+
"idempotency. Every re-run of init would report its own links as someone else's " +
|
|
68
|
+
"and tell the consumer to move them",
|
|
69
|
+
edits: [
|
|
70
|
+
[
|
|
71
|
+
SRC,
|
|
72
|
+
' if (realpathSync(entry) === want) return { status: "present" };',
|
|
73
|
+
' if (realpathSync(entry) === "") return { status: "present" };',
|
|
74
|
+
],
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "a foreign entry is replaced instead of reported",
|
|
79
|
+
harness: HARNESS,
|
|
80
|
+
expect: "a foreign DIRECTORY is left untouched",
|
|
81
|
+
disables:
|
|
82
|
+
"the one promise that makes init safe to run on a real project: a consumer's own skill " +
|
|
83
|
+
"of the same name, or a link they aimed elsewhere, is deleted without a word",
|
|
84
|
+
edits: [
|
|
85
|
+
[
|
|
86
|
+
SRC,
|
|
87
|
+
' unlinkSync,\n} from "node:fs";',
|
|
88
|
+
' unlinkSync,\n rmSync,\n} from "node:fs";',
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
SRC,
|
|
92
|
+
" if (!write || !fixable) {",
|
|
93
|
+
' if (!write || seen.status === "present") {',
|
|
94
|
+
],
|
|
95
|
+
[
|
|
96
|
+
SRC,
|
|
97
|
+
' symlinkSync(target, entry, "dir");',
|
|
98
|
+
' rmSync(entry, { recursive: true, force: true });\n symlinkSync(target, entry, "dir");',
|
|
99
|
+
],
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "the resolved store path is linked instead of the project's own spelling",
|
|
104
|
+
harness: HARNESS,
|
|
105
|
+
expect: "under pnpm the link goes through node_modules/<package>",
|
|
106
|
+
disables:
|
|
107
|
+
"the link surviving an upgrade under pnpm. The store directory carries the version in " +
|
|
108
|
+
"its name; the next install removes it and every skill link dangles until init is re-run",
|
|
109
|
+
edits: [
|
|
110
|
+
[
|
|
111
|
+
SRC,
|
|
112
|
+
" if (realpathSync(candidate) === dir) return { dir, spelled: candidate };",
|
|
113
|
+
' if (realpathSync(candidate) === "") return { dir, spelled: candidate };',
|
|
114
|
+
],
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "doctor's read-only call writes links",
|
|
119
|
+
harness: HARNESS,
|
|
120
|
+
expect:
|
|
121
|
+
"write:false reports every skill as missing and creates nothing",
|
|
122
|
+
disables:
|
|
123
|
+
"the difference between looking and changing. `paperlint doctor` would quietly repair the " +
|
|
124
|
+
"state it is supposed to report, so the report can never show the gap",
|
|
125
|
+
edits: [
|
|
126
|
+
[SRC, " if (!write || !fixable) {", " if (!fixable) {"],
|
|
127
|
+
[
|
|
128
|
+
SRC,
|
|
129
|
+
" if (write && shipped.names.length > 0) {",
|
|
130
|
+
" if (shipped.names.length > 0) {",
|
|
131
|
+
],
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes the shipped skills visible to Claude Code: one RELATIVE symlink per skill,
|
|
3
|
+
* `<project>/.claude/skills/<name> -> <the installed package>/skills/<name>`.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 WHY THIS EXISTS. The README used to say the skills "sit in `node_modules/…/skills/` and Claude
|
|
6
|
+
* Code reads them from there". It does not: Claude Code discovers project skills in
|
|
7
|
+
* `.claude/skills/<name>/SKILL.md` (plus user and plugin skills), never inside `node_modules`. So a
|
|
8
|
+
* consumer who followed the install to the letter had no `/paper-pipeline` at all (Codex review on
|
|
9
|
+
* #45). A plugin could not carry them either — it gets no `node_modules`, and 23 of 24 skills run
|
|
10
|
+
* scripts (`docs/install.md`, "Why it is shaped this way"). The one consumer where the skills
|
|
11
|
+
* DID work had made these links by hand; this module makes the same links, so the layout is the
|
|
12
|
+
* proven one rather than a new one.
|
|
13
|
+
*
|
|
14
|
+
* The links are what make the project-root-relative script paths inside the skills
|
|
15
|
+
* (`.claude/skills/paper-pipeline/scripts/x.mjs`) resolve in a consumer at all.
|
|
16
|
+
*
|
|
17
|
+
* ── THE THREE RULES ─────────────────────────────────────────────────────────
|
|
18
|
+
* what to link every subdirectory holding a SKILL.md under the package's
|
|
19
|
+
* `SHIPPED_SKILLS_DIR` (consumer.mjs). No list, no count.
|
|
20
|
+
* where it points the package as it RESOLVES BY NAME from the project, spelled through the
|
|
21
|
+
* project's own `node_modules/paperlint` when that path leads to
|
|
22
|
+
* the same place. Under pnpm the resolved path is the version-stamped
|
|
23
|
+
* `.pnpm/…` store directory; a link spelled that way dangles after the next
|
|
24
|
+
* upgrade, a link through `node_modules/paperlint` does not.
|
|
25
|
+
* what it touches only what is missing. An entry that already leads to the shipped skill is
|
|
26
|
+
* left alone; ANY other entry of the same name — a directory, a file, a link
|
|
27
|
+
* elsewhere, a dangling link — is someone else's, and is reported, not replaced.
|
|
28
|
+
*/
|
|
29
|
+
/* eslint-disable boundaries/dependencies -- legacy I/O, moves behind a port in #76 */
|
|
30
|
+
import {
|
|
31
|
+
existsSync,
|
|
32
|
+
lstatSync,
|
|
33
|
+
mkdirSync,
|
|
34
|
+
readlinkSync,
|
|
35
|
+
realpathSync,
|
|
36
|
+
symlinkSync,
|
|
37
|
+
unlinkSync,
|
|
38
|
+
} from "node:fs";
|
|
39
|
+
/* eslint-enable boundaries/dependencies */
|
|
40
|
+
import { createRequire } from "node:module";
|
|
41
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
42
|
+
import { pathToFileURL } from "node:url";
|
|
43
|
+
import {
|
|
44
|
+
installedSkills,
|
|
45
|
+
LEGACY_PACKAGE_NAME,
|
|
46
|
+
PACKAGE_NAME,
|
|
47
|
+
SHIPPED_SKILLS_DIR,
|
|
48
|
+
} from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
49
|
+
|
|
50
|
+
export { PACKAGE_NAME };
|
|
51
|
+
/** Where Claude Code looks for project skills, relative to the project root. */
|
|
52
|
+
export const SKILLS_HOME = join(".claude", "skills");
|
|
53
|
+
|
|
54
|
+
export type Located =
|
|
55
|
+
| { readonly dir: string; readonly spelled: string }
|
|
56
|
+
| { readonly error: string };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The installed package, asked of Node from the PROJECT, not from wherever this code runs. A copy
|
|
60
|
+
* run out of the npx cache is not the project's install, and a link into a cache is a link that
|
|
61
|
+
* disappears with it — so "does not resolve from here" is an answer, not something to paper over.
|
|
62
|
+
*
|
|
63
|
+
* @returns `dir` — the real directory; `spelled` — the same directory as the project names it.
|
|
64
|
+
*/
|
|
65
|
+
export function locatePackage(project: string): Located {
|
|
66
|
+
let manifest: string;
|
|
67
|
+
try {
|
|
68
|
+
const req = createRequire(
|
|
69
|
+
pathToFileURL(join(project, "__rpp_locate__.js")).href,
|
|
70
|
+
);
|
|
71
|
+
manifest = req.resolve(`${PACKAGE_NAME}/package.json`);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return {
|
|
74
|
+
error: `${(e as NodeJS.ErrnoException).code ?? "error"}: ${(e as Error).message.split("\n")[0]}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const dir = realpathSync(dirname(manifest));
|
|
78
|
+
// Node's own walk, without its realpath step: the first `node_modules/<name>` up the tree that
|
|
79
|
+
// leads to the package that resolved.
|
|
80
|
+
for (let d = realpathSync(project); ; d = dirname(d)) {
|
|
81
|
+
const candidate = join(d, "node_modules", PACKAGE_NAME);
|
|
82
|
+
try {
|
|
83
|
+
if (realpathSync(candidate) === dir) return { dir, spelled: candidate };
|
|
84
|
+
} catch {
|
|
85
|
+
// not here — keep walking
|
|
86
|
+
}
|
|
87
|
+
if (dirname(d) === d) break;
|
|
88
|
+
}
|
|
89
|
+
return { dir, spelled: dir };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The skills the package ships: every skill under its `SHIPPED_SKILLS_DIR`. */
|
|
93
|
+
export function shippedSkills(
|
|
94
|
+
pkgDir: string,
|
|
95
|
+
):
|
|
96
|
+
| { readonly skillsDir: string; readonly names: readonly string[] }
|
|
97
|
+
| { readonly error: string } {
|
|
98
|
+
const skillsDir = join(pkgDir, SHIPPED_SKILLS_DIR);
|
|
99
|
+
// A missing directory is an error, never an empty list: zero skills linked must not read as a
|
|
100
|
+
// clean run.
|
|
101
|
+
if (!existsSync(skillsDir))
|
|
102
|
+
return { error: `the package's skills directory is missing: ${skillsDir}` };
|
|
103
|
+
// The same function every reader of an installed skills directory calls (rpp#62): the writer
|
|
104
|
+
// and the readers of this fact share one definition of "a skill is here", links included.
|
|
105
|
+
try {
|
|
106
|
+
return { skillsDir, names: installedSkills(skillsDir) };
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { error: (e as Error).message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type LinkStatus =
|
|
113
|
+
"created" | "replaced" | "present" | "missing" | "foreign";
|
|
114
|
+
|
|
115
|
+
export interface SkillLink {
|
|
116
|
+
readonly name: string;
|
|
117
|
+
readonly status: LinkStatus;
|
|
118
|
+
/** For `foreign`: what is there instead, in words. */
|
|
119
|
+
readonly reason?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type LinkReport =
|
|
123
|
+
| {
|
|
124
|
+
readonly ok: true;
|
|
125
|
+
readonly home: string;
|
|
126
|
+
/** The link target of the first skill, e.g. `../../node_modules/paperlint/skills/x`. */
|
|
127
|
+
readonly example: string | null;
|
|
128
|
+
readonly links: readonly SkillLink[];
|
|
129
|
+
}
|
|
130
|
+
| { readonly ok: false; readonly error: string };
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* What occupies `entry`, judged against the directory it should lead to. A link spelled exactly as
|
|
134
|
+
* an install under the package's old name wrote it (`legacyTarget`) is `stale`: ours, from before
|
|
135
|
+
* the rename, and replaced on the next `init`.
|
|
136
|
+
*/
|
|
137
|
+
function inspect(
|
|
138
|
+
entry: string,
|
|
139
|
+
want: string,
|
|
140
|
+
legacyTarget: string,
|
|
141
|
+
): { status: "present" | "missing" | "foreign" | "stale"; reason?: string } {
|
|
142
|
+
let st;
|
|
143
|
+
try {
|
|
144
|
+
st = lstatSync(entry);
|
|
145
|
+
} catch {
|
|
146
|
+
return { status: "missing" };
|
|
147
|
+
}
|
|
148
|
+
if (st.isSymbolicLink() && readlinkSync(entry) === legacyTarget)
|
|
149
|
+
return {
|
|
150
|
+
status: "stale",
|
|
151
|
+
reason: `a link into ${LEGACY_PACKAGE_NAME}, the package's old name — \`npx ${PACKAGE_NAME} init\` replaces it`,
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
if (realpathSync(entry) === want) return { status: "present" };
|
|
155
|
+
} catch {
|
|
156
|
+
return st.isSymbolicLink()
|
|
157
|
+
? {
|
|
158
|
+
status: "foreign",
|
|
159
|
+
reason: `a dangling link to ${readlinkSync(entry)}`,
|
|
160
|
+
}
|
|
161
|
+
: { status: "foreign", reason: "an entry that cannot be resolved" };
|
|
162
|
+
}
|
|
163
|
+
if (st.isSymbolicLink())
|
|
164
|
+
return { status: "foreign", reason: `a link to ${readlinkSync(entry)}` };
|
|
165
|
+
return {
|
|
166
|
+
status: "foreign",
|
|
167
|
+
reason: st.isDirectory() ? "a directory" : "a file",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* One skill: inspect its entry and, when writing, create the link — or replace one an install
|
|
173
|
+
* under the package's old name made.
|
|
174
|
+
*/
|
|
175
|
+
function linkOne(
|
|
176
|
+
name: string,
|
|
177
|
+
{
|
|
178
|
+
entry,
|
|
179
|
+
target,
|
|
180
|
+
want,
|
|
181
|
+
write,
|
|
182
|
+
}: { entry: string; target: string; want: string; write: boolean },
|
|
183
|
+
): SkillLink {
|
|
184
|
+
const legacyTarget = target.replace(
|
|
185
|
+
`node_modules${sep}${PACKAGE_NAME}${sep}`,
|
|
186
|
+
`node_modules${sep}${LEGACY_PACKAGE_NAME}${sep}`,
|
|
187
|
+
);
|
|
188
|
+
const seen = inspect(entry, want, legacyTarget);
|
|
189
|
+
const fixable = seen.status === "missing" || seen.status === "stale";
|
|
190
|
+
if (!write || !fixable) {
|
|
191
|
+
const status = seen.status === "stale" ? "foreign" : seen.status;
|
|
192
|
+
return seen.reason
|
|
193
|
+
? { name, status, reason: seen.reason }
|
|
194
|
+
: { name, status };
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
if (seen.status === "stale") unlinkSync(entry);
|
|
198
|
+
symlinkSync(target, entry, "dir");
|
|
199
|
+
return { name, status: seen.status === "stale" ? "replaced" : "created" };
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return {
|
|
202
|
+
name,
|
|
203
|
+
status: "foreign",
|
|
204
|
+
reason: `could not create the link: ${(e as Error).message}`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Link every shipped skill into `<project>/.claude/skills/`, or — with `write: false` — only
|
|
211
|
+
* report which ones are missing (that is what `paperlint doctor` asks).
|
|
212
|
+
*/
|
|
213
|
+
export function linkSkills(
|
|
214
|
+
project: string,
|
|
215
|
+
{
|
|
216
|
+
write = true,
|
|
217
|
+
locate = locatePackage,
|
|
218
|
+
}: { write?: boolean; locate?: (p: string) => Located } = {},
|
|
219
|
+
): LinkReport {
|
|
220
|
+
const pkg = locate(project);
|
|
221
|
+
if ("error" in pkg)
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
error: `${PACKAGE_NAME} does not resolve from ${project} (${pkg.error})`,
|
|
225
|
+
};
|
|
226
|
+
const shipped = shippedSkills(pkg.dir);
|
|
227
|
+
if ("error" in shipped) return { ok: false, error: shipped.error };
|
|
228
|
+
// The same directory, as the project spells it: the real skills dir re-rooted on `spelled`.
|
|
229
|
+
const spelledSkills = join(pkg.spelled, relative(pkg.dir, shipped.skillsDir));
|
|
230
|
+
|
|
231
|
+
const home = join(project, SKILLS_HOME);
|
|
232
|
+
if (write && shipped.names.length > 0) {
|
|
233
|
+
try {
|
|
234
|
+
mkdirSync(home, { recursive: true });
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: `cannot create ${home}: ${(e as Error).message}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// A relative target resolves against the directory that PHYSICALLY holds the link, so it is
|
|
243
|
+
// computed from the real one — `.claude` may itself be a symlink.
|
|
244
|
+
const physicalHome = existsSync(home) ? realpathSync(home) : resolve(home);
|
|
245
|
+
|
|
246
|
+
let example: string | null = null;
|
|
247
|
+
const links = shipped.names.map((name): SkillLink => {
|
|
248
|
+
const target = relative(physicalHome, join(spelledSkills, name));
|
|
249
|
+
example ??= target;
|
|
250
|
+
return linkOne(name, {
|
|
251
|
+
entry: join(home, name),
|
|
252
|
+
target,
|
|
253
|
+
want: realpathSync(join(shipped.skillsDir, name)),
|
|
254
|
+
write,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
return { ok: true, home, example, links };
|
|
258
|
+
}
|