paperlint 2.1.0 → 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/README.md +233 -224
- 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.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +38 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +28 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +277 -265
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.js.map +1 -1
- package/dist/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- 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 +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +143 -172
- 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.map +1 -1
- package/dist/new-paper.js +2 -13
- 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 +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- 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 +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- 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 +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- 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.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +119 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +60 -23
- 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 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- 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/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 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- 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/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- 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/render-paper.harness.mjs +2 -2
- 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.harness.mjs +8 -4
- package/src/build.ts +62 -6
- package/src/cli.harness.mjs +147 -292
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +344 -304
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- 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 +186 -230
- 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.test.ts +1 -13
- package/src/new-paper.ts +2 -17
- package/src/paper-files.test.ts +290 -0
- package/src/paper-files.ts +144 -0
- package/src/paper-settings-commands.test.ts +84 -121
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- 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 +19 -7
- package/src/structure.harness.mjs +3 -1
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/types.ts +30 -18
- package/src/venue-rules.test.ts +20 -11
- package/src/venue-rules.ts +11 -13
- 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/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
|
@@ -4,7 +4,7 @@ description: Use when asking "does this paper read well?" / "is the writing any
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Agent, Skill, Bash(node .claude/skills/paper-pipeline/scripts/announce.mjs:*), Bash(node .claude/skills/paper-pipeline/scripts/ledger.mjs:*)]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:c0907d5c98e2c831 compiled from skills/grade-paper-writing/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# grade-paper-writing — grade how the paper READS, then fix it sentence by sentence
|
|
10
10
|
|
|
@@ -255,6 +255,25 @@ is what happened.
|
|
|
255
255
|
`PIPELINE-STATUS.md` (Wc row). Same rule as `pc-panel-review`'s ledger and `analyze-sibling-paper`'s
|
|
256
256
|
`siblings/` — review artifacts live colocated and ride with the paper to camera-ready/extension.
|
|
257
257
|
|
|
258
|
+
## The report's frontmatter — the findings, as records
|
|
259
|
+
|
|
260
|
+
The report opens with YAML frontmatter that lists what it found, one record per finding.
|
|
261
|
+
`paperlint lint` validates it (`review/frontmatter`): an **open** finding must name the
|
|
262
|
+
pipeline `cause` that let it through — fix that, and the text edit falls out of running the
|
|
263
|
+
fixed tool.
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
---
|
|
267
|
+
findings:
|
|
268
|
+
- id: 1
|
|
269
|
+
status: open # open | fixed | wontfix
|
|
270
|
+
cause: missing-skill # skill-defect | missing-skill | hook | rule — required when open
|
|
271
|
+
title: "§3 opens on a term the reader has not met"
|
|
272
|
+
- id: 2
|
|
273
|
+
status: fixed
|
|
274
|
+
---
|
|
275
|
+
```
|
|
276
|
+
|
|
258
277
|
## Record the verdict
|
|
259
278
|
|
|
260
279
|
🔴 LAST step, once the deliverable exists:
|
|
@@ -271,6 +271,25 @@ is what happened.
|
|
|
271
271
|
\`PIPELINE-STATUS.md\` (Wc row). Same rule as \`pc-panel-review\`'s ledger and \`analyze-sibling-paper\`'s
|
|
272
272
|
\`siblings/\` — review artifacts live colocated and ride with the paper to camera-ready/extension.
|
|
273
273
|
|
|
274
|
+
## The report's frontmatter — the findings, as records
|
|
275
|
+
|
|
276
|
+
The report opens with YAML frontmatter that lists what it found, one record per finding.
|
|
277
|
+
\`paperlint lint\` validates it (\`review/frontmatter\`): an **open** finding must name the
|
|
278
|
+
pipeline \`cause\` that let it through — fix that, and the text edit falls out of running the
|
|
279
|
+
fixed tool.
|
|
280
|
+
|
|
281
|
+
\`\`\`yaml
|
|
282
|
+
---
|
|
283
|
+
findings:
|
|
284
|
+
- id: 1
|
|
285
|
+
status: open # open | fixed | wontfix
|
|
286
|
+
cause: missing-skill # skill-defect | missing-skill | hook | rule — required when open
|
|
287
|
+
title: "§3 opens on a term the reader has not met"
|
|
288
|
+
- id: 2
|
|
289
|
+
status: fixed
|
|
290
|
+
---
|
|
291
|
+
\`\`\`
|
|
292
|
+
|
|
274
293
|
## Record the verdict
|
|
275
294
|
|
|
276
295
|
🔴 LAST step, once the deliverable exists:
|
|
@@ -29,5 +29,4 @@ export declare function installedSkills(dir: string): string[];
|
|
|
29
29
|
export declare function packageVenuesDir(): string;
|
|
30
30
|
export declare const SHIPPED_SKILLS_DIR: string;
|
|
31
31
|
export declare const PACKAGE_NAME: "paperlint";
|
|
32
|
-
export declare const LEGACY_PACKAGE_NAME: "research-paper-pipeline";
|
|
33
32
|
export declare const BIN_FILE: "bin/paperlint.mjs";
|
|
@@ -32,7 +32,6 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
import { spawnSync } from "node:child_process";
|
|
34
34
|
import {
|
|
35
|
-
CONFIG_KEY,
|
|
36
35
|
DEFAULT_SCRIPTS_ROOT,
|
|
37
36
|
consumerRoot,
|
|
38
37
|
consumerSkillsDir,
|
|
@@ -87,15 +86,12 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
87
86
|
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "consumer-harness-")));
|
|
88
87
|
process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
89
88
|
|
|
90
|
-
/** A throwaway consumer repository with the given `paperlint`
|
|
89
|
+
/** A throwaway consumer repository with the given root `paperlint.json` settings (or none). */
|
|
91
90
|
function fakeConsumer(block) {
|
|
92
91
|
const root = mkdtempSync(join(TMP, "repo-"));
|
|
93
|
-
writeFileSync(
|
|
94
|
-
|
|
95
|
-
JSON.stringify(
|
|
96
|
-
block === undefined ? { name: "x" } : { name: "x", [CONFIG_KEY]: block },
|
|
97
|
-
),
|
|
98
|
-
);
|
|
92
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "x" }));
|
|
93
|
+
if (block !== undefined)
|
|
94
|
+
writeFileSync(join(root, "paperlint.json"), JSON.stringify(block));
|
|
99
95
|
return root;
|
|
100
96
|
}
|
|
101
97
|
/** A directory that looks like an installed copy of this package. */
|
|
@@ -188,7 +184,7 @@ function installedDir(root) {
|
|
|
188
184
|
);
|
|
189
185
|
assert.match(
|
|
190
186
|
err.message,
|
|
191
|
-
/
|
|
187
|
+
/paperlint\.json/,
|
|
192
188
|
"the refusal must name the file the key goes in",
|
|
193
189
|
);
|
|
194
190
|
}
|
|
@@ -409,12 +405,12 @@ assert.equal(
|
|
|
409
405
|
);
|
|
410
406
|
assert.match(
|
|
411
407
|
missing.message,
|
|
412
|
-
/
|
|
408
|
+
/paperlint\.json/,
|
|
413
409
|
"the refusal must name the file the key goes in",
|
|
414
410
|
);
|
|
415
411
|
|
|
416
412
|
// And the default's version of that message must say the default was used — otherwise someone
|
|
417
|
-
// who declared nothing goes looking in
|
|
413
|
+
// who declared nothing goes looking in paperlint.json for a line that is not there.
|
|
418
414
|
assert.throws(
|
|
419
415
|
() => scriptsRoot({ env: {}, cwd: fakeConsumer(undefined) }),
|
|
420
416
|
/Nothing was declared/,
|
|
@@ -462,7 +458,7 @@ assert.equal(
|
|
|
462
458
|
assert.equal(s.ledger, "a/b/ledger.mjs");
|
|
463
459
|
|
|
464
460
|
// EXACTLY ONE slash, whatever the consumer typed. `scriptsRoot()` returns the declaration
|
|
465
|
-
// verbatim, so a trailing slash in
|
|
461
|
+
// verbatim, so a trailing slash in paperlint.json arrives here intact; `${root}/` would then give
|
|
466
462
|
// `a/b//`, which matches nothing — the silent direction again.
|
|
467
463
|
const typedSlash = pipelineScripts("a/b/");
|
|
468
464
|
assert.equal(
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
* ── THE LEDGER, THREE RUNGS ─────────────────────────────────────────────────
|
|
57
57
|
* 1. `PIPELINE_LEDGER` in the environment — always wins. This is what test harnesses set to
|
|
58
58
|
* keep fixture rows out of real history, so it must outrank a declaration on disk.
|
|
59
|
-
* 2. `
|
|
59
|
+
* 2. `{ "ledger": "…" }` in the CONSUMER's root `paperlint.json`, read
|
|
60
60
|
* from `process.cwd()`, resolved relative to it.
|
|
61
61
|
* 3. `runs.jsonl` beside this file — ONLY when this file is not inside `node_modules`, i.e.
|
|
62
62
|
* when the package is being developed in its own checkout. Inside `node_modules` with
|
|
@@ -75,7 +75,6 @@
|
|
|
75
75
|
*/
|
|
76
76
|
import {
|
|
77
77
|
existsSync,
|
|
78
|
-
readFileSync,
|
|
79
78
|
readdirSync,
|
|
80
79
|
readlinkSync,
|
|
81
80
|
realpathSync,
|
|
@@ -86,7 +85,11 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
86
85
|
|
|
87
86
|
// Re-exported, not re-declared — see the note in `lib/paper-config.mjs`. This file is not a hook
|
|
88
87
|
// and never needed its own copy.
|
|
89
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
CONFIG_FILE,
|
|
90
|
+
CONFIG_KEY,
|
|
91
|
+
settingsOf,
|
|
92
|
+
} from "../../../lib/paper-config.mjs";
|
|
90
93
|
export { CONFIG_KEY, settingsOf };
|
|
91
94
|
|
|
92
95
|
/**
|
|
@@ -144,13 +147,6 @@ export function packageVenuesDir() {
|
|
|
144
147
|
*/
|
|
145
148
|
export const PACKAGE_NAME = "paperlint";
|
|
146
149
|
|
|
147
|
-
/**
|
|
148
|
-
* The name the package was published under before 2.0.0. Only for recognising what an older
|
|
149
|
-
* install left behind (hook commands, skill links, the config key) so `init` can replace it and
|
|
150
|
-
* `doctor` can name it.
|
|
151
|
-
*/
|
|
152
|
-
export const LEGACY_PACKAGE_NAME = "research-paper-pipeline";
|
|
153
|
-
|
|
154
150
|
/**
|
|
155
151
|
* The package's one executable, relative to the package root — what `bin` in package.json names
|
|
156
152
|
* and what every hook command runs. Named like the package; `consumer.harness.mjs` checks it
|
|
@@ -245,16 +241,6 @@ function readlinkOr(entry) {
|
|
|
245
241
|
}
|
|
246
242
|
}
|
|
247
243
|
|
|
248
|
-
/** The consumer's parsed `package.json`, or `null` when there is none or it does not parse. */
|
|
249
|
-
export function consumerPkg(opts) {
|
|
250
|
-
const file = join(consumerRoot(opts), "package.json");
|
|
251
|
-
try {
|
|
252
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
253
|
-
} catch {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
244
|
/**
|
|
259
245
|
* True when `dir` lies inside a `node_modules` directory.
|
|
260
246
|
*
|
|
@@ -282,7 +268,7 @@ export function ledgerPath(
|
|
|
282
268
|
|
|
283
269
|
// Rung 2 — the consumer's declaration.
|
|
284
270
|
const root = consumerRoot({ env, cwd });
|
|
285
|
-
const declared = settingsOf(
|
|
271
|
+
const declared = settingsOf(consumerRoot({ env, cwd }))?.ledger;
|
|
286
272
|
// 🔴 `declared === undefined`, NOT `declared ?? default` — the same distinction `papersRoot()`
|
|
287
273
|
// makes and for the same reason: `"ledger": null` is a keystroke, not an absence, and reading
|
|
288
274
|
// it as "nothing was declared" would silently pick a different file than the one asked for.
|
|
@@ -299,8 +285,8 @@ export function ledgerPath(
|
|
|
299
285
|
throw new Error(
|
|
300
286
|
`${CONFIG_KEY}: no ledger location is declared, and the default (a file beside this ` +
|
|
301
287
|
`module) is inside node_modules, which \`npm ci\` deletes.\n` +
|
|
302
|
-
`Declare where the ledger lives, in ${join(root,
|
|
303
|
-
`
|
|
288
|
+
`Declare where the ledger lives, in ${join(root, CONFIG_FILE)}:\n` +
|
|
289
|
+
` { "ledger": "docs/pipeline-runs.jsonl" }\n` +
|
|
304
290
|
`or set PIPELINE_LEDGER for a single run.\n` +
|
|
305
291
|
`This is thrown rather than defaulted on purpose: appending to a path under ` +
|
|
306
292
|
`node_modules succeeds, so the rows would look recorded right up until the next ` +
|
|
@@ -321,7 +307,7 @@ export function ledgerPathExisting(hereDir, opts) {
|
|
|
321
307
|
if (!existsSync(p))
|
|
322
308
|
throw new Error(
|
|
323
309
|
`${CONFIG_KEY}: the ledger "${p}" does not exist. Create it (an empty file is a valid ` +
|
|
324
|
-
`empty ledger) or fix the "ledger" declaration in
|
|
310
|
+
`empty ledger) or fix the "ledger" declaration in ${CONFIG_FILE}.`,
|
|
325
311
|
);
|
|
326
312
|
return p;
|
|
327
313
|
}
|
|
@@ -374,7 +360,7 @@ export const DEFAULT_SCRIPTS_ROOT = ".claude/skills/paper-pipeline/scripts";
|
|
|
374
360
|
*/
|
|
375
361
|
export function scriptsRoot({ env = process.env, cwd = process.cwd() } = {}) {
|
|
376
362
|
const root = consumerRoot({ env, cwd });
|
|
377
|
-
const declared = settingsOf(
|
|
363
|
+
const declared = settingsOf(consumerRoot({ env, cwd }))?.scripts;
|
|
378
364
|
// 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — the same distinction `papersRoot()`
|
|
379
365
|
// and `ledgerPath()` make, for the same reason: `"scripts": null` is a keystroke, not an
|
|
380
366
|
// absence, and silently substituting the default for it hides a typo behind a working run.
|
|
@@ -393,8 +379,8 @@ export function scriptsRoot({ env = process.env, cwd = process.cwd() } = {}) {
|
|
|
393
379
|
`instruction would point at a directory the repository does not track.\n` +
|
|
394
380
|
`Keep a symlink where the prose already looks —\n` +
|
|
395
381
|
` ${join(root, DEFAULT_SCRIPTS_ROOT)} -> node_modules/${CONFIG_KEY}/skills/paper-pipeline/scripts\n` +
|
|
396
|
-
`— or declare the real, repository-relative location in ${join(root,
|
|
397
|
-
`
|
|
382
|
+
`— or declare the real, repository-relative location in ${join(root, CONFIG_FILE)}:\n` +
|
|
383
|
+
` { "scripts": "path/to/pipeline/scripts" }\n` +
|
|
398
384
|
`(If nothing was declared, the root itself is under node_modules: run the command from ` +
|
|
399
385
|
`the consumer repository, or export CLAUDE_PROJECT_DIR.)`,
|
|
400
386
|
);
|
|
@@ -403,9 +389,9 @@ export function scriptsRoot({ env = process.env, cwd = process.cwd() } = {}) {
|
|
|
403
389
|
`${CONFIG_KEY}: the pipeline scripts path "${rel}" does not exist under ${root}.\n` +
|
|
404
390
|
(declared === undefined
|
|
405
391
|
? `Nothing was declared, so the default "${DEFAULT_SCRIPTS_ROOT}" was used. Create the ` +
|
|
406
|
-
`symlink there, or declare the real location in
|
|
407
|
-
`
|
|
408
|
-
: `It is declared in
|
|
392
|
+
`symlink there, or declare the real location in ${CONFIG_FILE}:\n` +
|
|
393
|
+
` { "scripts": "path/to/pipeline/scripts" }`
|
|
394
|
+
: `It is declared in ${CONFIG_FILE} as ` +
|
|
409
395
|
`"scripts": ${JSON.stringify(declared)}. Fix it there, or create the directory.`) +
|
|
410
396
|
`\nThis is thrown rather than ignored on purpose: this value is a PREFIX that callers ` +
|
|
411
397
|
`filter prose with, so a wrong one matches no instruction at all and every check built ` +
|
|
@@ -453,7 +439,7 @@ export function consumerTimezone({
|
|
|
453
439
|
env = process.env,
|
|
454
440
|
cwd = process.cwd(),
|
|
455
441
|
} = {}) {
|
|
456
|
-
const declared = settingsOf(
|
|
442
|
+
const declared = settingsOf(consumerRoot({ env, cwd }))?.timezone;
|
|
457
443
|
// 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — the same distinction every carrier
|
|
458
444
|
// above makes: `"timezone": null` is a keystroke, not an absence.
|
|
459
445
|
const tz = declared === undefined ? DEFAULT_TIMEZONE : declared;
|
|
@@ -467,7 +453,7 @@ export function consumerTimezone({
|
|
|
467
453
|
throw new RangeError(
|
|
468
454
|
`${CONFIG_KEY}: "timezone" is ${JSON.stringify(tz)}, which is not an IANA time zone this ` +
|
|
469
455
|
`runtime knows (e.g. "Europe/Berlin", "America/New_York", "UTC").\n` +
|
|
470
|
-
`Fix it in
|
|
456
|
+
`Fix it in ${CONFIG_FILE}.\n` +
|
|
471
457
|
`This throws rather than falling back to ${DEFAULT_TIMEZONE} on purpose: a silent fallback ` +
|
|
472
458
|
`would put every deadline anchor at the wrong hour while looking like it worked.`,
|
|
473
459
|
);
|
|
@@ -505,7 +491,7 @@ export function consumerContactEmail({
|
|
|
505
491
|
env = process.env,
|
|
506
492
|
cwd = process.cwd(),
|
|
507
493
|
} = {}) {
|
|
508
|
-
const declared = settingsOf(
|
|
494
|
+
const declared = settingsOf(consumerRoot({ env, cwd }))?.contactEmail;
|
|
509
495
|
if (declared === undefined || declared === null) return null;
|
|
510
496
|
if (typeof declared !== "string" || !declared.includes("@"))
|
|
511
497
|
throw new TypeError(
|
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
* as success, so an exported test object would report ✓ without running anything.
|
|
22
22
|
*/
|
|
23
23
|
import assert from "node:assert/strict";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_PAPERS_ROOT,
|
|
26
|
+
PAPERS_DIR_FIELD,
|
|
27
|
+
settingsOf,
|
|
28
|
+
} from "../../../lib/paper-config.mjs";
|
|
25
29
|
import {
|
|
26
30
|
existsSync,
|
|
27
31
|
mkdirSync,
|
|
@@ -57,9 +61,7 @@ const X = await import(join(HERE, "extract-ref-facts.mjs"));
|
|
|
57
61
|
// A directory on disk answers the same question without knowing any name in advance.
|
|
58
62
|
const PAPERS_ROOT = join(
|
|
59
63
|
ROOT,
|
|
60
|
-
|
|
61
|
-
PAPERS_DIR_FIELD
|
|
62
|
-
] ?? "papers",
|
|
64
|
+
settingsOf(ROOT)?.[PAPERS_DIR_FIELD] ?? DEFAULT_PAPERS_ROOT,
|
|
63
65
|
);
|
|
64
66
|
const REAL_PAPERS = existsSync(PAPERS_ROOT)
|
|
65
67
|
? readdirSync(PAPERS_ROOT, { withFileTypes: true })
|
|
@@ -33,13 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import { record, ABSTENTIONS } from "./ledger.mjs";
|
|
35
35
|
import { execFileSync } from "node:child_process";
|
|
36
|
-
import {
|
|
37
|
-
existsSync,
|
|
38
|
-
mkdirSync,
|
|
39
|
-
readFileSync,
|
|
40
|
-
readdirSync,
|
|
41
|
-
writeFileSync,
|
|
42
|
-
} from "node:fs";
|
|
36
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
43
37
|
import { resolve, join, dirname } from "node:path";
|
|
44
38
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
45
39
|
import { isMain, settingsOf } from "./consumer.mjs";
|
|
@@ -83,10 +77,7 @@ const ROOT = consumerRoot();
|
|
|
83
77
|
// simply do not run: `needs()` below checks the directory on disk, so the absence reads as
|
|
84
78
|
// ABSTAINED `input-missing`, not as a crash and not as a clean run.
|
|
85
79
|
const CITE_CHECKS =
|
|
86
|
-
process.env.PIPELINE_CITE_CHECKS ||
|
|
87
|
-
settingsOf(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")))
|
|
88
|
-
?.citeChecks ||
|
|
89
|
-
null;
|
|
80
|
+
process.env.PIPELINE_CITE_CHECKS || settingsOf(ROOT)?.citeChecks || null;
|
|
90
81
|
// The citation-checker directory exists and is declared. It sits in the `needs()` of every row
|
|
91
82
|
// that calls it: without that a wrong or undeclared path would fail with a python stack trace, and
|
|
92
83
|
// in `flags` mode a stack trace is counted line by line and goes into the ledger as a FINDING whose
|
|
@@ -4,7 +4,7 @@ description: Turn a venue's CFP dates into a scheduled, buffered plan on the Goo
|
|
|
4
4
|
allowed-tools: [Read, Write, Grep, Glob, mcp__Google_Calendar__list_events, mcp__Google_Calendar__search_events, mcp__Google_Calendar__create_event, mcp__Google_Calendar__update_event, Bash(node .claude/skills/paper-pipeline/scripts/announce.mjs:*), Bash(node .claude/skills/paper-pipeline/scripts/ledger.mjs:*)]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:faf0c175f1a0c5e5 compiled from skills/plan-paper-timeline/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# plan-paper-timeline — CFP dates → a buffered, filing-aware schedule
|
|
10
10
|
|
|
@@ -96,7 +96,7 @@ Use `mcp__Google_Calendar__create_event` for three anchors per paper. 🔴 **Rea
|
|
|
96
96
|
assume it** — it is the `timezone` carrier, and it is the author's, not the pipeline's:
|
|
97
97
|
|
|
98
98
|
```
|
|
99
|
-
node -p "((
|
|
99
|
+
node -p "(() => { try { return require('./paperlint.json')?.timezone } catch {} })() ?? 'UTC'"
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
The example below shows the default, `UTC`; substitute whatever that command prints.
|
|
@@ -113,7 +113,7 @@ Use \`mcp__Google_Calendar__create_event\` for three anchors per paper. 🔴 **R
|
|
|
113
113
|
assume it** — it is the \`timezone\` carrier, and it is the author's, not the pipeline's:
|
|
114
114
|
|
|
115
115
|
\`\`\`
|
|
116
|
-
node -p "((
|
|
116
|
+
node -p "(() => { try { return require('./paperlint.json')?.timezone } catch {} })() ?? 'UTC'"
|
|
117
117
|
\`\`\`
|
|
118
118
|
|
|
119
119
|
The example below shows the default, \`UTC\`; substitute whatever that command prints.
|
|
@@ -383,7 +383,7 @@ try {
|
|
|
383
383
|
//
|
|
384
384
|
// 🔴 THIS USED TO COMPARE AGAINST ONE HARD-CODED ZONE — the first consumer's. That is exactly
|
|
385
385
|
// the thing a package may not know: the same CFP date is a different wall-clock hour for every
|
|
386
|
-
// author, so the zone is the `timezone` CARRIER, read from the
|
|
386
|
+
// author, so the zone is the `timezone` CARRIER, read from the project's paperlint.json. What
|
|
387
387
|
// the PRESCRIPTION in §3 must show is therefore not anybody's real zone but the documented
|
|
388
388
|
// DEFAULT, next to the instruction telling the model to substitute the declared one.
|
|
389
389
|
//
|
|
@@ -53,8 +53,8 @@ process.exit(
|
|
|
53
53
|
edits: [
|
|
54
54
|
[
|
|
55
55
|
SKILL,
|
|
56
|
-
"
|
|
57
|
-
"
|
|
56
|
+
"require('./paperlint.json')?.timezone } catch {} })() ?? 'UTC'",
|
|
57
|
+
"require('./paperlint.json')?.papersDir } catch {} })() ?? 'papers'",
|
|
58
58
|
],
|
|
59
59
|
],
|
|
60
60
|
harness: HARNESS,
|
|
@@ -348,9 +348,8 @@ fi
|
|
|
348
348
|
# and it does it WIDER: the block here skipped review mode, the rule judges the artifact always. Two
|
|
349
349
|
# sources of truth about one fact drift apart — so the duplicate was deleted, not kept "just in case".
|
|
350
350
|
#
|
|
351
|
-
# 🔴 The condition under which this becomes a loss: the rule looks at papers
|
|
352
|
-
#
|
|
353
|
-
# fonts. Today there are none (all three real papers are declared).
|
|
351
|
+
# 🔴 The condition under which this becomes a loss: the rule looks at papers whose `paperlint.json`
|
|
352
|
+
# names a venue preset. A paper with a build but no preset gets its fonts checked by nobody.
|
|
354
353
|
|
|
355
354
|
# --- chktex, if installed: LaTeX-source typography the log cannot see ---
|
|
356
355
|
# Adopted 2026-08-24 instead of writing our own. Measured first: out of the box it produced 28
|
|
@@ -36,8 +36,8 @@ import { dirname, join } from "node:path";
|
|
|
36
36
|
import { checkSkill } from "../../lib/skill-checks.mjs";
|
|
37
37
|
import { papersRoot } from "../../eslint-rules/papers.mjs";
|
|
38
38
|
import {
|
|
39
|
-
consumerPkg,
|
|
40
39
|
consumerRoot,
|
|
40
|
+
settingsOf,
|
|
41
41
|
} from "../paper-pipeline/scripts/consumer.mjs";
|
|
42
42
|
|
|
43
43
|
await checkSkill("render-paper");
|
|
@@ -308,7 +308,7 @@ await checkSkill("render-paper");
|
|
|
308
308
|
// and say so out loud.
|
|
309
309
|
const root = (() => {
|
|
310
310
|
try {
|
|
311
|
-
return papersRoot(
|
|
311
|
+
return papersRoot(settingsOf(consumerRoot()), consumerRoot());
|
|
312
312
|
} catch {
|
|
313
313
|
return null;
|
|
314
314
|
}
|
|
@@ -4,7 +4,7 @@ description: Verify every citation is a real work with correct metadata, and tha
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:2bb9f6f1233094cf compiled from skills/verify-citations/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# verify-citations — every cite real, the delta explicit
|
|
10
10
|
|
|
@@ -136,6 +136,15 @@ any `false` (fabrication)** — `unresolvable` alone is advisory and does NOT fa
|
|
|
136
136
|
narrowed-false philosophy. API responses are cached to `scripts/.cite-cache.json` (gitignored) so re-runs are
|
|
137
137
|
cheap and deterministic.
|
|
138
138
|
|
|
139
|
+
## 5a. Both checks run on every `paperlint build`
|
|
140
|
+
|
|
141
|
+
`npx paperlint build` runs `verify-cites` and `bib-authors` over the paper's bibliography after the PDF is
|
|
142
|
+
built and records the verdicts, with the SHA-256 of the bibliography it checked, in
|
|
143
|
+
`<paper>/_build/references.json`. `paperlint lint` reads that record offline: `paper/cite-exists` and
|
|
144
|
+
`paper/author-list` report a failing entry on its own line, `paper/refs-fresh` says when the bibliography
|
|
145
|
+
changed since, and `paper/refs-checked` warns when nothing was recorded or the build had no network. The
|
|
146
|
+
step never fails the build. Run the scripts by hand (below) to read a single verdict in full.
|
|
147
|
+
|
|
139
148
|
## 5b. The author-list gate — `scripts/bib-authors.mjs`
|
|
140
149
|
|
|
141
150
|
`verify-cites.mjs` answers *"does this citation exist, and does the id point at it?"*. There is a second
|
|
@@ -152,6 +152,15 @@ any \`false\` (fabrication)** — \`unresolvable\` alone is advisory and does NO
|
|
|
152
152
|
narrowed-false philosophy. API responses are cached to \`scripts/.cite-cache.json\` (gitignored) so re-runs are
|
|
153
153
|
cheap and deterministic.
|
|
154
154
|
|
|
155
|
+
## 5a. Both checks run on every \`paperlint build\`
|
|
156
|
+
|
|
157
|
+
\`npx paperlint build\` runs \`verify-cites\` and \`bib-authors\` over the paper's bibliography after the PDF is
|
|
158
|
+
built and records the verdicts, with the SHA-256 of the bibliography it checked, in
|
|
159
|
+
\`<paper>/_build/references.json\`. \`paperlint lint\` reads that record offline: \`paper/cite-exists\` and
|
|
160
|
+
\`paper/author-list\` report a failing entry on its own line, \`paper/refs-fresh\` says when the bibliography
|
|
161
|
+
changed since, and \`paper/refs-checked\` warns when nothing was recorded or the build had no network. The
|
|
162
|
+
step never fails the build. Run the scripts by hand (below) to read a single verdict in full.
|
|
163
|
+
|
|
155
164
|
## 5b. The author-list gate — \`scripts/bib-authors.mjs\`
|
|
156
165
|
|
|
157
166
|
\`verify-cites.mjs\` answers *"does this citation exist, and does the id point at it?"*. There is a second
|
|
@@ -192,7 +192,7 @@ function bibTextFrom(target) {
|
|
|
192
192
|
* Only three fields are needed (type, key, author, title, booktitle/journal), and a full
|
|
193
193
|
* BibTeX grammar would be a second thing to maintain. Brace-depth counting is enough and
|
|
194
194
|
* is exercised by the colocated test. */
|
|
195
|
-
function parseBib(text) {
|
|
195
|
+
export function parseBib(text) {
|
|
196
196
|
const out = [];
|
|
197
197
|
const re = /@(\w+)\s*\{\s*([^,\s]+)\s*,/g;
|
|
198
198
|
let m;
|
|
@@ -289,7 +289,7 @@ export function claimsPublished(e) {
|
|
|
289
289
|
|
|
290
290
|
/* ---------- DBLP ---------- */
|
|
291
291
|
|
|
292
|
-
async function dblpHits(title) {
|
|
292
|
+
export async function dblpHits(title) {
|
|
293
293
|
const url = `${DBLP}/?q=${encodeURIComponent(title)}&format=json&h=6`;
|
|
294
294
|
// Per-request timeout: a check that hangs is indistinguishable from a check that is dead,
|
|
295
295
|
// and this one runs 25+ requests. Measured 2026-08-24: a single unbounded query stalled the
|
|
@@ -335,23 +335,31 @@ export function compare(ourSurnames, theirSurnames) {
|
|
|
335
335
|
return { missing, extra, orderDiffers };
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
338
|
+
/**
|
|
339
|
+
* The comparison over parsed entries, the network behind `lookup` (DBLP by default). Returns the
|
|
340
|
+
* buckets main() prints and `paperlint build` records: `findings` (the author list differs from
|
|
341
|
+
* the published version's), `matched` (compared, equal), `skipped` (not applicable), `unchecked`
|
|
342
|
+
* (the lookup FAILED — never a pass). An entry in none of them was a preprint entry, which may
|
|
343
|
+
* carry preprint metadata.
|
|
344
|
+
*
|
|
345
|
+
* @param lookup title → DBLP-shaped hits; throws on a failed request (`retryable` for a 429)
|
|
346
|
+
* @param pause ms → a promise; DBLP asks for gentle clients. A test passes `() => {}`.
|
|
347
|
+
*/
|
|
348
|
+
export async function checkAuthors(
|
|
349
|
+
parsed,
|
|
350
|
+
{
|
|
351
|
+
lookup = dblpHits,
|
|
352
|
+
pause = (ms) =>
|
|
353
|
+
new Promise((r) => {
|
|
354
|
+
setTimeout(r, ms);
|
|
355
|
+
}),
|
|
356
|
+
} = {},
|
|
357
|
+
) {
|
|
352
358
|
const findings = [];
|
|
353
359
|
const skipped = []; // legitimately not applicable
|
|
354
360
|
const unchecked = []; // we FAILED to check — must never be reported as a pass
|
|
361
|
+
const matched = []; // compared against the published record, and equal
|
|
362
|
+
const entries = parsed.filter((e) => e.title && e.author);
|
|
355
363
|
|
|
356
364
|
for (const e of parsed.filter((x) => x.unparsed)) {
|
|
357
365
|
skipped.push({
|
|
@@ -377,7 +385,7 @@ async function main() {
|
|
|
377
385
|
let hits = null;
|
|
378
386
|
for (let attempt = 0; attempt < 3 && hits === null; attempt++) {
|
|
379
387
|
try {
|
|
380
|
-
hits = await
|
|
388
|
+
hits = await lookup(e.title.replace(/[{}]/g, ""));
|
|
381
389
|
} catch (err) {
|
|
382
390
|
const last = attempt === 2;
|
|
383
391
|
if (last)
|
|
@@ -385,10 +393,7 @@ async function main() {
|
|
|
385
393
|
key: e.key,
|
|
386
394
|
why: `DBLP lookup failed: ${err.message}`,
|
|
387
395
|
});
|
|
388
|
-
else
|
|
389
|
-
await new Promise((r) => {
|
|
390
|
-
setTimeout(r, 1500 * (attempt + 1));
|
|
391
|
-
});
|
|
396
|
+
else await pause(1500 * (attempt + 1));
|
|
392
397
|
}
|
|
393
398
|
}
|
|
394
399
|
if (hits === null) continue;
|
|
@@ -408,7 +413,9 @@ async function main() {
|
|
|
408
413
|
const ours = surnames(e.author);
|
|
409
414
|
const theirs = surnames(rec.authors.join(" and "));
|
|
410
415
|
const d = compare(ours, theirs);
|
|
411
|
-
if (d.missing.length || d.extra.length || d.orderDiffers)
|
|
416
|
+
if (!(d.missing.length || d.extra.length || d.orderDiffers))
|
|
417
|
+
matched.push(e.key);
|
|
418
|
+
else {
|
|
412
419
|
findings.push({
|
|
413
420
|
key: e.key,
|
|
414
421
|
venue: `${rec.venue} ${rec.year}`.trim(),
|
|
@@ -417,12 +424,27 @@ async function main() {
|
|
|
417
424
|
...d,
|
|
418
425
|
});
|
|
419
426
|
}
|
|
420
|
-
|
|
421
|
-
await new Promise((r) => {
|
|
422
|
-
setTimeout(r, 900);
|
|
423
|
-
}); // DBLP asks for gentle clients; 350 ms drew 429s
|
|
427
|
+
await pause(900); // DBLP asks for gentle clients; 350 ms drew 429s
|
|
424
428
|
}
|
|
425
429
|
|
|
430
|
+
return { findings, skipped, unchecked, matched };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function die(msg) {
|
|
434
|
+
console.error(`bib-authors: ${msg}`);
|
|
435
|
+
process.exit(2);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function main() {
|
|
439
|
+
const args = process.argv.slice(2).filter((a) => a !== "--json");
|
|
440
|
+
const asJson = process.argv.includes("--json");
|
|
441
|
+
if (!args[0])
|
|
442
|
+
die("usage: bib-authors.mjs <paper-dir|file.bib|file.tex> [--json]");
|
|
443
|
+
|
|
444
|
+
const { text, file, markdown } = bibTextFrom(args[0]);
|
|
445
|
+
const parsed = markdown ? parseMarkdownRefs(text) : parseBib(text);
|
|
446
|
+
const entries = parsed.filter((e) => e.title && e.author);
|
|
447
|
+
const { findings, skipped, unchecked } = await checkAuthors(parsed);
|
|
426
448
|
if (asJson) {
|
|
427
449
|
console.log(
|
|
428
450
|
JSON.stringify(
|