paperlint 9.0.0 → 9.0.1

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.
Files changed (44) hide show
  1. package/.github/CLAUDE.md +22 -0
  2. package/.github/CLAUDE.md.spec.ts +20 -0
  3. package/CLAUDE.md +59 -515
  4. package/CONTRIBUTING.md +2 -2
  5. package/dist/CLAUDE.md.spec.d.ts +3 -0
  6. package/dist/CLAUDE.md.spec.d.ts.map +1 -0
  7. package/dist/CLAUDE.md.spec.js +62 -0
  8. package/dist/CLAUDE.md.spec.js.map +1 -0
  9. package/dist/build.js +1 -1
  10. package/dist/eslint-rules/CLAUDE.md.spec.d.ts +3 -0
  11. package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
  12. package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
  13. package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
  14. package/dist/eslint-rules/install-path-literals.d.ts +1 -1
  15. package/dist/eslint-rules/install-path-literals.js +4 -4
  16. package/dist/eslint-rules/install-path-literals.js.map +1 -1
  17. package/dist/eslint-rules/pdf-last-page-balance.js +1 -1
  18. package/dist/lib/agent-cli-version.js +1 -1
  19. package/dist/lib/agent-cli-version.js.map +1 -1
  20. package/dist/tex-requirements.js +1 -1
  21. package/dist/toolchain.js +1 -1
  22. package/docs/e2e.md +1 -1
  23. package/docs/incidents.md +5 -5
  24. package/eslint-rules/CLAUDE.md +44 -0
  25. package/eslint-rules/CLAUDE.md.spec.ts +43 -0
  26. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  27. package/eslint-rules/install-path-literals.ts +4 -4
  28. package/eslint-rules/pdf-last-page-balance.ts +1 -1
  29. package/eslint.config.mjs +6 -3
  30. package/hooks/CLAUDE.md +34 -0
  31. package/hooks/CLAUDE.md.spec.ts +30 -0
  32. package/lib/agent-cli-version.ts +1 -1
  33. package/package.json +2 -2
  34. package/plugin/CLAUDE.md +22 -0
  35. package/plugin/CLAUDE.md.spec.ts +22 -0
  36. package/scripts/coverage-config.test.ts +2 -1
  37. package/skills/CLAUDE.md +19 -0
  38. package/skills/CLAUDE.md.spec.ts +18 -0
  39. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
  40. package/src/CLAUDE.md +23 -1
  41. package/src/CLAUDE.md.spec.ts +72 -0
  42. package/src/build.ts +1 -1
  43. package/src/tex-requirements.ts +1 -1
  44. package/src/toolchain.ts +1 -1
package/CONTRIBUTING.md CHANGED
@@ -79,7 +79,7 @@ Most of the package's rules run on users' papers and are described for users in
79
79
  files.
80
80
 
81
81
  One rule checks one requirement, and a rule that judges a venue requirement is validated on real
