paperlint 3.1.0 → 3.1.1
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 +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -8
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.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/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +13 -9
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +27 -7
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +3 -3
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -65
- package/src/structure.mutations.mjs +0 -83
package/src/cli.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
} from "./paper-settings.ts";
|
|
57
57
|
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
58
58
|
import { onlineReferences } from "./adapters/references/index.ts";
|
|
59
|
+
import type { CheckReferences } from "./ports/check-references.ts";
|
|
59
60
|
import {
|
|
60
61
|
narrowToOwners,
|
|
61
62
|
ownedPatterns,
|
|
@@ -64,7 +65,7 @@ import {
|
|
|
64
65
|
} from "./paper-files.ts";
|
|
65
66
|
import {
|
|
66
67
|
paperPreset,
|
|
67
|
-
|
|
68
|
+
settingsProblemLine,
|
|
68
69
|
presetProblemText,
|
|
69
70
|
resolvePreset,
|
|
70
71
|
shippedPresets,
|
|
@@ -80,7 +81,12 @@ import {
|
|
|
80
81
|
type TexRequirements,
|
|
81
82
|
} from "./tex-requirements.ts";
|
|
82
83
|
import { doctor } from "./doctor.ts";
|
|
83
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
askOnTerminal,
|
|
86
|
+
askOrDefault,
|
|
87
|
+
init,
|
|
88
|
+
processInteractivity,
|
|
89
|
+
} from "./init.ts";
|
|
84
90
|
import {
|
|
85
91
|
DEFAULT_FORMAT,
|
|
86
92
|
FORMATS,
|
|
@@ -467,8 +473,8 @@ export function paperRuleBlocks(
|
|
|
467
473
|
const out = { preset: [] as RuleBlock[], own: [] as RuleBlock[] };
|
|
468
474
|
for (const dir of papers) {
|
|
469
475
|
const p = paperPreset(dir, PRESET_DEPS);
|
|
470
|
-
if (p.kind === "settings-problem"
|
|
471
|
-
return { ok: false, error:
|
|
476
|
+
if (p.kind === "settings-problem")
|
|
477
|
+
return { ok: false, error: settingsProblemLine(dir, p.problem) };
|
|
472
478
|
const blocks = rulesOfPaper(dir, p);
|
|
473
479
|
if (!blocks.ok) return blocks;
|
|
474
480
|
out.preset.push(...blocks.value.preset);
|
|
@@ -484,9 +490,9 @@ export function paperRuleBlocks(
|
|
|
484
490
|
*/
|
|
485
491
|
function rulesOfPaper(
|
|
486
492
|
dir: string,
|
|
487
|
-
p: PaperPreset,
|
|
493
|
+
p: Exclude<PaperPreset, { kind: "settings-problem" }>,
|
|
488
494
|
): Parsed<{ preset: RuleBlock[]; own: RuleBlock[] }> {
|
|
489
|
-
const
|
|
495
|
+
const { settings } = p;
|
|
490
496
|
const fromPreset =
|
|
491
497
|
p.kind === "resolved"
|
|
492
498
|
? parseRuleEntries(
|
|
@@ -577,6 +583,9 @@ export function parseSettings(
|
|
|
577
583
|
const lintRoot = (home: string, paths: readonly string[]): string =>
|
|
578
584
|
commonDir([home, ...paths]);
|
|
579
585
|
|
|
586
|
+
/** A path as the reader typed it: relative to where the command runs, or whole when it IS there. */
|
|
587
|
+
const shown = (cwd: string, p: string): string => relative(cwd, p) || p;
|
|
588
|
+
|
|
580
589
|
/** The longest shared leading run of path segments. */
|
|
581
590
|
const commonDir = (paths: readonly string[]): string => {
|
|
582
591
|
const [first = [], ...rest] = paths.map((p) => p.split(sep));
|
|
@@ -609,24 +618,27 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
609
618
|
// gate that fails on advice gets muted entirely.
|
|
610
619
|
maxWarnings: -1,
|
|
611
620
|
};
|
|
621
|
+
// Consumed from the front: a value flag takes the next argument by shifting it off.
|
|
612
622
|
const rest = [...argv];
|
|
613
|
-
|
|
623
|
+
const first = rest[0];
|
|
624
|
+
if (first && !first.startsWith("-")) {
|
|
625
|
+
out.cmd = first;
|
|
626
|
+
rest.shift();
|
|
627
|
+
}
|
|
614
628
|
|
|
615
629
|
// 🔴 A FLAG WHOSE VALUE WAS TAKEN AWAY IS A REFUSAL, NOT A DEFAULT. The compiler found this
|
|
616
|
-
// during the move to TypeScript:
|
|
630
|
+
// during the move to TypeScript: the value past the last argument is `undefined`, and
|
|
617
631
|
// `paperlint lint --config` (the value forgotten, or eaten by a substitution in CI) silently turned
|
|
618
632
|
// into "no config given" — that is, it went to auto-discovery and linted against SOMEONE ELSE'S
|
|
619
633
|
// file, saying nothing. The failure is one-sided and toward silence, so it is cured by
|
|
620
634
|
// behaviour, not by a type cast.
|
|
621
|
-
const valueFor = (flag: string
|
|
622
|
-
const v = rest
|
|
635
|
+
const valueFor = (flag: string): string | undefined => {
|
|
636
|
+
const v = rest.shift();
|
|
623
637
|
if (v === undefined) out.missingValue = flag;
|
|
624
638
|
return v;
|
|
625
639
|
};
|
|
626
640
|
|
|
627
|
-
for (let
|
|
628
|
-
const arg = rest[i];
|
|
629
|
-
if (arg === undefined) continue;
|
|
641
|
+
for (let arg = rest.shift(); arg !== undefined; arg = rest.shift()) {
|
|
630
642
|
// `--flag=value` is `--flag value` for every flag that takes a value: the CI action writes
|
|
631
643
|
// `--max-warnings="$N"`, and that form used to fall through to the list of paths.
|
|
632
644
|
const eq = arg.indexOf("=");
|
|
@@ -636,7 +648,7 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
636
648
|
: undefined;
|
|
637
649
|
const a = inline === undefined ? arg : arg.slice(0, eq);
|
|
638
650
|
const take = (): string | undefined => {
|
|
639
|
-
if (inline === undefined) return valueFor(a
|
|
651
|
+
if (inline === undefined) return valueFor(a);
|
|
640
652
|
if (inline === "") out.missingValue = a;
|
|
641
653
|
return inline === "" ? undefined : inline;
|
|
642
654
|
};
|
|
@@ -684,11 +696,12 @@ const VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
|
684
696
|
* the CI action to its release tag; an unreadable manifest yields `undefined`, and init then keeps
|
|
685
697
|
* the placeholder instead of guessing.
|
|
686
698
|
*/
|
|
687
|
-
function ownVersion(
|
|
699
|
+
export function ownVersion(
|
|
700
|
+
readManifest: () => string = () =>
|
|
701
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
702
|
+
): string | undefined {
|
|
688
703
|
try {
|
|
689
|
-
const v = JSON.parse(
|
|
690
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
691
|
-
)?.version;
|
|
704
|
+
const v = JSON.parse(readManifest())?.version;
|
|
692
705
|
return typeof v === "string" ? v : undefined;
|
|
693
706
|
} catch {
|
|
694
707
|
return undefined;
|
|
@@ -742,12 +755,9 @@ export function readConfig(
|
|
|
742
755
|
//
|
|
743
756
|
// 🔴 IN `--json` MODE — TO stderr. Machine output must be ONE parsable document: a line before
|
|
744
757
|
// the array breaks any `| jq`, and it breaks it for the consumer, not for us.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
relative(cwd, configPath) || CONFIG_FILE,
|
|
749
|
-
root,
|
|
750
|
-
);
|
|
758
|
+
const shownPath = shown(cwd, configPath);
|
|
759
|
+
(a.json ? err : log)(`config: ${shownPath}`);
|
|
760
|
+
const settings = parseSettings(parsed, shownPath, root);
|
|
751
761
|
if (!settings.ok) {
|
|
752
762
|
err(settings.error);
|
|
753
763
|
return { code: 2 };
|
|
@@ -885,7 +895,7 @@ export async function createPaperAt(
|
|
|
885
895
|
},
|
|
886
896
|
): Promise<number> {
|
|
887
897
|
const result = newPaper(papersRoot, name, format, { venue });
|
|
888
|
-
const here = (p: string): string =>
|
|
898
|
+
const here = (p: string): string => shown(cwd, p);
|
|
889
899
|
for (const line of reportNewPaper(result, here)) log(line);
|
|
890
900
|
if (!result.ok) return 2;
|
|
891
901
|
const config = here(join(result.dir, CONFIG_FILE));
|
|
@@ -939,7 +949,7 @@ export async function chooseVenue(
|
|
|
939
949
|
`--kind needs --venue: a kind is a page limit of one venue preset — \`paperlint new <name> --venue <preset> --kind ${flags.kind}\``,
|
|
940
950
|
);
|
|
941
951
|
const asked = async (q: string): Promise<string> =>
|
|
942
|
-
(await ask
|
|
952
|
+
(await askOrDefault(ask, q))?.trim() ?? "";
|
|
943
953
|
const venue =
|
|
944
954
|
flags.venue ??
|
|
945
955
|
(interactive
|
|
@@ -1047,10 +1057,10 @@ async function runNew(
|
|
|
1047
1057
|
}
|
|
1048
1058
|
format = a.format;
|
|
1049
1059
|
} else if (processInteractivity(a.yes).interactive) {
|
|
1050
|
-
const f =
|
|
1051
|
-
(
|
|
1052
|
-
);
|
|
1053
|
-
if (isFormat(f
|
|
1060
|
+
const f = (
|
|
1061
|
+
await askOrDefault(ask, `format: tex / md [${DEFAULT_FORMAT}] `)
|
|
1062
|
+
)?.trim();
|
|
1063
|
+
if (isFormat(f)) format = f;
|
|
1054
1064
|
}
|
|
1055
1065
|
const cfg = readConfig({ ...a, json: false }, { log: () => {}, err, cwd });
|
|
1056
1066
|
if (cfg.code !== undefined) return cfg.code;
|
|
@@ -1064,7 +1074,7 @@ async function runNew(
|
|
|
1064
1074
|
}
|
|
1065
1075
|
if (roots.length > 1)
|
|
1066
1076
|
log(
|
|
1067
|
-
`several papers directories are declared — using the first: ${
|
|
1077
|
+
`several papers directories are declared — using the first: ${shown(cwd, papersRoot)}`,
|
|
1068
1078
|
);
|
|
1069
1079
|
const paperDir = join(papersRoot, name);
|
|
1070
1080
|
// An existing paperlint.json is never overwritten, so a venue for it is refused, not dropped.
|
|
@@ -1105,7 +1115,13 @@ async function runBuild(
|
|
|
1105
1115
|
log,
|
|
1106
1116
|
err,
|
|
1107
1117
|
cwd,
|
|
1108
|
-
|
|
1118
|
+
checkReferences,
|
|
1119
|
+
}: {
|
|
1120
|
+
log: typeof console.log;
|
|
1121
|
+
err: typeof console.error;
|
|
1122
|
+
cwd: string;
|
|
1123
|
+
checkReferences: CheckReferences;
|
|
1124
|
+
},
|
|
1109
1125
|
): Promise<number> {
|
|
1110
1126
|
const cfg = readConfig(a, { log, err, cwd });
|
|
1111
1127
|
if (cfg.code !== undefined) return cfg.code;
|
|
@@ -1137,7 +1153,7 @@ async function runBuild(
|
|
|
1137
1153
|
cwd,
|
|
1138
1154
|
dryRun: a.dryRun,
|
|
1139
1155
|
log,
|
|
1140
|
-
checkReferences
|
|
1156
|
+
checkReferences,
|
|
1141
1157
|
engine: () => engineEnv(targets, a, { log, err }),
|
|
1142
1158
|
});
|
|
1143
1159
|
if (out.kind === "no-engine") return 1;
|
|
@@ -1209,7 +1225,12 @@ const SIMPLE: Readonly<
|
|
|
1209
1225
|
string,
|
|
1210
1226
|
(
|
|
1211
1227
|
a: Args,
|
|
1212
|
-
io: {
|
|
1228
|
+
io: {
|
|
1229
|
+
log: typeof console.log;
|
|
1230
|
+
err: typeof console.error;
|
|
1231
|
+
cwd: string;
|
|
1232
|
+
checkReferences: CheckReferences;
|
|
1233
|
+
},
|
|
1213
1234
|
) => number | Promise<number>
|
|
1214
1235
|
>
|
|
1215
1236
|
> = {
|
|
@@ -1238,6 +1259,44 @@ function hostBanalInstaller(): ToolInstaller {
|
|
|
1238
1259
|
return banalInstaller({ ...ports, download }, s);
|
|
1239
1260
|
}
|
|
1240
1261
|
|
|
1262
|
+
/**
|
|
1263
|
+
* init's TeX Live step: whether paperlint's own tree is installed, and how to install it —
|
|
1264
|
+
* `runToolchain` over what this project's papers need. `toolchain` is the seam a test replaces:
|
|
1265
|
+
* the real one downloads ~270 MB.
|
|
1266
|
+
*/
|
|
1267
|
+
export function initTexLive(
|
|
1268
|
+
a: Args,
|
|
1269
|
+
{
|
|
1270
|
+
log,
|
|
1271
|
+
err,
|
|
1272
|
+
cwd,
|
|
1273
|
+
}: { log: typeof console.log; err: typeof console.error; cwd: string },
|
|
1274
|
+
toolchain: typeof runToolchain = runToolchain,
|
|
1275
|
+
): { readonly installed: () => boolean; readonly install: () => number } {
|
|
1276
|
+
return {
|
|
1277
|
+
installed: () => cachedTree(cacheRoot(process.env)) !== null,
|
|
1278
|
+
install: () =>
|
|
1279
|
+
toolchain({
|
|
1280
|
+
check: false,
|
|
1281
|
+
log,
|
|
1282
|
+
err,
|
|
1283
|
+
banal: hostBanalInstaller(),
|
|
1284
|
+
tex: toolchainTex(resolve(cwd, a.paths[0] ?? ".")),
|
|
1285
|
+
}),
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* The first papers directory the CLI would lint from `cwd`, or null when the config does not read
|
|
1291
|
+
* or names none — what `doctor` (and `init`, through it) compares the hooks' directory against.
|
|
1292
|
+
*/
|
|
1293
|
+
function cliPapers(a: Args, cwd: string): string | null {
|
|
1294
|
+
const read = readConfig(a, { log: () => {}, err: () => {}, cwd });
|
|
1295
|
+
return read.code === undefined
|
|
1296
|
+
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1297
|
+
: null;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1241
1300
|
/** `paperlint init`: its flags checked here, the install itself in `init.ts`. */
|
|
1242
1301
|
async function runInit(
|
|
1243
1302
|
a: Args,
|
|
@@ -1270,26 +1329,9 @@ async function runInit(
|
|
|
1270
1329
|
format: isFormat(a.format) ? a.format : null,
|
|
1271
1330
|
createPaper: (papersRoot, name, format) =>
|
|
1272
1331
|
createPaperAt(papersRoot, name, format, { log, err, cwd }),
|
|
1273
|
-
tex: {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
runToolchain({
|
|
1277
|
-
check: false,
|
|
1278
|
-
log,
|
|
1279
|
-
err,
|
|
1280
|
-
banal: hostBanalInstaller(),
|
|
1281
|
-
tex: toolchainTex(resolve(cwd, a.paths[0] ?? ".")),
|
|
1282
|
-
}),
|
|
1283
|
-
},
|
|
1284
|
-
resolveCliPapers: (root: string): string | null => {
|
|
1285
|
-
const read = readConfig(
|
|
1286
|
-
{ ...a, config: null },
|
|
1287
|
-
{ log: () => {}, err: () => {}, cwd: root },
|
|
1288
|
-
);
|
|
1289
|
-
return read.code === undefined
|
|
1290
|
-
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1291
|
-
: null;
|
|
1292
|
-
},
|
|
1332
|
+
tex: initTexLive(a, { log, err, cwd }),
|
|
1333
|
+
resolveCliPapers: (root: string): string | null =>
|
|
1334
|
+
cliPapers({ ...a, config: null }, root),
|
|
1293
1335
|
});
|
|
1294
1336
|
}
|
|
1295
1337
|
|
|
@@ -1299,10 +1341,16 @@ export async function run(
|
|
|
1299
1341
|
log = console.log,
|
|
1300
1342
|
err = console.error,
|
|
1301
1343
|
cwd = process.cwd(),
|
|
1344
|
+
checkReferences = onlineReferences,
|
|
1302
1345
|
}: {
|
|
1303
1346
|
log?: typeof console.log;
|
|
1304
1347
|
err?: typeof console.error;
|
|
1305
1348
|
cwd?: string;
|
|
1349
|
+
/**
|
|
1350
|
+
* `build`'s online reference check — the real services by default. The e2e build passes a
|
|
1351
|
+
* fake: no test depends on Crossref, Semantic Scholar or DBLP answering.
|
|
1352
|
+
*/
|
|
1353
|
+
checkReferences?: CheckReferences;
|
|
1306
1354
|
} = {},
|
|
1307
1355
|
): Promise<number> {
|
|
1308
1356
|
const a = parseArgs(argv);
|
|
@@ -1335,11 +1383,7 @@ export async function run(
|
|
|
1335
1383
|
// broken is precisely its job. So a failed read becomes "the CLI would lint nothing", which is
|
|
1336
1384
|
// what it prints, rather than an early exit that tells the reader nothing about the hooks.
|
|
1337
1385
|
if (a.cmd === "doctor") {
|
|
1338
|
-
const
|
|
1339
|
-
const papers =
|
|
1340
|
-
read.code === undefined
|
|
1341
|
-
? (toPaths(papersDirOf(read.opts))[0] ?? null)
|
|
1342
|
-
: null;
|
|
1386
|
+
const papers = cliPapers(a, cwd);
|
|
1343
1387
|
return doctor({
|
|
1344
1388
|
log,
|
|
1345
1389
|
cwd,
|
|
@@ -1348,7 +1392,7 @@ export async function run(
|
|
|
1348
1392
|
});
|
|
1349
1393
|
}
|
|
1350
1394
|
const simple = SIMPLE[a.cmd];
|
|
1351
|
-
if (simple) return await simple(a, { log, err, cwd });
|
|
1395
|
+
if (simple) return await simple(a, { log, err, cwd, checkReferences });
|
|
1352
1396
|
if (a.cmd === "check")
|
|
1353
1397
|
err(
|
|
1354
1398
|
`\`check\` is now \`lint\` — running it anyway. Update the call to \`paperlint lint\`.`,
|
|
@@ -1404,13 +1448,15 @@ export async function run(
|
|
|
1404
1448
|
rules: [...papers.value.preset, ...(opts.rules ?? []), ...papers.value.own],
|
|
1405
1449
|
};
|
|
1406
1450
|
|
|
1451
|
+
// Loaded lazily because only `lint` needs it. If the module cannot be loaded (a broken install:
|
|
1452
|
+
// the file missing from the package), lint still runs over the markdown papers rather than
|
|
1453
|
+
// failing outright — the LaTeX rules simply have no language to run in.
|
|
1407
1454
|
let texLanguage: unknown = null;
|
|
1408
1455
|
try {
|
|
1409
|
-
// @ts-expect-error — the module is .mjs and has no types
|
|
1410
|
-
// case here, it is caught by the catch below.
|
|
1456
|
+
// @ts-expect-error — the module is .mjs and has no types
|
|
1411
1457
|
({ texLanguage } = await import("../eslint-rules/latex-language.mjs"));
|
|
1412
1458
|
} catch {
|
|
1413
|
-
|
|
1459
|
+
texLanguage = null;
|
|
1414
1460
|
}
|
|
1415
1461
|
|
|
1416
1462
|
const eslint = new ESLint({
|
|
@@ -1423,7 +1469,7 @@ export async function run(
|
|
|
1423
1469
|
const unowned = await firstUnownedFile(eslint, paths);
|
|
1424
1470
|
if (unowned !== null) {
|
|
1425
1471
|
err(
|
|
1426
|
-
`${
|
|
1472
|
+
`${shown(cwd, unowned)} is not a file paperlint lints — it lints ${PAPER_FILE_PATTERNS.join(", ")}`,
|
|
1427
1473
|
);
|
|
1428
1474
|
return 2;
|
|
1429
1475
|
}
|
|
@@ -1450,7 +1496,7 @@ export async function run(
|
|
|
1450
1496
|
// is not invoked — and, not being invoked, it physically cannot report that.
|
|
1451
1497
|
if (results.length === 0) {
|
|
1452
1498
|
err(
|
|
1453
|
-
`nothing was linted under ${paths.map((x) =>
|
|
1499
|
+
`nothing was linted under ${paths.map((x) => shown(cwd, x)).join(", ")} — no PIPELINE-STATUS.md, paper.md/tex or reviews/ found there. A clean report over zero files is not a clean report.`,
|
|
1454
1500
|
);
|
|
1455
1501
|
return 1;
|
|
1456
1502
|
}
|
|
@@ -1499,7 +1545,7 @@ const isEmptySet = (e: unknown): boolean => {
|
|
|
1499
1545
|
return (
|
|
1500
1546
|
fail?.messageTemplate === "file-not-found" ||
|
|
1501
1547
|
fail?.messageTemplate === "all-matched-files-ignored" ||
|
|
1502
|
-
/No files matching/i.test(fail?.message
|
|
1548
|
+
/No files matching/i.test(String(fail?.message))
|
|
1503
1549
|
);
|
|
1504
1550
|
};
|
|
1505
1551
|
|
package/src/doctor.harness.mjs
CHANGED
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
* this file can tell the two apart.
|
|
10
10
|
*
|
|
11
11
|
* Run: npx vigiles test src/doctor.harness.mjs
|
|
12
|
-
* Killed by: src/doctor.mutations.mjs
|
|
13
12
|
*/
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
13
|
import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
|
|
16
14
|
import {
|
|
17
15
|
mkdtempSync,
|
|
@@ -23,6 +21,7 @@ import {
|
|
|
23
21
|
import { tmpdir } from "node:os";
|
|
24
22
|
import { dirname, join } from "node:path";
|
|
25
23
|
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { createChecker } from "../lib/check.mjs";
|
|
26
25
|
|
|
27
26
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
27
|
const { doctor, detectPapers, PROGRAMS, found } = await import(
|
|
@@ -32,11 +31,7 @@ const { papersRoot } = await import(
|
|
|
32
31
|
join(HERE, "..", "hooks", "paper-edit-guard.hook.mjs")
|
|
33
32
|
);
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
const check = (label, cond) => {
|
|
37
|
-
n++;
|
|
38
|
-
assert.ok(cond, label);
|
|
39
|
-
};
|
|
34
|
+
const check = createChecker();
|
|
40
35
|
|
|
41
36
|
/** A consumer on disk: a papers directory, and maybe a declaration in the root paperlint.json. */
|
|
42
37
|
function consumer({ papersDir, declared, makeDir = true }) {
|
|
@@ -333,5 +328,5 @@ const runDoctor = (
|
|
|
333
328
|
}
|
|
334
329
|
|
|
335
330
|
console.log(
|
|
336
|
-
`✓ ${
|
|
331
|
+
`✓ ${check.count} assertions passed — paperlint doctor: an install can vouch for itself`,
|
|
337
332
|
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doctor.ts` — the edges the harness does not stage: an unreadable directory during paper
|
|
3
|
+
* discovery, a settings file that names no papers directory, an occupied skill entry with no
|
|
4
|
+
* reason, and a hooks section that cannot be read at all.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import {
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { test } from "vitest";
|
|
17
|
+
import { detectPapers, doctor } from "./doctor.ts";
|
|
18
|
+
|
|
19
|
+
const project = (): string =>
|
|
20
|
+
realpathSync(mkdtempSync(join(tmpdir(), "doctor-")));
|
|
21
|
+
|
|
22
|
+
test("detectPapers: a directory that does not exist has no papers", () => {
|
|
23
|
+
assert.deepEqual(detectPapers(join(project(), "gone")), []);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("detectPapers: a child that cannot be listed is skipped, its siblings are still found", () => {
|
|
27
|
+
const root = project();
|
|
28
|
+
mkdirSync(join(root, "locked"));
|
|
29
|
+
mkdirSync(join(root, "papers", "p1"), { recursive: true });
|
|
30
|
+
writeFileSync(join(root, "papers", "p1", "paper.tex"), "");
|
|
31
|
+
const readdir = (dir: string) => {
|
|
32
|
+
if (dir === join(root, "locked")) throw new Error("EACCES");
|
|
33
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
34
|
+
};
|
|
35
|
+
assert.deepEqual(detectPapers(root, 2, readdir), ["papers"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("doctor: a settings file silent on papersDir, an occupied entry, an unreadable hooks file", () => {
|
|
39
|
+
const root = project();
|
|
40
|
+
writeFileSync(join(root, "paperlint.json"), "{}\n");
|
|
41
|
+
// `.claude/settings.json` as a DIRECTORY: it exists, and reading it throws EISDIR.
|
|
42
|
+
mkdirSync(join(root, ".claude", "settings.json"), { recursive: true });
|
|
43
|
+
const lines: string[] = [];
|
|
44
|
+
const code = doctor({
|
|
45
|
+
cwd: root,
|
|
46
|
+
log: (s: string) => lines.push(s),
|
|
47
|
+
run: (() => ({ status: 0 })) as never,
|
|
48
|
+
cliPapers: join(root, "papers"),
|
|
49
|
+
skillLinks: () => ({
|
|
50
|
+
ok: true,
|
|
51
|
+
home: join(root, ".claude", "skills"),
|
|
52
|
+
example: null,
|
|
53
|
+
links: [{ name: "paper-pipeline", status: "foreign" }],
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
const out = lines.join("\n");
|
|
57
|
+
assert.match(
|
|
58
|
+
out,
|
|
59
|
+
/✓ paperlint\.json names no papersDir — the default "papers"/,
|
|
60
|
+
);
|
|
61
|
+
assert.match(out, /paper-pipeline — occupied, not the shipped skill/);
|
|
62
|
+
assert.match(out, /\nhooks\n {2}⚠ not checked — EISDIR/);
|
|
63
|
+
assert.equal(typeof code, "number");
|
|
64
|
+
});
|
package/src/doctor.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Prior art for the shape: `brew doctor`, `flutter doctor`, `npm doctor`, `expo-doctor`.
|
|
22
22
|
*/
|
|
23
23
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
24
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
24
|
+
import { type Dirent, existsSync, readFileSync, readdirSync } from "node:fs";
|
|
25
25
|
import { join, relative, resolve } from "node:path";
|
|
26
26
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
27
27
|
import { spawnSync } from "node:child_process";
|
|
@@ -101,14 +101,22 @@ export const PROGRAMS: readonly Program[] = [
|
|
|
101
101
|
export const found = (bin: string, run = spawnSync): boolean =>
|
|
102
102
|
run("command", ["-v", bin], { shell: true, stdio: "ignore" }).status === 0;
|
|
103
103
|
|
|
104
|
+
/** Lists a directory's entries; the disk by default, a test stages an unreadable child. */
|
|
105
|
+
type ReadDir = (dir: string) => Dirent[];
|
|
106
|
+
const readDir: ReadDir = (dir) => readdirSync(dir, { withFileTypes: true });
|
|
107
|
+
|
|
104
108
|
/** Directories that look like papers, used to REPLACE a guessed default with a measurement. */
|
|
105
|
-
export function detectPapers(
|
|
109
|
+
export function detectPapers(
|
|
110
|
+
cwd: string,
|
|
111
|
+
depth = 2,
|
|
112
|
+
readdir: ReadDir = readDir,
|
|
113
|
+
): string[] {
|
|
106
114
|
const hits: string[] = [];
|
|
107
115
|
const skip = new Set(["node_modules", ".git", "dist", "_build", ".claude"]);
|
|
108
116
|
const walk = (dir: string, left: number): void => {
|
|
109
117
|
let entries;
|
|
110
118
|
try {
|
|
111
|
-
entries =
|
|
119
|
+
entries = readdir(dir);
|
|
112
120
|
} catch {
|
|
113
121
|
return;
|
|
114
122
|
}
|
|
@@ -119,7 +127,7 @@ export function detectPapers(cwd: string, depth = 2): string[] {
|
|
|
119
127
|
// A directory whose CHILDREN look like papers is the papers root — not the paper itself.
|
|
120
128
|
let children;
|
|
121
129
|
try {
|
|
122
|
-
children =
|
|
130
|
+
children = readdir(here);
|
|
123
131
|
} catch {
|
|
124
132
|
continue;
|
|
125
133
|
}
|
|
@@ -10,6 +10,10 @@ test("map and andThen run on ok and pass an error through untouched", () => {
|
|
|
10
10
|
map(ok(2), (n) => n + 1),
|
|
11
11
|
ok(3),
|
|
12
12
|
);
|
|
13
|
+
assert.deepEqual(
|
|
14
|
+
map(half(3), () => assert.fail("mapped after an error")),
|
|
15
|
+
err("3 is odd"),
|
|
16
|
+
);
|
|
13
17
|
assert.deepEqual(andThen(ok(8), half), ok(4));
|
|
14
18
|
// Guards: an error short-circuits — the function after it is not called.
|
|
15
19
|
assert.deepEqual(
|
package/src/domain/result.ts
CHANGED
|
@@ -22,8 +22,3 @@ export const match = <T, E, R>(
|
|
|
22
22
|
r: Result<T, E>,
|
|
23
23
|
on: { readonly ok: (t: T) => R; readonly err: (e: E) => R },
|
|
24
24
|
): R => (r.ok ? on.ok(r.value) : on.err(r.error));
|
|
25
|
-
|
|
26
|
-
/** The `default` of an exhaustive `switch`: a new variant is a compile error, not a silent fall-through. */
|
|
27
|
-
export function assertNever(x: never): never {
|
|
28
|
-
throw new Error(`unhandled variant: ${JSON.stringify(x)}`);
|
|
29
|
-
}
|
package/src/engine.harness.mjs
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* `probeTree` through a fake runner.
|
|
8
8
|
* All pure or port-driven: no TeX needed. The real-TeX half is `test/e2e/build.mjs`.
|
|
9
9
|
*/
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
10
|
import {
|
|
12
11
|
chmodSync,
|
|
13
12
|
mkdirSync,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
import { tmpdir } from "node:os";
|
|
20
19
|
import { dirname, join } from "node:path";
|
|
21
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { createChecker } from "../lib/check.mjs";
|
|
22
22
|
|
|
23
23
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
const {
|
|
@@ -32,11 +32,7 @@ const {
|
|
|
32
32
|
supportedPlatform,
|
|
33
33
|
} = await import(join(HERE, "engine.ts"));
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
const check = (label, cond, detail = "") => {
|
|
37
|
-
assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
|
|
38
|
-
n++;
|
|
39
|
-
};
|
|
35
|
+
const check = createChecker();
|
|
40
36
|
|
|
41
37
|
// ── 1. resolveEngine ────────────────────────────────────────────────────────────────────
|
|
42
38
|
const tree = (label, missing = []) => ({ label, bin: `/${label}`, missing });
|
|
@@ -301,4 +297,28 @@ check(
|
|
|
301
297
|
) === "[]",
|
|
302
298
|
);
|
|
303
299
|
|
|
304
|
-
|
|
300
|
+
check(
|
|
301
|
+
"missingDependencies: a tlmgr that cannot start reports nothing (the tree is judged by probeTree alone)",
|
|
302
|
+
JSON.stringify(
|
|
303
|
+
missingDependencies("/tl/bin", () => ({ error: new Error("ENOENT") })),
|
|
304
|
+
) === "[]",
|
|
305
|
+
);
|
|
306
|
+
check(
|
|
307
|
+
"missingDependencies: a section that runs to the end of the output, with no header after it",
|
|
308
|
+
JSON.stringify(
|
|
309
|
+
missingDependencies("/tl/bin", () => ({
|
|
310
|
+
stdout:
|
|
311
|
+
"\f DEPENDS WITHOUT PACKAGES:\nunicode-data in: latex-bin\nhyphen-base in: x",
|
|
312
|
+
status: 2,
|
|
313
|
+
})),
|
|
314
|
+
) === '["hyphen-base","unicode-data"]',
|
|
315
|
+
);
|
|
316
|
+
check(
|
|
317
|
+
"a runner that returns no stdout at all: tlmgr lists nothing, kpsewhich finds nothing",
|
|
318
|
+
JSON.stringify(missingDependencies("/tl/bin", () => ({ status: 0 }))) ===
|
|
319
|
+
"[]" &&
|
|
320
|
+
JSON.stringify(probeTree("/tl/bin", PKGS, () => ({ status: 0 }))) ===
|
|
321
|
+
JSON.stringify(Object.keys(PKGS).sort()),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
console.log(`engine: ${check.count} checks passed`);
|