paperlint 9.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CLAUDE.md +22 -0
- package/.github/CLAUDE.md.spec.ts +20 -0
- package/CLAUDE.md +59 -515
- package/CONTRIBUTING.md +2 -2
- package/README.md +68 -128
- 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.d.ts +1 -1
- package/dist/build.js +2 -2
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -6
- package/dist/cli.js.map +1 -1
- package/dist/domain/layout-tree.d.ts +19 -0
- package/dist/domain/layout-tree.d.ts.map +1 -0
- package/dist/domain/layout-tree.js +32 -0
- package/dist/domain/layout-tree.js.map +1 -0
- package/dist/domain/lookup-cache.d.ts +2 -2
- package/dist/domain/lookup-cache.js +2 -2
- package/dist/domain/paper-layout.d.ts +81 -0
- package/dist/domain/paper-layout.d.ts.map +1 -0
- package/dist/domain/paper-layout.js +259 -0
- package/dist/domain/paper-layout.js.map +1 -0
- package/dist/domain/repro-layout.d.ts +80 -0
- package/dist/domain/repro-layout.d.ts.map +1 -0
- package/dist/domain/repro-layout.js +247 -0
- package/dist/domain/repro-layout.js.map +1 -0
- package/dist/domain/scaffold-layout.d.ts +28 -0
- package/dist/domain/scaffold-layout.d.ts.map +1 -0
- package/dist/domain/scaffold-layout.js +81 -0
- package/dist/domain/scaffold-layout.js.map +1 -0
- 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/eslint-rules/prereg-coverage.d.ts +98 -0
- package/dist/eslint-rules/prereg-coverage.d.ts.map +1 -0
- package/dist/eslint-rules/prereg-coverage.js +274 -0
- package/dist/eslint-rules/prereg-coverage.js.map +1 -0
- package/dist/folder-layout-hook.d.ts +21 -0
- package/dist/folder-layout-hook.d.ts.map +1 -0
- package/dist/folder-layout-hook.js +117 -0
- package/dist/folder-layout-hook.js.map +1 -0
- package/dist/folder-layout-rule.d.ts +22 -0
- package/dist/folder-layout-rule.d.ts.map +1 -0
- package/dist/folder-layout-rule.js +107 -0
- package/dist/folder-layout-rule.js.map +1 -0
- package/dist/init.js +1 -1
- package/dist/init.js.map +1 -1
- package/dist/lib/agent-cli-version.js +1 -1
- package/dist/lib/agent-cli-version.js.map +1 -1
- package/dist/new-paper.d.ts +4 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +40 -6
- package/dist/new-paper.js.map +1 -1
- package/dist/references.d.ts +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/repro-layout-rule.d.ts +16 -0
- package/dist/repro-layout-rule.d.ts.map +1 -0
- package/dist/repro-layout-rule.js +81 -0
- package/dist/repro-layout-rule.js.map +1 -0
- package/dist/tex-requirements.js +1 -1
- package/dist/toolchain.js +1 -1
- package/docs/ci.md +60 -0
- package/docs/configuration.md +2 -0
- package/docs/e2e.md +3 -3
- package/docs/incidents.md +5 -5
- package/docs/install.md +20 -16
- package/docs/layout.md +154 -0
- package/docs/optional-rules.md +5 -4
- package/docs/references.md +5 -2
- package/docs/rules/paper/allowed-files-only.md +192 -0
- package/docs/rules/repro/allowed-files-only.md +128 -0
- package/docs/rules/study/declare-sample.md +146 -0
- package/docs/rules.md +53 -50
- package/docs/skills.md +6 -3
- package/docs/venues.md +94 -0
- 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-rules/prereg-coverage.test.ts +448 -0
- package/eslint-rules/prereg-coverage.ts +419 -0
- 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/lib/skill-trigger-cases.harness.mjs +51 -5
- package/lib/skill-trigger-cases.mjs +53 -15
- package/package.json +2 -2
- package/plugin/CLAUDE.md +22 -0
- package/plugin/CLAUDE.md.spec.ts +22 -0
- package/plugin/hooks/hooks.json +9 -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/analyze-sibling-paper/SKILL.md +2 -2
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +1 -1
- package/skills/build-benchmark/SKILL.md +14 -1
- package/skills/build-benchmark/SKILL.md.spec.ts +13 -0
- package/skills/build-label-page/SKILL.md +118 -0
- package/skills/build-label-page/SKILL.md.spec.ts +129 -0
- package/skills/build-label-page/build-label-page.eval.mjs +15 -0
- package/skills/build-label-page/build-label-page.test.ts +959 -0
- package/skills/build-label-page/references/checklist.md +148 -0
- package/skills/build-label-page/references/example-generator.py +803 -0
- package/skills/find-venue/SKILL.md +5 -3
- package/skills/find-venue/SKILL.md.spec.ts +4 -2
- package/skills/grade-paper-writing/SKILL.md +7 -7
- package/skills/grade-paper-writing/SKILL.md.spec.ts +6 -6
- package/skills/map-prior-work/SKILL.md +2 -2
- package/skills/map-prior-work/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +8 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +7 -5
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +19 -9
- package/skills/paper-pipeline/references/acceptance-gate.md +19 -19
- package/skills/paper-pipeline/references/artifact-checklist.md +5 -0
- package/skills/paper-pipeline/references/pipeline-status-template.md +30 -1
- package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +1 -1
- package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +48 -48
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +8 -8
- package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +48 -48
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +2 -2
- package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +3 -3
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-after.log +81 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-preflight-before.log +81 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-ablation-setupdiff.log +59 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.jsonl +1 -0
- package/skills/paper-pipeline/repro/2026-10-13-grade-paper-writing-trigger.log +22 -0
- package/skills/paper-pipeline/repro/README.md +4 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +2 -2
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +2 -2
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +4 -2
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +7 -2
- package/skills/paper-pipeline/scripts/generated-code.mjs +8 -4
- package/skills/paper-pipeline/scripts/generated-code.test.ts +15 -1
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
- package/skills/study-accepted-papers/SKILL.md +163 -161
- package/skills/study-accepted-papers/SKILL.md.spec.ts +167 -165
- package/skills/study-accepted-papers/references/base-rates.md +69 -0
- package/skills/study-accepted-papers/references/mechanisms.md +181 -0
- package/skills/verify-citations/SKILL.md +2 -2
- package/skills/verify-citations/SKILL.md.spec.ts +1 -1
- package/src/CLAUDE.md +23 -1
- package/src/CLAUDE.md.spec.ts +72 -0
- package/src/build.ts +2 -2
- package/src/cli.harness.mjs +42 -0
- package/src/cli.test.ts +90 -6
- package/src/cli.ts +78 -5
- package/src/domain/layout-tree.test.ts +32 -0
- package/src/domain/layout-tree.ts +47 -0
- package/src/domain/lookup-cache.ts +2 -2
- package/src/domain/paper-layout.test.ts +264 -0
- package/src/domain/paper-layout.ts +336 -0
- package/src/domain/repro-layout.test.ts +222 -0
- package/src/domain/repro-layout.ts +311 -0
- package/src/domain/scaffold-layout.ts +104 -0
- package/src/folder-layout-hook.test.ts +214 -0
- package/src/folder-layout-hook.ts +141 -0
- package/src/folder-layout-rule.test.ts +235 -0
- package/src/folder-layout-rule.ts +151 -0
- package/src/hooks-settings.harness.ts +9 -8
- package/src/init.ts +1 -1
- package/src/layout-docs.test.ts +52 -0
- package/src/new-paper.harness.mjs +23 -1
- package/src/new-paper.test.ts +299 -6
- package/src/new-paper.ts +51 -5
- package/src/paper-files.test.ts +9 -6
- package/src/references.test.ts +5 -5
- package/src/references.ts +2 -2
- package/src/repro-layout-rule.test.ts +114 -0
- package/src/repro-layout-rule.ts +121 -0
- package/src/rule-cases.test.ts +98 -0
- package/src/tex-requirements.ts +1 -1
- package/src/toolchain.ts +1 -1
- package/templates/paper/PIPELINE-STATUS.md +41 -7
package/CLAUDE.md
CHANGED
|
@@ -1,560 +1,104 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- vigiles:sha256:e445489aca0dd970 compiled from CLAUDE.md.spec.ts -->
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
knowledge base.
|
|
3
|
+
# CLAUDE.md
|
|
5
4
|
|
|
6
|
-
##
|
|
7
|
-
|
|
8
|
-
**Move every paper-writing convention a machine can decide OUT of prose and INTO an engine
|
|
9
|
-
that fails the build** — and keep everything else honestly labelled as prose. The engine is
|
|
10
|
-
ESLint. The unit of progress is "one more verdict decided by the engine instead of by a
|
|
11
|
-
hand-written script".
|
|
12
|
-
|
|
13
|
-
That last clause is the whole direction, and it is the part that keeps being forgotten:
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
prose in a guideline → hand-written script → ESLint rule in the engine
|
|
17
|
-
rots silently runs, but is OURS editor-time · free AST · suppressible
|
|
18
|
-
← where this started ← where most of it is ← where it is going
|
|
19
|
-
```
|
|
5
|
+
## Paperlint
|
|
20
6
|
|
|
21
|
-
|
|
22
|
-
only when the engine genuinely cannot express the check — and "cannot" means MEASURED, not
|
|
23
|
-
assumed. Two things that sound like limits and are not:
|
|
24
|
-
|
|
25
|
-
- _"ESLint only sees one file"_ — true of its AST, false of the rule: a rule is an ordinary
|
|
26
|
-
JS module and may call `execFileSync("git", …)` or read a sibling file. If the reason to
|
|
27
|
-
stay a script is "it needs git", that reason is weak; measure the real cost before using it.
|
|
28
|
-
- _"this runs programs, not lints files"_ — that is a real limit, and the answer is the seam
|
|
29
|
-
already proven here: **a script PRODUCES facts into a JSON file, and ESLint JUDGES that
|
|
30
|
-
file.** The verdict lands in the engine even though the work did not.
|
|
31
|
-
|
|
32
|
-
## 📊 STATE — measured 2026-09-16 (re-measure, never cite)
|
|
33
|
-
|
|
34
|
-
| | |
|
|
35
|
-
| ----------------------- | -----------------------------------------------------------------------------------------: |
|
|
36
|
-
| ESLint rules | **5** — `latex-language` · `tex-build` · `papers` · `review-findings-cause` · `doc-fields` |
|
|
37
|
-
| harnesses | **57** |
|
|
38
|
-
| skills | **24** |
|
|
39
|
-
| hooks (runnable `.mjs`) | **5** |
|
|
40
|
-
| repo-wide scripts | 5 |
|
|
41
|
-
| files tracked / commits | 308 / 43 |
|
|
42
|
-
|
|
43
|
-
**A real consumer dogfoods this package on every CI run**, so a breaking change here turns a
|
|
44
|
-
paper pipeline red somewhere else the same day. That is deliberate — it is the only thing
|
|
45
|
-
keeping the extraction honest.
|
|
46
|
-
|
|
47
|
-
⚠️ **This table is a SNAPSHOT, not a fact about today, and it has already gone stale once.**
|
|
48
|
-
The line standing here until 2026-09-16 said "two ESLint rule modules" while six were shipped,
|
|
49
|
-
24 skills had moved in, and hooks existed at all. Re-measure with `git ls-files` before
|
|
50
|
-
repeating any number from it.
|
|
7
|
+
Machine-checkable gates for writing a research paper in git, extracted from a private knowledge base.
|
|
51
8
|
|
|
52
|
-
|
|
9
|
+
This file loads on every turn, so it holds only what applies to **any** change here. A rule that applies to one kind of work lives in that folder's `CLAUDE.md`, which loads when a file there is read or edited (table below); a fact a machine checks lives beside that check. Every `CLAUDE.md` here is compiled from the `CLAUDE.md.spec.ts` beside it: edit the spec, then `npx vigiles compile`. What broke is in `docs/incidents.md`; the measurements behind the rules are in [git](https://github.com/zernie/paperlint/blob/b4a3dc3a81d436186f41db2d69b65fd93f1af3b6/CLAUDE.md#L62-L199).
|
|
53
10
|
|
|
54
|
-
|
|
55
|
-
npm install
|
|
56
|
-
```
|
|
11
|
+
## 🎯 THE GOAL, in one sentence
|
|
57
12
|
|
|
58
|
-
|
|
59
|
-
harness, spec and hook resolves through it. A container where `npm install` never ran fails
|
|
60
|
-
in ways that look like broken code rather than a missing install.
|
|
61
|
-
|
|
62
|
-
## The ten rules that decide what may live here — and what may not be written
|
|
63
|
-
|
|
64
|
-
**1. Mechanism goes to vigiles, data stays here.** A file that names nothing local — no rule
|
|
65
|
-
of ours, no fixture of ours — is machinery, and machinery belongs in
|
|
66
|
-
[vigiles](https://github.com/zernie/vigiles). Ask it in two steps: is this mechanism or data?
|
|
67
|
-
If mechanism — does it know about _this_ domain? If not, it is not ours.
|
|
68
|
-
|
|
69
|
-
**2. A check over an AST is a LINT RULE, not a script.** If it walks `.ts`/`.js`/`.tex` and
|
|
70
|
-
looks at declarations, names or nodes, write an ESLint rule: it fires in the editor on save,
|
|
71
|
-
gets the AST for free, and has a suppression syntax people already know. Scripts are for
|
|
72
|
-
corpus-wide questions — index connectivity, ratios across many files — not for one file's
|
|
73
|
-
nodes.
|
|
74
|
-
|
|
75
|
-
**3. Every check needs BOTH halves, or it is not tested.** It must FIRE on a planted defect
|
|
76
|
-
and stay QUIET on a clean fixture. A check that has only been seen quiet is
|
|
77
|
-
indistinguishable from a dead one — silence is its success state. Prove the fire half by
|
|
78
|
-
seeing the test go RED before the fix lands ([`docs/testing.md`](docs/testing.md)).
|
|
79
|
-
|
|
80
|
-
**One rule, one requirement.** A rule checks one thing a user would want to switch off, or set a
|
|
81
|
-
severity for, on its own; two checks with different reasons to disable them are two rules. Several
|
|
82
|
-
messageIds for facets of one requirement are fine. Example: `pdf/limits` checked body pages,
|
|
83
|
-
reference pages and the reference font size — the first two are one page budget and became
|
|
84
|
-
`format/page-limit`; the font size is a different requirement and moved to `pdf/body-size`.
|
|
85
|
-
|
|
86
|
-
**A rule that judges a venue requirement is validated on real accepted papers of that venue** (or
|
|
87
|
-
of its parent venue, with the same template), built to PDF, with the ground truth established
|
|
88
|
-
independently of the rule — rendered and looked at — plus variant fixtures derived from those real
|
|
89
|
-
sources, one change each. Synthetic minimal fixtures alone are not enough: they are written by the
|
|
90
|
-
same hand as the rule, and share its blind spots. Example: `format/page-limit`'s body count, on the
|
|
91
|
-
built ACSAC papers in `fixtures/accepted-papers/` (`test/e2e/tex/page-count.e2e.ts`).
|
|
92
|
-
|
|
93
|
-
**A new rule ships with its page**, `docs/rules/<group>/<rule>.md`, in these sections: What it
|
|
94
|
-
catches · Why · Examples (failing / passing) · Options / preset fields · What it does not check ·
|
|
95
|
-
How to fix. Its `meta.docs.url` points at that page, and its row in `docs/rules.md` is one line
|
|
96
|
-
linking there. `src/rule-docs.test.ts` holds every rule the config registers to this; the rules
|
|
97
|
-
older than the convention wait in its `AWAITING_PAGE` list, which only shrinks — #131 writes their
|
|
98
|
-
pages.
|
|
99
|
-
|
|
100
|
-
**4. `exit 0` with empty output is NOT "clean".** A rule whose glob matched no files reports
|
|
101
|
-
exactly like a rule that passed. Any rule shipped here must be loud when its input set is
|
|
102
|
-
empty. This is the specific defect that blocks stage 1 of the plan: in the source base a
|
|
103
|
-
fresh clone yields RC=0, 652 findings, zero errors — because 19 rules saw no files at all.
|
|
104
|
-
|
|
105
|
-
**5. "It can't be done in the engine" must be MEASURED, not assumed.** Every one of these was
|
|
106
|
-
stated confidently on 2026-09-16 and every one fell to a single command:
|
|
107
|
-
|
|
108
|
-
| the claim | what one command showed |
|
|
109
|
-
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
110
|
-
| "a lint rule can't know a git date, so this stays a script" | a rule is plain JS; `execFileSync("git", …)` is legal in it. The real costs (per-file invocation, `--cache` keyed on content) are solvable, so this was a preference dressed as a limit |
|
|
111
|
-
| "we need our own glob expander" | `fs.globSync` ships in Node 22 and returned the identical set. 31 hand-written lines of regex existed for nothing |
|
|
112
|
-
| "the tool can't run a file that config excludes" | it can, and says so: `matches exclude … — running because you named it` |
|
|
113
|
-
|
|
114
|
-
⇒ Before writing machinery, **run the thing you are about to replace and paste its output.**
|
|
115
|
-
"I couldn't get it to work" is data about the attempt, not about the tool.
|
|
116
|
-
|
|
117
|
-
**6. A program shipped by this package MUST NOT depend on the consumer's cwd.** Measured
|
|
118
|
-
2026-09-16: three hooks here read their config as `provide("pkg", "cat package.json")`. The
|
|
119
|
-
consumer's session changed directory into a subfolder for unrelated reasons, `cat` failed, and
|
|
120
|
-
the Bash gate — which fails closed, correctly — denied **every command in that session**,
|
|
121
|
-
including the one that would undo it. The nudges next to it would have failed _silently_,
|
|
122
|
-
which is worse.
|
|
123
|
-
|
|
124
|
-
Resolve paths from the repository root (`git rev-parse --show-toplevel`) or from the module's
|
|
125
|
-
own location, never from where the caller happens to stand. The blast radius of a cwd
|
|
126
|
-
assumption is not this package — it is somebody else's whole session.
|
|
127
|
-
|
|
128
|
-
**7. Do not describe what you have not opened.** A `README` here nearly shipped the line
|
|
129
|
-
"MIT — see LICENSE" on 2026-09-16. There is no `LICENSE` file and `package.json` says
|
|
130
|
-
`UNLICENSED`. Publishing is irreversible and this repo is public: every factual claim in a
|
|
131
|
-
document meant for strangers gets checked against the disk in the same pass that writes it.
|
|
132
|
-
|
|
133
|
-
**8. 🔴 NEVER WRITE A GLOB OR A REGEX INSIDE A BLOCK COMMENT.** An asterisk followed by a
|
|
134
|
-
slash **ends the comment**, wherever it appears — in a path (a folder wildcard), in a regex
|
|
135
|
-
whose last literal is an asterisk (one matching bold markup, for instance), in a quoted
|
|
136
|
-
example. The rest of the comment becomes code, the file stops parsing, and the error points
|
|
137
|
-
at a line further down that is perfectly fine.
|
|
138
|
-
|
|
139
|
-
⚠️ Note this rule does not quote the sequence either, not even here. A documented example is
|
|
140
|
-
the thing that gets copied into code — and that is exactly how the fourth occurrence
|
|
141
|
-
happened: it was copied out of a comment written to explain the first three.
|
|
142
|
-
|
|
143
|
-
This is not a hypothetical and not a rare slip. **It fired four times in a single session on
|
|
144
|
-
2026-09-16** — three in the consumer, once here — and each time the diagnosis cost minutes
|
|
145
|
-
because `SyntaxError: Unexpected token '.'` says nothing about comments.
|
|
146
|
-
|
|
147
|
-
| instead of | write |
|
|
148
|
-
| --------------------------------------- | ----------------------------------------------------- |
|
|
149
|
-
| a glob with an asterisk and slash | spell it: "every folder under papers" |
|
|
150
|
-
| a regex with an asterisk before a slash | describe what it matched, in words |
|
|
151
|
-
| an example needing both | put it in a line comment (two slashes), never a block |
|
|
152
|
-
|
|
153
|
-
⇒ **In a block comment, prose describes the pattern; it never quotes it.** If the exact
|
|
154
|
-
characters matter, they belong in the code or in a line comment beside it.
|
|
155
|
-
|
|
156
|
-
**9. Measure the DEFECT before proposing the fix — and read "this is quick" as a warning.**
|
|
157
|
-
Rule 5 is about the tool you are replacing; this one is about the order of work.
|
|
158
|
-
|
|
159
|
-
1. **Show the defect**: command output, or a file quote with a line number. A proposed fix with
|
|
160
|
-
no exhibited defect is not a fix, it is a preference.
|
|
161
|
-
2. **Name the layer and the channel** it touches: ESLint rule · skill · hook · CLI · CI action
|
|
162
|
-
· path resolution. More than one is a conversation, not a commit.
|
|
163
|
-
3. **An adversarial second pass is encouraged, and it is not free.** Spend it on a fork in the
|
|
164
|
-
road, on anything that goes outward, and on a conclusion you are about to act on.
|
|
165
|
-
4. **A quick fix is almost never quick** — it is quick to _propose_ precisely because nothing
|
|
166
|
-
was opened.
|
|
167
|
-
|
|
168
|
-
Four proposals were made and withdrawn in one session on 2026-09-17 for exactly this reason —
|
|
169
|
-
[`docs/incidents.md`](docs/incidents.md).
|
|
170
|
-
|
|
171
|
-
**10. Effects live in adapters, and WHERE paperlint IS INSTALLED lives in ONE of them.** Rule 6 generalised: the caller's cwd is one case of it. Checking logic — lint rules,
|
|
172
|
-
skills, hooks — must not know its own location, nor its distance from anything else. Every
|
|
173
|
-
answer to _where_ comes from one of two modules, each for one side: the consumer (its root, the
|
|
174
|
-
package's name) from `skills/paper-pipeline/scripts/consumer.mjs`, which adapts per channel — own
|
|
175
|
-
checkout · `node_modules` · plugin cache · CI; the package's own data (`presets/`, `presets/tex/`)
|
|
176
|
-
from `src/package-dirs.ts`. A skill naming a script by an install-specific path in its own prose
|
|
177
|
-
walks around that door, and 208 such literals across 190 lines do exactly that.
|
|
178
|
-
|
|
179
|
-
<!-- The ports' paths above are resolved by `npm run check` (vigiles lint), since #67. -->
|
|
180
|
-
<!-- vigiles:file skills/paper-pipeline/scripts/consumer.mjs -->
|
|
181
|
-
<!-- vigiles:file src/package-dirs.ts -->
|
|
182
|
-
|
|
183
|
-
The layer rules for `src/` live in [`src/CLAUDE.md`](src/CLAUDE.md) and are enforced by the linter.
|
|
184
|
-
|
|
185
|
-
⏳ **Still owed: the install-path half** — a lint rule that makes an install-specific path literal
|
|
186
|
-
outside `consumer.mjs` a finding. Prose will not hold this class — four silent breakages happened
|
|
187
|
-
_while_ comments explaining the hazard sat directly above the code
|
|
188
|
-
([`docs/incidents.md`](docs/incidents.md)).
|
|
189
|
-
|
|
190
|
-
**11. Installing and using paperlint must be as smooth as possible.** Count the actions between "I
|
|
191
|
-
want this" and "it works": every command to copy, flag to pass or file to edit is one more place
|
|
192
|
-
to give up. The target is `npm i` plus one command. A per-paper script, a manual TeX install or a
|
|
193
|
-
"now add this to your config" step is a defect in paperlint, not a user task. The only exception is a
|
|
194
|
-
choice that really belongs to the user (irreversible, paid, privacy), and then it is named at the
|
|
195
|
-
moment it is asked. The converse holds too: an automatic step that can fail silently is worse
|
|
196
|
-
than an explicit one — it works, or it says loudly that it did not. Measured example of the
|
|
197
|
-
converse: Tectonic installs as one file but silently replaced Times with Latin Modern on a plain
|
|
198
|
-
`article` paper and still exited 0 (#35, #59).
|
|
199
|
-
|
|
200
|
-
## Before changing the command surface or a delivery channel — read the prior art
|
|
201
|
-
|
|
202
|
-
[`docs/prior-art/`](docs/prior-art/README.md) records how comparable tools solved the same
|
|
203
|
-
problems, each claim with the URL that was checked: Quarto and Vale (the domain and content
|
|
204
|
-
analogues), Biome (one tool, one config, one command), Danger and reviewdog (who decides to
|
|
205
|
-
fail a run), `unicorn/expiring-todo-comments` and Semgrep (checks that read a clock or a diff).
|
|
206
|
-
|
|
207
|
-
It is here as a POINTER and stays a pointer: this file is read on every turn, so it carries
|
|
208
|
-
the instruction and never the evidence — the same split as rules 9 and 10 and `docs/incidents.md`.
|
|
209
|
-
|
|
210
|
-
🔴 The argument "we need another command for X" is, in every tool examined, an argument that
|
|
211
|
-
the CONFIG is not declaring something. Check that before adding a verb.
|
|
212
|
-
|
|
213
|
-
Rejected on 2026-09-27, so it is not proposed again: a separate `paperlint refs` command for the
|
|
214
|
-
reference check. The slowness it would have moved (#107) was a cache thrown away after every build,
|
|
215
|
-
not a missing verb. The fix keeps the check in `build` and commits what the services answered in
|
|
216
|
-
`<paper>/repro/references-cache.json` — **raw responses, never verdicts**: verdicts are derived
|
|
217
|
-
again on every build, so a checker fix reaches every paper ([`docs/references.md`](docs/references.md)).
|
|
218
|
-
|
|
219
|
-
## Distribution — one install path: npm, then `paperlint init`
|
|
220
|
-
|
|
221
|
-
`npm i -D paperlint` brings all the code — rules, skills, hooks, scripts. `paperlint init`
|
|
222
|
-
then does what only a command can, because it depends on the project it lands in: it finds the
|
|
223
|
-
papers directory and declares it in a root `paperlint.json` when it is not the default `papers`, links each skill into `.claude/skills/`, writes
|
|
224
|
-
the hook commands into `.claude/settings.json` (vigiles' `mergeRegistrations`, reading
|
|
225
|
-
`plugin/hooks/hooks.json` as the one source), and offers a CI workflow that runs the action from
|
|
226
|
-
`node_modules` after `npm ci`. `paperlint doctor` reads all of it back. Details: `docs/install.md`.
|
|
227
|
-
|
|
228
|
-
**There is no Claude Code plugin or marketplace entry; it was removed in the release after 1.0.0
|
|
229
|
-
(#82).** Do not bring it back without answering these, each measured:
|
|
230
|
-
|
|
231
|
-
- **A plugin cannot carry the code.** Claude Code runs `npm ci --ignore-scripts` for a plugin only
|
|
232
|
-
when its root holds a `package.json` and a lockfile, with a 60-second timeout, and _"a failed or
|
|
233
|
-
skipped install never blocks the plugin"_: on a slow network the hooks load and fail with
|
|
234
|
-
`cannot find module vigiles`, silently.
|
|
235
|
-
- **The skills need the npm package anyway.** 23 of 24 skills run scripts under
|
|
236
|
-
`paper-pipeline/scripts`, which resolve only through `node_modules/paperlint/`.
|
|
237
|
-
A plugin-only consumer got skills whose first command fails.
|
|
238
|
-
- **Its manifests carried versions nothing updated** (0.0.1 and 0.1.0 while npm was at 1.0.0), and
|
|
239
|
-
Claude Code decides plugin updates from that number.
|
|
240
|
-
|
|
241
|
-
`plugin/hooks/hooks.json` stays where it is: it is not a plugin any more, it is the hook wiring
|
|
242
|
-
`paperlint init` merges into the consumer's settings.
|
|
243
|
-
|
|
244
|
-
## Delivery — how this repo's contents reach a consumer (measured 2026-09-11)
|
|
245
|
-
|
|
246
|
-
Three channels, each measured on a fixture rather than assumed. The consumer here is the
|
|
247
|
-
private knowledge base this was extracted from; nothing below is specific to it.
|
|
248
|
-
|
|
249
|
-
### Skills ship as `skills/`, and the consumer SYMLINKS them
|
|
250
|
-
|
|
251
|
-
**Do not move skills to `.claude/skills/` inside this repo.** They live in `skills/`
|
|
252
|
-
(`SHIPPED_SKILLS_DIR` in `skills/paper-pipeline/scripts/consumer.mjs`, read by the linker and the
|
|
253
|
-
install e2e), which is the standard layout. An ecosystem scan of 855 npm packages (by published
|
|
254
|
-
tarball, not repository) found **336 shipping `skills/<n>/SKILL.md` against 15 shipping `.claude/skills/`** — 22 : 1. The
|
|
255
|
-
top of the market is entirely on `skills/`: `@vitejs/devtools-kit` (330 896 downloads/wk),
|
|
256
|
-
`@slidev/cli` (56 809), `anthropics/skills` (175 673 stars).
|
|
257
|
-
|
|
258
|
-
**The consumer's side is a symlink per skill, made by `paperlint init` (`src/link-skills.ts`):**
|
|
13
|
+
**Move every paper-writing convention a machine can decide OUT of prose and INTO an engine that fails the build** — and keep everything else honestly labelled as prose. The engine is ESLint. The unit of progress is "one more verdict decided by the engine instead of by a hand-written script".
|
|
259
14
|
|
|
260
15
|
```
|
|
261
|
-
|
|
16
|
+
prose in a guideline → hand-written script → ESLint rule in the engine
|
|
17
|
+
rots silently runs, but is OURS editor-time · free AST · suppressible
|
|
262
18
|
```
|
|
263
19
|
|
|
264
|
-
|
|
265
|
-
`name:` in the frontmatter — so the link must be named exactly as the skill.
|
|
266
|
-
|
|
267
|
-
⚠️ Do NOT rely on a consumer picking `.claude/skills/` up out of `node_modules` on its own.
|
|
268
|
-
It does happen — such a directory is an ordinary nested one — but only **lazily and
|
|
269
|
-
silently**, the first time the agent happens to read a file inside that package. A symlink
|
|
270
|
-
loads at startup, deterministically. Measured both ways on claude 2.1.268.
|
|
271
|
-
|
|
272
|
-
### Hooks ship as `.mjs`, NEVER as `.hook.ts`
|
|
273
|
-
|
|
274
|
-
Measured on a fixture — one hook, four locations, both halves (an input that must be denied
|
|
275
|
-
and one that must pass), exit code taken without a pipe:
|
|
20
|
+
🔴 **A hand-written script is a WAYPOINT, not a destination.** A new one is allowed only when the engine genuinely cannot express the check, and "cannot" means MEASURED. A rule is an ordinary JS module and may call `execFileSync("git", …)` or read a sibling file. When the work has to run a program, **a script PRODUCES facts into a JSON file, and ESLint JUDGES that file.**
|
|
276
21
|
|
|
277
|
-
|
|
278
|
-
| ----------------------------------------------------- | ----------------------- | --------------------------- |
|
|
279
|
-
| `node_modules/<pkg>/.claude/hooks/probe.hook.**mjs**` | RC=2, fires | RC=0, silent |
|
|
280
|
-
| symlink into `node_modules`, `.mjs` | RC=2, fires | RC=0, silent |
|
|
281
|
-
| local control, `.mjs` | RC=2, fires | RC=0, silent |
|
|
282
|
-
| `node_modules/<pkg>/.claude/hooks/probe.hook.**ts**` | RC=2 `cannot be loaded` | **RC=2 `cannot be loaded`** |
|
|
22
|
+
**A real consumer dogfoods this package on every CI run**, so a breaking change here turns a paper pipeline red somewhere else the same day. CI runs on every pull request (`.github/workflows/ci.yml`). Counts of rules, skills and hooks: measure with `git ls-files`.
|
|
283
23
|
|
|
284
|
-
|
|
285
|
-
**everything**, including `echo hi`. A consumer in that state cannot run any Bash command,
|
|
286
|
-
and the one command that would repair it is also Bash.
|
|
287
|
-
|
|
288
|
-
⇒ **The package ships `.mjs`** — the consumer gets something that loads.
|
|
289
|
-
|
|
290
|
-
(Not established: _why_ the TypeScript loader refuses a path inside `node_modules`. The real
|
|
291
|
-
cause is swallowed by a `catch` in vigiles' `hook-runtime.js`, and calling `loadHookProgram`
|
|
292
|
-
directly measures a different load path — it fails even on the control. Knowing _that_ is
|
|
293
|
-
enough to choose the carrier.)
|
|
294
|
-
|
|
295
|
-
#### 🔴 CORRECTED 2026-09-12, when the first three hooks actually moved: THERE IS NO `.hook.ts` TWIN
|
|
296
|
-
|
|
297
|
-
This section used to promise «the `.hook.ts` source lives HERE and is typechecked HERE; the
|
|
298
|
-
package ships the compiled `.mjs`». Shipping the first three hooks retired that plan, and the
|
|
299
|
-
reason is worth keeping: **a twin can drift from its build, and nothing would notice.** One file
|
|
300
|
-
cannot.
|
|
24
|
+
## First command in a fresh container
|
|
301
25
|
|
|
302
|
-
|
|
303
|
-
anything but `vigiles/hook`, because the import list _is_ the capability surface. That check is a
|
|
304
|
-
function, `checkHookImports`, and `hooks/hooks.harness.ts` runs it over every shipped
|
|
305
|
-
`.hook.mjs` directly. Same check, applied to the artifact that actually executes, with no second
|
|
306
|
-
file to keep in step. What is lost is `tsc` on the hook body and the typed `e.ctx` — named here
|
|
307
|
-
rather than left as an omission.
|
|
26
|
+
`npm install` — not optional: `vigiles` is a real dependency, and every harness, spec and hook resolves through it. Without it, failures look like broken code rather than a missing install.
|
|
308
27
|
|
|
309
|
-
|
|
310
|
-
Measured 2026-09-12: the check failed on `paper-edit-guard.hook.mjs`'s own docblock, which quoted
|
|
311
|
-
a rejected import while explaining why it was rejected. Do not loosen the check to make prose
|
|
312
|
-
fit — it is the same check any future `compile`/`lint` pass applies to the shipped file. Describe
|
|
313
|
-
a forbidden import in words instead of writing one.
|
|
28
|
+
## Where the other rules live
|
|
314
29
|
|
|
315
|
-
|
|
30
|
+
| when you are… | read | holds |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| writing a lint rule, or a check that judges prose | [`eslint-rules/CLAUDE.md`](eslint-rules/CLAUDE.md) | lint rule over script, what holds each requirement of a new rule, the five writing principles |
|
|
33
|
+
| writing or changing a hook | [`hooks/CLAUDE.md`](hooks/CLAUDE.md) | how hooks ship, and what was rejected |
|
|
34
|
+
| writing or changing a skill | [`skills/CLAUDE.md`](skills/CLAUDE.md) | how skills ship |
|
|
35
|
+
| in `src/` | [`src/CLAUDE.md`](src/CLAUDE.md) | the two layer axes |
|
|
36
|
+
| thinking of a Claude Code plugin | [`plugin/CLAUDE.md`](plugin/CLAUDE.md) | why there is none, and what must be measured to bring it back |
|
|
37
|
+
| changing CI, Dependabot or the release | [`.github/CLAUDE.md`](.github/CLAUDE.md) | cost, Dependabot cadence, release from the PR title |
|
|
38
|
+
| writing a test | [`docs/testing.md`](docs/testing.md), [`docs/e2e.md`](docs/e2e.md) | which kind of test, fakes, the e2e triggers |
|
|
316
39
|
|
|
317
|
-
|
|
318
|
-
this package. It RUNS — deny input RC=2 with the reason, allow input RC=0 and silent — and it
|
|
319
|
-
cannot be maintained:
|
|
40
|
+
## Before every push
|
|
320
41
|
|
|
321
|
-
|
|
322
|
-
$ npx vigiles compile
|
|
323
|
-
✗ .vigiles/hooks/probe.hook.ts — hook program uses capabilities outside `vigiles/hook`:
|
|
324
|
-
<pkg>/hooks/decide.mjs — only the sanctioned API is allowed (capability = API surface).
|
|
325
|
-
```
|
|
42
|
+
`npm run check`, exit code read without a pipe. Red first · assert the whole value · no test touches the real network · 100% coverage, no `c8 ignore`. In a container without TeX Live, only the TeX e2e gate fails, and it says so; `npx paperlint toolchain` installs it.
|
|
326
43
|
|
|
327
|
-
|
|
328
|
-
re-stamped** — and the runtime fails CLOSED on a stamp that no longer matches its source:
|
|
44
|
+
## Commits
|
|
329
45
|
|
|
330
|
-
|
|
331
|
-
vigiles: hook … does not match its compiled stamp (tampered).
|
|
332
|
-
… the way out is a FILE WRITE, not a command — this refusal blocks the recompile too.
|
|
333
|
-
```
|
|
46
|
+
Conventional-commit subject, body says what was MEASURED, not what was intended. A number in a commit message that no command produced is the thing this repo exists to make impossible.
|
|
334
47
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
keeps the stamp question from arising (no sidecar ⇒ no check) and pins the source by lockfile
|
|
338
|
-
integrity instead — stronger than a local stamp, since a consumer cannot hand-edit an installed
|
|
339
|
-
tree without the next install reverting it.
|
|
48
|
+
**The subject is also the release**: semantic-release reads the squash commit, which is the PR title. `feat:` → minor, `fix:` or `perf:` → patch, `!` or a `BREAKING CHANGE:` footer → major,
|
|
49
|
+
`docs:` `chore:` `ci:` `test:` `refactor:` → no release. `.github/workflows/pr-title.yml` rejects a title without a conventional prefix.
|
|
340
50
|
|
|
341
|
-
|
|
51
|
+
## Rules
|
|
342
52
|
|
|
343
|
-
|
|
344
|
-
no compile step on the consumer's side:
|
|
53
|
+
### Mechanism Goes To Vigiles
|
|
345
54
|
|
|
346
|
-
|
|
347
|
-
{
|
|
348
|
-
"type": "command",
|
|
349
|
-
"command": "node \"$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js\" hook-runtime run-program \"$CLAUDE_PROJECT_DIR/node_modules/paperlint/hooks/paper-edit-guard.hook.mjs\""
|
|
350
|
-
}
|
|
351
|
-
```
|
|
55
|
+
**Guidance only** — A file that names nothing local — no rule or fixture of ours — is machinery, and belongs in [vigiles](https://github.com/zernie/vigiles); data stays here.
|
|
352
56
|
|
|
353
|
-
|
|
354
|
-
the point — so the papers-root resolver is spelled out in all three hook files. Duplication that
|
|
355
|
-
cannot be removed is CHECKED instead: part VII of the harness compares the captured values
|
|
356
|
-
against each other, rather than grepping for a literal (a substring search finds the same text in
|
|
357
|
-
the prose _about_ the value one line above it).
|
|
57
|
+
### Measure Before Calling It Impossible
|
|
358
58
|
|
|
359
|
-
|
|
59
|
+
**Guidance only** — "It can't be done in the engine" must be MEASURED, not assumed. Each of these was stated confidently on 2026-09-16 and fell to a single command:
|
|
360
60
|
|
|
361
|
-
|
|
61
|
+
| the claim | what one command showed |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| "a lint rule can't know a git date, so this stays a script" | a rule is plain JS; `execFileSync("git", …)` is legal in it, and its real costs are solvable |
|
|
64
|
+
| "we need our own glob expander" | `fs.globSync` ships in Node 22 and returned the identical set |
|
|
65
|
+
| "the tool can't run a file that config excludes" | it can, and says so: `matches exclude … — running because you named it` |
|
|
362
66
|
|
|
363
|
-
|
|
364
|
-
resolve OK -> <consumer>/node_modules/vigiles/dist/hook.js
|
|
365
|
-
```
|
|
67
|
+
Before writing machinery, **run the thing you are about to replace and paste its output.**
|
|
366
68
|
|
|
367
|
-
|
|
368
|
-
for its own `vigiles test` and `vigiles compile`. That is `devDependencies`, which `npm i` of
|
|
369
|
-
a dependency does not install. Putting it in `dependencies` risks npm installing a **second**
|
|
370
|
-
copy under `node_modules/paperlint/node_modules/vigiles` whenever the ranges
|
|
371
|
-
drift — two runtimes, two sets of stamps and state.
|
|
372
|
-
|
|
373
|
-
🔴 **THE PARAGRAPH ABOVE WAS TRUE AND THE INSTALL DID THE OPPOSITE — measured 2026-09-17.**
|
|
374
|
-
`devDependencies` is not the only entry naming `vigiles`: `peerDependencies` names it too, and
|
|
375
|
-
**npm 7+ installs peers automatically**. So every consumer got it anyway, together with its
|
|
376
|
-
transitive weight. `npm pack`, then install the tarball into an empty project:
|
|
377
|
-
|
|
378
|
-
| | packages | `du -sm node_modules` |
|
|
379
|
-
| ------------------------------------------------------- | -------: | --------------------: |
|
|
380
|
-
| peer as declared before | 188 | **142 MB** |
|
|
381
|
-
| `peerDependenciesMeta: { vigiles: { optional: true } }` | 164 | **56 MB** |
|
|
382
|
-
|
|
383
|
-
The 86 MB are `@ast-grep/napi` (51 MB) and `typescript` (23 MB), pulled through `vigiles` — and
|
|
384
|
-
paid for by a consumer who only wants the ESLint rules and never loads a hook.
|
|
385
|
-
|
|
386
|
-
`optional: true` is the entry that matches what this section already argues: the consumer brings
|
|
387
|
-
its own `vigiles` _when it uses the hooks_, and npm stops deciding that for them. Both halves
|
|
388
|
-
measured on the 56 MB tree: `eslint-rules/latex-language.mjs` and the skill scripts load and
|
|
389
|
-
run (RC=0), while
|
|
390
|
-
`hooks/paper-edit-guard.hook.mjs` fails with `ERR_MODULE_NOT_FOUND` — which is this contract
|
|
391
|
-
working, not a defect, exactly as argued below.
|
|
392
|
-
|
|
393
|
-
⚠️ The consumer in this project's own base is unaffected: it declares `vigiles` itself
|
|
394
|
-
(`devDependencies: ^27.2.0`), so nothing about its tree changes.
|
|
395
|
-
|
|
396
|
-
### The `.bib` parser is a dependency: the one bibtex reader
|
|
397
|
-
|
|
398
|
-
`@retorquere/bibtex-parser` is a `dependency`. It is the one reader of `.bib` text in paperlint
|
|
399
|
-
(`src/ports/bib-reader.ts`, adapter `src/adapters/bibtex/`): `recordedBibliography`, `bib/reachable-entry`,
|
|
400
|
-
the build's reference check, `extract-ref-facts` and `bib-authors` all read through it, so it can
|
|
401
|
-
no longer be opt-in. It costs about 15 MB of the install (9 MB itself, plus
|
|
402
|
-
`wink-eng-lite-web-model` and `unicode2latex`).
|
|
403
|
-
|
|
404
|
-
Where it reads differently from bibtex — bibtex has no comment syntax, so an entry behind `%` or
|
|
405
|
-
inside `@comment{…}` is one bibtex typesets — paperlint does not emulate bibtex: the build records
|
|
406
|
-
what bibtex read, and `paper/refs-checked` names the difference (docs/design/paper-sources.md §7).
|
|
407
|
-
|
|
408
|
-
🔴 **Therefore the pin here and the pin in the consumer move TOGETHER, in one pass.** A major
|
|
409
|
-
mismatch means a hook compiled by one version is executed by another: the stamp does not
|
|
410
|
-
verify, the hook does not load, and `PreToolUse` refuses every command. That already happened
|
|
411
|
-
in the consumer on 2026-09-10 (25.1.0 -> 27.1.4) and cost real recovery work.
|
|
412
|
-
|
|
413
|
-
```bash
|
|
414
|
-
npm ls vigiles # prints `invalid` when what is installed does not satisfy the manifest
|
|
415
|
-
```
|
|
69
|
+
### Do Not Describe What You Have Not Opened
|
|
416
70
|
|
|
417
|
-
|
|
418
|
-
like a freshness check.** It compares what is INSTALLED against this repo's MANIFEST. It says
|
|
419
|
-
nothing about the registry. Measured 2026-09-16: manifest `^27.1.4`, installed `27.1.6`,
|
|
420
|
-
published `27.2.0` — exit code **0**, because the range is satisfied. The repo had been one
|
|
421
|
-
minor behind for days and every local check was green.
|
|
71
|
+
**Guidance only** — Every factual claim in a document meant for strangers is checked against the disk in the same pass that writes it.
|
|
422
72
|
|
|
423
|
-
|
|
73
|
+
### No Glob Or Regex In A Block Comment
|
|
424
74
|
|
|
425
|
-
|
|
426
|
-
because the obvious objection to a bot in this org is already recorded and does NOT apply:
|
|
427
|
-
a bot was switched off in a sibling repository for burning Actions minutes — **that
|
|
428
|
-
repository is private**. This one is public, minutes are free, so the objection does not
|
|
429
|
-
travel. If this repo is ever made private, revisit the file along with it.
|
|
75
|
+
**Guidance only** — 🔴 An asterisk followed by a slash ends the comment wherever it appears. In a block comment, prose describes the pattern and never quotes it; exact characters go in code or a line comment.
|
|
430
76
|
|
|
431
|
-
|
|
432
|
-
until you read the reference:
|
|
77
|
+
### Measure The Defect Before The Fix
|
|
433
78
|
|
|
434
|
-
|
|
435
|
-
| ------------------- | ---------: | --------------------------------------------------------------------------------------- |
|
|
436
|
-
| `schedule.interval` | — | the check runs on the schedule and only on the schedule |
|
|
437
|
-
| **`cooldown`** | **3 days** | _"a new version is not considered for a version update until 3 days after its release"_ |
|
|
79
|
+
**Guidance only** — Read "this is quick" as a warning. Show the defect (output, or a quoted line), name the layer and channel (more than one is a conversation, not a commit), and spend an adversarial pass on forks and on anything that goes outward.
|
|
438
80
|
|
|
439
|
-
|
|
440
|
-
`daily`, and `vigiles` is listed in `cooldown.exclude`, so for THIS package the window is one
|
|
441
|
-
schedule tick.
|
|
81
|
+
### Installing Paperlint Is Smooth
|
|
442
82
|
|
|
443
|
-
**
|
|
444
|
-
yanked hours later. That is a real risk for a third-party package and an empty one for our own
|
|
445
|
-
— we would be the ones yanking it, and we can ship several versions of it in a single day, so
|
|
446
|
-
a three-day hold would have the bot proposing the version from the day before yesterday.
|
|
83
|
+
**Guidance only** — The target is `npm i` plus one command; any further manual step is a paperlint defect, except a choice that really belongs to the user, named when it is asked. An automatic step that can fail silently is worse than an explicit one.
|
|
447
84
|
|
|
448
|
-
|
|
449
|
-
the primary path, because no schedule can outrun same-day releases.** The primary path is the
|
|
450
|
-
rule already recorded in the consumer's base — merge a PR in `vigiles`, bump every consumer in
|
|
451
|
-
the same pass. The bot is the backstop for the case that actually bit us: the rule named ONE
|
|
452
|
-
consumer while there were two, and this repo sat forgotten on `^27.1.4`.
|
|
85
|
+
### Every Check Has Both Halves
|
|
453
86
|
|
|
454
|
-
|
|
455
|
-
it: `dependabot.yml` configures _version_ updates only. **Security** updates are a separate
|
|
456
|
-
mechanism driven by advisories and a repository SETTING; their cadence cannot be changed from
|
|
457
|
-
this file, and deleting the file would not stop them.
|
|
87
|
+
**Guidance only** — Every check — lint rule, script, hook, harness — fires on a planted defect AND stays quiet on a clean fixture, and the test goes red before the fix lands. A check only ever seen quiet is indistinguishable from a dead one. [Details](docs/testing.md)
|
|
458
88
|
|
|
459
|
-
|
|
460
|
-
for updates**.
|
|
89
|
+
### Never The Callers Cwd
|
|
461
90
|
|
|
462
|
-
|
|
91
|
+
**Guidance only** — No program this package ships (`bin/`, `lib/`, hooks, skill scripts) depends on the directory it is started from: resolve from `git rev-parse --show-toplevel` or from the module's own location.
|
|
463
92
|
|
|
464
|
-
|
|
465
|
-
lints the repository, and asks ESLint for the effective config of every linted file. A rule
|
|
466
|
-
enabled for **zero** files is named and the script exits 1.
|
|
93
|
+
### One Module Knows Where Paperlint Is Installed
|
|
467
94
|
|
|
468
|
-
|
|
469
|
-
node scripts/rules-see-files.mjs # also part of npm run check
|
|
470
|
-
```
|
|
95
|
+
**Guidance only** — Lint rules, skills, hooks and scripts never compute where paperlint is installed: the package's own data comes from `src/package-dirs.ts`, the consumer's side from `skills/paper-pipeline/scripts/consumer.mjs`. `port/js-install-path` and `port/md-install-path` flag a literal. [Details](eslint-rules/install-path-literals.ts)
|
|
471
96
|
|
|
472
|
-
|
|
473
|
-
block whose glob is empty while a different block is busy, so "some glob matched something" is
|
|
474
|
-
not evidence about the rule you care about. Both halves are tested
|
|
475
|
-
(`scripts/rules-see-files.harness.mjs`), under-reporting and over-reporting each by an
|
|
476
|
-
assertion of its own.
|
|
477
|
-
|
|
478
|
-
## Cost
|
|
479
|
-
|
|
480
|
-
⛽ **This repository is PUBLIC, so its Actions minutes are FREE.** Verified against the API on
|
|
481
|
-
2026-09-17: `"private": false`, `"visibility": "public"`, and three active workflows — `ci`,
|
|
482
|
-
`dependabot auto-merge`, and Dependabot's own updates runner.
|
|
483
|
-
|
|
484
|
-
🔴 **This paragraph said the exact opposite until now, and the correction is the lesson, not the
|
|
485
|
-
fact.** It read «This is a **private** repository … Until then there is no CI here, and that is
|
|
486
|
-
deliberate» — both halves false, and false in the file an agent loads FIRST. The flip to public
|
|
487
|
-
happened on 2026-09-12 and _was_ recorded, at `.github/workflows/ci.yml:8-9`, which is a file
|
|
488
|
-
nobody opens before deciding whether there is any CI to check. Reported as issue #6.
|
|
489
|
-
|
|
490
|
-
⚠️ So the rule this leaves behind is about WHERE a correction lands: a measurement written into
|
|
491
|
-
the artifact it describes is not written down for the reader who needs it. **Status that changes
|
|
492
|
-
what a session DOES belongs in this file**; the workflow header can carry the detail.
|
|
493
|
-
|
|
494
|
-
What stays true, because the reasoning outlives the flip: minutes on a **private** repo come out
|
|
495
|
-
of the account-wide 3000/month shared with every other private repo, and the budget is decided
|
|
496
|
-
**before** the first workflow file, not after the first bill. **If this repository is ever made
|
|
497
|
-
private again, this section and `.github/dependabot.yml` are revisited together** — the bot is
|
|
498
|
-
justified two hundred lines above precisely by these minutes being free.
|
|
499
|
-
|
|
500
|
-
## Writing checks — what keeps them from pushing papers toward a blog post
|
|
501
|
-
|
|
502
|
-
The rules and skills that judge a paper's prose follow five principles. A new rule, or a new
|
|
503
|
-
criterion in a skill, is held to them.
|
|
504
|
-
|
|
505
|
-
1. **Every writing check is two-sided.** A check with only a ceiling — too long, too many markers,
|
|
506
|
-
too many of X — pushes every paper toward clipped, unsignposted prose, and the one equilibrium of
|
|
507
|
-
a set of ceilings is a blog post. A new check has a floor as well, or measures the distance to a
|
|
508
|
-
reference (accepted papers of the target venue); never only "less is better".
|
|
509
|
-
2. **Rules count, skills judge.** What is countable on the parse tree — a density, a number with no
|
|
510
|
-
owner (`tex/claim-provenance`), a missing section — is a lint rule. What needs reading — register
|
|
511
|
-
relative to a venue, genre, whether a claim changed strength — is a skill criterion. A skill does
|
|
512
|
-
not grep prose for what a rule can count; a rule is not asked to judge register.
|
|
513
|
-
3. **The model reader is the venue's reviewer, not only a smart outsider.** A check that simulates a
|
|
514
|
-
reader includes a skimming reviewer who reads the abstract, the introduction and the conclusion,
|
|
515
|
-
and judges whether it reads as a paper at that venue and whose result each sentence states.
|
|
516
|
-
4. **Thresholds are calibrated, not borrowed.** A threshold is set with the same counter on real
|
|
517
|
-
accepted papers, never taken from a published figure measured by a different counter. Those
|
|
518
|
-
papers are [`fixtures/accepted-papers/`](fixtures/accepted-papers/README.md), and every rule's
|
|
519
|
-
count on them is recorded there.
|
|
520
|
-
5. **A rewrite is accepted on what reviewers see.** That is the combined count of every surface form
|
|
521
|
-
of a tic (a narrow rule is gamed by rewording), plus a claims-preservation check that no claim was
|
|
522
|
-
softened or strengthened — not the one pattern the triggering rule matched.
|
|
523
|
-
|
|
524
|
-
## Testing
|
|
525
|
-
|
|
526
|
-
| you are testing | kind |
|
|
527
|
-
| -------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
528
|
-
| a pure function | unit — vitest, `<module>.test.ts` beside it |
|
|
529
|
-
| a use case or adapter that reaches disk, a process, the network, the clock | integration — vitest, fakes through the ports |
|
|
530
|
-
| a hook's decision, a skill's contract | harness — vigiles, `<surface>.harness.ts` |
|
|
531
|
-
| the installed package, a real TeX build | e2e — `test/e2e/<area>/*.e2e.ts`, `npm run test:e2e:<area>` |
|
|
532
|
-
|
|
533
|
-
Red first · assert the whole value · test what the code does, never what its source says · no test
|
|
534
|
-
touches the real network · 100% coverage, no `c8 ignore`. **Before every push: `npm run check`,
|
|
535
|
-
exit code read without a pipe.** Everything else — where files live, how fakes are injected, the
|
|
536
|
-
`--min=1` trap: [`docs/testing.md`](docs/testing.md).
|
|
537
|
-
|
|
538
|
-
**A new rule** needs unit tests beside it and a reporting and a silent case in
|
|
539
|
-
`src/rule-cases.test.ts`, which runs every registered rule through real ESLint and fails on one
|
|
540
|
-
without cases. An e2e fixture only when [`docs/e2e.md`](docs/e2e.md)'s triggers say so.
|
|
541
|
-
|
|
542
|
-
**New code is functional.** In `src/`, `eslint-rules/` and `lib/` (tests aside) there is no
|
|
543
|
-
`let`, no mutation, no loop, and types are read-only (`eslint-plugin-functional`); a `switch` over a
|
|
544
|
-
union names every member. Collections go through [Remeda](https://remedajs.com/) — `lodash`,
|
|
545
|
-
`lodash-es` and `ramda` are refused by `npm run lint`. Old code is listed in
|
|
546
|
-
`eslint-suppressions.json`, ESLint's own bulk suppressions, which may only shrink: fixing a listed
|
|
547
|
-
site without `npx eslint . --prune-suppressions` fails lint, and #134 burns the list down.
|
|
97
|
+
### Prior Art Before A Command Or A Channel
|
|
548
98
|
|
|
549
|
-
|
|
99
|
+
**Guidance only** — Before adding a CLI command or changing a delivery channel (skill links, hook wiring, the composite action, the npm package), read how comparable tools did it. "We need another command for X" has, in every tool examined, been the CONFIG not declaring something. [Details](docs/prior-art/README.md)
|
|
550
100
|
|
|
551
|
-
|
|
552
|
-
a commit message that no command produced is the thing this repo exists to make impossible.
|
|
101
|
+
### New Code Is Functional
|
|
553
102
|
|
|
554
|
-
**
|
|
555
|
-
(
|
|
556
|
-
`feat:` → minor, `fix:` or `perf:` → patch, `!` or a `BREAKING CHANGE:` footer → major,
|
|
557
|
-
`docs:` `chore:` `ci:` `test:` `refactor:` → no release. `pr-title.yml` rejects a title without
|
|
558
|
-
a conventional prefix, and for a one-commit PR checks the commit subject too, since GitHub
|
|
559
|
-
squashes to that. npm, the git tag and the GitHub Release always carry the same version. Never
|
|
560
|
-
`npm publish` by hand.
|
|
103
|
+
**Enforced by:** `eslint/functional/no-let`
|
|
104
|
+
**Why:** In `src/`, `eslint-rules/` and `lib/`, tests and harnesses aside: no `let`, no mutation, no loop, read-only types; collections through [Remeda](https://remedajs.com/). `functional/immutable-data`, `functional/no-loop-statements` and `functional/prefer-immutable-types` hold the rest, and `eslint-suppressions.json` (old code) may only shrink.
|