paperlint 2.0.0 → 2.1.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/workflows/ci.yml +4 -4
- package/CLAUDE.md +3 -3
- package/CONTRIBUTING.md +21 -0
- package/LICENSE +1 -1
- package/README.md +81 -24
- package/action.harness.mjs +10 -10
- package/action.mutations.mjs +3 -3
- package/action.yml +11 -11
- package/bin/{rpp.mjs → paperlint.mjs} +2 -2
- package/dist/adapters/banal/invocation.js +3 -3
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +1 -1
- package/dist/adapters/banal/run.js +1 -1
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +4 -4
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +4 -4
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/banal/xml.js +1 -1
- package/dist/adapters/banal/xml.js.map +1 -1
- package/dist/adapters/curl/download.io.d.ts.map +1 -1
- package/dist/adapters/curl/download.io.js +2 -2
- package/dist/adapters/curl/download.io.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -3
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +29 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +174 -98
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +21 -6
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +2 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +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 -18
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +70 -42
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +13 -10
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +74 -42
- package/dist/init.js.map +1 -1
- package/dist/link-skills.js +2 -2
- 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 +17 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +68 -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/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +186 -0
- package/dist/presets.js.map +1 -0
- package/dist/rules-config.d.ts +8 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +13 -8
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +2 -1
- 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/toolchain.d.ts +2 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +8 -8
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +4 -4
- 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 +387 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +73 -17
- package/docs/e2e.md +3 -4
- package/docs/install.md +11 -3
- package/docs/optional-rules.md +26 -25
- package/docs/rules.md +132 -3
- package/docs/toolchain.md +5 -5
- package/eslint-rules/paper-typography.mjs +1 -1
- package/eslint-rules/paper-typography.mutations.mjs +1 -1
- package/eslint-rules/papers.mjs +6 -2
- package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
- package/eslint-rules/pdf-last-page-balance.mjs +23 -2
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/build-e2e/guards/paper.tex +1 -1
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.json +1 -1
- package/fixtures/real-markdown-paper/baseline.mjs +2 -2
- package/fixtures/toolchain-mirror/install-tl +2 -2
- package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
- package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
- package/hooks/hooks.harness.mjs +1 -1
- package/lib/paper-config.d.mts +7 -0
- package/lib/paper-config.harness.mjs +3 -3
- package/lib/paper-config.mjs +34 -3
- package/lib/skill-trigger-cases.harness.mjs +1 -1
- package/package.json +4 -4
- package/plugin/hooks/hooks.json +3 -3
- package/scripts/check.mjs +1 -1
- package/scripts/harness-api.frozen.json +1 -1
- package/scripts/harness-api.test.ts +2 -2
- package/scripts/layer-legacy.frozen.json +1 -1
- package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
- package/scripts/mutation-batteries.frozen.json +2 -2
- package/scripts/release-config.test.ts +1 -1
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -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/description-language.eval.mjs +1 -1
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
- package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
- package/skills/render-paper/SKILL.md +3 -3
- package/skills/render-paper/SKILL.md.spec.ts +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +6 -6
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- 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 +120 -30
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.test.ts +3 -3
- package/src/adapters/banal/invocation.ts +3 -3
- package/src/adapters/banal/locate.test.ts +4 -4
- package/src/adapters/banal/run.ts +2 -2
- package/src/adapters/banal/settings.test.ts +12 -6
- package/src/adapters/banal/settings.ts +9 -5
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/banal/xml.ts +1 -1
- package/src/adapters/curl/download.io.ts +4 -2
- package/src/adapters/curl/download.test.ts +3 -1
- package/src/adapters/node/files.test.ts +1 -1
- package/src/adapters/node/process.test.ts +2 -2
- package/src/adapters/node/workspace.test.ts +4 -4
- package/src/build-engine.harness.mjs +2 -2
- package/src/build-engine.ts +11 -3
- package/src/build.harness.mjs +27 -22
- package/src/build.mutations.mjs +3 -2
- package/src/build.ts +6 -5
- package/src/cli.harness.mjs +72 -131
- package/src/cli.mutations.mjs +7 -32
- package/src/cli.ts +204 -121
- package/src/doctor.harness.mjs +9 -25
- package/src/doctor.ts +28 -5
- package/src/engine.harness.mjs +4 -2
- package/src/engine.ts +2 -2
- package/src/facts-file.test.ts +54 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +24 -15
- package/src/hooks-settings.mutations.mjs +2 -4
- package/src/hooks-settings.ts +92 -49
- package/src/init.ts +88 -52
- package/src/latex-log.harness.mjs +1 -1
- package/src/link-skills.harness.mjs +3 -1
- package/src/link-skills.mutations.mjs +1 -1
- package/src/link-skills.ts +2 -2
- package/src/new-paper.harness.mjs +10 -7
- package/src/new-paper.test.ts +145 -0
- package/src/new-paper.ts +22 -2
- package/src/paper-settings-commands.test.ts +311 -0
- package/src/paper-settings.test.ts +220 -0
- package/src/paper-settings.ts +206 -0
- package/src/pdf-facts.harness.mjs +1 -1
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +307 -0
- package/src/rules-config.ts +14 -9
- package/src/structure.harness.mjs +4 -4
- package/src/structure.ts +2 -1
- package/src/tex-requirements.harness.mjs +12 -18
- package/src/tex-requirements.ts +179 -29
- package/src/toolchain.harness.mjs +34 -25
- package/src/toolchain.ts +14 -8
- package/src/types.ts +4 -4
- package/src/venue-rules.test.ts +545 -0
- package/src/venue-rules.ts +573 -0
- package/templates/paper/paperlint.json +4 -0
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/plugin/.claude-plugin/plugin.json +0 -8
package/src/doctor.ts
CHANGED
|
@@ -37,9 +37,11 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
LEGACY_CONFIG_KEY,
|
|
39
39
|
LEGACY_KEY_MESSAGE,
|
|
40
|
+
LEGACY_PAPER_SETTINGS_FILE,
|
|
41
|
+
PAPER_SETTINGS_FILE,
|
|
40
42
|
declaredSettings,
|
|
41
43
|
} from "../lib/paper-config.mjs";
|
|
42
|
-
import { PAPER_MARKERS } from "./build.ts";
|
|
44
|
+
import { PAPER_MARKERS, papersIn } from "./build.ts";
|
|
43
45
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
44
46
|
import { doctorHooks } from "./hooks-settings.ts";
|
|
45
47
|
|
|
@@ -233,6 +235,28 @@ function papersVerdict(
|
|
|
233
235
|
return { lines: out, bad };
|
|
234
236
|
}
|
|
235
237
|
|
|
238
|
+
/**
|
|
239
|
+
* A pre-2.1.0 `venue.json` left in a paper: it is no longer read, so that paper's venue checks
|
|
240
|
+
* silently would not run if nothing named it. `init` moves it.
|
|
241
|
+
*/
|
|
242
|
+
function leftoverSettingsVerdict(
|
|
243
|
+
root: string,
|
|
244
|
+
papersDir: string | null,
|
|
245
|
+
): { lines: string[]; bad: number } {
|
|
246
|
+
if (papersDir === null) return { lines: [], bad: 0 };
|
|
247
|
+
const lines = papersIn(resolve(root, papersDir), [
|
|
248
|
+
...PAPER_MARKERS,
|
|
249
|
+
LEGACY_PAPER_SETTINGS_FILE,
|
|
250
|
+
])
|
|
251
|
+
.map((dir) => join(dir, LEGACY_PAPER_SETTINGS_FILE))
|
|
252
|
+
.filter((f) => existsSync(f))
|
|
253
|
+
.map(
|
|
254
|
+
(f) =>
|
|
255
|
+
` ✗ ${relative(root, f)} is no longer read — paperlint 2.1.0 renamed it ${PAPER_SETTINGS_FILE}, so this paper's venue checks do not run. \`npx paperlint init\` moves it`,
|
|
256
|
+
);
|
|
257
|
+
return { lines, bad: lines.length };
|
|
258
|
+
}
|
|
259
|
+
|
|
236
260
|
export interface DoctorOptions {
|
|
237
261
|
log?: typeof console.log;
|
|
238
262
|
cwd?: string;
|
|
@@ -282,8 +306,6 @@ export function doctor({
|
|
|
282
306
|
out.push(...declaration.lines);
|
|
283
307
|
bad += declaration.bad;
|
|
284
308
|
}
|
|
285
|
-
if (existsSync(join(root, "rpp.json")))
|
|
286
|
-
out.push(` ⚠ rpp.json is present — deprecated; the hooks never read it`);
|
|
287
309
|
|
|
288
310
|
out.push("", "papers directory");
|
|
289
311
|
const hookRoot = rawPkg ? papersRoot(rawPkg) : null;
|
|
@@ -295,8 +317,9 @@ export function doctor({
|
|
|
295
317
|
` the hooks will guard ${hookSays ?? "(nothing — the guard refuses and says why on first use)"}`,
|
|
296
318
|
);
|
|
297
319
|
const verdict = papersVerdict(root, cliPapers, hookSays);
|
|
298
|
-
|
|
299
|
-
|
|
320
|
+
const leftover = leftoverSettingsVerdict(root, cliPapers ?? hookSays);
|
|
321
|
+
out.push(...verdict.lines, ...leftover.lines);
|
|
322
|
+
bad += verdict.bad + leftover.bad;
|
|
300
323
|
|
|
301
324
|
// A skill that is not linked is ADVISORY, like a missing program: `paperlint lint`, the hooks and CI
|
|
302
325
|
// work without it, and an entry of the same name that `init` refused to replace is the
|
package/src/engine.harness.mjs
CHANGED
|
@@ -53,7 +53,7 @@ const facts = (o) => ({
|
|
|
53
53
|
...o,
|
|
54
54
|
});
|
|
55
55
|
const ROWS = [
|
|
56
|
-
// Guards: the order —
|
|
56
|
+
// Guards: the order — paperlint's own verified cache first, so a build does not depend on whatever TeX
|
|
57
57
|
// Live the machine happens to carry.
|
|
58
58
|
[
|
|
59
59
|
"a complete cache wins, even over a complete system TeX",
|
|
@@ -198,7 +198,9 @@ check(
|
|
|
198
198
|
// to start, while `paperlint toolchain --check` reported the tree verified. Checked on disk, not
|
|
199
199
|
// through an injected predicate, because the predicate is exactly what was wrong.
|
|
200
200
|
{
|
|
201
|
-
const bin = realpathSync(
|
|
201
|
+
const bin = realpathSync(
|
|
202
|
+
mkdtempSync(join(tmpdir(), "paperlint-engine-bin-")),
|
|
203
|
+
);
|
|
202
204
|
try {
|
|
203
205
|
mkdirSync(join(bin, "texcount"));
|
|
204
206
|
writeFileSync(join(bin, "checkcites"), "#!/bin/sh\n");
|
package/src/engine.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* TeX Live qualifies only when `kpsewhich` finds every file the venue declares (issue #26).
|
|
9
9
|
*
|
|
10
10
|
* The order (the Playwright pattern, decided 2026-09-24):
|
|
11
|
-
* 1.
|
|
11
|
+
* 1. paperlint's own cache (`paperlint toolchain` installs it) — when it has everything;
|
|
12
12
|
* 2. a TeX Live already on PATH — when IT has everything;
|
|
13
13
|
* 3. neither: on a terminal, ask once and install; without one (CI, an agent), refuse in one line
|
|
14
14
|
* that names `npx paperlint toolchain` and the missing packages. Never a silent install without a
|
|
@@ -41,7 +41,7 @@ export interface TreeProbe {
|
|
|
41
41
|
export interface EngineFacts {
|
|
42
42
|
/** install-tl-unx runs here (Linux, macOS). */
|
|
43
43
|
readonly supported: boolean;
|
|
44
|
-
/**
|
|
44
|
+
/** paperlint's cached TeX Live, probed — or null when there is none. */
|
|
45
45
|
readonly cache: TreeProbe | null;
|
|
46
46
|
/** The `pdflatex` on PATH, probed — or null when there is none. */
|
|
47
47
|
readonly system: TreeProbe | null;
|
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,41 @@ 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 venue.json alone is refused with the command that moves it, not read", () => {
|
|
276
|
+
const files = memoryFiles({
|
|
277
|
+
[`${PAPER}/venue.json`]: '{"venue":"agenticdev"}',
|
|
278
|
+
});
|
|
279
|
+
assert.throws(() => declaredVenue(files, PAPER), /npx paperlint init/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("declaredVenue: a paperlint.json with an unknown key is refused, naming the key", () => {
|
|
283
|
+
const files = memoryFiles({
|
|
284
|
+
[`${PAPER}/paperlint.json`]: '{"venu":"agenticdev"}',
|
|
285
|
+
});
|
|
286
|
+
assert.throws(() => declaredVenue(files, PAPER), /unknown key "venu"/);
|
|
240
287
|
});
|
package/src/facts-file.ts
CHANGED
|
@@ -40,13 +40,16 @@ import { describeFailure, type PdfFacts, type PdfReader } from "./pdf-facts.ts";
|
|
|
40
40
|
import {
|
|
41
41
|
flatGeometry,
|
|
42
42
|
type FactsGeometryFields,
|
|
43
|
+
type FlatGeometry,
|
|
43
44
|
type Geometry,
|
|
44
45
|
} from "./domain/geometry.ts";
|
|
45
|
-
import { sha256Hex } from "./domain/sha256.ts";
|
|
46
|
+
import { parseSha256, sha256Hex, type Sha256 } from "./domain/sha256.ts";
|
|
46
47
|
import type { MeasureGeometry } from "./ports/measure-geometry.ts";
|
|
47
48
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
48
49
|
import type { Files } from "./ports/files.ts";
|
|
49
50
|
import { err, ok, type Result } from "./domain/result.ts";
|
|
51
|
+
import { paperPreset, paperPresetProblem } from "./presets.ts";
|
|
52
|
+
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
50
53
|
|
|
51
54
|
export const FACTS_SCHEMA = 2;
|
|
52
55
|
export const FACTS_DIR = "_build";
|
|
@@ -56,41 +59,42 @@ export const FACTS_FILE = "paper.facts.json";
|
|
|
56
59
|
export const factsPath = (paperDir: string): string =>
|
|
57
60
|
join(paperDir, FACTS_DIR, FACTS_FILE);
|
|
58
61
|
|
|
59
|
-
/** The `venue.json` a paper declares: where it is submitted, as which kind, and where its PDF is. */
|
|
60
|
-
export interface VenueDecl {
|
|
61
|
-
readonly venue: string;
|
|
62
|
-
readonly kind: string | null;
|
|
63
|
-
readonly pdf: string | null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
62
|
/**
|
|
67
63
|
* A path as the caller gave it. The `Files` adapter resolves a relative one against the process's
|
|
68
64
|
* cwd, which is what these callers have always meant; the brand is not a claim this code checked it.
|
|
69
65
|
*/
|
|
70
66
|
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const venue = field("venue");
|
|
79
|
-
return venue ? { venue, kind: field("kind"), pdf: field("pdf") } : null;
|
|
68
|
+
/** What the facts need from a paper's `paperlint.json`: its venue's label, its kind, where its PDF is. */
|
|
69
|
+
export interface VenueDecl {
|
|
70
|
+
/** The venue preset's display label (`agenticdev`), or null when the paper extends none. */
|
|
71
|
+
readonly label: string | null;
|
|
72
|
+
readonly kind: string | null;
|
|
73
|
+
readonly pdf: string | null;
|
|
80
74
|
}
|
|
81
75
|
|
|
82
76
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
77
|
+
* What a paper's `paperlint.json` declares, or null when it has none. THROWS with a one-line reason
|
|
78
|
+
* when the file does not parse, its `extends` does not resolve, or only the pre-2.1.0 `venue.json`
|
|
79
|
+
* is there: the build reports that as its `facts` failure rather than building against no venue.
|
|
85
80
|
*/
|
|
86
81
|
export function declaredVenue(
|
|
87
82
|
files: Files,
|
|
88
83
|
paperDir: string,
|
|
84
|
+
venuesDir: string = packageVenuesDir(),
|
|
89
85
|
): VenueDecl | null {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
const p = paperPreset(paperDir, { files, venuesDir });
|
|
87
|
+
const problem = paperPresetProblem(paperDir, p);
|
|
88
|
+
if (problem !== null) throw new Error(problem);
|
|
89
|
+
if (p.kind === "resolved")
|
|
90
|
+
return {
|
|
91
|
+
label: p.preset.label,
|
|
92
|
+
kind: p.settings.kind,
|
|
93
|
+
pdf: p.settings.pdf,
|
|
94
|
+
};
|
|
95
|
+
return p.kind === "none" && p.settings
|
|
96
|
+
? { label: null, kind: p.settings.kind, pdf: p.settings.pdf }
|
|
97
|
+
: null;
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
// ── the document ────────────────────────────────────────────────────────────────────────
|
|
@@ -184,7 +188,7 @@ export interface MeasureOptions {
|
|
|
184
188
|
readonly kind?: string | null;
|
|
185
189
|
/** The page-geometry measurer (banal, as the composition root wired it). */
|
|
186
190
|
readonly measure: MeasureGeometry;
|
|
187
|
-
/** Where the PDF and `
|
|
191
|
+
/** Where the PDF and `paperlint.json` are read from. */
|
|
188
192
|
readonly files: Files;
|
|
189
193
|
}
|
|
190
194
|
|
|
@@ -215,7 +219,7 @@ export async function measurePaper(
|
|
|
215
219
|
const facts = factsDocument({
|
|
216
220
|
pdf: posix(relative(paperDir, pdf)),
|
|
217
221
|
sha: sha256Hex(bytes),
|
|
218
|
-
venue: o.venue ?? decl?.
|
|
222
|
+
venue: o.venue ?? decl?.label ?? null,
|
|
219
223
|
kind: o.kind ?? decl?.kind ?? null,
|
|
220
224
|
read: r.facts,
|
|
221
225
|
geometry,
|
|
@@ -239,3 +243,120 @@ export function writeFactsFile(
|
|
|
239
243
|
);
|
|
240
244
|
return out;
|
|
241
245
|
}
|
|
246
|
+
|
|
247
|
+
// ── reading ─────────────────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/** What a rule judges, read back from the facts file and typed once. */
|
|
250
|
+
export interface ReadFacts {
|
|
251
|
+
/** As the file spells it: relative to the paper directory, or absolute. */
|
|
252
|
+
readonly pdf: string;
|
|
253
|
+
readonly sha: Sha256;
|
|
254
|
+
readonly fonts: readonly FontEntry[];
|
|
255
|
+
/** The geometry columns, or null when no measurer ran (`geometry_source` is null). */
|
|
256
|
+
readonly geometry: FlatGeometry | null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Why a facts file cannot be judged. */
|
|
260
|
+
export type FactsProblem =
|
|
261
|
+
| { readonly kind: "schema"; readonly got: string }
|
|
262
|
+
| { readonly kind: "broken"; readonly why: string };
|
|
263
|
+
|
|
264
|
+
const isRecord = (v: unknown): v is Readonly<Record<string, unknown>> =>
|
|
265
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
266
|
+
const isNum = (v: unknown): v is number =>
|
|
267
|
+
typeof v === "number" && Number.isFinite(v);
|
|
268
|
+
const numOrNull = (v: unknown): v is number | null => v === null || isNum(v);
|
|
269
|
+
|
|
270
|
+
function fontOf(v: unknown): FontEntry | null {
|
|
271
|
+
if (!isRecord(v)) return null;
|
|
272
|
+
const { name, type, embedded, program } = v;
|
|
273
|
+
return typeof name === "string" &&
|
|
274
|
+
typeof type === "string" &&
|
|
275
|
+
typeof embedded === "boolean" &&
|
|
276
|
+
typeof program === "string"
|
|
277
|
+
? { name, type, embedded, program }
|
|
278
|
+
: null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const MAYBE_NUMBERS = [
|
|
282
|
+
"page_w_in",
|
|
283
|
+
"page_h_in",
|
|
284
|
+
"columns",
|
|
285
|
+
"body_pt",
|
|
286
|
+
"ref_pt",
|
|
287
|
+
] as const;
|
|
288
|
+
const NUMBERS = ["body_pages", "ref_pages", "appendix_pages"] as const;
|
|
289
|
+
|
|
290
|
+
/** The geometry columns when a measurer ran; the shape error otherwise. */
|
|
291
|
+
function geometryOf(
|
|
292
|
+
d: Readonly<Record<string, unknown>>,
|
|
293
|
+
): Result<FlatGeometry | null, string> {
|
|
294
|
+
if (d["geometry_source"] === null) return ok(null);
|
|
295
|
+
if (typeof d["geometry_source"] !== "string")
|
|
296
|
+
return err("`geometry_source` is neither a measurer's name nor null");
|
|
297
|
+
const bad =
|
|
298
|
+
MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
|
|
299
|
+
NUMBERS.find((k) => !isNum(d[k]));
|
|
300
|
+
if (bad) return err(`\`${bad}\` is not a number`);
|
|
301
|
+
const pagesByType = d["pages_by_type"];
|
|
302
|
+
if (!isRecord(pagesByType) || !Object.values(pagesByType).every(isNum))
|
|
303
|
+
return err("`pages_by_type` is not an object of page counts");
|
|
304
|
+
const n = (k: (typeof MAYBE_NUMBERS)[number]) => d[k] as number | null;
|
|
305
|
+
return ok({
|
|
306
|
+
page_w_in: n("page_w_in"),
|
|
307
|
+
page_h_in: n("page_h_in"),
|
|
308
|
+
columns: n("columns"),
|
|
309
|
+
body_pt: n("body_pt"),
|
|
310
|
+
ref_pt: n("ref_pt"),
|
|
311
|
+
body_pages: d["body_pages"] as number,
|
|
312
|
+
ref_pages: d["ref_pages"] as number,
|
|
313
|
+
appendix_pages: d["appendix_pages"] as number,
|
|
314
|
+
pages_by_type: pagesByType as Readonly<Record<string, number>>,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The PDF the facts describe: its path and its sha256. */
|
|
319
|
+
function artifactOf(
|
|
320
|
+
d: Readonly<Record<string, unknown>>,
|
|
321
|
+
): Result<{ pdf: string; sha: Sha256 }, string> {
|
|
322
|
+
const pdf = d["pdf"];
|
|
323
|
+
if (typeof pdf !== "string" || pdf === "") return err("no `pdf` path");
|
|
324
|
+
try {
|
|
325
|
+
return ok({ pdf, sha: parseSha256(String(d["pdf_sha256"])) });
|
|
326
|
+
} catch {
|
|
327
|
+
return err("no `pdf_sha256` of 64 hex digits");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The facts file's text → what a rule judges, or why it cannot be judged. The one reader of the
|
|
333
|
+
* file in `src/`, beside its one writer: a field renamed here and not there is a compile error in
|
|
334
|
+
* this module, not a rule that silently reads `undefined`. Pure.
|
|
335
|
+
*/
|
|
336
|
+
export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
337
|
+
let d: unknown;
|
|
338
|
+
try {
|
|
339
|
+
d = JSON.parse(text);
|
|
340
|
+
} catch (e) {
|
|
341
|
+
return err({ kind: "broken", why: `not JSON (${(e as Error).message})` });
|
|
342
|
+
}
|
|
343
|
+
if (!isRecord(d)) return err({ kind: "broken", why: "not a JSON object" });
|
|
344
|
+
if (d["schema"] !== FACTS_SCHEMA)
|
|
345
|
+
return err({ kind: "schema", got: JSON.stringify(d["schema"] ?? null) });
|
|
346
|
+
const artifact = artifactOf(d);
|
|
347
|
+
if (!artifact.ok) return err({ kind: "broken", why: artifact.error });
|
|
348
|
+
const rawFonts = d["fonts"];
|
|
349
|
+
const fonts = Array.isArray(rawFonts) ? rawFonts.map(fontOf) : null;
|
|
350
|
+
if (fonts === null || fonts.some((f) => f === null))
|
|
351
|
+
return err({
|
|
352
|
+
kind: "broken",
|
|
353
|
+
why: "`fonts` is not a list of { name, type, embedded, program }",
|
|
354
|
+
});
|
|
355
|
+
const geometry = geometryOf(d);
|
|
356
|
+
if (!geometry.ok) return err({ kind: "broken", why: geometry.error });
|
|
357
|
+
return ok({
|
|
358
|
+
...artifact.value,
|
|
359
|
+
fonts: fonts as FontEntry[],
|
|
360
|
+
geometry: geometry.value,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
@@ -40,7 +40,6 @@ const {
|
|
|
40
40
|
doctorHooks,
|
|
41
41
|
readSettings,
|
|
42
42
|
MANAGED_BY,
|
|
43
|
-
LEGACY_MANAGED_BY,
|
|
44
43
|
SETTINGS_PATH,
|
|
45
44
|
} = await import(join(HERE, "hooks-settings.ts"));
|
|
46
45
|
const { claudeCodeHookProtocol } = await import("vigiles/claude-code");
|
|
@@ -66,7 +65,7 @@ check(
|
|
|
66
65
|
wiring.names.join() ===
|
|
67
66
|
"paper-edit-guard,paper-skills-nudge,paper-status-gates",
|
|
68
67
|
);
|
|
69
|
-
const work = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
68
|
+
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-hooks-")));
|
|
70
69
|
try {
|
|
71
70
|
const empty = join(work, "empty-hooks.json");
|
|
72
71
|
writeFileSync(empty, '{"hooks":{}}');
|
|
@@ -89,11 +88,16 @@ try {
|
|
|
89
88
|
`node "$CLAUDE_PROJECT_DIR/${MANAGED_BY}" hook paper-status-gates`,
|
|
90
89
|
{ name: "paper-status-gates", ours: true, legacy: false },
|
|
91
90
|
],
|
|
92
|
-
// Guards: what an install
|
|
91
|
+
// Guards: what an older install wrote is recognised as ours-but-stale. Spelled literally, not
|
|
92
|
+
// built from a constant: these are what 1.x and 2.0.0 actually put in users' settings files.
|
|
93
93
|
[
|
|
94
|
-
`node "$CLAUDE_PROJECT_DIR
|
|
94
|
+
`node "$CLAUDE_PROJECT_DIR/node_modules/research-paper-pipeline/bin/rpp.mjs" hook paper-edit-guard`,
|
|
95
95
|
{ name: "paper-edit-guard", ours: false, legacy: true },
|
|
96
96
|
],
|
|
97
|
+
[
|
|
98
|
+
`node "\${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs" hook paper-status-gates`,
|
|
99
|
+
{ name: "paper-status-gates", ours: false, legacy: true },
|
|
100
|
+
],
|
|
97
101
|
[
|
|
98
102
|
`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"`,
|
|
99
103
|
{ name: "paper-edit-guard", ours: false, legacy: false },
|
|
@@ -107,11 +111,11 @@ try {
|
|
|
107
111
|
{ name: "paper-skills-nudge", ours: false, legacy: false },
|
|
108
112
|
],
|
|
109
113
|
[
|
|
110
|
-
`node /abs/proj/node_modules/paperlint/bin/
|
|
114
|
+
`node /abs/proj/node_modules/paperlint/bin/paperlint.mjs hook paper-edit-guard`,
|
|
111
115
|
{ name: "paper-edit-guard", ours: false, legacy: false },
|
|
112
116
|
],
|
|
113
117
|
[`node my-own-lint.mjs`, null],
|
|
114
|
-
[`node node_modules/paperlint/bin/
|
|
118
|
+
[`node node_modules/paperlint/bin/paperlint.mjs lint`, null],
|
|
115
119
|
];
|
|
116
120
|
for (const [cmd, want] of cases)
|
|
117
121
|
check(
|
|
@@ -154,37 +158,42 @@ try {
|
|
|
154
158
|
);
|
|
155
159
|
}
|
|
156
160
|
|
|
157
|
-
// ── an install
|
|
158
|
-
|
|
161
|
+
// ── what an older install wrote is migrated, the user's own command kept ─────────────────
|
|
162
|
+
// Two real histories, spelled literally: 1.x wrote the old package directory, 2.0.0 wrote the
|
|
163
|
+
// new directory with the old entry file name. Neither file exists after an upgrade.
|
|
164
|
+
for (const [release, stale] of [
|
|
165
|
+
["1.x", "node_modules/research-paper-pipeline/bin/rpp.mjs"],
|
|
166
|
+
["2.0.0", "node_modules/paperlint/bin/rpp.mjs"],
|
|
167
|
+
]) {
|
|
159
168
|
const legacyWired = JSON.parse(
|
|
160
169
|
JSON.stringify(merge({}, wiring.compiled, MANAGED_BY)).replaceAll(
|
|
161
170
|
MANAGED_BY,
|
|
162
|
-
|
|
171
|
+
stale,
|
|
163
172
|
),
|
|
164
173
|
);
|
|
165
174
|
legacyWired.hooks.PostToolUse[0].hooks.push({
|
|
166
175
|
type: "command",
|
|
167
176
|
command: "node my-own-lint.mjs",
|
|
168
177
|
});
|
|
169
|
-
const dir = project(
|
|
178
|
+
const dir = project(`legacy-${release}`, legacyWired);
|
|
170
179
|
const before = doctorHooks(dir, wiring).join("\n");
|
|
171
180
|
check(
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
`🔴 doctor names hook commands ${release} left behind — the missing file, and the fix`,
|
|
182
|
+
before.includes(`run ${stale}, which this version does not install`) &&
|
|
174
183
|
/npx paperlint init` replaces them/.test(before),
|
|
175
184
|
);
|
|
176
185
|
const r = wireHooks(dir, merge, wiring);
|
|
177
186
|
const s = JSON.parse(text(dir));
|
|
178
187
|
const counts = [...wiredCounts(s, wiring.names).values()];
|
|
179
188
|
check(
|
|
180
|
-
|
|
189
|
+
`🔴 init replaces what ${release} wrote: every hook wired once, none left at the old path`,
|
|
181
190
|
r.status === "written" &&
|
|
182
191
|
r.replaced === wiring.names.length &&
|
|
183
192
|
counts.every((c) => c.ours === 1 && c.legacy === 0 && c.other === 0) &&
|
|
184
|
-
!text(dir).includes(
|
|
193
|
+
!text(dir).includes(stale),
|
|
185
194
|
);
|
|
186
195
|
check(
|
|
187
|
-
|
|
196
|
+
`and the user's own command in the same matcher survives the ${release} migration`,
|
|
188
197
|
text(dir).includes("node my-own-lint.mjs"),
|
|
189
198
|
);
|
|
190
199
|
}
|
|
@@ -27,11 +27,9 @@ process.exit(
|
|
|
27
27
|
expect: '→ {"name":"paper-edit-guard","ours":false,"legacy":true}',
|
|
28
28
|
disables:
|
|
29
29
|
"the duplicate guard for every bin spelling but ours. A hook wired by hand as " +
|
|
30
|
-
"`npx paperlint hook …` or through an absolute
|
|
30
|
+
"`npx paperlint hook …` or through an absolute path to the bin reads as ours, init merges its own " +
|
|
31
31
|
"copy beside it, and the hook runs twice per event",
|
|
32
|
-
edits: [
|
|
33
|
-
[SRC, " ours: t === MANAGED_BY,", " ours: true,"],
|
|
34
|
-
],
|
|
32
|
+
edits: [[SRC, " ours: t === MANAGED_BY,", " ours: true,"]],
|
|
35
33
|
},
|
|
36
34
|
{
|
|
37
35
|
name: "another spelling no longer stops the write",
|