paperlint 3.2.0 → 4.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 +2 -2
- package/dist/adapters/banal/failure.d.ts +1 -7
- package/dist/adapters/banal/failure.d.ts.map +1 -1
- package/dist/adapters/banal/failure.js +5 -7
- package/dist/adapters/banal/failure.js.map +1 -1
- package/dist/adapters/banal/index.d.ts.map +1 -1
- package/dist/adapters/banal/index.js +8 -11
- package/dist/adapters/banal/index.js.map +1 -1
- package/dist/adapters/banal/install.d.ts.map +1 -1
- package/dist/adapters/banal/install.js +4 -3
- package/dist/adapters/banal/install.js.map +1 -1
- package/dist/adapters/banal/invocation.d.ts +9 -4
- package/dist/adapters/banal/invocation.d.ts.map +1 -1
- package/dist/adapters/banal/invocation.js +13 -1
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/locate.d.ts +12 -9
- package/dist/adapters/banal/locate.d.ts.map +1 -1
- package/dist/adapters/banal/locate.js +19 -12
- package/dist/adapters/banal/locate.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +10 -1
- package/dist/adapters/banal/run.d.ts.map +1 -1
- package/dist/adapters/banal/run.js +8 -4
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +1 -1
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +2 -1
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +8 -2
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/node/process.io.d.ts +6 -3
- package/dist/adapters/node/process.io.d.ts.map +1 -1
- package/dist/adapters/node/process.io.js.map +1 -1
- package/dist/adapters/node/workspace.io.d.ts.map +1 -1
- package/dist/adapters/node/workspace.io.js +4 -2
- package/dist/adapters/node/workspace.io.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +4 -2
- package/dist/adapters/references/index.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 +8 -1
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +2 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -6
- package/dist/build.js.map +1 -1
- package/dist/caller-path.d.ts +3 -0
- package/dist/caller-path.d.ts.map +1 -0
- package/dist/caller-path.js +13 -0
- package/dist/caller-path.js.map +1 -0
- package/dist/cli.d.ts +10 -17
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +62 -12
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +12 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/domain/paths.d.ts +8 -2
- package/dist/domain/paths.d.ts.map +1 -1
- package/dist/domain/paths.js +12 -1
- package/dist/domain/paths.js.map +1 -1
- package/dist/domain/ready.d.ts +24 -0
- package/dist/domain/ready.d.ts.map +1 -0
- package/dist/domain/ready.js +11 -0
- package/dist/domain/ready.js.map +1 -0
- package/dist/domain/record.d.ts +8 -0
- package/dist/domain/record.d.ts.map +1 -0
- package/dist/domain/record.js +8 -0
- package/dist/domain/record.js.map +1 -0
- package/dist/domain/sha256.d.ts.map +1 -1
- package/dist/domain/sha256.js +4 -2
- package/dist/domain/sha256.js.map +1 -1
- package/dist/engine.d.ts +23 -22
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/eslint-rules/bib-reachable-entry.d.ts +1 -1
- package/dist/eslint-rules/bib-reachable-entry.js.map +1 -1
- package/dist/eslint-rules/install-path-literals.d.ts +2 -2
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- package/dist/eslint-rules/paper-research-question.d.ts +1 -1
- package/dist/eslint-rules/paper-research-question.js.map +1 -1
- package/dist/eslint-rules/paper-stages.d.ts +2 -2
- package/dist/eslint-rules/paper-stages.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +6 -6
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
- package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -1
- package/dist/eslint-rules/review-frontmatter.d.ts +2 -2
- package/dist/eslint-rules/review-frontmatter.js.map +1 -1
- package/dist/eslint-rules/sibling-frontmatter.d.ts +1 -1
- package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -1
- package/dist/eslint-rules/temp-root-realpath.d.ts +1 -1
- package/dist/eslint-rules/temp-root-realpath.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts +2 -2
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +29 -26
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -2
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +3 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +18 -8
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +1 -5
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +5 -1
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -6
- package/dist/paper-files.js.map +1 -1
- package/dist/paper-settings.js +3 -3
- package/dist/paper-settings.js.map +1 -1
- package/dist/pdf-facts.d.ts +51 -1
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +23 -15
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.d.ts.map +1 -1
- package/dist/pdf-geometry.js +2 -3
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/tool-installer.d.ts +2 -14
- package/dist/ports/tool-installer.d.ts.map +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +16 -4
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +4 -4
- package/dist/references.js.map +1 -1
- package/dist/rules-config.d.ts +26 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +9 -9
- package/dist/rules-config.js.map +1 -1
- package/dist/tex-requirements.d.ts +4 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/types.d.ts +5 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +20 -7
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +2 -2
- package/eslint-rules/bib-reachable-entry.ts +1 -1
- package/eslint-rules/install-path-literals.ts +2 -2
- package/eslint-rules/paper-research-question.ts +1 -1
- package/eslint-rules/paper-stages.ts +2 -2
- package/eslint-rules/paper-typography.ts +2 -2
- package/eslint-rules/pdf-last-page-balance.ts +1 -1
- package/eslint-rules/review-frontmatter.ts +1 -1
- package/eslint-rules/temp-root-realpath.ts +1 -1
- package/eslint-rules/tex-build.ts +2 -2
- package/eslint.config.mjs +7 -0
- package/fixtures/install-path-literals/clean.fixture.mjs +1 -1
- package/fixtures/install-path-literals/clean.md +2 -2
- package/fixtures/install-path-literals/defect.fixture.mjs +1 -1
- package/fixtures/install-path-literals/defect.md +2 -2
- package/hooks/paper-status-gates.sh +9 -19
- package/lib/paper-config.harness.mjs +1 -1
- package/lib/paper-config.ts +1 -5
- package/lib/skill-checks.mjs +24 -381
- package/lib/skill-checks.test.mjs +63 -115
- package/lib/skill-corpus.mjs +10 -251
- package/lib/skill-corpus.test.mjs +2 -13
- package/lib/skill-trigger-cases.mjs +1 -1
- package/package.json +1 -1
- package/scripts/coverage-config.test.ts +0 -1
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/analyze-sibling-paper/SKILL.md +1 -39
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +0 -38
- package/skills/argument-arc/SKILL.md +2 -36
- package/skills/argument-arc/SKILL.md.spec.ts +1 -43
- package/skills/build-benchmark/SKILL.md +1 -41
- package/skills/build-benchmark/SKILL.md.spec.ts +0 -40
- package/skills/camera-ready/SKILL.md +1 -34
- package/skills/camera-ready/SKILL.md.spec.ts +0 -33
- package/skills/cold-read-diff/SKILL.md +1 -35
- package/skills/cold-read-diff/SKILL.md.spec.ts +0 -34
- package/skills/draft-paper/SKILL.md +2 -41
- package/skills/draft-paper/SKILL.md.spec.ts +1 -50
- package/skills/extend-paper/SKILL.md +2 -33
- package/skills/extend-paper/SKILL.md.spec.ts +1 -42
- package/skills/find-venue/SKILL.md +2 -38
- package/skills/find-venue/SKILL.md.spec.ts +1 -47
- package/skills/grade-paper-writing/SKILL.md +3 -38
- package/skills/grade-paper-writing/SKILL.md.spec.ts +2 -47
- package/skills/grade-paper-writing/prose-lint.mjs +2 -3
- package/skills/grade-paper-writing/prose-lint.test.ts +1 -1
- package/skills/harden-paper/SKILL.md +1 -39
- package/skills/harden-paper/SKILL.md.spec.ts +0 -38
- package/skills/map-prior-work/SKILL.md +1 -35
- package/skills/map-prior-work/SKILL.md.spec.ts +0 -34
- package/skills/osf-artifact-upload/osf-artifact-upload.harness.ts +3 -4
- package/skills/paper-adversarial-review/SKILL.md +2 -35
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -43
- package/skills/paper-pipeline/SKILL.md +2 -28
- package/skills/paper-pipeline/SKILL.md.spec.ts +1 -27
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
- package/skills/paper-pipeline/references/acceptance-gate.md +1 -6
- package/skills/paper-pipeline/references/pipeline-status-template.md +8 -35
- package/skills/paper-pipeline/scripts/README.md +30 -225
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +4 -8
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
- package/skills/paper-pipeline/scripts/consumer.mjs +23 -104
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -42
- package/skills/paper-status/SKILL.md.spec.ts +1 -41
- package/skills/pc-panel-review/SKILL.md +2 -46
- package/skills/pc-panel-review/SKILL.md.spec.ts +1 -45
- package/skills/plan-paper-timeline/SKILL.md +2 -36
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +0 -36
- package/skills/render-paper/SKILL.md +1 -37
- package/skills/render-paper/SKILL.md.spec.ts +0 -36
- package/skills/research-ideate/SKILL.md +2 -39
- package/skills/research-ideate/SKILL.md.spec.ts +1 -47
- package/skills/study-accepted-papers/SKILL.md +1 -34
- package/skills/study-accepted-papers/SKILL.md.spec.ts +0 -33
- package/skills/submit-paper/SKILL.md +1 -33
- package/skills/submit-paper/SKILL.md.spec.ts +0 -32
- package/skills/sweep-design-space/SKILL.md +1 -38
- package/skills/sweep-design-space/SKILL.md.spec.ts +0 -37
- package/skills/tighten-paper/SKILL.md +2 -39
- package/skills/tighten-paper/SKILL.md.spec.ts +1 -47
- package/skills/tighten-paper/structure.mjs +4 -11
- package/skills/verify-citations/SKILL.md +2 -38
- package/skills/verify-citations/SKILL.md.spec.ts +1 -37
- package/src/adapters/banal/failure.ts +9 -10
- package/src/adapters/banal/index.test.ts +2 -2
- package/src/adapters/banal/index.ts +8 -10
- package/src/adapters/banal/install.ts +9 -4
- package/src/adapters/banal/invocation.test.ts +20 -8
- package/src/adapters/banal/invocation.ts +23 -8
- package/src/adapters/banal/locate.test.ts +2 -2
- package/src/adapters/banal/locate.ts +27 -21
- package/src/adapters/banal/run.test.ts +2 -2
- package/src/adapters/banal/run.ts +16 -10
- package/src/adapters/banal/settings.ts +2 -2
- package/src/adapters/curl/download.test.ts +4 -4
- package/src/adapters/memory/index.ts +4 -3
- package/src/adapters/node/files.test.ts +5 -3
- package/src/adapters/node/process.io.ts +9 -2
- package/src/adapters/node/process.test.ts +4 -4
- package/src/adapters/node/workspace.io.ts +9 -4
- package/src/adapters/references/index.ts +7 -2
- package/src/build-engine.test.ts +11 -6
- package/src/build-engine.ts +10 -5
- package/src/build.test.ts +10 -11
- package/src/build.ts +8 -9
- package/src/caller-path.ts +13 -0
- package/src/cli.harness.mjs +0 -2
- package/src/cli.test.ts +19 -8
- package/src/cli.ts +82 -25
- package/src/doctor.test.ts +1 -1
- package/src/doctor.ts +14 -5
- package/src/domain/paths.test.ts +15 -0
- package/src/domain/paths.ts +20 -1
- package/src/domain/ready.ts +31 -0
- package/src/domain/record.test.ts +20 -0
- package/src/domain/record.ts +12 -0
- package/src/domain/sha256.ts +5 -3
- package/src/engine.ts +30 -2
- package/src/facts-file.ts +43 -28
- package/src/hooks-settings.harness.ts +17 -0
- package/src/hooks-settings.ts +28 -4
- package/src/init.test.ts +25 -5
- package/src/init.ts +26 -12
- package/src/new-paper.harness.mjs +7 -23
- package/src/new-paper.ts +7 -3
- package/src/paper-files.test.ts +22 -19
- package/src/paper-files.ts +16 -27
- package/src/paper-settings.test.ts +2 -2
- package/src/paper-settings.ts +5 -5
- package/src/pdf-facts.test.ts +16 -5
- package/src/pdf-facts.ts +79 -22
- package/src/pdf-geometry.ts +5 -3
- package/src/ports/tool-installer.ts +2 -17
- package/src/presets.ts +28 -14
- package/src/references.test.ts +25 -19
- package/src/references.ts +4 -5
- package/src/rules-config.ts +40 -11
- package/src/tex-requirements.test.ts +21 -0
- package/src/tex-requirements.ts +29 -6
- package/src/toolchain.test.ts +9 -5
- package/src/toolchain.ts +2 -2
- package/src/types.ts +5 -3
- package/src/venue-rules.test.ts +8 -4
- package/src/venue-rules.ts +21 -11
- package/skills/paper-pipeline/scripts/announce.mjs +0 -67
- package/skills/paper-pipeline/scripts/announce.test.mjs +0 -102
- package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +0 -51
- package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +0 -52
- package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +0 -6
- package/skills/paper-pipeline/scripts/fixtures/scorecard.ts +0 -100
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +0 -302
- package/skills/paper-pipeline/scripts/ledger.mjs +0 -623
- package/skills/paper-pipeline/scripts/ledger.test.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +0 -474
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +0 -328
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +0 -750
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +0 -169
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +0 -73
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +0 -648
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +0 -468
- package/skills/paper-pipeline/scripts/status.mjs +0 -295
- package/skills/paper-pipeline/scripts/status.test.mjs +0 -232
package/src/cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { spawnSync } from "node:child_process";
|
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { join, dirname, resolve, relative, basename, sep } from "node:path";
|
|
32
32
|
import markdown from "@eslint/markdown";
|
|
33
|
+
import { z } from "zod";
|
|
33
34
|
// Types come from consumer.d.mts beside it.
|
|
34
35
|
import {
|
|
35
36
|
isMain,
|
|
@@ -112,6 +113,7 @@ import {
|
|
|
112
113
|
shippedRuleIds,
|
|
113
114
|
unknownKeys,
|
|
114
115
|
type Parsed,
|
|
116
|
+
type ConfigBlock,
|
|
115
117
|
type RuleBlock,
|
|
116
118
|
type RuleEntry,
|
|
117
119
|
} from "./rules-config.ts";
|
|
@@ -127,6 +129,7 @@ import reviewFrontmatter from "#eslint-rules/review-frontmatter";
|
|
|
127
129
|
import siblingFrontmatter from "#eslint-rules/sibling-frontmatter";
|
|
128
130
|
import pdfRules from "#eslint-rules/pdf-last-page-balance";
|
|
129
131
|
import { messageOf } from "./domain/text.ts";
|
|
132
|
+
import { isRecord } from "./domain/record.ts";
|
|
130
133
|
|
|
131
134
|
/** The shipped venue presets, read from the package's venues directory — the one list. */
|
|
132
135
|
const SHIPPED_VENUES = (): string[] => shippedPresets(packageVenuesDir());
|
|
@@ -220,7 +223,7 @@ settings — paperlint.json, at two levels, one schema. Both are optional.
|
|
|
220
223
|
export function buildConfig(
|
|
221
224
|
opts: PaperlintConfig = {},
|
|
222
225
|
texLanguage: unknown,
|
|
223
|
-
):
|
|
226
|
+
): ConfigBlock[] {
|
|
224
227
|
const paperRules = { ...researchQuestion.rules, ...typography.rules };
|
|
225
228
|
// The reference rules judge `_build/references.json`, and only on `paper.tex`.
|
|
226
229
|
const texPaperRules = {
|
|
@@ -238,7 +241,7 @@ export function buildConfig(
|
|
|
238
241
|
languageOptions: { frontmatter: "yaml" },
|
|
239
242
|
};
|
|
240
243
|
|
|
241
|
-
const cfg:
|
|
244
|
+
const cfg: ConfigBlock[] = [
|
|
242
245
|
// 🔴 THE PROJECT'S PAPER TEMPLATE IS NOT A PAPER. `paperlint new` reads `<papers>/.template/`, and
|
|
243
246
|
// its files carry every marker a paper does. Flat config does NOT ignore dot-directories by
|
|
244
247
|
// default (only `node_modules/` and `.git/`), so without this block `paperlint lint` would lint the
|
|
@@ -329,6 +332,21 @@ export function buildConfig(
|
|
|
329
332
|
return cfg;
|
|
330
333
|
}
|
|
331
334
|
|
|
335
|
+
/**
|
|
336
|
+
* paperlint's blocks as ESLint's config type — the one place the two meet. ESLint types a plugin's
|
|
337
|
+
* rule as one that runs on ANY language; paperlint's run on the language they were written for, so
|
|
338
|
+
* no static proof that a block fits exists (`ConfigBlock`, rules-config.ts). ESLint checks every block
|
|
339
|
+
* when it loads the config and throws on one it cannot use; what is left to check here is that each
|
|
340
|
+
* is an object.
|
|
341
|
+
*/
|
|
342
|
+
const isEslintBlock = (b: unknown): b is Linter.Config =>
|
|
343
|
+
typeof b === "object" && b !== null;
|
|
344
|
+
|
|
345
|
+
export function eslintConfig(blocks: readonly ConfigBlock[]): Linter.Config[] {
|
|
346
|
+
const loose: readonly unknown[] = blocks;
|
|
347
|
+
return loose.filter(isEslintBlock);
|
|
348
|
+
}
|
|
349
|
+
|
|
332
350
|
/**
|
|
333
351
|
* The rule ids a consumer may name in `rules`: every rule paperlint's own config defines, read off that
|
|
334
352
|
* config rather than listed again. `@eslint/markdown` is a dependency's plugin, not paperlint's.
|
|
@@ -357,9 +375,7 @@ export interface RulePlugin {
|
|
|
357
375
|
export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
|
|
358
376
|
const out: Record<string, { rules: Record<string, unknown> }> = {};
|
|
359
377
|
for (const block of buildConfig({}, texLanguage ?? { sentinel: "tex" }))
|
|
360
|
-
for (const [name, plugin] of Object.entries(
|
|
361
|
-
(block as { plugins?: Record<string, RulePlugin> }).plugins ?? {},
|
|
362
|
-
))
|
|
378
|
+
for (const [name, plugin] of Object.entries(block.plugins ?? {}))
|
|
363
379
|
if (plugin !== markdown)
|
|
364
380
|
out[name] = { rules: { ...out[name]?.rules, ...plugin.rules } };
|
|
365
381
|
if (out["tex"] && texLanguage !== undefined)
|
|
@@ -382,12 +398,10 @@ export function rulePlugins(texLanguage?: unknown): Record<string, RulePlugin> {
|
|
|
382
398
|
* switched off in THIS run — `paperlint lint` still reports a directive that silences nothing.
|
|
383
399
|
*/
|
|
384
400
|
export function rulesOff(texLanguage?: unknown): Linter.Config[] {
|
|
385
|
-
return [
|
|
401
|
+
return eslintConfig([
|
|
386
402
|
{
|
|
387
403
|
name: "paperlint/rules-off",
|
|
388
|
-
plugins: rulePlugins(texLanguage)
|
|
389
|
-
Linter.Config["plugins"]
|
|
390
|
-
>,
|
|
404
|
+
plugins: rulePlugins(texLanguage),
|
|
391
405
|
rules: Object.fromEntries([...SHIPPED_RULES].map((id) => [id, "off"])),
|
|
392
406
|
},
|
|
393
407
|
{
|
|
@@ -395,7 +409,7 @@ export function rulesOff(texLanguage?: unknown): Linter.Config[] {
|
|
|
395
409
|
files: PAPER_FILE_PATTERNS,
|
|
396
410
|
linterOptions: { reportUnusedDisableDirectives: "off" },
|
|
397
411
|
},
|
|
398
|
-
];
|
|
412
|
+
]);
|
|
399
413
|
}
|
|
400
414
|
|
|
401
415
|
/** Whether a rule entry (`"error"`, `2`, `["warn", {…}]`) turns the rule on. */
|
|
@@ -412,7 +426,7 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
|
|
|
412
426
|
[...SHIPPED_RULES].filter(
|
|
413
427
|
(id) =>
|
|
414
428
|
!buildConfig({}, { sentinel: "tex language" }).some((b) =>
|
|
415
|
-
isOn(
|
|
429
|
+
isOn(b.rules?.[id]),
|
|
416
430
|
),
|
|
417
431
|
),
|
|
418
432
|
);
|
|
@@ -429,8 +443,13 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
|
|
|
429
443
|
* papers a and b (#103) — a block that reaches a paper outside this run is not dead. A glob that
|
|
430
444
|
* reaches no paper of the project at all still fails, from a subset as from the whole.
|
|
431
445
|
*/
|
|
446
|
+
/** What `calculateConfigForFile` answers, read for its rules: undefined for a file outside ESLint's cwd or scope. */
|
|
447
|
+
const ComputedConfig = z
|
|
448
|
+
.object({ rules: z.record(z.string(), z.unknown()).optional() })
|
|
449
|
+
.optional();
|
|
450
|
+
|
|
432
451
|
export async function silentOptionalRules(
|
|
433
|
-
eslint: ESLint,
|
|
452
|
+
eslint: Pick<ESLint, "calculateConfigForFile">,
|
|
434
453
|
files: readonly string[],
|
|
435
454
|
opts: PaperlintConfig,
|
|
436
455
|
): Promise<string[]> {
|
|
@@ -444,8 +463,7 @@ export async function silentOptionalRules(
|
|
|
444
463
|
const reached = new Set<string>();
|
|
445
464
|
for (const f of new Set(files.filter((p) => basename(p) === MAIN))) {
|
|
446
465
|
// Undefined for a file outside ESLint's cwd or scope: it reaches nothing.
|
|
447
|
-
const cfg = (await eslint.calculateConfigForFile(f))
|
|
448
|
-
{ rules?: Record<string, unknown> } | undefined;
|
|
466
|
+
const cfg = ComputedConfig.parse(await eslint.calculateConfigForFile(f));
|
|
449
467
|
for (const id of turnedOn) if (isOn(cfg?.rules?.[id])) reached.add(id);
|
|
450
468
|
}
|
|
451
469
|
return [...turnedOn].filter((id) => !reached.has(id));
|
|
@@ -543,6 +561,20 @@ const PAPER_FILE_PATTERNS: string[] = ownedPatterns(
|
|
|
543
561
|
buildConfig({}, { sentinel: "tex language" }),
|
|
544
562
|
);
|
|
545
563
|
|
|
564
|
+
/** `structure` as `checkStructure` reads it: off, or lists of file names over the defaults. */
|
|
565
|
+
const Names = z.array(z.string()).exactOptional();
|
|
566
|
+
const Structure = z
|
|
567
|
+
.union([
|
|
568
|
+
z.literal(false),
|
|
569
|
+
z.object({
|
|
570
|
+
markers: Names,
|
|
571
|
+
require: Names,
|
|
572
|
+
requireOneOf: z.array(z.array(z.string())).exactOptional(),
|
|
573
|
+
ignore: Names,
|
|
574
|
+
}),
|
|
575
|
+
])
|
|
576
|
+
.optional();
|
|
577
|
+
|
|
546
578
|
/**
|
|
547
579
|
* The root `paperlint.json` after the boundary: an unknown key is refused by name, the paper
|
|
548
580
|
* defaults (`extends`, `kind`, `pdf`) are checked the way a paper's own are, and `rules` becomes
|
|
@@ -553,10 +585,9 @@ export function parseSettings(
|
|
|
553
585
|
where: string,
|
|
554
586
|
baseDir: string,
|
|
555
587
|
): Parsed<PaperlintConfig> {
|
|
556
|
-
if (
|
|
588
|
+
if (!isRecord(json))
|
|
557
589
|
return { ok: false, error: `${where}: must be a JSON object` };
|
|
558
|
-
const raw = json
|
|
559
|
-
const opts = raw as PaperlintConfig;
|
|
590
|
+
const raw = json;
|
|
560
591
|
const unknown = unknownKeys(raw);
|
|
561
592
|
if (unknown.length > 0)
|
|
562
593
|
return {
|
|
@@ -567,9 +598,22 @@ export function parseSettings(
|
|
|
567
598
|
};
|
|
568
599
|
const defaults = stringFields(raw);
|
|
569
600
|
if (!defaults.ok) return { ok: false, error: `${where}: ${defaults.error}` };
|
|
601
|
+
const structure = Structure.safeParse(raw["structure"]);
|
|
602
|
+
if (!structure.success)
|
|
603
|
+
return {
|
|
604
|
+
ok: false,
|
|
605
|
+
error: `${where}: "structure" must be false, or an object whose markers, require and ignore are lists of file names and requireOneOf a list of such lists`,
|
|
606
|
+
};
|
|
570
607
|
const rules = parseRuleBlocks(raw["rules"], where, SHIPPED_RULES, baseDir);
|
|
571
608
|
if (!rules.ok) return rules;
|
|
572
|
-
return {
|
|
609
|
+
return {
|
|
610
|
+
ok: true,
|
|
611
|
+
value: {
|
|
612
|
+
papersDir: raw[PAPERS_DIR_FIELD],
|
|
613
|
+
structure: structure.data,
|
|
614
|
+
rules: rules.value,
|
|
615
|
+
},
|
|
616
|
+
};
|
|
573
617
|
}
|
|
574
618
|
|
|
575
619
|
/**
|
|
@@ -770,7 +814,7 @@ export function readConfig(
|
|
|
770
814
|
|
|
771
815
|
/** The papers directory field of the settings, read by its one declared name; the default when absent. */
|
|
772
816
|
export function papersDirOf(opts: PaperlintConfig): unknown {
|
|
773
|
-
const declared =
|
|
817
|
+
const declared = opts.papersDir;
|
|
774
818
|
return declared === undefined ? DEFAULT_PAPERS_ROOT : declared;
|
|
775
819
|
}
|
|
776
820
|
|
|
@@ -839,7 +883,12 @@ export function runHook(
|
|
|
839
883
|
createRequire(import.meta.url).resolve(spec),
|
|
840
884
|
}: {
|
|
841
885
|
err?: typeof console.error;
|
|
842
|
-
|
|
886
|
+
/** Runs the hook with the terminal's streams: `spawnSync` is one, and only its status is read. */
|
|
887
|
+
run?: (
|
|
888
|
+
file: string,
|
|
889
|
+
args: readonly string[],
|
|
890
|
+
options: { readonly stdio: "inherit" },
|
|
891
|
+
) => { readonly status: number | null };
|
|
843
892
|
resolve?: (spec: string) => string;
|
|
844
893
|
} = {},
|
|
845
894
|
): number {
|
|
@@ -1466,7 +1515,7 @@ export async function run(
|
|
|
1466
1515
|
const eslint = new ESLint({
|
|
1467
1516
|
cwd: lintRoot(root, paths),
|
|
1468
1517
|
overrideConfigFile: true,
|
|
1469
|
-
overrideConfig: buildConfig(withPapers, texLanguage)
|
|
1518
|
+
overrideConfig: eslintConfig(buildConfig(withPapers, texLanguage)),
|
|
1470
1519
|
fix: a.fix,
|
|
1471
1520
|
});
|
|
1472
1521
|
|
|
@@ -1543,12 +1592,20 @@ async function firstUnownedFile(
|
|
|
1543
1592
|
* is outside paperlint's scope (`all-matched-files-ignored` — a papers directory holding only
|
|
1544
1593
|
* vendored scripts). Both mean "nothing was linted", which the caller reports itself.
|
|
1545
1594
|
*/
|
|
1595
|
+
/** What a thrown ESLint error says about itself; anything else thrown says nothing. */
|
|
1596
|
+
const EslintFailure = z
|
|
1597
|
+
.object({
|
|
1598
|
+
messageTemplate: z.string().optional(),
|
|
1599
|
+
message: z.string().optional(),
|
|
1600
|
+
})
|
|
1601
|
+
.catch({});
|
|
1602
|
+
|
|
1546
1603
|
const isEmptySet = (e: unknown): boolean => {
|
|
1547
|
-
const fail = e
|
|
1604
|
+
const fail = EslintFailure.parse(e);
|
|
1548
1605
|
return (
|
|
1549
|
-
fail
|
|
1550
|
-
fail
|
|
1551
|
-
/No files matching/i.test(String(fail
|
|
1606
|
+
fail.messageTemplate === "file-not-found" ||
|
|
1607
|
+
fail.messageTemplate === "all-matched-files-ignored" ||
|
|
1608
|
+
/No files matching/i.test(String(fail.message))
|
|
1552
1609
|
);
|
|
1553
1610
|
};
|
|
1554
1611
|
|
package/src/doctor.test.ts
CHANGED
|
@@ -44,7 +44,7 @@ test("doctor: a settings file silent on papersDir, an occupied entry, an unreada
|
|
|
44
44
|
const code = doctor({
|
|
45
45
|
cwd: root,
|
|
46
46
|
log: (s: string) => lines.push(s),
|
|
47
|
-
run: (
|
|
47
|
+
run: () => ({ status: 0 }),
|
|
48
48
|
cliPapers: join(root, "papers"),
|
|
49
49
|
skillLinks: () => ({
|
|
50
50
|
ok: true,
|
package/src/doctor.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { PAPER_MARKERS } from "./build.ts";
|
|
|
37
37
|
import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
|
|
38
38
|
import { doctorHooks } from "./hooks-settings.ts";
|
|
39
39
|
import { messageOf } from "./domain/text.ts";
|
|
40
|
+
import { fieldOf } from "./domain/record.ts";
|
|
40
41
|
|
|
41
42
|
export interface Program {
|
|
42
43
|
readonly bin: string;
|
|
@@ -99,7 +100,17 @@ export const PROGRAMS: readonly Program[] = [
|
|
|
99
100
|
},
|
|
100
101
|
];
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Asks the shell whether a program is on PATH. `spawnSync` is one; a fake answers with a status
|
|
105
|
+
* alone, because a status is all `found` reads.
|
|
106
|
+
*/
|
|
107
|
+
export type ProgramProbe = (
|
|
108
|
+
file: string,
|
|
109
|
+
args: readonly string[],
|
|
110
|
+
options: { readonly shell: true; readonly stdio: "ignore" },
|
|
111
|
+
) => { readonly status: number | null };
|
|
112
|
+
|
|
113
|
+
export const found = (bin: string, run: ProgramProbe = spawnSync): boolean =>
|
|
103
114
|
run("command", ["-v", bin], { shell: true, stdio: "ignore" }).status === 0;
|
|
104
115
|
|
|
105
116
|
/** Lists a directory's entries; the disk by default, a test stages an unreadable child. */
|
|
@@ -176,9 +187,7 @@ function declarationVerdict(raw: string | null): {
|
|
|
176
187
|
bad: 1,
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
|
-
const declared = (settings
|
|
180
|
-
PAPERS_DIR_FIELD
|
|
181
|
-
];
|
|
190
|
+
const declared = fieldOf(settings, PAPERS_DIR_FIELD);
|
|
182
191
|
return {
|
|
183
192
|
lines: [
|
|
184
193
|
declared === undefined
|
|
@@ -234,7 +243,7 @@ export interface DoctorOptions {
|
|
|
234
243
|
cwd?: string;
|
|
235
244
|
/** The project root the hooks are anchored to. Claude Code sets this; it defaults to `cwd`. */
|
|
236
245
|
projectDir?: string;
|
|
237
|
-
run?:
|
|
246
|
+
run?: ProgramProbe;
|
|
238
247
|
/** The directory the CLI resolved from its own config, so both sides can be compared. */
|
|
239
248
|
cliPapers?: string | null;
|
|
240
249
|
/** Reads the skill links without writing any. Injected only so a test can fake the install. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { absolutePath, joinPath } from "./paths.ts";
|
|
4
|
+
|
|
5
|
+
test("absolutePath keeps an absolute path as it is", () => {
|
|
6
|
+
assert.equal(absolutePath("/a/b"), "/a/b");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("absolutePath refuses a relative path instead of letting the process's cwd decide it", () => {
|
|
10
|
+
assert.throws(() => absolutePath("a/b"), /not an absolute path: a\/b/);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("joinPath stays under its absolute base", () => {
|
|
14
|
+
assert.equal(joinPath(absolutePath("/a"), "b", "c"), "/a/b/c");
|
|
15
|
+
});
|
package/src/domain/paths.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
/** Paths as the domain holds them. */
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
2
3
|
import type { Opaque } from "ts-essentials";
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* An absolute path. Minted by `absolutePath` (a checked string) or `joinPath` (an absolute base and
|
|
7
|
+
* segments) only, so nothing holding one has to ask whether it is relative.
|
|
8
|
+
*/
|
|
5
9
|
export type AbsolutePath = Opaque<string, "AbsolutePath">;
|
|
10
|
+
|
|
11
|
+
// The brand's one owner: the predicate IS the check, so no assertion is needed to mint the type.
|
|
12
|
+
const isAbsolutePath = (p: string): p is AbsolutePath => isAbsolute(p);
|
|
13
|
+
|
|
14
|
+
/** A path the caller holds as text, checked where it becomes a value. A relative one throws. */
|
|
15
|
+
export function absolutePath(p: string): AbsolutePath {
|
|
16
|
+
if (!isAbsolutePath(p)) throw new Error(`not an absolute path: ${p}`);
|
|
17
|
+
return p;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `segments` under `base`: absolute because `base` is. */
|
|
21
|
+
export const joinPath = (
|
|
22
|
+
base: AbsolutePath,
|
|
23
|
+
...segments: string[]
|
|
24
|
+
): AbsolutePath => absolutePath(join(base, ...segments));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** A program `paperlint toolchain` installed or found: present, verified, running. */
|
|
2
|
+
|
|
3
|
+
// Not exported: a `Ready` carries this property, and the one way to write it is `ready` below.
|
|
4
|
+
const READY = Symbol("Ready");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Present, verified, running. BRANDED: a literal `{ where, fresh, verified }` is not a `Ready`, so
|
|
8
|
+
* only an installer's adapter — one minting call each (banal's is `ready` in
|
|
9
|
+
* `adapters/banal/index.ts`) — can make one, and a ready line cannot be printed without it.
|
|
10
|
+
*/
|
|
11
|
+
export interface Ready {
|
|
12
|
+
/** Where it is: the program's path or its bin directory. */
|
|
13
|
+
readonly where: string;
|
|
14
|
+
/** This run installed it. */
|
|
15
|
+
readonly fresh: boolean;
|
|
16
|
+
/** What was checked before calling it ready, as a person reads it. */
|
|
17
|
+
readonly verified: string;
|
|
18
|
+
readonly [READY]: true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** An installer's verdict that its program is ready. Called by an installer adapter, after its checks. */
|
|
22
|
+
export const ready = (r: {
|
|
23
|
+
readonly where: string;
|
|
24
|
+
readonly fresh: boolean;
|
|
25
|
+
readonly verified: string;
|
|
26
|
+
}): Ready => ({
|
|
27
|
+
where: r.where,
|
|
28
|
+
fresh: r.fresh,
|
|
29
|
+
verified: r.verified,
|
|
30
|
+
[READY]: true,
|
|
31
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import { fieldOf, isRecord, numbersOf } from "./record.ts";
|
|
4
|
+
|
|
5
|
+
test("isRecord: an object, and not null, an array or a string", () => {
|
|
6
|
+
assert.deepEqual(
|
|
7
|
+
[isRecord({}), isRecord(null), isRecord([]), isRecord("x")],
|
|
8
|
+
[true, false, false, false],
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("fieldOf reads an own or inherited field of a record, and nothing of anything else", () => {
|
|
13
|
+
assert.equal(fieldOf({ a: 1 }, "a"), 1);
|
|
14
|
+
assert.equal(fieldOf(new TypeError("x"), "name"), "TypeError");
|
|
15
|
+
assert.equal(fieldOf("text", "length"), undefined);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("numbersOf keeps numbers as they are", () => {
|
|
19
|
+
assert.deepEqual(numbersOf([1, -0.5, 0]), [1, -0.5, 0]);
|
|
20
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Reading fields off a value typed `unknown` — a parsed JSON file, an object a library hands back. */
|
|
2
|
+
|
|
3
|
+
/** Any non-null object that is not an array, readable by key. */
|
|
4
|
+
export const isRecord = (v: unknown): v is Readonly<Record<string, unknown>> =>
|
|
5
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6
|
+
|
|
7
|
+
/** `v[key]` when `v` is a record; undefined for anything else. Inherited fields count, as `.` reads them. */
|
|
8
|
+
export const fieldOf = (v: unknown, key: string): unknown =>
|
|
9
|
+
isRecord(v) ? v[key] : undefined;
|
|
10
|
+
|
|
11
|
+
/** A number array a library types as `any[]`, read as the numbers it holds. */
|
|
12
|
+
export const numbersOf = (xs: readonly unknown[]): number[] => xs.map(Number);
|
package/src/domain/sha256.ts
CHANGED
|
@@ -9,13 +9,15 @@ import type { Opaque } from "ts-essentials";
|
|
|
9
9
|
export type Sha256 = Opaque<string, "Sha256">;
|
|
10
10
|
|
|
11
11
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
12
|
+
// The brand's one owner: the predicate IS the check, so no assertion is needed to mint the type.
|
|
13
|
+
const isSha256 = (s: string): s is Sha256 => SHA256.test(s);
|
|
12
14
|
|
|
13
15
|
/** A sha256 written down as text — a pin, an option — checked where it is written, not where it is used. */
|
|
14
16
|
export function parseSha256(s: string): Sha256 {
|
|
15
|
-
if (!
|
|
17
|
+
if (!isSha256(s))
|
|
16
18
|
throw new Error(`not a sha256 (64 lowercase hex digits): ${s}`);
|
|
17
|
-
return s
|
|
19
|
+
return s;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export const sha256Hex = (bytes: Uint8Array): Sha256 =>
|
|
21
|
-
createHash("sha256").update(bytes).digest("hex")
|
|
23
|
+
parseSha256(createHash("sha256").update(bytes).digest("hex"));
|
package/src/engine.ts
CHANGED
|
@@ -26,8 +26,36 @@ import { basename, delimiter, join } from "node:path";
|
|
|
26
26
|
import type { TexRequirements } from "./tex-requirements.ts";
|
|
27
27
|
import { printed } from "./domain/text.ts";
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
export
|
|
29
|
+
/** The options paperlint hands a process runner: text out, both streams captured. */
|
|
30
|
+
export interface RunOptions {
|
|
31
|
+
readonly encoding: "utf8" | "latin1";
|
|
32
|
+
readonly stdio: ["ignore", "pipe", "pipe"];
|
|
33
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
34
|
+
readonly cwd?: string;
|
|
35
|
+
readonly maxBuffer?: number;
|
|
36
|
+
readonly timeout?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What paperlint reads of a finished process. `spawnSync`'s result has this shape; a fake needs
|
|
41
|
+
* nothing more. Both streams are optional: they are `undefined` when the spawn itself failed.
|
|
42
|
+
*/
|
|
43
|
+
export interface RunResult {
|
|
44
|
+
readonly status: number | null;
|
|
45
|
+
readonly stdout?: string;
|
|
46
|
+
readonly stderr?: string;
|
|
47
|
+
readonly error?: Error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A process runner: `spawnSync`'s text overload, narrowed to what paperlint passes and reads — the
|
|
52
|
+
* port the harness replaces. `spawnSync` itself is one.
|
|
53
|
+
*/
|
|
54
|
+
export type Runner = (
|
|
55
|
+
file: string,
|
|
56
|
+
args: readonly string[],
|
|
57
|
+
options: RunOptions,
|
|
58
|
+
) => RunResult;
|
|
31
59
|
|
|
32
60
|
/** One TeX Live and what it lacks for the papers at hand. */
|
|
33
61
|
export interface TreeProbe {
|
package/src/facts-file.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
} from "./domain/geometry.ts";
|
|
46
46
|
import { parseSha256, sha256Hex, type Sha256 } from "./domain/sha256.ts";
|
|
47
47
|
import type { MeasureGeometry } from "./ports/measure-geometry.ts";
|
|
48
|
-
import
|
|
48
|
+
import { callerPath } from "./caller-path.ts";
|
|
49
49
|
import type { Files } from "./ports/files.ts";
|
|
50
50
|
import { err, ok, type Result } from "./domain/result.ts";
|
|
51
51
|
import { paperPreset, paperPresetProblem } from "./presets.ts";
|
|
@@ -60,12 +60,6 @@ export const FACTS_FILE = "paper.facts.json";
|
|
|
60
60
|
export const factsPath = (paperDir: string): string =>
|
|
61
61
|
join(paperDir, FACTS_DIR, FACTS_FILE);
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
* A path as the caller gave it. The `Files` adapter resolves a relative one against the process's
|
|
65
|
-
* cwd, which is what these callers have always meant; the brand is not a claim this code checked it.
|
|
66
|
-
*/
|
|
67
|
-
const at = (p: string): AbsolutePath => p as AbsolutePath;
|
|
68
|
-
|
|
69
63
|
/** What the facts need from a paper's `paperlint.json`: its venue's label, its kind, where its PDF is. */
|
|
70
64
|
export interface VenueDecl {
|
|
71
65
|
/** The venue preset's display label (`agenticdev`), or null when the paper extends none. */
|
|
@@ -213,7 +207,7 @@ export async function measurePaper(
|
|
|
213
207
|
): Promise<Result<Measured, string>> {
|
|
214
208
|
const r = await o.readPdf(pdf);
|
|
215
209
|
if (!r.ok) return err(describeFailure(r, pdf));
|
|
216
|
-
const bytes = o.files.readBytes(
|
|
210
|
+
const bytes = o.files.readBytes(callerPath(pdf));
|
|
217
211
|
if (bytes === null) return err(`${pdf}: gone after pdf.js read it`);
|
|
218
212
|
const geometry = o.measure.measure(r.facts.layout);
|
|
219
213
|
const decl = declaredVenue(o.files, paperDir);
|
|
@@ -239,7 +233,7 @@ export function writeFactsFile(
|
|
|
239
233
|
): string {
|
|
240
234
|
const out = factsPath(paperDir);
|
|
241
235
|
files.writeAtomic(
|
|
242
|
-
|
|
236
|
+
callerPath(out),
|
|
243
237
|
new TextEncoder().encode(`${JSON.stringify(facts, null, 2)}\n`),
|
|
244
238
|
);
|
|
245
239
|
return out;
|
|
@@ -288,6 +282,34 @@ const MAYBE_NUMBERS = [
|
|
|
288
282
|
] as const;
|
|
289
283
|
const NUMBERS = ["body_pages", "ref_pages", "appendix_pages"] as const;
|
|
290
284
|
|
|
285
|
+
/** The geometry columns as the file holds them once checked. */
|
|
286
|
+
type GeometryColumns = {
|
|
287
|
+
readonly [K in (typeof MAYBE_NUMBERS)[number]]: number | null;
|
|
288
|
+
} & { readonly [K in (typeof NUMBERS)[number]]: number } & {
|
|
289
|
+
readonly pages_by_type: Readonly<Record<string, number>>;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const isCounts = (v: unknown): v is Readonly<Record<string, number>> =>
|
|
293
|
+
isRecord(v) && Object.values(v).every(isNum);
|
|
294
|
+
|
|
295
|
+
/** Every geometry column has its type: the check the type claims, and nothing else. */
|
|
296
|
+
const hasGeometryColumns = (
|
|
297
|
+
d: Readonly<Record<string, unknown>>,
|
|
298
|
+
): d is Readonly<Record<string, unknown>> & GeometryColumns =>
|
|
299
|
+
MAYBE_NUMBERS.every((k) => numOrNull(d[k])) &&
|
|
300
|
+
NUMBERS.every((k) => isNum(d[k])) &&
|
|
301
|
+
isCounts(d["pages_by_type"]);
|
|
302
|
+
|
|
303
|
+
/** Which geometry column is wrong, for a record `hasGeometryColumns` refused. */
|
|
304
|
+
function geometryError(d: Readonly<Record<string, unknown>>): string {
|
|
305
|
+
const bad =
|
|
306
|
+
MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
|
|
307
|
+
NUMBERS.find((k) => !isNum(d[k]));
|
|
308
|
+
return bad
|
|
309
|
+
? `\`${bad}\` is not a number`
|
|
310
|
+
: "`pages_by_type` is not an object of page counts";
|
|
311
|
+
}
|
|
312
|
+
|
|
291
313
|
/** The geometry columns when a measurer ran; the shape error otherwise. */
|
|
292
314
|
function geometryOf(
|
|
293
315
|
d: Readonly<Record<string, unknown>>,
|
|
@@ -295,24 +317,17 @@ function geometryOf(
|
|
|
295
317
|
if (d["geometry_source"] === null) return ok(null);
|
|
296
318
|
if (typeof d["geometry_source"] !== "string")
|
|
297
319
|
return err("`geometry_source` is neither a measurer's name nor null");
|
|
298
|
-
|
|
299
|
-
MAYBE_NUMBERS.find((k) => !numOrNull(d[k])) ??
|
|
300
|
-
NUMBERS.find((k) => !isNum(d[k]));
|
|
301
|
-
if (bad) return err(`\`${bad}\` is not a number`);
|
|
302
|
-
const pagesByType = d["pages_by_type"];
|
|
303
|
-
if (!isRecord(pagesByType) || !Object.values(pagesByType).every(isNum))
|
|
304
|
-
return err("`pages_by_type` is not an object of page counts");
|
|
305
|
-
const n = (k: (typeof MAYBE_NUMBERS)[number]) => d[k] as number | null;
|
|
320
|
+
if (!hasGeometryColumns(d)) return err(geometryError(d));
|
|
306
321
|
return ok({
|
|
307
|
-
page_w_in:
|
|
308
|
-
page_h_in:
|
|
309
|
-
columns:
|
|
310
|
-
body_pt:
|
|
311
|
-
ref_pt:
|
|
312
|
-
body_pages: d
|
|
313
|
-
ref_pages: d
|
|
314
|
-
appendix_pages: d
|
|
315
|
-
pages_by_type:
|
|
322
|
+
page_w_in: d.page_w_in,
|
|
323
|
+
page_h_in: d.page_h_in,
|
|
324
|
+
columns: d.columns,
|
|
325
|
+
body_pt: d.body_pt,
|
|
326
|
+
ref_pt: d.ref_pt,
|
|
327
|
+
body_pages: d.body_pages,
|
|
328
|
+
ref_pages: d.ref_pages,
|
|
329
|
+
appendix_pages: d.appendix_pages,
|
|
330
|
+
pages_by_type: d.pages_by_type,
|
|
316
331
|
});
|
|
317
332
|
}
|
|
318
333
|
|
|
@@ -348,7 +363,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
|
348
363
|
if (!artifact.ok) return err({ kind: "broken", why: artifact.error });
|
|
349
364
|
const rawFonts = d["fonts"];
|
|
350
365
|
const fonts = Array.isArray(rawFonts) ? rawFonts.map(fontOf) : null;
|
|
351
|
-
if (fonts === null || fonts.
|
|
366
|
+
if (fonts === null || !fonts.every((f) => f !== null))
|
|
352
367
|
return err({
|
|
353
368
|
kind: "broken",
|
|
354
369
|
why: "`fonts` is not a list of { name, type, embedded, program }",
|
|
@@ -357,7 +372,7 @@ export function parseFactsText(text: string): Result<ReadFacts, FactsProblem> {
|
|
|
357
372
|
if (!geometry.ok) return err({ kind: "broken", why: geometry.error });
|
|
358
373
|
return ok({
|
|
359
374
|
...artifact.value,
|
|
360
|
-
fonts
|
|
375
|
+
fonts,
|
|
361
376
|
geometry: geometry.value,
|
|
362
377
|
});
|
|
363
378
|
}
|
|
@@ -118,6 +118,23 @@ try {
|
|
|
118
118
|
threw,
|
|
119
119
|
);
|
|
120
120
|
|
|
121
|
+
const malformed = join(work, "malformed-hooks.json");
|
|
122
|
+
writeFileSync(
|
|
123
|
+
malformed,
|
|
124
|
+
'{"hooks":{"PreToolUse":[{"hooks":[{"type":"command"}]}]}}',
|
|
125
|
+
);
|
|
126
|
+
let why = "";
|
|
127
|
+
try {
|
|
128
|
+
shippedWiring(malformed);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
why = e instanceof Error ? e.message : String(e);
|
|
131
|
+
}
|
|
132
|
+
check(
|
|
133
|
+
"a wiring file whose `hooks` is not Claude Code's shape THROWS, naming the shape",
|
|
134
|
+
why.startsWith(`${malformed}: "hooks" is not a map of event`),
|
|
135
|
+
why,
|
|
136
|
+
);
|
|
137
|
+
|
|
121
138
|
// ── which spelling runs which hook ─────────────────────────────────────────────────────
|
|
122
139
|
const ours = `node "\${CLAUDE_PROJECT_DIR}/${MANAGED_BY}" hook paper-edit-guard`;
|
|
123
140
|
const cases: [string, { name: string; ours: boolean } | null][] = [
|