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.
- package/.github/CLAUDE.md +22 -0
- package/.github/CLAUDE.md.spec.ts +20 -0
- package/CLAUDE.md +59 -515
- package/CONTRIBUTING.md +2 -2
- package/dist/CLAUDE.md.spec.d.ts +3 -0
- package/dist/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/CLAUDE.md.spec.js +62 -0
- package/dist/CLAUDE.md.spec.js.map +1 -0
- package/dist/build.js +1 -1
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts +3 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
- package/dist/eslint-rules/install-path-literals.d.ts +1 -1
- package/dist/eslint-rules/install-path-literals.js +4 -4
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js +1 -1
- package/dist/lib/agent-cli-version.js +1 -1
- package/dist/lib/agent-cli-version.js.map +1 -1
- package/dist/tex-requirements.js +1 -1
- package/dist/toolchain.js +1 -1
- package/docs/e2e.md +1 -1
- package/docs/incidents.md +5 -5
- package/eslint-rules/CLAUDE.md +44 -0
- package/eslint-rules/CLAUDE.md.spec.ts +43 -0
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/install-path-literals.ts +4 -4
- package/eslint-rules/pdf-last-page-balance.ts +1 -1
- package/eslint.config.mjs +6 -3
- package/hooks/CLAUDE.md +34 -0
- package/hooks/CLAUDE.md.spec.ts +30 -0
- package/lib/agent-cli-version.ts +1 -1
- package/package.json +2 -2
- package/plugin/CLAUDE.md +22 -0
- package/plugin/CLAUDE.md.spec.ts +22 -0
- package/scripts/coverage-config.test.ts +2 -1
- package/skills/CLAUDE.md +19 -0
- package/skills/CLAUDE.md.spec.ts +18 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
- package/src/CLAUDE.md +23 -1
- package/src/CLAUDE.md.spec.ts +72 -0
- package/src/build.ts +1 -1
- package/src/tex-requirements.ts +1 -1
- 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
|
|
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#
|
|
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 @@
|
|
|
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 (
|
|
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 @@
|
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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 —
|
|
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 —
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,uEAAuE;AACvE,MAAM,gBAAgB,GAAG;IACvB,iBAAiB,EAAE,qDAAqD;IACxE,eAAe,EAAE,aAAa;IAC9B,kBAAkB,EAAE,mBAAmB;IACvC,gBAAgB,EAAE,wCAAwC;CAC3D,CAAC;AAEF,+FAA+F;AAC/F,SAAS,eAAe,CAAC,CAAU;IACjC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACvC,KAAK,MAAM,CAAC,IAAI,gBAAgB;QAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC;IAC9D,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4FAA4F;AAC5F,2FAA2F;AAC3F,0FAA0F;AAC1F,MAAM,IAAI,GAAG,4CAA4C,CAAC;AAE1D,MAAM,QAAQ,GAAqC;IACjD,WAAW,EACT,mFAAmF;QACnF,
|
|
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
|
|
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
|
|
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,
|
|
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"}
|
package/dist/tex-requirements.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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.
|
|
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 —
|
|
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 —
|
|
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
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
|
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
|
{
|
package/hooks/CLAUDE.md
ADDED
|
@@ -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
|
+
});
|
package/lib/agent-cli-version.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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": "^
|
|
37
|
+
"vigiles": "^38.2.0",
|
|
38
38
|
"zod": "^4.6.5"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|