paperlint 2.0.1 → 3.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/CLAUDE.md +6 -1
- package/CONTRIBUTING.md +21 -0
- package/README.md +231 -180
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +5 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +16 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +40 -5
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +37 -20
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +377 -251
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +37 -52
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +33 -9
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +117 -17
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +40 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -109
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +2 -2
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +6 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +63 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +69 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +40 -0
- package/dist/paper-settings.d.ts.map +1 -0
- package/dist/paper-settings.js +144 -0
- package/dist/paper-settings.js.map +1 -0
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +184 -0
- package/dist/presets.js.map +1 -0
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +11 -2
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +25 -11
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +3 -2
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +63 -7
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +91 -24
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +20 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +109 -0
- package/dist/venue-rules.d.ts.map +1 -0
- package/dist/venue-rules.js +389 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +152 -77
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +26 -24
- package/docs/rules.md +181 -15
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +267 -192
- package/eslint-rules/paper-typography.test.mjs +297 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -27
- package/eslint-rules/pdf-last-page-balance.mjs +23 -5
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -18
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +90 -81
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +4 -13
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
- package/skills/paper-pipeline/SKILL.md +4 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/extract-pdf-facts.harness.mjs +5 -5
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/study-accepted-papers/SKILL.md +2 -1
- package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
- package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
- package/skills/submit-paper/references/venues/realm.jsonc +45 -43
- package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
- package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build-engine.ts +10 -2
- package/src/build.harness.mjs +31 -22
- package/src/build.ts +65 -8
- package/src/cli.harness.mjs +151 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +457 -288
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +41 -61
- package/src/facts-file.test.ts +47 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +193 -159
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/new-paper.harness.mjs +9 -6
- package/src/new-paper.test.ts +133 -0
- package/src/new-paper.ts +7 -2
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +274 -0
- package/src/paper-settings.test.ts +249 -0
- package/src/paper-settings.ts +208 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +297 -0
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +30 -13
- package/src/structure.harness.mjs +6 -4
- package/src/structure.ts +3 -2
- package/src/tex-requirements.harness.mjs +11 -17
- package/src/tex-requirements.ts +179 -29
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +554 -0
- package/src/venue-rules.ts +571 -0
- package/templates/paper/paperlint.json +4 -0
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
package/src/doctor.harness.mjs
CHANGED
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
* Killed by: src/doctor.mutations.mjs
|
|
13
13
|
*/
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
|
-
import {
|
|
16
|
-
OLD_PAPERS_DIR_FIELD,
|
|
17
|
-
PAPERS_DIR_FIELD,
|
|
18
|
-
} from "../lib/paper-config.mjs";
|
|
15
|
+
import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
|
|
19
16
|
import {
|
|
20
17
|
mkdtempSync,
|
|
21
18
|
mkdirSync,
|
|
@@ -41,8 +38,8 @@ const check = (label, cond) => {
|
|
|
41
38
|
assert.ok(cond, label);
|
|
42
39
|
};
|
|
43
40
|
|
|
44
|
-
/** A consumer on disk: a papers directory, and maybe a declaration in
|
|
45
|
-
function consumer({ papersDir,
|
|
41
|
+
/** A consumer on disk: a papers directory, and maybe a declaration in the root paperlint.json. */
|
|
42
|
+
function consumer({ papersDir, declared, makeDir = true }) {
|
|
46
43
|
const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-doctor-")));
|
|
47
44
|
if (makeDir && papersDir) {
|
|
48
45
|
mkdirSync(join(dir, papersDir, "some-paper"), { recursive: true });
|
|
@@ -51,9 +48,15 @@ function consumer({ papersDir, pkgKey, makeDir = true }) {
|
|
|
51
48
|
"# s\n",
|
|
52
49
|
);
|
|
53
50
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
writeFileSync(
|
|
52
|
+
join(dir, "package.json"),
|
|
53
|
+
JSON.stringify({ name: "consumer", version: "1.0.0" }, null, 2),
|
|
54
|
+
);
|
|
55
|
+
if (declared !== undefined)
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(dir, "paperlint.json"),
|
|
58
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: declared }, null, 2),
|
|
59
|
+
);
|
|
57
60
|
return dir;
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -76,7 +79,7 @@ const runDoctor = (
|
|
|
76
79
|
|
|
77
80
|
// ── I. A CONSISTENT INSTALL IS SILENT ───────────────────────────────────────────────────────
|
|
78
81
|
{
|
|
79
|
-
const dir = consumer({ papersDir: "papers",
|
|
82
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
80
83
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
81
84
|
check("a consistent install — exit ZERO", r.code === 0);
|
|
82
85
|
check(
|
|
@@ -86,30 +89,9 @@ const runDoctor = (
|
|
|
86
89
|
rmSync(dir, { recursive: true, force: true });
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
// ── I½. THE OLD FIELD NAME IS A FAILURE, NAMED ─────────────────────────────────────────────
|
|
90
|
-
{
|
|
91
|
-
const dir = consumer({ papersDir: "papers", pkgKey: "papers" });
|
|
92
|
-
writeFileSync(
|
|
93
|
-
join(dir, "package.json"),
|
|
94
|
-
JSON.stringify({
|
|
95
|
-
name: "consumer",
|
|
96
|
-
paperlint: { [OLD_PAPERS_DIR_FIELD]: "papers" },
|
|
97
|
-
}),
|
|
98
|
-
);
|
|
99
|
-
const r = runDoctor(dir, { cliPapers: null });
|
|
100
|
-
check("the old field name — exit NON-zero", r.code !== 0);
|
|
101
|
-
check(
|
|
102
|
-
"and doctor says what the field was renamed to",
|
|
103
|
-
r.out.includes(
|
|
104
|
-
`"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "paperlint"`,
|
|
105
|
-
),
|
|
106
|
-
);
|
|
107
|
-
rmSync(dir, { recursive: true, force: true });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
92
|
// ── II. THE VERY DEFECT: AN INSTALL FOLLOWING THE DOCS ─────────────────────────────────────
|
|
111
|
-
// The
|
|
112
|
-
//
|
|
93
|
+
// The CLI lints one directory and the hook guards another: the install that motivated doctor
|
|
94
|
+
// (issue #33). Here the CLI was pointed elsewhere while the hook took the default.
|
|
113
95
|
{
|
|
114
96
|
const dir = consumer({ papersDir: "writing/drafts" });
|
|
115
97
|
const r = runDoctor(dir, { cliPapers: "writing/drafts" });
|
|
@@ -137,21 +119,31 @@ const runDoctor = (
|
|
|
137
119
|
rmSync(dir, { recursive: true, force: true });
|
|
138
120
|
}
|
|
139
121
|
|
|
140
|
-
// ── II-bis.
|
|
141
|
-
// The
|
|
142
|
-
// It must not fail here (an error-level false positive costs more than a miss), but it must
|
|
143
|
-
// not stay silent either.
|
|
122
|
+
// ── II-bis. NO paperlint.json — EVERY DEFAULT, AND THAT IS A VALID PROJECT ─────────────────
|
|
123
|
+
// The CLI and the hooks both take `papers` when nothing is declared, so they cannot disagree.
|
|
144
124
|
{
|
|
145
125
|
const dir = consumer({ papersDir: "papers" });
|
|
146
126
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
147
|
-
check("
|
|
127
|
+
check("no paperlint.json — exit ZERO", r.code === 0);
|
|
148
128
|
check(
|
|
149
|
-
"
|
|
150
|
-
|
|
129
|
+
"and it says the defaults are in play, naming the directory",
|
|
130
|
+
/✓ no paperlint\.json — every setting has its default; papers are in "papers"/.test(
|
|
131
|
+
r.out,
|
|
132
|
+
),
|
|
151
133
|
);
|
|
134
|
+
rmSync(dir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── II-quater. A paperlint.json THAT DOES NOT PARSE — the guard refuses every command ─────
|
|
138
|
+
{
|
|
139
|
+
const dir = consumer({ papersDir: "papers" });
|
|
140
|
+
writeFileSync(join(dir, "paperlint.json"), '{ "papersDir": <<<<<<< HEAD\n');
|
|
141
|
+
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
142
|
+
check("an unparsable paperlint.json — a FAILURE", r.code === 2);
|
|
152
143
|
check(
|
|
153
|
-
"
|
|
154
|
-
|
|
144
|
+
"named, with the way out (a file edit, not a command)",
|
|
145
|
+
/✗ paperlint\.json is not valid JSON/.test(r.out) &&
|
|
146
|
+
/Edit or Write/.test(r.out),
|
|
155
147
|
);
|
|
156
148
|
rmSync(dir, { recursive: true, force: true });
|
|
157
149
|
}
|
|
@@ -163,7 +155,7 @@ const runDoctor = (
|
|
|
163
155
|
{
|
|
164
156
|
const dir = consumer({
|
|
165
157
|
papersDir: "papers",
|
|
166
|
-
|
|
158
|
+
declared: "papers",
|
|
167
159
|
makeDir: false,
|
|
168
160
|
});
|
|
169
161
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
@@ -177,42 +169,9 @@ const runDoctor = (
|
|
|
177
169
|
rmSync(dir, { recursive: true, force: true });
|
|
178
170
|
}
|
|
179
171
|
|
|
180
|
-
// ── II-quater. THE KEY'S OLD NAME IS READ AND NAMED; TWO DIFFERENT KEYS ARE A FAILURE ──────
|
|
181
|
-
{
|
|
182
|
-
const dir = consumer({ papersDir: "papers" });
|
|
183
|
-
writeFileSync(
|
|
184
|
-
join(dir, "package.json"),
|
|
185
|
-
JSON.stringify({
|
|
186
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "papers" },
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
|
-
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
190
|
-
check(
|
|
191
|
-
"the old key — read (not a failure), and named with the new one",
|
|
192
|
-
r.code === 0 &&
|
|
193
|
-
/⚠ "research-paper-pipeline" in package\.json is the old name/.test(
|
|
194
|
-
r.out,
|
|
195
|
-
) &&
|
|
196
|
-
/the hooks will guard papers/.test(r.out),
|
|
197
|
-
);
|
|
198
|
-
writeFileSync(
|
|
199
|
-
join(dir, "package.json"),
|
|
200
|
-
JSON.stringify({
|
|
201
|
-
paperlint: { [PAPERS_DIR_FIELD]: "papers" },
|
|
202
|
-
"research-paper-pipeline": { [PAPERS_DIR_FIELD]: "elsewhere" },
|
|
203
|
-
}),
|
|
204
|
-
);
|
|
205
|
-
const both = runDoctor(dir, { cliPapers: "papers" });
|
|
206
|
-
check(
|
|
207
|
-
"both keys with different contents — a failure",
|
|
208
|
-
both.code === 2 && /✗ package\.json has both/.test(both.out),
|
|
209
|
-
);
|
|
210
|
-
rmSync(dir, { recursive: true, force: true });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
172
|
// ── III. TWO DECLARATIONS HAVE DRIFTED APART ────────────────────────────────────────────────
|
|
214
173
|
{
|
|
215
|
-
const dir = consumer({ papersDir: "writing/drafts",
|
|
174
|
+
const dir = consumer({ papersDir: "writing/drafts", declared: "papers" });
|
|
216
175
|
mkdirSync(join(dir, "papers"), { recursive: true });
|
|
217
176
|
const r = runDoctor(dir, { cliPapers: "writing/drafts" });
|
|
218
177
|
check("both declarations exist, but differ — a FAILURE", r.code === 2);
|
|
@@ -223,12 +182,10 @@ const runDoctor = (
|
|
|
223
182
|
// This is load-bearing: a copy of the logic would drift silently and print a confident wrong
|
|
224
183
|
// answer.
|
|
225
184
|
{
|
|
226
|
-
const dir = consumer({ papersDir: "docs/papers",
|
|
185
|
+
const dir = consumer({ papersDir: "docs/papers", declared: "docs/papers/" });
|
|
227
186
|
const r = runDoctor(dir, { cliPapers: "docs/papers" });
|
|
228
187
|
const fromHook = papersRoot(
|
|
229
|
-
JSON.stringify({
|
|
230
|
-
paperlint: { [PAPERS_DIR_FIELD]: "docs/papers/" },
|
|
231
|
-
}),
|
|
188
|
+
JSON.stringify({ [PAPERS_DIR_FIELD]: "docs/papers/" }),
|
|
232
189
|
);
|
|
233
190
|
check("the hook itself trims the trailing slash", fromHook === "docs/papers");
|
|
234
191
|
check(
|
|
@@ -244,7 +201,7 @@ const runDoctor = (
|
|
|
244
201
|
|
|
245
202
|
// ── V. THE HOOK'S REFUSAL IS PASSED ALONG, NOT TURNED INTO A DIRECTORY ─────────────────────
|
|
246
203
|
{
|
|
247
|
-
const dir = consumer({ papersDir: "papers",
|
|
204
|
+
const dir = consumer({ papersDir: "papers", declared: "" });
|
|
248
205
|
const r = runDoctor(dir, { cliPapers: "papers" });
|
|
249
206
|
check(
|
|
250
207
|
"an empty string in the declaration — the hook refuses, and doctor NAMES it",
|
|
@@ -255,7 +212,7 @@ const runDoctor = (
|
|
|
255
212
|
|
|
256
213
|
// ── VI. EXTERNAL PROGRAMS ARE A FACT, NOT A VERDICT ─────────────────────────────────────────
|
|
257
214
|
{
|
|
258
|
-
const dir = consumer({ papersDir: "papers",
|
|
215
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
259
216
|
const none = runDoctor(dir, { cliPapers: "papers", have: () => 1 });
|
|
260
217
|
check(
|
|
261
218
|
"🔴 a missing tex install does NOT fail the run — a gate on advice would mute the whole thing",
|
|
@@ -330,7 +287,7 @@ const runDoctor = (
|
|
|
330
287
|
// nothing anywhere said so. The link state itself is `link-skills.harness.mjs`'s subject; here
|
|
331
288
|
// only doctor's REPORTING of it is judged, so the state is handed in rather than built on disk.
|
|
332
289
|
{
|
|
333
|
-
const dir = consumer({ papersDir: "papers",
|
|
290
|
+
const dir = consumer({ papersDir: "papers", declared: "papers" });
|
|
334
291
|
const state = {
|
|
335
292
|
ok: true,
|
|
336
293
|
home: join(dir, ".claude", "skills"),
|
package/src/doctor.mutations.mjs
CHANGED
|
@@ -46,8 +46,8 @@ process.exit(
|
|
|
46
46
|
edits: [
|
|
47
47
|
[
|
|
48
48
|
SRC,
|
|
49
|
-
|
|
50
|
-
' const hookRoot =
|
|
49
|
+
' const hookRoot = papersRoot(raw ?? "{}");',
|
|
50
|
+
' const hookRoot = raw === null ? "papers" : (() => { try { return JSON.parse(raw)?.papersDir ?? "papers"; } catch { return "papers"; } })();',
|
|
51
51
|
],
|
|
52
52
|
],
|
|
53
53
|
},
|
|
@@ -115,18 +115,18 @@ process.exit(
|
|
|
115
115
|
],
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
|
-
name: "
|
|
118
|
+
name: "an unparsable paperlint.json is reported as all-clear",
|
|
119
119
|
harness: HARNESS,
|
|
120
|
-
expect: "
|
|
120
|
+
expect: "an unparsable paperlint.json — a FAILURE",
|
|
121
121
|
disables:
|
|
122
|
-
"the
|
|
123
|
-
"
|
|
124
|
-
"
|
|
122
|
+
"the one failure left in the settings file: while it does not parse, the edit guard " +
|
|
123
|
+
"refuses every Bash command. A doctor that counts it as fine sends the reader looking " +
|
|
124
|
+
"everywhere but the file",
|
|
125
125
|
edits: [
|
|
126
126
|
[
|
|
127
127
|
SRC,
|
|
128
|
-
|
|
129
|
-
"
|
|
128
|
+
" ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 1,",
|
|
129
|
+
" ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 0,",
|
|
130
130
|
],
|
|
131
131
|
],
|
|
132
132
|
},
|
package/src/doctor.ts
CHANGED
|
@@ -29,16 +29,10 @@ import { spawnSync } from "node:child_process";
|
|
|
29
29
|
// the hook itself rather than at a shared module.
|
|
30
30
|
import {
|
|
31
31
|
papersRoot,
|
|
32
|
-
CONFIG_KEY,
|
|
33
32
|
DEFAULT_PAPERS_ROOT,
|
|
34
33
|
PAPERS_DIR_FIELD,
|
|
35
|
-
|
|
34
|
+
CONFIG_FILE,
|
|
36
35
|
} from "../hooks/paper-edit-guard.hook.mjs";
|
|
37
|
-
import {
|
|
38
|
-
LEGACY_CONFIG_KEY,
|
|
39
|
-
LEGACY_KEY_MESSAGE,
|
|
40
|
-
declaredSettings,
|
|
41
|
-
} from "../lib/paper-config.mjs";
|
|
42
36
|
import { PAPER_MARKERS } from "./build.ts";
|
|
43
37
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
44
38
|
import { doctorHooks } from "./hooks-settings.ts";
|
|
@@ -146,51 +140,44 @@ export function detectPapers(cwd: string, depth = 2): string[] {
|
|
|
146
140
|
}
|
|
147
141
|
|
|
148
142
|
/**
|
|
149
|
-
* The
|
|
150
|
-
*
|
|
143
|
+
* The root `paperlint.json`'s verdict. Absent, or silent on `papersDir`, is fine: the default is
|
|
144
|
+
* `papers`, and the CLI and the hooks take it alike. Only a file that does not parse is a failure —
|
|
145
|
+
* the edit guard refuses every Bash command while it cannot read it.
|
|
151
146
|
*/
|
|
152
|
-
function declarationVerdict(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (found.legacy) out.push(` ⚠ ${LEGACY_KEY_MESSAGE}`);
|
|
168
|
-
// The old field name is a failure, not a warning: every reader refuses it.
|
|
169
|
-
if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD))
|
|
147
|
+
function declarationVerdict(raw: string | null): {
|
|
148
|
+
lines: string[];
|
|
149
|
+
bad: number;
|
|
150
|
+
} {
|
|
151
|
+
if (raw === null)
|
|
152
|
+
return {
|
|
153
|
+
lines: [
|
|
154
|
+
` ✓ no ${CONFIG_FILE} — every setting has its default; papers are in "${DEFAULT_PAPERS_ROOT}"`,
|
|
155
|
+
],
|
|
156
|
+
bad: 0,
|
|
157
|
+
};
|
|
158
|
+
let settings: unknown;
|
|
159
|
+
try {
|
|
160
|
+
settings = JSON.parse(raw);
|
|
161
|
+
} catch (e) {
|
|
170
162
|
return {
|
|
171
163
|
lines: [
|
|
172
|
-
|
|
173
|
-
`
|
|
164
|
+
` ✗ ${CONFIG_FILE} is not valid JSON: ${(e as Error).message}`,
|
|
165
|
+
` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,
|
|
174
166
|
],
|
|
175
167
|
bad: 1,
|
|
176
168
|
};
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
else
|
|
190
|
-
out.push(
|
|
191
|
-
` ✓ package.json → ${key}.${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
|
|
192
|
-
);
|
|
193
|
-
return { lines: out, bad: 0 };
|
|
169
|
+
}
|
|
170
|
+
const declared = (settings as Record<string, unknown> | null)?.[
|
|
171
|
+
PAPERS_DIR_FIELD
|
|
172
|
+
];
|
|
173
|
+
return {
|
|
174
|
+
lines: [
|
|
175
|
+
declared === undefined
|
|
176
|
+
? ` ✓ ${CONFIG_FILE} names no ${PAPERS_DIR_FIELD} — the default "${DEFAULT_PAPERS_ROOT}"`
|
|
177
|
+
: ` ✓ ${CONFIG_FILE} → ${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
|
|
178
|
+
],
|
|
179
|
+
bad: 0,
|
|
180
|
+
};
|
|
194
181
|
}
|
|
195
182
|
|
|
196
183
|
/**
|
|
@@ -261,7 +248,7 @@ export function doctor({
|
|
|
261
248
|
skillLinks = (r: string) => linkSkills(r, { write: false }),
|
|
262
249
|
}: DoctorOptions = {}): number {
|
|
263
250
|
const root = projectDir ?? cwd;
|
|
264
|
-
const
|
|
251
|
+
const configPath = join(root, CONFIG_FILE);
|
|
265
252
|
const out: string[] = [
|
|
266
253
|
"",
|
|
267
254
|
"paperlint doctor — what is wired, and what only looks wired",
|
|
@@ -269,22 +256,15 @@ export function doctor({
|
|
|
269
256
|
];
|
|
270
257
|
let bad = 0;
|
|
271
258
|
|
|
272
|
-
out.push("
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
` the hooks read their papers directory from there and refuse without it`,
|
|
278
|
-
);
|
|
279
|
-
bad++;
|
|
280
|
-
} else {
|
|
281
|
-
const declaration = declarationVerdict(rawPkg);
|
|
282
|
-
out.push(...declaration.lines);
|
|
283
|
-
bad += declaration.bad;
|
|
284
|
-
}
|
|
259
|
+
out.push("settings");
|
|
260
|
+
const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : null;
|
|
261
|
+
const declaration = declarationVerdict(raw);
|
|
262
|
+
out.push(...declaration.lines);
|
|
263
|
+
bad += declaration.bad;
|
|
285
264
|
|
|
286
265
|
out.push("", "papers directory");
|
|
287
|
-
|
|
266
|
+
// The hook is given what its provider gives it: the file, or `{}` when there is none.
|
|
267
|
+
const hookRoot = papersRoot(raw ?? "{}");
|
|
288
268
|
const hookSays = typeof hookRoot === "string" ? hookRoot : null;
|
|
289
269
|
out.push(
|
|
290
270
|
` the CLI will lint ${cliPapers ?? "(nothing — no declaration found)"}`,
|
package/src/facts-file.test.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* real banal is `test/e2e/banal.mjs`.
|
|
9
9
|
*/
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
11
14
|
import { test } from "vitest";
|
|
12
15
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
13
16
|
import {
|
|
@@ -147,12 +150,23 @@ const good: PdfReader = async () => ({
|
|
|
147
150
|
ok: true,
|
|
148
151
|
facts: { pages: 2, fonts: FONTS, last: lastPage(60, 60), layout: [] },
|
|
149
152
|
});
|
|
150
|
-
/**
|
|
153
|
+
/** The shipped presets at their real paths: `extends` resolves through the same Files port. */
|
|
154
|
+
const SHIPPED_PRESETS = Object.fromEntries(
|
|
155
|
+
readdirSync(packageVenuesDir())
|
|
156
|
+
.filter((f) => /\.jsonc?$/.test(f))
|
|
157
|
+
.map((f) => [
|
|
158
|
+
join(packageVenuesDir(), f),
|
|
159
|
+
readFileSync(join(packageVenuesDir(), f)),
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
/** Options over in-memory files: the PDF and paperlint.json on "disk", a measurer answering `geometry`. */
|
|
151
164
|
function setup(geometry: Geometry = NONE) {
|
|
152
165
|
const files = memoryFiles({
|
|
153
166
|
[PDF]: PDF_BYTES,
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
...SHIPPED_PRESETS,
|
|
168
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
169
|
+
extends: "paperlint:agenticdev",
|
|
156
170
|
kind: "short",
|
|
157
171
|
}),
|
|
158
172
|
});
|
|
@@ -170,7 +184,7 @@ function setup(geometry: Geometry = NONE) {
|
|
|
170
184
|
return { files, measured, o };
|
|
171
185
|
}
|
|
172
186
|
|
|
173
|
-
test("measure: venue and kind from
|
|
187
|
+
test("measure: venue and kind from paperlint.json, pdf relative to the paper, the file's sha256", async () => {
|
|
174
188
|
const { o } = setup();
|
|
175
189
|
const m = await measurePaper(PAPER, PDF, o);
|
|
176
190
|
assert.ok(m.ok);
|
|
@@ -233,8 +247,34 @@ test("writeFactsFile: <paper>/_build/paper.facts.json holds exactly the document
|
|
|
233
247
|
assert.deepEqual(JSON.parse(text), m.value.facts);
|
|
234
248
|
});
|
|
235
249
|
|
|
236
|
-
test("declaredVenue: none without
|
|
250
|
+
test("declaredVenue: none without paperlint.json; no label when it extends no preset", () => {
|
|
237
251
|
assert.equal(declaredVenue(memoryFiles(), PAPER), null);
|
|
238
|
-
const files = memoryFiles({
|
|
239
|
-
|
|
252
|
+
const files = memoryFiles({
|
|
253
|
+
[`${PAPER}/paperlint.json`]: '{"kind":"x","pdf":"b/p.pdf"}',
|
|
254
|
+
});
|
|
255
|
+
assert.deepEqual(declaredVenue(files, PAPER), {
|
|
256
|
+
label: null,
|
|
257
|
+
kind: "x",
|
|
258
|
+
pdf: "b/p.pdf",
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("declaredVenue: the label is the preset's, and an extends that resolves nowhere is refused", () => {
|
|
263
|
+
const ok = memoryFiles({
|
|
264
|
+
...SHIPPED_PRESETS,
|
|
265
|
+
[`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
|
|
266
|
+
});
|
|
267
|
+
assert.equal(declaredVenue(ok, PAPER)?.label, "aisec");
|
|
268
|
+
const typo = memoryFiles({
|
|
269
|
+
...SHIPPED_PRESETS,
|
|
270
|
+
[`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisek"}',
|
|
271
|
+
});
|
|
272
|
+
assert.throws(() => declaredVenue(typo, PAPER), /shipped presets/);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("declaredVenue: a paperlint.json with an unknown key is refused, naming the key", () => {
|
|
276
|
+
const files = memoryFiles({
|
|
277
|
+
[`${PAPER}/paperlint.json`]: '{"venu":"agenticdev"}',
|
|
278
|
+
});
|
|
279
|
+
assert.throws(() => declaredVenue(files, PAPER), /unknown key "venu"/);
|
|
240
280
|
});
|