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/plugin/CLAUDE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!-- vigiles:sha256:6312a3b8f9804eba compiled from plugin/CLAUDE.md.spec.ts -->
|
|
2
|
+
|
|
3
|
+
# CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## `plugin/`
|
|
6
|
+
|
|
7
|
+
Not a plugin any more. `plugin/hooks/hooks.json` is the hook wiring that `paperlint init` merges
|
|
8
|
+
into the consumer's `.claude/settings.json`, and it is the only source of that wiring.
|
|
9
|
+
|
|
10
|
+
## Why there is no Claude Code plugin
|
|
11
|
+
|
|
12
|
+
There is no Claude Code plugin or marketplace entry; it was removed in the release after 1.0.0
|
|
13
|
+
(#82). Do not bring it back without answering each of these, measured (`docs/install.md`,
|
|
14
|
+
"Why it is shaped this way"):
|
|
15
|
+
|
|
16
|
+
- **A plugin cannot carry the code.** Claude Code installs a plugin's dependencies with a 60-second
|
|
17
|
+
timeout, and a failed install never blocks the plugin: the hooks load and fail with
|
|
18
|
+
`cannot find module vigiles`, silently.
|
|
19
|
+
- **The skills need the npm package anyway.** 23 of 24 skills ran scripts that resolve only through
|
|
20
|
+
`node_modules/paperlint/`.
|
|
21
|
+
- **Its manifests carried versions nothing updated**, and Claude Code decides plugin updates from
|
|
22
|
+
that number.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Compiled to plugin/CLAUDE.md by `npx vigiles compile`. Edit THIS file, never the markdown.
|
|
2
|
+
import { file, instructionFile, prose } from "vigiles/spec";
|
|
3
|
+
|
|
4
|
+
export default instructionFile({
|
|
5
|
+
sections: {
|
|
6
|
+
"`plugin/`": prose`Not a plugin any more. ${file("plugin/hooks/hooks.json")} is the hook wiring that \`paperlint init\` merges
|
|
7
|
+
into the consumer's \`.claude/settings.json\`, and it is the only source of that wiring.`,
|
|
8
|
+
|
|
9
|
+
"Why there is no Claude Code plugin": prose`There is no Claude Code plugin or marketplace entry; it was removed in the release after 1.0.0
|
|
10
|
+
(#82). Do not bring it back without answering each of these, measured (${file("docs/install.md")},
|
|
11
|
+
"Why it is shaped this way"):
|
|
12
|
+
|
|
13
|
+
- **A plugin cannot carry the code.** Claude Code installs a plugin's dependencies with a 60-second
|
|
14
|
+
timeout, and a failed install never blocks the plugin: the hooks load and fail with
|
|
15
|
+
\`cannot find module vigiles\`, silently.
|
|
16
|
+
- **The skills need the npm package anyway.** 23 of 24 skills ran scripts that resolve only through
|
|
17
|
+
\`node_modules/paperlint/\`.
|
|
18
|
+
- **Its manifests carried versions nothing updated**, and Claude Code decides plugin updates from
|
|
19
|
+
that number.`,
|
|
20
|
+
},
|
|
21
|
+
rules: {},
|
|
22
|
+
});
|
|
@@ -400,8 +400,9 @@ test("the exclude list is exactly the justified set", () => {
|
|
|
400
400
|
"**/*.test.*",
|
|
401
401
|
// PAID LLM evaluations, never run by `npm test`
|
|
402
402
|
"**/*.eval.mjs",
|
|
403
|
-
// skill sources vigiles compiles
|
|
403
|
+
// skill and CLAUDE.md sources vigiles compiles; a glob's `**` skips a dot directory
|
|
404
404
|
"**/*.spec.ts",
|
|
405
|
+
".github/*.spec.ts",
|
|
405
406
|
// type declarations
|
|
406
407
|
"**/*.d.ts",
|
|
407
408
|
"**/*.d.mts",
|
package/skills/CLAUDE.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- vigiles:sha256:e0aa2cec8926b04f compiled from skills/CLAUDE.md.spec.ts -->
|
|
2
|
+
|
|
3
|
+
# CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## `skills/`
|
|
6
|
+
|
|
7
|
+
The skills this package ships. Loaded when working under `skills/`. The consumer's root and the
|
|
8
|
+
package's location come from `skills/paper-pipeline/scripts/consumer.mjs`; the links are made by
|
|
9
|
+
`src/link-skills.ts`; install-specific paths are flagged by `eslint-rules/install-path-literals.ts`.
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
### Skills Ship In Skills
|
|
14
|
+
|
|
15
|
+
**Guidance only** — Skills ship in `skills/`, never `.claude/skills/`. `paperlint init` symlinks each one into the consumer's `.claude/skills/<name>`; the link's directory name is the skill's name. `skills/` is the npm layout: of 855 packages scanned, 336 shipped `skills/` against 15 `.claude/skills/`. Rejected: leaving the skills for Claude Code to find inside `node_modules` (`docs/install.md`); a symlink loads at startup, deterministically.
|
|
16
|
+
|
|
17
|
+
### A Criterion That Judges Prose
|
|
18
|
+
|
|
19
|
+
**Guidance only** — It follows the five principles in `eslint-rules/CLAUDE.md` ("A rule that judges prose"). A skill does not grep prose for what a rule can count.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Compiled to skills/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
|
+
"`skills/`": prose`The skills this package ships. Loaded when working under \`skills/\`. The consumer's root and the
|
|
7
|
+
package's location come from ${file("skills/paper-pipeline/scripts/consumer.mjs")}; the links are made by
|
|
8
|
+
${file("src/link-skills.ts")}; install-specific paths are flagged by ${file("eslint-rules/install-path-literals.ts")}.`,
|
|
9
|
+
},
|
|
10
|
+
rules: {
|
|
11
|
+
"skills-ship-in-skills": guidance(
|
|
12
|
+
"Skills ship in `skills/`, never `.claude/skills/`. `paperlint init` symlinks each one into the consumer's `.claude/skills/<name>`; the link's directory name is the skill's name. `skills/` is the npm layout: of 855 packages scanned, 336 shipped `skills/` against 15 `.claude/skills/`. Rejected: leaving the skills for Claude Code to find inside `node_modules` (`docs/install.md`); a symlink loads at startup, deterministically.",
|
|
13
|
+
),
|
|
14
|
+
"a-criterion-that-judges-prose": guidance(
|
|
15
|
+
'It follows the five principles in `eslint-rules/CLAUDE.md` ("A rule that judges prose"). A skill does not grep prose for what a rule can count.',
|
|
16
|
+
),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -1,51 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* plan-paper-timeline — the EFFECTS tier. What the run
|
|
3
|
-
* SKILL.md says it
|
|
2
|
+
* plan-paper-timeline — the EFFECTS tier. What the run actually does, not what
|
|
3
|
+
* SKILL.md says it does.
|
|
4
4
|
*
|
|
5
5
|
* SIBLING, NOT REPLACEMENT. `plan-paper-timeline.harness.mjs` next door is static
|
|
6
6
|
* (`checkSkill()`): frontmatter, wiring, script paths, verdict vocabulary. It is untouched.
|
|
7
7
|
* This file runs the REAL `claude` CLI against a SCRIPTED MOCK MODEL (`runHarnessTest`),
|
|
8
|
-
* so it costs $0 and no API key
|
|
9
|
-
* cannot: whether the tools this skill DECLARES EXIST WHERE IT RUNS.
|
|
8
|
+
* so it costs $0 and no API key.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* declared entries are MCP calendar tools. A declaration naming a tool that is not there is
|
|
15
|
-
* invisible to every static check in this repo and to `vigiles audit`, which scores this
|
|
16
|
-
* corpus Structure 100.
|
|
10
|
+
* Run A: the skill activates, its body reaches the model, and a declared, granted tool
|
|
11
|
+
* (`Read`) works. Run B: §3's prescribed `create_event` call, parsed out of SKILL.md,
|
|
12
|
+
* replayed against a strict calendar double.
|
|
17
13
|
*
|
|
18
|
-
* ──
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* ── WHAT THIS PINS (a characterization test, deliberately) ───────────────────────
|
|
27
|
-
* Under `claude -p` at `CHARACTERIZED_CLI` (`lib/agent-cli-version.mjs`, 2.1.227) the whole
|
|
28
|
-
* `mcp__*` surface is ABSENT, and so is `ToolSearch`.
|
|
29
|
-
*
|
|
30
|
-
* 🔴 THAT NUMBER IS A VALUE, NOT PROSE, and it was prose until issue #7. The probe below used
|
|
31
|
-
* to run with `stdio: "ignore"`, so the version the CLI printed was never read — this tier
|
|
32
|
-
* characterized whatever the runner installed that day while this paragraph kept saying 2.1.227.
|
|
33
|
-
* Measured: 2.1.273 on the reporter's machine and on this one, forty-six patch releases of drift
|
|
34
|
-
* that nothing in a green log could show. The version is now OBSERVED, reported in the summary
|
|
35
|
-
* below, and named in every failure message — so a red says which version it characterized.
|
|
36
|
-
* That means:
|
|
37
|
-
* 1. §3 of this skill is UNEXECUTABLE in any headless / CI context. It is an
|
|
38
|
-
* interactive-session-only step. Nothing in the file says so.
|
|
39
|
-
* 2. 🔴 THE RECORDED `ToolSearch` FINDING DOES NOT REPRODUCE HERE, AND THE CORRECTION
|
|
40
|
-
* MATTERS. `.claude/lib/skill-effects-results/README.md` (2026-08-10, $0.82)
|
|
41
|
-
* recorded "the model called ToolSearch … it isn't in allowed-tools," with the caveat
|
|
42
|
-
* that it might be an artifact of the headless environment. It is the opposite:
|
|
43
|
-
* headless has NO ToolSearch to call. Adding `ToolSearch` to `allowed-tools` would
|
|
44
|
-
* therefore fix nothing here. The reachability of these four tools is a property of
|
|
45
|
-
* the ENVIRONMENT, not of the declaration.
|
|
46
|
-
*
|
|
47
|
-
* If either fact changes, this file fails and says which one — that is its whole job.
|
|
48
|
-
* It is not asserting that absence is good.
|
|
14
|
+
* ── WHY RUN A NO LONGER PROBES THE CALENDAR ─────────────────────────────────────
|
|
15
|
+
* It used to call `mcp__Google_Calendar__list_events` and `ToolSearch` and pin "No such
|
|
16
|
+
* tool available" as a finding about headless runs. It was a finding about the FIXTURE:
|
|
17
|
+
* the run happens in an empty `mkdtemp` with no MCP server configured, so the calendar
|
|
18
|
+
* tool is missing by construction, whatever the CLI can do. vigiles 38 refuses a
|
|
19
|
+
* scripted call to a tool the CLI does not have, for exactly this reason. Run B is
|
|
20
|
+
* where the calendar step is tested, with the surface supplied.
|
|
49
21
|
*
|
|
50
22
|
* Cost: $0 (scripted mock model, no API key). Wall clock ~30-60s for one CLI spawn.
|
|
51
23
|
*/
|
|
@@ -114,37 +86,19 @@ const r = await runHarnessTest({
|
|
|
114
86
|
transcript: true,
|
|
115
87
|
timeoutMs: 180_000,
|
|
116
88
|
prompt: "plan the AISec timeline",
|
|
117
|
-
// The SESSION grant
|
|
118
|
-
|
|
119
|
-
// a control and a measurement inside ONE permission configuration.
|
|
120
|
-
allowedTools: ["Read", "Write", "Skill", "ToolSearch", ...declaredMcp],
|
|
89
|
+
// The SESSION grant: the tools the script below calls, and no others.
|
|
90
|
+
allowedTools: ["Read", "Skill"],
|
|
121
91
|
files: { "cfp.md": "AISec 2026 — submission 24 Jul AoE\n" },
|
|
122
92
|
model: [
|
|
123
93
|
{ tool: "Skill", input: { skill: "plan-paper-timeline" } },
|
|
124
|
-
// CONTROL: a tool that is present and permitted.
|
|
125
|
-
// verdict below would be a broken run, not a finding.
|
|
94
|
+
// CONTROL: a tool that is present and permitted.
|
|
126
95
|
{ tool: "Read", input: { file_path: "cfp.md" } },
|
|
127
|
-
// READ-ONLY calendar probe. Never create_event / update_event — see header.
|
|
128
|
-
{
|
|
129
|
-
tool: "mcp__Google_Calendar__list_events",
|
|
130
|
-
input: { calendarId: "primary" },
|
|
131
|
-
},
|
|
132
|
-
// The tool the 2026-08-10 sweep said was missing from the declaration.
|
|
133
|
-
{
|
|
134
|
-
tool: "ToolSearch",
|
|
135
|
-
input: {
|
|
136
|
-
query: "select:mcp__Google_Calendar__create_event",
|
|
137
|
-
max_results: 1,
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
96
|
{ text: "done" },
|
|
141
97
|
],
|
|
142
98
|
});
|
|
143
99
|
|
|
144
100
|
try {
|
|
145
101
|
const call = (name) => r.toolCalls.find((c) => c.name === name);
|
|
146
|
-
const absent = (c) =>
|
|
147
|
-
c !== undefined && c.isError && /No such tool available/.test(c.resultText);
|
|
148
102
|
|
|
149
103
|
// ── 1. The run actually happened and the skill actually loaded ──────────────────
|
|
150
104
|
// Asserted FIRST and separately from everything below. A confident "nothing was
|
|
@@ -172,48 +126,14 @@ try {
|
|
|
172
126
|
throw new Error(
|
|
173
127
|
`POSITIVE CONTROL FAILED: Read — a tool this skill declares and the session grants — ` +
|
|
174
128
|
`did not succeed (${ctrl ? ctrl.resultText.slice(0, 160) : "never called"}). ` +
|
|
175
|
-
`
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
// ── 2. The pinned environment fact: the declared MCP surface is not there ───────
|
|
179
|
-
const cal = call("mcp__Google_Calendar__list_events");
|
|
180
|
-
if (!absent(cal))
|
|
181
|
-
throw new Error(
|
|
182
|
-
`THE ENVIRONMENT CHANGED — and this is good news that must be read, not silenced. ` +
|
|
183
|
-
`Observed \`claude\` ${cli.version} (characterized against ${cli.characterized}). ` +
|
|
184
|
-
`\`mcp__Google_Calendar__list_events\` is now REACHABLE from \`claude -p\` ` +
|
|
185
|
-
`(result: ${cal ? cal.resultText.slice(0, 200) : "no call recorded"}). ` +
|
|
186
|
-
`Consequences: (a) §3 of this skill is now executable headlessly, so it can finally be ` +
|
|
187
|
-
`tested for real instead of characterized; (b) 🔴 every future run of this tier can now ` +
|
|
188
|
-
`WRITE A REAL CALENDAR — re-check the safety argument in this file's header BEFORE ` +
|
|
189
|
-
`adding a create_event probe; (c) update this assertion deliberately, do not delete it.`,
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const ts = call("ToolSearch");
|
|
193
|
-
if (!absent(ts))
|
|
194
|
-
throw new Error(
|
|
195
|
-
`\`ToolSearch\` is now available under \`claude -p\` ${cli.version} ` +
|
|
196
|
-
`(characterized against ${cli.characterized}) (result: ` +
|
|
197
|
-
`${ts ? ts.resultText.slice(0, 200) : "no call recorded"}). The 2026-08-10 sweep recorded ` +
|
|
198
|
-
`"ToolSearch missing from allowed-tools" as a finding about THIS SKILL; measured on ` +
|
|
199
|
-
`2026-08-11 it was a fact about the ENVIRONMENT (headless had no ToolSearch at all). ` +
|
|
200
|
-
`If it exists now, that finding becomes actionable again: add it to allowed-tools.`,
|
|
129
|
+
`The skill loaded but the run cannot use the tools it declares.`,
|
|
201
130
|
);
|
|
202
131
|
|
|
203
|
-
// ──
|
|
132
|
+
// ── 2. The version goes in the REPORT, not only into failures ──────────────────
|
|
204
133
|
console.log(
|
|
205
134
|
`✓ plan-paper-timeline [effects]: skill activated and its body reached the model; ` +
|
|
206
135
|
`Read (declared+granted) works — the run is live.\n` +
|
|
207
|
-
|
|
208
|
-
// silently still holds is indistinguishable from one nobody re-checked, so the run says
|
|
209
|
-
// which version it held against — and says it whether or not the numbers agree.
|
|
210
|
-
` CLI: ${cli.note}\n` +
|
|
211
|
-
` PINNED: ${String(declaredMcp.length)} of its 10 declared entries are MCP calendar tools, ` +
|
|
212
|
-
`and the whole mcp__* surface is ABSENT under \`claude -p\`; \`ToolSearch\` is absent too.\n` +
|
|
213
|
-
` → §3 ("Create the calendar events") is an INTERACTIVE-SESSION-ONLY step. Nothing in ` +
|
|
214
|
-
`SKILL.md says so, and no static check can see it.\n` +
|
|
215
|
-
` → Corrects the 2026-08-10 sweep: the ToolSearch gap was environmental, not a defect ` +
|
|
216
|
-
`in this declaration.`,
|
|
136
|
+
` CLI: ${cli.note}`,
|
|
217
137
|
);
|
|
218
138
|
} finally {
|
|
219
139
|
r.cleanup();
|
|
@@ -222,9 +142,8 @@ try {
|
|
|
222
142
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
223
143
|
// RUN B — §3 EXECUTED, not characterized
|
|
224
144
|
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// checked by anything. Run B supplies the surface with a stand-in
|
|
145
|
+
// A headless run in an empty work dir has no calendar surface, so without a stand-in
|
|
146
|
+
// the skill's CENTRAL STEP is checked by nothing. Run B supplies the surface with a stand-in
|
|
228
147
|
// (`fixtures/fake-google-calendar.ts`) carrying the REAL API's required-field sets,
|
|
229
148
|
// so the step becomes executable for the first time.
|
|
230
149
|
//
|
package/src/CLAUDE.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- vigiles:sha256:f51051aae2ef09f8 compiled from src/CLAUDE.md.spec.ts -->
|
|
2
|
+
|
|
3
|
+
# CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## `src/`
|
|
2
6
|
|
|
3
7
|
The package's TypeScript: the CLI, the build, the toolchain installer, and the measuring that feeds
|
|
4
8
|
the facts file. Loaded when working under `src/`; the repository-wide rules are in the root
|
|
5
9
|
`CLAUDE.md`.
|
|
6
10
|
|
|
11
|
+
## The two layer axes
|
|
12
|
+
|
|
7
13
|
**`src/` is hexagonal on two axes, and the linter enforces both** (`eslint.config.mjs`; fixtures in
|
|
8
14
|
`test/fixtures/layers/`, run by `test/eslint-layers.test.ts`; issue #76).
|
|
9
15
|
|
|
@@ -49,3 +55,19 @@ against `process.cwd()`, so lint started from any other directory classifies not
|
|
|
49
55
|
`checkAllOrigins: true`: without it no `module:` policy is evaluated, so axis B and the library bans
|
|
50
56
|
report nothing — dropping it turns the fixtures `src/domain/reads-disk.ts` and
|
|
51
57
|
`src/adapters/one/disk.ts` clean (measured).
|
|
58
|
+
|
|
59
|
+
## Readers and commands
|
|
60
|
+
|
|
61
|
+
`@retorquere/bibtex-parser` is the one `.bib` reader, behind `src/ports/bib-reader.ts`.
|
|
62
|
+
Rejected on 2026-09-27, so not proposed again: a separate `paperlint refs` command; the slowness it
|
|
63
|
+
would have moved was a cache thrown away after every build (`docs/references.md`).
|
|
64
|
+
|
|
65
|
+
## Rules
|
|
66
|
+
|
|
67
|
+
### Layers
|
|
68
|
+
|
|
69
|
+
**Enforced by:** `eslint/boundaries/dependencies`
|
|
70
|
+
|
|
71
|
+
### Effect Globals
|
|
72
|
+
|
|
73
|
+
**Enforced by:** `eslint/no-restricted-globals`
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
|
|
4
|
+
export default instructionFile({
|
|
5
|
+
sections: {
|
|
6
|
+
"`src/`": prose`The package's TypeScript: the CLI, the build, the toolchain installer, and the measuring that feeds
|
|
7
|
+
the facts file. Loaded when working under \`src/\`; the repository-wide rules are in the root
|
|
8
|
+
\`CLAUDE.md\`.`,
|
|
9
|
+
|
|
10
|
+
"The two layer axes": prose`**\`src/\` is hexagonal on two axes, and the linter enforces both** (\`eslint.config.mjs\`; fixtures in
|
|
11
|
+
\`test/fixtures/layers/\`, run by ${file("test/eslint-layers.test.ts")}; issue #76).
|
|
12
|
+
|
|
13
|
+
**Axis A — knowledge. Core means the DOMAIN, not "no I/O".** A file belongs to an adapter when it
|
|
14
|
+
exists because of one external program or format: _if banal / pdf.js / TeX Live / ESLint / Claude
|
|
15
|
+
Code disappeared tomorrow, which files would change? Those are its adapter, wherever they sit
|
|
16
|
+
today._ The domain is what would not change if every one of them were swapped. Purity is not the
|
|
17
|
+
test — a parser for banal's JSON is pure and is still banal's.
|
|
18
|
+
|
|
19
|
+
| element | where | holds | may import |
|
|
20
|
+
| --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
|
21
|
+
| \`domain\` | \`src/domain/\` | values and decisions in the paper's vocabulary | \`domain\`; \`ts-essentials\`, \`node:path\`, \`node:crypto\` |
|
|
22
|
+
| \`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 |
|
|
23
|
+
| \`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\` |
|
|
24
|
+
| \`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 |
|
|
25
|
+
| \`root\` | \`src/cli.ts\` | reads \`process.*\`, builds adapters, wires ports | anything |
|
|
26
|
+
|
|
27
|
+
Folders are ELEMENTS of \`eslint-plugin-boundaries\`, single files are CATEGORIES (\`root\`, \`app\`,
|
|
28
|
+
\`test\`, \`io\`): v7 matches element patterns as folders. Everyone — tests included — reaches an
|
|
29
|
+
adapter through its \`index.ts\`; the files inside one adapter import each other freely.
|
|
30
|
+
|
|
31
|
+
**Axis B — purity.** An effect (disk, process, network, env, clock, TTY) may be written only in
|
|
32
|
+
\`src/adapters/<tool>/<x>.io.ts\` and \`src/cli.ts\`. Every other file, adapters' parsers included,
|
|
33
|
+
takes a port. Test: _can a call here fail for a reason not in its arguments?_ Imports are checked
|
|
34
|
+
through the \`io\` file category in \`boundaries/dependencies\`; the \`process\` and \`fetch\` globals by
|
|
35
|
+
\`no-restricted-globals\`.
|
|
36
|
+
|
|
37
|
+
**The legacy app files are held by a ratchet, not exempted.** A site that still breaks either axis
|
|
38
|
+
carries \`// eslint-disable-next-line <rule> -- legacy <I/O|layer>, moves behind a port in #76\`.
|
|
39
|
+
Unused directives are errors (\`reportUnusedDisableDirectives\`), so a fixed site must drop its
|
|
40
|
+
comment; the per-file counts are frozen in \`scripts/layer-legacy.frozen.json\` and checked by
|
|
41
|
+
${file("scripts/layer-legacy-frozen.ts")} (\`npm run check\`, CI), which counts ESLint's own
|
|
42
|
+
\`suppressedMessages\` and fails when a file's count grows, a file not in the list carries one, or
|
|
43
|
+
anything under \`src/domain/\`, \`src/ports/\` or \`src/adapters/\` does. A NEW module that needs the
|
|
44
|
+
world is a new \`*.io.ts\` in the adapter of the program it talks to — never a new directive.
|
|
45
|
+
|
|
46
|
+
Parse at the boundary, into types that cannot hold an invalid state (a banal path that exists only
|
|
47
|
+
after its sha256 and probe passed; a \`Ready\` only an installer mints; an outcome as a discriminated
|
|
48
|
+
union, not a string); the inside never re-validates.
|
|
49
|
+
|
|
50
|
+
⚠️ **Two settings are load-bearing.** \`boundaries/root-path\`: without it the plugin matches paths
|
|
51
|
+
against \`process.cwd()\`, so lint started from any other directory classifies nothing and passes.
|
|
52
|
+
\`checkAllOrigins: true\`: without it no \`module:\` policy is evaluated, so axis B and the library bans
|
|
53
|
+
report nothing — dropping it turns the fixtures \`src/domain/reads-disk.ts\` and
|
|
54
|
+
\`src/adapters/one/disk.ts\` clean (measured).`,
|
|
55
|
+
|
|
56
|
+
"Readers and commands": prose`\`@retorquere/bibtex-parser\` is the one \`.bib\` reader, behind ${file("src/ports/bib-reader.ts")}.
|
|
57
|
+
Rejected on 2026-09-27, so not proposed again: a separate \`paperlint refs\` command; the slowness it
|
|
58
|
+
would have moved was a cache thrown away after every build (${file("docs/references.md")}).`,
|
|
59
|
+
},
|
|
60
|
+
rules: {
|
|
61
|
+
layers: enforce(
|
|
62
|
+
"eslint/boundaries/dependencies",
|
|
63
|
+
"Both axes: what a layer may import, and where an effect may be written.",
|
|
64
|
+
{ render: "id" },
|
|
65
|
+
),
|
|
66
|
+
"effect-globals": enforce(
|
|
67
|
+
"eslint/no-restricted-globals",
|
|
68
|
+
"The process and fetch globals only where an effect may be written.",
|
|
69
|
+
{ render: "id" },
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
});
|
package/src/build.ts
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";
|
package/src/tex-requirements.ts
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/src/toolchain.ts
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.
|