82
- accepted papers built to PDF — both in [`CLAUDE.md`](CLAUDE.md#the-ten-rules-that-decide-what-may-live-here--and-what-may-not-be-written), rule 3.
82
+ accepted papers built to PDF — both in [`eslint-rules/CLAUDE.md`](eslint-rules/CLAUDE.md).
83
83
 
84
84
  A new rule ships with its page, `docs/rules/<group>/<rule>.md`, in these sections: What it
85
85
  catches · Why · Examples (failing / passing) · Options / preset fields · What it does not check ·
@@ -93,7 +93,7 @@ length, markers, hedges — drives papers toward clipped, unsignposted prose tha
93
93
  blog post, so a new one also has a floor, or measures the distance to accepted papers of the target
94
94
  venue, with its threshold measured by the same counter on those papers. What the parse tree can
95
95
  count is a rule; what needs reading (register, genre) is a skill criterion. The five principles are
96
- in [`CLAUDE.md`](CLAUDE.md#writing-checks--what-keeps-them-from-pushing-papers-toward-a-blog-post).
96
+ in [`eslint-rules/CLAUDE.md`](eslint-rules/CLAUDE.md#a-rule-that-judges-prose).
97
97
 
98
98
  ## Maintainer docs
99
99
 
@@ -0,0 +1,3 @@
1
+ declare const _default: import("vigiles/spec").ClaudeSpec;
2
+ export default _default;
3
+ //# sourceMappingURL=CLAUDE.md.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CLAUDE.md.spec.d.ts","sourceRoot":"","sources":["../src/CLAUDE.md.spec.ts"],"names":[],"mappings":";AAGA,wBAoEG"}
@@ -0,0 +1,62 @@
1
+ // Compiled to src/CLAUDE.md by `npx vigiles compile`. Edit THIS file, never the markdown.
2
+ import { enforce, file, instructionFile, prose } from "vigiles/spec";
3
+ export default instructionFile({
4
+ sections: {
5
+ "`src/`": prose `The package's TypeScript: the CLI, the build, the toolchain installer, and the measuring that feeds
6
+ the facts file. Loaded when working under \`src/\`; the repository-wide rules are in the root
7
+ \`CLAUDE.md\`.`,
8
+ "The two layer axes": prose `**\`src/\` is hexagonal on two axes, and the linter enforces both** (\`eslint.config.mjs\`; fixtures in
9
+ \`test/fixtures/layers/\`, run by ${file("test/eslint-layers.test.ts")}; issue #76).
10
+
11
+ **Axis A — knowledge. Core means the DOMAIN, not "no I/O".** A file belongs to an adapter when it
12
+ exists because of one external program or format: _if banal / pdf.js / TeX Live / ESLint / Claude
13
+ Code disappeared tomorrow, which files would change? Those are its adapter, wherever they sit
14
+ today._ The domain is what would not change if every one of them were swapped. Purity is not the
15
+ test — a parser for banal's JSON is pure and is still banal's.
16
+
17
+ | element | where | holds | may import |
18
+ | --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
19
+ | \`domain\` | \`src/domain/\` | values and decisions in the paper's vocabulary | \`domain\`; \`ts-essentials\`, \`node:path\`, \`node:crypto\` |
20
+ | \`port\` | \`src/ports/\` | interfaces **shaped by what the inside needs**, never by what a tool does (Graça: "created to fit the Application Core needs and not simply mimic the tools APIs") | \`domain\`, \`port\`; the same three externals |
21
+ | \`app\` | \`src/*.ts\` | use cases over ports; **no I/O defaults** — a use case without its deps does not compile | \`domain\`, \`port\`, \`app\`, the package's \`.mjs\`; Node's \`util\`/\`url\`/\`module\` |
22
+ | \`adapter\` | \`src/adapters/<tool>/\` | everything that knows \`<tool>\` exists, pure parsers included; used through its \`index.ts\` | \`domain\`, \`port\`, its own folder, its tool's packages, the package's \`.mjs\`. **Never** \`app\`, **never** another adapter |
23
+ | \`root\` | \`src/cli.ts\` | reads \`process.*\`, builds adapters, wires ports | anything |
24
+
25
+ Folders are ELEMENTS of \`eslint-plugin-boundaries\`, single files are CATEGORIES (\`root\`, \`app\`,
26
+ \`test\`, \`io\`): v7 matches element patterns as folders. Everyone — tests included — reaches an
27
+ adapter through its \`index.ts\`; the files inside one adapter import each other freely.
28
+
29
+ **Axis B — purity.** An effect (disk, process, network, env, clock, TTY) may be written only in
30
+ \`src/adapters/<tool>/<x>.io.ts\` and \`src/cli.ts\`. Every other file, adapters' parsers included,
31
+ takes a port. Test: _can a call here fail for a reason not in its arguments?_ Imports are checked
32
+ through the \`io\` file category in \`boundaries/dependencies\`; the \`process\` and \`fetch\` globals by
33
+ \`no-restricted-globals\`.
34
+
35
+ **The legacy app files are held by a ratchet, not exempted.** A site that still breaks either axis
36
+ carries \`// eslint-disable-next-line <rule> -- legacy <I/O|layer>, moves behind a port in #76\`.
37
+ Unused directives are errors (\`reportUnusedDisableDirectives\`), so a fixed site must drop its
38
+ comment; the per-file counts are frozen in \`scripts/layer-legacy.frozen.json\` and checked by
39
+ ${file("scripts/layer-legacy-frozen.ts")} (\`npm run check\`, CI), which counts ESLint's own
40
+ \`suppressedMessages\` and fails when a file's count grows, a file not in the list carries one, or
41
+ anything under \`src/domain/\`, \`src/ports/\` or \`src/adapters/\` does. A NEW module that needs the
42
+ world is a new \`*.io.ts\` in the adapter of the program it talks to — never a new directive.
43
+
44
+ Parse at the boundary, into types that cannot hold an invalid state (a banal path that exists only
45
+ after its sha256 and probe passed; a \`Ready\` only an installer mints; an outcome as a discriminated
46
+ union, not a string); the inside never re-validates.
47
+
48
+ ⚠️ **Two settings are load-bearing.** \`boundaries/root-path\`: without it the plugin matches paths
49
+ against \`process.cwd()\`, so lint started from any other directory classifies nothing and passes.
50
+ \`checkAllOrigins: true\`: without it no \`module:\` policy is evaluated, so axis B and the library bans
51
+ report nothing — dropping it turns the fixtures \`src/domain/reads-disk.ts\` and
52
+ \`src/adapters/one/disk.ts\` clean (measured).`,
53
+ "Readers and commands": prose `\`@retorquere/bibtex-parser\` is the one \`.bib\` reader, behind ${file("src/ports/bib-reader.ts")}.
54
+ Rejected on 2026-09-27, so not proposed again: a separate \`paperlint refs\` command; the slowness it
55
+ would have moved was a cache thrown away after every build (${file("docs/references.md")}).`,
56
+ },
57
+ rules: {
58
+ layers: enforce("eslint/boundaries/dependencies", "Both axes: what a layer may import, and where an effect may be written.", { render: "id" }),
59
+ "effect-globals": enforce("eslint/no-restricted-globals", "The process and fetch globals only where an effect may be written.", { render: "id" }),
60
+ },
61
+ });
62
+ //# sourceMappingURL=CLAUDE.md.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CLAUDE.md.spec.js","sourceRoot":"","sources":["../src/CLAUDE.md.spec.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErE,eAAe,eAAe,CAAC;IAC7B,QAAQ,EAAE;QACR,QAAQ,EAAE,KAAK,CAAA;;eAEJ;QAEX,oBAAoB,EAAE,KAAK,CAAA;oCACK,IAAI,CAAC,4BAA4B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BpE,IAAI,CAAC,gCAAgC,CAAC;;;;;;;;;;;;;+CAaO;QAE3C,sBAAsB,EAAE,KAAK,CAAA,oEAAoE,IAAI,CAAC,yBAAyB,CAAC;;8DAEtE,IAAI,CAAC,oBAAoB,CAAC,IAAI;KACzF;IACD,KAAK,EAAE;QACL,MAAM,EAAE,OAAO,CACb,gCAAgC,EAChC,yEAAyE,EACzE,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB;QACD,gBAAgB,EAAE,OAAO,CACvB,8BAA8B,EAC9B,oEAAoE,EACpE,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB;KACF;CACF,CAAC,CAAC"}
package/dist/build.js CHANGED
@@ -34,7 +34,7 @@
34
34
  * The decision of the LaTeX loop is `latex-loop.ts`, reading the log is `latex-log.ts`; both are
35
35
  * pure. This file is the shell: it runs processes through the injected `run` (the existing port —
36
36
  * `spawnSync` by default) and reads the files a pass left behind. Where paperlint's own files live is
37
- * answered by `package-dirs.ts`, the one module allowed to know it (rule 10).
37
+ * answered by `package-dirs.ts`, the one module allowed to know it (root CLAUDE.md).
38
38
  */
39
39
  // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
40
40
  import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
@@ -0,0 +1,3 @@
1
+ declare const _default: import("vigiles/spec").ClaudeSpec;
2
+ export default _default;
3
+ //# sourceMappingURL=CLAUDE.md.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CLAUDE.md.spec.d.ts","sourceRoot":"","sources":["../../eslint-rules/CLAUDE.md.spec.ts"],"names":[],"mappings":";AAMA,wBAoCG"}
@@ -0,0 +1,39 @@
1
+ // Compiled to eslint-rules/CLAUDE.md by `npx vigiles compile`. Edit THIS file, never the markdown.
2
+ //
3
+ // The requirements below are held by tests and scripts, not by an ESLint rule `enforce()` could
4
+ // name, so they are prose with verified file references rather than `enforce()` rules.
5
+ import { file, guidance, instructionFile, prose } from "vigiles/spec";
6
+ export default instructionFile({
7
+ sections: {
8
+ "`eslint-rules/`": prose `The rules this package ships. Loaded when working under \`eslint-rules/\`; the rules for every change
9
+ are in the root \`CLAUDE.md\`. Each requirement below names what holds it; the reasoning lives in
10
+ that file's header.`,
11
+ "What a new rule ships with, and what holds it": prose `| requirement | held by |
12
+ | --- | --- |
13
+ | **Both halves** (root \`CLAUDE.md\`), for every registered rule | ${file("src/rule-cases.test.ts")} fails on a registered rule with no cases |
14
+ | **One rule, one requirement**: two checks with different reasons to switch them off are two rules | review |
15
+ | **A venue requirement is validated on real accepted papers** of that venue, built to PDF, with the ground truth established independently | review; the papers are in ${file("fixtures/accepted-papers/README.md")} |
16
+ | **Its page**, \`docs/rules/<group>/<rule>.md\`, and \`meta.docs.url\` pointing at it | ${file("src/rule-docs.test.ts")}; its \`AWAITING_PAGE\` list only shrinks |
17
+ | **Loud on an empty input set**: a rule that matched no files must not report like one that passed | ${file("scripts/rules-see-files.mjs")}, per rule and not per glob, in \`npm run check\` |
18
+
19
+ The page's sections: What it catches · Why · Examples (failing / passing) · Options / preset fields ·
20
+ What it does not check · How to fix. Its row in ${file("docs/rules.md")} is one line linking there.`,
21
+ "A rule that judges prose": prose `A rule, or a skill criterion, that judges a paper's prose is held to five principles. Without them
22
+ the checks push every paper toward a blog post.
23
+
24
+ 1. **Two-sided**: a floor as well as a ceiling, or the distance to accepted papers of the target
25
+ venue. A set of ceilings alone converges on clipped, unsignposted prose.
26
+ 2. **Rules count, skills judge.** Countable on the parse tree → a lint rule; needs reading (register,
27
+ genre, whether a claim changed strength) → a skill criterion.
28
+ 3. **The model reader is the venue's reviewer**, including one who skims the abstract, the
29
+ introduction and the conclusion.
30
+ 4. **Thresholds are calibrated, not borrowed**: set with the same counter on the accepted papers,
31
+ never taken from a figure measured by a different counter.
32
+ 5. **A rewrite is accepted on what reviewers see**: the combined count of every surface form of a
33
+ tic, plus a check that no claim was softened or strengthened.`,
34
+ },
35
+ rules: {
36
+ "lint-rule-over-script": guidance("A check over a syntax tree is a lint rule, not a script. It fires in the editor on save, gets the AST for free, and has a suppression syntax people already know. A script is for corpus-wide questions — index connectivity, ratios across many files. When the work has to run a program, a script may do the work and write facts to a JSON file, and a rule judges that file: the verdict still lands in the engine."),
37
+ },
38
+ });
39
+ //# sourceMappingURL=CLAUDE.md.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CLAUDE.md.spec.js","sourceRoot":"","sources":["../../eslint-rules/CLAUDE.md.spec.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,EAAE;AACF,gGAAgG;AAChG,uFAAuF;AACvF,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAEtE,eAAe,eAAe,CAAC;IAC7B,QAAQ,EAAE;QACR,iBAAiB,EAAE,KAAK,CAAA;;oBAER;QAEhB,+CAA+C,EAAE,KAAK,CAAA;;sEAEY,IAAI,CAAC,wBAAwB,CAAC;;0KAEsE,IAAI,CAAC,oCAAoC,CAAC;2FACzH,IAAI,CAAC,uBAAuB,CAAC;wGAChB,IAAI,CAAC,6BAA6B,CAAC;;;kDAGzF,IAAI,CAAC,eAAe,CAAC,6BAA6B;QAEhG,0BAA0B,EAAE,KAAK,CAAA;;;;;;;;;;;;iEAY4B;KAC9D;IACD,KAAK,EAAE;QACL,uBAAuB,EAAE,QAAQ,CAC/B,0ZAA0Z,CAC3Z;KACF;CACF,CAAC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Rule 10's mechanical half: nothing outside the port may name a file by WHERE IT IS
2
+ * The install-location rule's mechanical half (root CLAUDE.md): nothing outside the port may name a file by WHERE IT IS
3
3
  * INSTALLED.
4
4
  *
5
5
  * The class this exists for has broken this repository five times, silently every time, and
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Rule 10's mechanical half: nothing outside the port may name a file by WHERE IT IS
2
+ * The install-location rule's mechanical half (root CLAUDE.md): nothing outside the port may name a file by WHERE IT IS
3
3
  * INSTALLED.
4
4
  *
5
5
  * The class this exists for has broken this repository five times, silently every time, and
@@ -43,7 +43,7 @@ function channelPrefixIn(s) {
43
43
  const PORT = "skills/paper-pipeline/scripts/consumer.mjs";
44
44
  const MESSAGES = {
45
45
  installPath: "This names a file by where it is INSTALLED ('{{prefix}}'), so it resolves in one " +
46
- "delivery channel and does not exist in another. Rule 10: every answer to *where* comes " +
46
+ "delivery channel and does not exist in another. Every answer to *where* comes " +
47
47
  `from the port, ${PORT}. In a skill, use the substitution the harness performs ` +
48
48
  "(CLAUDE_SKILL_DIR / CLAUDE_PLUGIN_ROOT); in a module, ask the port.",
49
49
  };
@@ -52,7 +52,7 @@ const mdInstallPath = {
52
52
  meta: {
53
53
  type: "problem",
54
54
  docs: {
55
- description: "a skill must not name its scripts by an install-specific path — rule 10, the port owns where",
55
+ description: "a skill must not name its scripts by an install-specific path — the port owns where",
56
56
  },
57
57
  schema: [],
58
58
  messages: MESSAGES,
@@ -73,7 +73,7 @@ const jsInstallPath = {
73
73
  meta: {
74
74
  type: "problem",
75
75
  docs: {
76
- description: "a shipped module must not carry an install-specific path literal — rule 10, the port owns where",
76
+ description: "a shipped module must not carry an install-specific path literal — the port owns where",
77
77
  },
78
78
  schema: [],
79
79
  messages: MESSAGES,
@@ -1 +1 @@
1
- {"version":3,"file":"install-path-literals.js","sourceRoot":"","sources":["../../eslint-rules/install-path-literals.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"install-path-literals.js","sourceRoot":"","sources":["../../eslint-rules/install-path-literals.ts"],"names":[],"mappings":"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"}
@@ -11,7 +11,7 @@
11
11
  * { "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] } }
12
12
  *
13
13
  * (That is a line comment's worth of JSON in a block comment only because it has no asterisk
14
- * followed by a slash; see rule 8 of CLAUDE.md before editing it.)
14
+ * followed by a slash; see the block-comment rule in the root CLAUDE.md before editing it.)
15
15
  *
16
16
  * ── WHAT IT READS: THE FACTS THE BUILD WROTE, NOT THE PDF ───────────────────────
17
17
  * `paperlint build` measures the PDF with pdf.js and writes `<paper>/_build/paper.facts.json`. This rule
@@ -54,7 +54,7 @@ export function observeAgentCli({ spawnSync, program = "claude", characterized =
54
54
  const version = VERSION_IN.exec(raw)?.[1] ?? "";
55
55
  // ⚠️ EXIT 0 AND NOTHING ON STDOUT IS NOT A CLEAN OBSERVATION — it is exactly what the discarded
56
56
  // probe looked like, and reporting `characterized against ""` would be the same lie in a new
57
- // place. This is rule 4 of CLAUDE.md applied to the check that enforces it.
57
+ // place. This is the green zero of scripts/rules-see-files.mjs, applied to the check that enforces it.
58
58
  if (version === "")
59
59
  throw new Error(`\`${program} --version\` exited 0 but printed no version on stdout (got ${JSON.stringify(raw)}` +
60
60
  `${probe.stderr ? `, stderr ${JSON.stringify(probe.stderr.trim().slice(0, 200))}` : ""}). ` +
@@ -1 +1 @@
1
- {"version":3,"file":"agent-cli-version.js","sourceRoot":"","sources":["../../lib/agent-cli-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C,+DAA+D;AAC/D,MAAM,UAAU,GAAG,qBAAqB,CAAC;AA6BzC,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,OAAO,GAAG,QAAQ,EAClB,aAAa,GAAG,iBAAiB,GAKlC;IACC,2FAA2F;IAC3F,2FAA2F;IAC3F,oDAAoD;IACpD,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAEtE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QACpB,OAAO;YACL,OAAO,EAAE,KAAK;YACd,GAAG,EAAE,EAAE;YACP,OAAO,EAAE,EAAE;YACX,aAAa;YACb,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,SAAS,OAAO,oDAAoD;SAC3E,CAAC;IAEJ,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAEhD,gGAAgG;IAChG,6FAA6F;IAC7F,4EAA4E;IAC5E,IAAI,OAAO,KAAK,EAAE;QAChB,MAAM,IAAI,KAAK,CACb,KAAK,OAAO,+DAA+D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE;YAC9F,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK;YAC3F,8FAA8F;YAC9F,kEAAkE,CACrE,CAAC;IAEJ,MAAM,OAAO,GAAG,OAAO,KAAK,aAAa,CAAC;IAC1C,OAAO;QACL,OAAO,EAAE,IAAI;QACb,GAAG;QACH,OAAO;QACP,aAAa;QACb,OAAO;QACP,IAAI,EAAE,OAAO;YACX,CAAC,CAAC,cAAc,OAAO,MAAM,OAAO,oDAAoD;gBACtF,GAAG,aAAa,8EAA8E;gBAC9F,uEAAuE;YACzE,CAAC,CAAC,cAAc,OAAO,MAAM,OAAO,6DAA6D;KACpG,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"agent-cli-version.js","sourceRoot":"","sources":["../../lib/agent-cli-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C,+DAA+D;AAC/D,MAAM,UAAU,GAAG,qBAAqB,CAAC;AA6BzC,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,OAAO,GAAG,QAAQ,EAClB,aAAa,GAAG,iBAAiB,GAKlC;IACC,2FAA2F;IAC3F,2FAA2F;IAC3F,oDAAoD;IACpD,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAEtE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QACpB,OAAO;YACL,OAAO,EAAE,KAAK;YACd,GAAG,EAAE,EAAE;YACP,OAAO,EAAE,EAAE;YACX,aAAa;YACb,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,SAAS,OAAO,oDAAoD;SAC3E,CAAC;IAEJ,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAEhD,gGAAgG;IAChG,6FAA6F;IAC7F,uGAAuG;IACvG,IAAI,OAAO,KAAK,EAAE;QAChB,MAAM,IAAI,KAAK,CACb,KAAK,OAAO,+DAA+D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE;YAC9F,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK;YAC3F,8FAA8F;YAC9F,kEAAkE,CACrE,CAAC;IAEJ,MAAM,OAAO,GAAG,OAAO,KAAK,aAAa,CAAC;IAC1C,OAAO;QACL,OAAO,EAAE,IAAI;QACb,GAAG;QACH,OAAO;QACP,aAAa;QACb,OAAO;QACP,IAAI,EAAE,OAAO;YACX,CAAC,CAAC,cAAc,OAAO,MAAM,OAAO,oDAAoD;gBACtF,GAAG,aAAa,8EAA8E;gBAC9F,uEAAuE;YACzE,CAAC,CAAC,cAAc,OAAO,MAAM,OAAO,6DAA6D;KACpG,CAAC;AACJ,CAAC"}
@@ -17,7 +17,7 @@
17
17
  * of every violation, it is never read as "no packages".
18
18
  *
19
19
  * Where the profiles live is answered by `presetsDir()` in `package-dirs.ts` — the one module
20
- * allowed to know where this package's data is installed (rule 10).
20
+ * allowed to know where this package's data is installed (root CLAUDE.md).
21
21
  */
22
22
  // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
23
23
  import { readFileSync, readdirSync } from "node:fs";
package/dist/toolchain.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * declare, into paperlint's own cache, and banal (the page-geometry script HotCRP runs) at its pinned
4
4
  * commit (`adapters/banal/`). `paperlint build` offers the TeX Live install on a terminal.
5
5
  *
6
- * 🔴 WHY paperlint INSTALLS TeX AT ALL (rule 11). "Install TeX Live yourself" was a manual step with a
6
+ * 🔴 WHY paperlint INSTALLS TeX AT ALL (root CLAUDE.md: installing must be smooth). "Install TeX Live yourself" was a manual step with a
7
7
  * trap in it: the distribution packages cost 2.1 GB, and a smaller hand-picked set silently typeset
8
8
  * acmart papers in Computer Modern. The engine decision (2026-09-24) is TeX Live's pdflatex,
9
9
  * installed the way Playwright installs browsers: one command, a cache directory, idempotent.
package/docs/e2e.md CHANGED
@@ -137,7 +137,7 @@ be paperlint's own cache, because the runner has no other.
137
137
  within seconds; `--check` must exit 0; then the `acmart` fixture is built with PATH holding `node`
138
138
  only, so no other TeX Live and no PDF tool can stand in, and the PDF must carry Libertine and Biolinum
139
139
  and no Computer Modern face. Without `PAPERLINT_TEXLIVE_DIR` its tests are skipped: installing ~270 MB
140
- into a home directory as a side effect of `npm run check` is the unasked install rule 11 forbids.
140
+ into a home directory as a side effect of `npm run check` is the unasked install the root `CLAUDE.md` forbids.
141
141
  `src/toolchain.harness.mjs` covers the installer's logic (mirror fallback, archive check, time
142
142
  limit, verification, idempotence) against a fake mirror on disk, without the network.
143
143
 
package/docs/incidents.md CHANGED
@@ -4,7 +4,7 @@
4
4
  else. The measurement that produced a rule lives here, and is read only when someone argues
5
5
  with the rule or wants to know how it was found.
6
6
 
7
- Two things never move into this file: a measurement that **is** the rule (rule 5's table of
7
+ Two things never move into this file: a measurement that **is** the rule (the root `CLAUDE.md`'s table of
8
8
  claims that fell to one command — without it the rule reads "measure things", which nobody
9
9
  obeys), and evidence that already lives in a code comment beside the code it explains. Those
10
10
  get a pointer, never a copy.
@@ -13,7 +13,7 @@ Entries are append-only and dated. A superseded entry is marked, not deleted.
13
13
 
14
14
  ---
15
15
 
16
- ## 2026-09-17 — four fixes proposed and withdrawn in one session (rule 9)
16
+ ## 2026-09-17 — four fixes proposed and withdrawn in one session (measure the defect first)
17
17
 
18
18
  Each was proposed before the defect it claimed to fix had been exhibited, and each fell to a
19
19
  single command:
@@ -33,7 +33,7 @@ make the path resolve.
33
33
 
34
34
  Cost of the pattern: the four proposals took more of the session than the work they proposed.
35
35
 
36
- ## 2026-09-17 — the install-location class, four silent breakages (rule 10)
36
+ ## 2026-09-17 — the install-location class, four silent breakages (where paperlint is installed)
37
37
 
38
38
  One class — _code that knows where it is on disk_ — has broken this repository four times, and
39
39
  not one of the four announced itself:
@@ -50,8 +50,8 @@ The first three are documented in the code they broke, and that is where the det
50
50
  `skills/paper-pipeline/scripts/consumer.mjs` (`ledgerPath`, three steps).
51
51
 
52
52
  🔴 The load-bearing observation is not the count. All four repetitions happened **while
53
- comments explaining the exact hazard sat directly above the code** — which is why rule 10 owes
54
- a lint rule and not another paragraph.
53
+ comments explaining the exact hazard sat directly above the code** — which is why that rule got
54
+ a lint rule (`eslint-rules/install-path-literals.ts`) and not another paragraph.
55
55
 
56
56
  A fifth instance of the same class was measured the same day and is not yet fixed: the guard
57
57
  in `ledgerPath` asks `insideNodeModules(hereDir)`, and a plugin cache is not `node_modules`, so
@@ -0,0 +1,44 @@
1
+ <!-- vigiles:sha256:5231f736e3b1a263 compiled from eslint-rules/CLAUDE.md.spec.ts -->
2
+
3
+ # CLAUDE.md
4
+
5
+ ## `eslint-rules/`
6
+
7
+ The rules this package ships. Loaded when working under `eslint-rules/`; the rules for every change
8
+ are in the root `CLAUDE.md`. Each requirement below names what holds it; the reasoning lives in
9
+ that file's header.
10
+
11
+ ## What a new rule ships with, and what holds it
12
+
13
+ | requirement | held by |
14
+ | --- | --- |
15
+ | **Both halves** (root `CLAUDE.md`), for every registered rule | `src/rule-cases.test.ts` fails on a registered rule with no cases |
16
+ | **One rule, one requirement**: two checks with different reasons to switch them off are two rules | review |
17
+ | **A venue requirement is validated on real accepted papers** of that venue, built to PDF, with the ground truth established independently | review; the papers are in `fixtures/accepted-papers/README.md` |
18
+ | **Its page**, `docs/rules/<group>/<rule>.md`, and `meta.docs.url` pointing at it | `src/rule-docs.test.ts`; its `AWAITING_PAGE` list only shrinks |
19
+ | **Loud on an empty input set**: a rule that matched no files must not report like one that passed | `scripts/rules-see-files.mjs`, per rule and not per glob, in `npm run check` |
20
+
21
+ The page's sections: What it catches · Why · Examples (failing / passing) · Options / preset fields ·
22
+ What it does not check · How to fix. Its row in `docs/rules.md` is one line linking there.
23
+
24
+ ## A rule that judges prose
25
+
26
+ A rule, or a skill criterion, that judges a paper's prose is held to five principles. Without them
27
+ the checks push every paper toward a blog post.
28
+
29
+ 1. **Two-sided**: a floor as well as a ceiling, or the distance to accepted papers of the target
30
+ venue. A set of ceilings alone converges on clipped, unsignposted prose.
31
+ 2. **Rules count, skills judge.** Countable on the parse tree → a lint rule; needs reading (register,
32
+ genre, whether a claim changed strength) → a skill criterion.
33
+ 3. **The model reader is the venue's reviewer**, including one who skims the abstract, the
34
+ introduction and the conclusion.
35
+ 4. **Thresholds are calibrated, not borrowed**: set with the same counter on the accepted papers,
36
+ never taken from a figure measured by a different counter.
37
+ 5. **A rewrite is accepted on what reviewers see**: the combined count of every surface form of a
38
+ tic, plus a check that no claim was softened or strengthened.
39
+
40
+ ## Rules
41
+
42
+ ### Lint Rule Over Script
43
+
44
+ **Guidance only** — A check over a syntax tree is a lint rule, not a script. It fires in the editor on save, gets the AST for free, and has a suppression syntax people already know. A script is for corpus-wide questions — index connectivity, ratios across many files. When the work has to run a program, a script may do the work and write facts to a JSON file, and a rule judges that file: the verdict still lands in the engine.
@@ -0,0 +1,43 @@
1
+ // Compiled to eslint-rules/CLAUDE.md by `npx vigiles compile`. Edit THIS file, never the markdown.
2
+ //
3
+ // The requirements below are held by tests and scripts, not by an ESLint rule `enforce()` could
4
+ // name, so they are prose with verified file references rather than `enforce()` rules.
5
+ import { file, guidance, instructionFile, prose } from "vigiles/spec";
6
+
7
+ export default instructionFile({
8
+ sections: {
9
+ "`eslint-rules/`": prose`The rules this package ships. Loaded when working under \`eslint-rules/\`; the rules for every change
10
+ are in the root \`CLAUDE.md\`. Each requirement below names what holds it; the reasoning lives in
11
+ that file's header.`,
12
+
13
+ "What a new rule ships with, and what holds it": prose`| requirement | held by |
14
+ | --- | --- |
15
+ | **Both halves** (root \`CLAUDE.md\`), for every registered rule | ${file("src/rule-cases.test.ts")} fails on a registered rule with no cases |
16
+ | **One rule, one requirement**: two checks with different reasons to switch them off are two rules | review |
17
+ | **A venue requirement is validated on real accepted papers** of that venue, built to PDF, with the ground truth established independently | review; the papers are in ${file("fixtures/accepted-papers/README.md")} |
18
+ | **Its page**, \`docs/rules/<group>/<rule>.md\`, and \`meta.docs.url\` pointing at it | ${file("src/rule-docs.test.ts")}; its \`AWAITING_PAGE\` list only shrinks |
19
+ | **Loud on an empty input set**: a rule that matched no files must not report like one that passed | ${file("scripts/rules-see-files.mjs")}, per rule and not per glob, in \`npm run check\` |
20
+
21
+ The page's sections: What it catches · Why · Examples (failing / passing) · Options / preset fields ·
22
+ What it does not check · How to fix. Its row in ${file("docs/rules.md")} is one line linking there.`,
23
+
24
+ "A rule that judges prose": prose`A rule, or a skill criterion, that judges a paper's prose is held to five principles. Without them
25
+ the checks push every paper toward a blog post.
26
+
27
+ 1. **Two-sided**: a floor as well as a ceiling, or the distance to accepted papers of the target
28
+ venue. A set of ceilings alone converges on clipped, unsignposted prose.
29
+ 2. **Rules count, skills judge.** Countable on the parse tree → a lint rule; needs reading (register,
30
+ genre, whether a claim changed strength) → a skill criterion.
31
+ 3. **The model reader is the venue's reviewer**, including one who skims the abstract, the
32
+ introduction and the conclusion.
33
+ 4. **Thresholds are calibrated, not borrowed**: set with the same counter on the accepted papers,
34
+ never taken from a figure measured by a different counter.
35
+ 5. **A rewrite is accepted on what reviewers see**: the combined count of every surface form of a
36
+ tic, plus a check that no claim was softened or strengthened.`,
37
+ },
38
+ rules: {
39
+ "lint-rule-over-script": guidance(
40
+ "A check over a syntax tree is a lint rule, not a script. It fires in the editor on save, gets the AST for free, and has a suppression syntax people already know. A script is for corpus-wide questions — index connectivity, ratios across many files. When the work has to run a program, a script may do the work and write facts to a JSON file, and a rule judges that file: the verdict still lands in the engine.",
41
+ ),
42
+ },
43
+ });
@@ -19,7 +19,7 @@ import markdown from "@eslint/markdown";
19
19
  import { recordCheck } from "vigiles";
20
20
  import port, { PORT } from "./install-path-literals.ts";
21
21
 
22
- // Resolved from THIS file, never from the caller's cwd — rule 6, and the mistake this very
22
+ // Resolved from THIS file, never from the caller's cwd (root CLAUDE.md), and the mistake this very
23
23
  // class of bug is about.
24
24
  const HERE = dirname(fileURLToPath(import.meta.url));
25
25
  const ROOT = join(HERE, "..");
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Rule 10's mechanical half: nothing outside the port may name a file by WHERE IT IS
2
+ * The install-location rule's mechanical half (root CLAUDE.md): nothing outside the port may name a file by WHERE IT IS
3
3
  * INSTALLED.
4
4
  *
5
5
  * The class this exists for has broken this repository five times, silently every time, and
@@ -47,7 +47,7 @@ const PORT = "skills/paper-pipeline/scripts/consumer.mjs";
47
47
  const MESSAGES: Readonly<Record<string, string>> = {
48
48
  installPath:
49
49
  "This names a file by where it is INSTALLED ('{{prefix}}'), so it resolves in one " +
50
- "delivery channel and does not exist in another. Rule 10: every answer to *where* comes " +
50
+ "delivery channel and does not exist in another. Every answer to *where* comes " +
51
51
  `from the port, ${PORT}. In a skill, use the substitution the harness performs ` +
52
52
  "(CLAUDE_SKILL_DIR / CLAUDE_PLUGIN_ROOT); in a module, ask the port.",
53
53
  };
@@ -58,7 +58,7 @@ const mdInstallPath = {
58
58
  type: "problem" as const,
59
59
  docs: {
60
60
  description:
61
- "a skill must not name its scripts by an install-specific path — rule 10, the port owns where",
61
+ "a skill must not name its scripts by an install-specific path — the port owns where",
62
62
  },
63
63
  schema: [],
64
64
  messages: MESSAGES,
@@ -81,7 +81,7 @@ const jsInstallPath = {
81
81
  type: "problem" as const,
82
82
  docs: {
83
83
  description:
84
- "a shipped module must not carry an install-specific path literal — rule 10, the port owns where",
84
+ "a shipped module must not carry an install-specific path literal — the port owns where",
85
85
  },
86
86
  schema: [],
87
87
  messages: MESSAGES,
@@ -11,7 +11,7 @@
11
11
  * { "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] } }
12
12
  *
13
13
  * (That is a line comment's worth of JSON in a block comment only because it has no asterisk
14
- * followed by a slash; see rule 8 of CLAUDE.md before editing it.)
14
+ * followed by a slash; see the block-comment rule in the root CLAUDE.md before editing it.)
15
15
  *
16
16
  * ── WHAT IT READS: THE FACTS THE BUILD WROTE, NOT THE PDF ───────────────────────
17
17
  * `paperlint build` measures the PDF with pdf.js and writes `<paper>/_build/paper.facts.json`. This rule
package/eslint.config.mjs CHANGED
@@ -250,6 +250,9 @@ const ROOT = { file: { categories: "root" } };
250
250
  */
251
251
  export const layerBoundaries = (root) => ({
252
252
  files: ["src/**/*.ts"],
253
+ // The source of src/CLAUDE.md sits beside it because `vigiles compile` requires that; it is an
254
+ // instruction file, not a layer of the package.
255
+ ignores: ["src/CLAUDE.md.spec.ts"],
253
256
  plugins: { boundaries },
254
257
  settings: {
255
258
  "boundaries/root-path": root,
@@ -681,7 +684,7 @@ export default [
681
684
  languageOptions: { ecmaVersion: 2024, sourceType: "module" },
682
685
  plugins: { local: localRules, port: portRules },
683
686
  rules: {
684
- // Rule 10's mechanical half, code side. `warn` and not `error`, unlike its neighbour
687
+ // The install-location rule's mechanical half (root CLAUDE.md), code side. `warn` and not `error`, unlike its neighbour
685
688
  // above: this one does NOT open on a clean corpus. Two shipped modules print a command
686
689
  // for a human that names an install-specific path, and neither is fixable by the
687
690
  // answer the skills get — a printed command has to be resolved through the port at
@@ -741,7 +744,7 @@ export default [
741
744
  rules: { "n/no-missing-import": "error" },
742
745
  },
743
746
  /**
744
- * Rule 10's mechanical half, prose side — and this is where the debt actually is: 76
747
+ * The install-location rule's mechanical half (root CLAUDE.md), prose side — and this is where the debt actually is: 76
745
748
  * findings across 29 skills on a healthy checkout, every one of them a command that
746
749
  * resolves in one delivery channel and is absent in another.
747
750
  *
@@ -789,7 +792,7 @@ export default [
789
792
  // ⚠️ `npx eslint .` therefore reports six warnings on a healthy checkout: the four defect
790
793
  // fixtures are DEFECTIVE ON PURPOSE, and that is what makes the fire half of every harness
791
794
  // real. Do not silence them by adding an ignore — a rule that lints only clean inputs is
792
- // one whose firing path nothing exercises, which is rule 4 wearing a different hat. The
795
+ // one whose firing path nothing exercises, which is the green zero of scripts/rules-see-files.mjs wearing a different hat. The
793
796
  // pass/fail signal lives in `npm test`, not in the warning count of `npm run lint`.
794
797
  },
795
798
  {
@@ -0,0 +1,34 @@
1
+ <!-- vigiles:sha256:0407b53b2036133e compiled from hooks/CLAUDE.md.spec.ts -->
2
+
3
+ # CLAUDE.md
4
+
5
+ ## `hooks/`
6
+
7
+ The hooks this package ships into a consumer's Claude Code session. Loaded when working under
8
+ `hooks/`. Every shipped hook is run in both halves, firing and silent, by `hooks/hooks.harness.ts`.
9
+
10
+ A bug here does not break this repository; it breaks somebody else's whole session. The Bash gate
11
+ fails closed, so a hook that cannot read its config denies every command, including the one that
12
+ would undo it.
13
+
14
+ ## Rules
15
+
16
+ ### Ship Hook Mjs Never Hook Ts
17
+
18
+ **Guidance only** — A `.ts` hook inside `node_modules` fails to load and blocks every Bash command. Rejected, so not proposed again: a `.hook.ts` twin compiled to the `.mjs` (a twin drifts from its build, and nothing would notice), and a thin `.hook.ts` in the consumer importing the logic from here (`vigiles compile` refuses the import, so the hook can never be re-stamped, and the runtime fails closed on the stale stamp). [Measurements](https://github.com/zernie/paperlint/blob/b4a3dc3a81d436186f41db2d69b65fd93f1af3b6/CLAUDE.md#L272-L357).
19
+
20
+ ### No Sibling Imports
21
+
22
+ **Guidance only** — A shipped hook cannot import a sibling module. The papers-root resolver is repeated in each hook, and `hooks.harness.ts` compares the copies.
23
+
24
+ ### No Import Shaped Text
25
+
26
+ **Guidance only** — `checkHookImports` (run by `hooks.harness.ts`) is a text regex. Describe a forbidden import in words; never write one, not even in a comment.
27
+
28
+ ### One Wiring File
29
+
30
+ **Guidance only** — The wiring is `plugin/hooks/hooks.json`. `paperlint init` merges it into the consumer's `.claude/settings.json`; there is no second copy.
31
+
32
+ ### Vigiles Moves With The Consumer
33
+
34
+ **Guidance only** — `vigiles` moves with the consumer's pin, in one pass. A major mismatch makes `PreToolUse` refuse every command. `npm ls vigiles` compares what is installed with the manifest, not with the registry; Dependabot (`.github/dependabot.yml`) carries the registry half.
@@ -0,0 +1,30 @@
1
+ // Compiled to hooks/CLAUDE.md by `npx vigiles compile`. Edit THIS file, never the markdown.
2
+ import { file, guidance, instructionFile, prose } from "vigiles/spec";
3
+
4
+ export default instructionFile({
5
+ sections: {
6
+ "`hooks/`": prose`The hooks this package ships into a consumer's Claude Code session. Loaded when working under
7
+ \`hooks/\`. Every shipped hook is run in both halves, firing and silent, by ${file("hooks/hooks.harness.ts")}.
8
+
9
+ A bug here does not break this repository; it breaks somebody else's whole session. The Bash gate
10
+ fails closed, so a hook that cannot read its config denies every command, including the one that
11
+ would undo it.`,
12
+ },
13
+ rules: {
14
+ "ship-hook-mjs-never-hook-ts": guidance(
15
+ "A `.ts` hook inside `node_modules` fails to load and blocks every Bash command. Rejected, so not proposed again: a `.hook.ts` twin compiled to the `.mjs` (a twin drifts from its build, and nothing would notice), and a thin `.hook.ts` in the consumer importing the logic from here (`vigiles compile` refuses the import, so the hook can never be re-stamped, and the runtime fails closed on the stale stamp). [Measurements](https://github.com/zernie/paperlint/blob/b4a3dc3a81d436186f41db2d69b65fd93f1af3b6/CLAUDE.md#L272-L357).",
16
+ ),
17
+ "no-sibling-imports": guidance(
18
+ "A shipped hook cannot import a sibling module. The papers-root resolver is repeated in each hook, and `hooks.harness.ts` compares the copies.",
19
+ ),
20
+ "no-import-shaped-text": guidance(
21
+ "`checkHookImports` (run by `hooks.harness.ts`) is a text regex. Describe a forbidden import in words; never write one, not even in a comment.",
22
+ ),
23
+ "one-wiring-file": guidance(
24
+ "The wiring is `plugin/hooks/hooks.json`. `paperlint init` merges it into the consumer's `.claude/settings.json`; there is no second copy.",
25
+ ),
26
+ "vigiles-moves-with-the-consumer": guidance(
27
+ "`vigiles` moves with the consumer's pin, in one pass. A major mismatch makes `PreToolUse` refuse every command. `npm ls vigiles` compares what is installed with the manifest, not with the registry; Dependabot (`.github/dependabot.yml`) carries the registry half.",
28
+ ),
29
+ },
30
+ });
@@ -95,7 +95,7 @@ export function observeAgentCli({
95
95
 
96
96
  // ⚠️ EXIT 0 AND NOTHING ON STDOUT IS NOT A CLEAN OBSERVATION — it is exactly what the discarded
97
97
  // probe looked like, and reporting `characterized against ""` would be the same lie in a new
98
- // place. This is rule 4 of CLAUDE.md applied to the check that enforces it.
98
+ // place. This is the green zero of scripts/rules-see-files.mjs, applied to the check that enforces it.
99
99
  if (version === "")
100
100
  throw new Error(
101
101
  `\`${program} --version\` exited 0 but printed no version on stdout (got ${JSON.stringify(raw)}` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "9.0.0",
3
+ "version": "9.0.1",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "ts-essentials": "^10.2.1",
35
35
  "typescript": "^6.0.3",
36
36
  "unpdf": "^1.8.1",
37
- "vigiles": "^32.1.0",
37
+ "vigiles": "^38.2.0",
38
38
  "zod": "^4.6.5"
39
39
  },
40
40
  "devDependencies": {