paperlint 3.0.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/README.md +179 -180
- 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-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- 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/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.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 +4 -4
- 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/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- 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/structure.js +1 -1
- package/dist/structure.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 +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- 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 +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -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.schema.json +1 -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/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- 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-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- 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 +77 -7
- package/src/engine.ts +35 -0
- 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 +4 -4
- 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/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -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/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- 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 -79
- package/src/structure.mutations.mjs +0 -83
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 {
|
|
@@ -28,14 +28,11 @@ const {
|
|
|
28
28
|
whichOnPath,
|
|
29
29
|
isExecutable,
|
|
30
30
|
probeTree,
|
|
31
|
+
missingDependencies,
|
|
31
32
|
supportedPlatform,
|
|
32
33
|
} = await import(join(HERE, "engine.ts"));
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
const check = (label, cond, detail = "") => {
|
|
36
|
-
assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
|
|
37
|
-
n++;
|
|
38
|
-
};
|
|
35
|
+
const check = createChecker();
|
|
39
36
|
|
|
40
37
|
// ── 1. resolveEngine ────────────────────────────────────────────────────────────────────
|
|
41
38
|
const tree = (label, missing = []) => ({ label, bin: `/${label}`, missing });
|
|
@@ -251,4 +248,77 @@ check(
|
|
|
251
248
|
}
|
|
252
249
|
}
|
|
253
250
|
|
|
254
|
-
|
|
251
|
+
// ── missingDependencies: what an install that "continued anyway" left out ──────────────────
|
|
252
|
+
// Real stdout of `tlmgr check depends` on a tree whose install-tl lost unicode-data to a mirror
|
|
253
|
+
// (2026-09-26): the pdflatex format could not be built, yet every venue .sty was present.
|
|
254
|
+
// Byte for byte: tlmgr opens every section header with a FORM FEED, then a space.
|
|
255
|
+
const BROKEN_DEPENDS =
|
|
256
|
+
"\f DEPENDS WITHOUT PACKAGES:\n" +
|
|
257
|
+
"unicode-data in: collection-basic latex-bin luahbtex luatex\n" +
|
|
258
|
+
"\f PACKAGES NOT IN ANY COLLECTION: acmart booktabs caption\n";
|
|
259
|
+
{
|
|
260
|
+
const calls = [];
|
|
261
|
+
const run = (cmd, args) => {
|
|
262
|
+
calls.push([cmd, args]);
|
|
263
|
+
return {
|
|
264
|
+
stdout: BROKEN_DEPENDS,
|
|
265
|
+
stderr: "tlmgr: An error has occurred.",
|
|
266
|
+
status: 2,
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
const missing = missingDependencies("/tl/bin", run);
|
|
270
|
+
check(
|
|
271
|
+
"missingDependencies: asks THAT tree's tlmgr `check depends`",
|
|
272
|
+
calls.length === 1 &&
|
|
273
|
+
calls[0][0] === "/tl/bin/tlmgr" &&
|
|
274
|
+
JSON.stringify(calls[0][1]) === '["check","depends"]',
|
|
275
|
+
JSON.stringify(calls),
|
|
276
|
+
);
|
|
277
|
+
// Guards: a package the tree's own database requires but never installed is a gap.
|
|
278
|
+
check(
|
|
279
|
+
"🔴 missingDependencies: names the dependency the install dropped",
|
|
280
|
+
JSON.stringify(missing) === '["unicode-data"]',
|
|
281
|
+
JSON.stringify(missing),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
check(
|
|
285
|
+
"missingDependencies: packages outside any collection are not gaps",
|
|
286
|
+
JSON.stringify(
|
|
287
|
+
missingDependencies("/tl/bin", () => ({
|
|
288
|
+
stdout: "\f PACKAGES NOT IN ANY COLLECTION: acmart booktabs\n",
|
|
289
|
+
status: 2,
|
|
290
|
+
})),
|
|
291
|
+
) === "[]",
|
|
292
|
+
);
|
|
293
|
+
check(
|
|
294
|
+
"missingDependencies: a clean tree has none",
|
|
295
|
+
JSON.stringify(
|
|
296
|
+
missingDependencies("/tl/bin", () => ({ stdout: "", status: 0 })),
|
|
297
|
+
) === "[]",
|
|
298
|
+
);
|
|
299
|
+
|
|
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`);
|
package/src/engine.ts
CHANGED
|
@@ -164,6 +164,41 @@ export function probeTree(
|
|
|
164
164
|
return missingPackages(packages, String(r.stdout ?? ""));
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Packages the tree's OWN database lists as dependencies but never installed, by its `tlmgr check
|
|
169
|
+
* depends`. 🔴 The venue's `.sty` files are not enough evidence of a working tree: install-tl that
|
|
170
|
+
* cannot download a package prints "continuing anyway", and a missing `unicode-data` leaves every
|
|
171
|
+
* `.sty` in place while the pdflatex format cannot be built — pdflatex then dies before writing a
|
|
172
|
+
* log. Only paperlint's cache trees are asked: their tlmgr writes to a directory we own. A tlmgr
|
|
173
|
+
* that cannot start returns nothing here; the tree is then judged by `probeTree` alone.
|
|
174
|
+
*/
|
|
175
|
+
export function missingDependencies(
|
|
176
|
+
bin: string,
|
|
177
|
+
run: Runner = spawnSync,
|
|
178
|
+
): string[] {
|
|
179
|
+
const r = run(join(bin, "tlmgr"), ["check", "depends"], {
|
|
180
|
+
encoding: "utf8",
|
|
181
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
182
|
+
});
|
|
183
|
+
if (r.error) return [];
|
|
184
|
+
const lines = String(r.stdout ?? "").split("\n");
|
|
185
|
+
const start = lines.findIndex(
|
|
186
|
+
(l) => l.trim() === "DEPENDS WITHOUT PACKAGES:",
|
|
187
|
+
);
|
|
188
|
+
if (start < 0) return [];
|
|
189
|
+
// The section runs until tlmgr's next header. Headers open with a form feed and a space, entries
|
|
190
|
+
// with the package name, so a header is any line with leading whitespace.
|
|
191
|
+
const section = lines.slice(start + 1);
|
|
192
|
+
const end = section.findIndex((l) => l !== l.trimStart() || !l.trim());
|
|
193
|
+
return [
|
|
194
|
+
...new Set(
|
|
195
|
+
(end < 0 ? section : section.slice(0, end)).map((l) =>
|
|
196
|
+
l.split(" in: ")[0]!.trim(),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
].sort();
|
|
200
|
+
}
|
|
201
|
+
|
|
167
202
|
/** The first directory on `path` holding a runnable `name` — what a shell would run. */
|
|
168
203
|
export function whichOnPath(
|
|
169
204
|
name: string,
|
package/src/facts-file.test.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
factsDocument,
|
|
19
19
|
factsPath,
|
|
20
20
|
measurePaper,
|
|
21
|
+
parseFactsText,
|
|
21
22
|
writeFactsFile,
|
|
22
23
|
type MeasureOptions,
|
|
23
24
|
} from "./facts-file.ts";
|
|
@@ -278,3 +279,112 @@ test("declaredVenue: a paperlint.json with an unknown key is refused, naming the
|
|
|
278
279
|
});
|
|
279
280
|
assert.throws(() => declaredVenue(files, PAPER), /unknown key "venu"/);
|
|
280
281
|
});
|
|
282
|
+
|
|
283
|
+
test("a PDF with no text has an empty font list, not a failed document", () => {
|
|
284
|
+
const d = factsDocument({
|
|
285
|
+
pdf: "paper.pdf",
|
|
286
|
+
sha: "a".repeat(64),
|
|
287
|
+
venue: null,
|
|
288
|
+
kind: null,
|
|
289
|
+
read: {
|
|
290
|
+
pages: 1,
|
|
291
|
+
fonts: { kind: "no-text" },
|
|
292
|
+
last: lastPage(0, 0),
|
|
293
|
+
layout: [],
|
|
294
|
+
},
|
|
295
|
+
geometry: NONE,
|
|
296
|
+
});
|
|
297
|
+
assert.deepEqual(d.fonts, []);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("measure: a PDF gone between pdf.js's read and the hash is an error, not a hash of nothing", async () => {
|
|
301
|
+
const { o, files } = setup();
|
|
302
|
+
files.map.delete(PDF);
|
|
303
|
+
const m = await measurePaper(PAPER, PDF, o);
|
|
304
|
+
assert.equal(!m.ok && m.error, `${PDF}: gone after pdf.js read it`);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("measure: no paperlint.json and no override — venue and kind are null", async () => {
|
|
308
|
+
const { o, files } = setup();
|
|
309
|
+
files.map.delete(`${PAPER}/paperlint.json`);
|
|
310
|
+
const m = await measurePaper(PAPER, PDF, o);
|
|
311
|
+
assert.deepEqual(m.ok && [m.value.facts.venue, m.value.facts.kind], [
|
|
312
|
+
null,
|
|
313
|
+
null,
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ── reading the file back: every refusal names what is wrong ────────────────────────────
|
|
318
|
+
const written = (geometry: Geometry = MEASURED) => ({
|
|
319
|
+
...doc(lastPage(60, 30), geometry),
|
|
320
|
+
});
|
|
321
|
+
const parse = (d: unknown) => parseFactsText(JSON.stringify(d));
|
|
322
|
+
const why = (d: unknown): string => {
|
|
323
|
+
const r = parse(d);
|
|
324
|
+
return !r.ok && r.error.kind === "broken" ? r.error.why : "";
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
test("parseFactsText: a written document reads back, with and without geometry", () => {
|
|
328
|
+
const measured = parse(written());
|
|
329
|
+
assert.ok(measured.ok);
|
|
330
|
+
assert.equal(measured.value.geometry?.columns, 2);
|
|
331
|
+
const none = parse(written(NONE));
|
|
332
|
+
assert.equal(none.ok && none.value.geometry, null);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("parseFactsText: every malformed shape is refused with its reason", () => {
|
|
336
|
+
assert.equal(why([1]), "not a JSON object");
|
|
337
|
+
assert.equal(why({ ...written(), pdf: "" }), "no `pdf` path");
|
|
338
|
+
assert.equal(why({ ...written(), pdf: 7 }), "no `pdf` path");
|
|
339
|
+
assert.equal(
|
|
340
|
+
why({ ...written(), pdf_sha256: "xyz" }),
|
|
341
|
+
"no `pdf_sha256` of 64 hex digits",
|
|
342
|
+
);
|
|
343
|
+
assert.equal(
|
|
344
|
+
why({ ...written(), fonts: [{ name: "x" }] }),
|
|
345
|
+
"`fonts` is not a list of { name, type, embedded, program }",
|
|
346
|
+
);
|
|
347
|
+
assert.equal(
|
|
348
|
+
why({ ...written(), fonts: ["Times"] }),
|
|
349
|
+
"`fonts` is not a list of { name, type, embedded, program }",
|
|
350
|
+
);
|
|
351
|
+
assert.equal(
|
|
352
|
+
why({ ...written(), geometry_source: 3 }),
|
|
353
|
+
"`geometry_source` is neither a measurer's name nor null",
|
|
354
|
+
);
|
|
355
|
+
assert.equal(
|
|
356
|
+
why({ ...written(), columns: "two" }),
|
|
357
|
+
"`columns` is not a number",
|
|
358
|
+
);
|
|
359
|
+
assert.equal(
|
|
360
|
+
why({ ...written(), ref_pages: null }),
|
|
361
|
+
"`ref_pages` is not a number",
|
|
362
|
+
);
|
|
363
|
+
assert.equal(
|
|
364
|
+
why({ ...written(), pages_by_type: { body: "3" } }),
|
|
365
|
+
"`pages_by_type` is not an object of page counts",
|
|
366
|
+
);
|
|
367
|
+
assert.equal(
|
|
368
|
+
why({ ...written(), pages_by_type: [3] }),
|
|
369
|
+
"`pages_by_type` is not an object of page counts",
|
|
370
|
+
);
|
|
371
|
+
assert.equal(
|
|
372
|
+
why({ ...written(), fonts: "Times" }),
|
|
373
|
+
"`fonts` is not a list of { name, type, embedded, program }",
|
|
374
|
+
);
|
|
375
|
+
const notJson = parseFactsText("{");
|
|
376
|
+
assert.match(
|
|
377
|
+
!notJson.ok && notJson.error.kind === "broken" ? notJson.error.why : "",
|
|
378
|
+
/^not JSON \(/,
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("parseFactsText: a missing schema is reported as null, another schema by its value", () => {
|
|
383
|
+
const missing = parse({ ...written(), schema: undefined });
|
|
384
|
+
assert.deepEqual(!missing.ok && missing.error, {
|
|
385
|
+
kind: "schema",
|
|
386
|
+
got: "null",
|
|
387
|
+
});
|
|
388
|
+
const other = parse({ ...written(), schema: 1 });
|
|
389
|
+
assert.deepEqual(!other.ok && other.error, { kind: "schema", got: "1" });
|
|
390
|
+
});
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
* ⚠️ Assertions at the TOP LEVEL: `vigiles test` imports the file and counts "did not throw"
|
|
16
16
|
* as a pass.
|
|
17
17
|
*/
|
|
18
|
-
import assert from "node:assert/strict";
|
|
19
|
-
import { recordCheck } from "vigiles";
|
|
20
18
|
import {
|
|
21
19
|
existsSync,
|
|
22
20
|
mkdirSync,
|
|
@@ -29,6 +27,7 @@ import {
|
|
|
29
27
|
import { tmpdir } from "node:os";
|
|
30
28
|
import { dirname, join } from "node:path";
|
|
31
29
|
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { createChecker } from "../lib/check.mjs";
|
|
32
31
|
|
|
33
32
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
34
33
|
const {
|
|
@@ -44,12 +43,7 @@ const {
|
|
|
44
43
|
const { claudeCodeHookProtocol } = await import("vigiles/claude-code");
|
|
45
44
|
const merge = (e, c, m) => claudeCodeHookProtocol.mergeRegistrations(e, c, m);
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
const check = (label, cond) => {
|
|
49
|
-
n++;
|
|
50
|
-
assert.ok(cond, label);
|
|
51
|
-
recordCheck(label);
|
|
52
|
-
};
|
|
46
|
+
const check = createChecker();
|
|
53
47
|
|
|
54
48
|
// ── the contract with vigiles, checked before anything relies on it ──────────────────────
|
|
55
49
|
check(
|
|
@@ -64,6 +58,40 @@ check(
|
|
|
64
58
|
wiring.names.join() ===
|
|
65
59
|
"paper-edit-guard,paper-skills-nudge,paper-status-gates",
|
|
66
60
|
);
|
|
61
|
+
// ── settings written by hand: shapes Claude Code would ignore are skipped, not crashed on ──
|
|
62
|
+
check(
|
|
63
|
+
"an event that is not a list, an entry with no `hooks` list, a handler with no command → no commands",
|
|
64
|
+
JSON.stringify(
|
|
65
|
+
wiredCounts(
|
|
66
|
+
{
|
|
67
|
+
hooks: {
|
|
68
|
+
PreToolUse: "npx paperlint hook paper-edit-guard",
|
|
69
|
+
PostToolUse: [{ matcher: "Edit" }, null, { hooks: "x" }],
|
|
70
|
+
Stop: [{ hooks: [{ type: "command" }, null] }],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
wiring.names,
|
|
74
|
+
),
|
|
75
|
+
) ===
|
|
76
|
+
JSON.stringify(wiring.names.map((name) => ({ name, ours: 0, other: 0 }))),
|
|
77
|
+
);
|
|
78
|
+
{
|
|
79
|
+
const file = join(tmpdir(), `paperlint-no-hooks-${String(process.pid)}.json`);
|
|
80
|
+
writeFileSync(file, "{}\n");
|
|
81
|
+
let thrown = "";
|
|
82
|
+
try {
|
|
83
|
+
shippedWiring(file);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
thrown = e.message;
|
|
86
|
+
}
|
|
87
|
+
rmSync(file);
|
|
88
|
+
check(
|
|
89
|
+
"a wiring file without `hooks` names no hook — and THROWS instead of wiring nothing",
|
|
90
|
+
thrown === `${file} names no paperlint hook — there is nothing to wire`,
|
|
91
|
+
thrown,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-hooks-")));
|
|
68
96
|
try {
|
|
69
97
|
const empty = join(work, "empty-hooks.json");
|
|
@@ -135,7 +163,7 @@ try {
|
|
|
135
163
|
check(
|
|
136
164
|
"no settings file → it is created with the three hooks",
|
|
137
165
|
r.status === "written" &&
|
|
138
|
-
|
|
166
|
+
wiredCounts(s, wiring.names).every(
|
|
139
167
|
(c) => c.ours === 1 && c.other === 0,
|
|
140
168
|
),
|
|
141
169
|
);
|
|
@@ -172,7 +200,7 @@ try {
|
|
|
172
200
|
);
|
|
173
201
|
check(
|
|
174
202
|
"and ours are there, once each",
|
|
175
|
-
|
|
203
|
+
wiredCounts(s, wiring.names).every((c) => c.ours === 1),
|
|
176
204
|
);
|
|
177
205
|
}
|
|
178
206
|
|
|
@@ -224,7 +252,9 @@ try {
|
|
|
224
252
|
// The control: without the check, vigiles' merge WOULD add a second copy. This is the
|
|
225
253
|
// reason the check exists, measured rather than asserted.
|
|
226
254
|
const merged = merge(handWired, wiring.compiled, MANAGED_BY);
|
|
227
|
-
const c = wiredCounts(merged, wiring.names).
|
|
255
|
+
const c = wiredCounts(merged, wiring.names).find(
|
|
256
|
+
(c) => c.name === "paper-edit-guard",
|
|
257
|
+
);
|
|
228
258
|
check(
|
|
229
259
|
"(control) vigiles' merge alone would wire paper-edit-guard TWICE here",
|
|
230
260
|
c.ours === 1 && c.other === 1,
|
|
@@ -247,7 +277,7 @@ try {
|
|
|
247
277
|
|
|
248
278
|
// ── doctor's section ───────────────────────────────────────────────────────────────────
|
|
249
279
|
const doc = (settings) => {
|
|
250
|
-
const dir = project(`doc-${String(
|
|
280
|
+
const dir = project(`doc-${String(check.count)}`, settings);
|
|
251
281
|
return doctorHooks(dir, wiring).join("\n");
|
|
252
282
|
};
|
|
253
283
|
const wiredOnce = merge({}, wiring.compiled, MANAGED_BY);
|
|
@@ -306,5 +336,5 @@ try {
|
|
|
306
336
|
}
|
|
307
337
|
|
|
308
338
|
console.log(
|
|
309
|
-
`✓ ${String(
|
|
339
|
+
`✓ ${String(check.count)} assertions passed — the hooks land in .claude/settings.json once`,
|
|
310
340
|
);
|