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/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
|
);
|
package/src/hooks-settings.ts
CHANGED
|
@@ -93,8 +93,8 @@ interface Run {
|
|
|
93
93
|
*/
|
|
94
94
|
function parseRun(command: string): Run | null {
|
|
95
95
|
const tokens = command.trim().split(/\s+/).map(bare);
|
|
96
|
-
for (
|
|
97
|
-
const run = runAt(tokens, i);
|
|
96
|
+
for (const [i, token] of tokens.entries()) {
|
|
97
|
+
const run = runAt(token, tokens.slice(i + 1, i + 3));
|
|
98
98
|
if (run) return run;
|
|
99
99
|
}
|
|
100
100
|
return null;
|
|
@@ -108,17 +108,13 @@ function insidePackage(parts: readonly string[]): readonly string[] {
|
|
|
108
108
|
return at === -1 ? [] : parts.slice(at + 2);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/** The run token
|
|
112
|
-
function runAt(
|
|
113
|
-
const t = posix.normalize(
|
|
111
|
+
/** The run `token` starts, if it names a paperlint hook; `after` is the two tokens that follow it. */
|
|
112
|
+
function runAt(token: string, after: readonly string[]): Run | null {
|
|
113
|
+
const t = posix.normalize(token);
|
|
114
114
|
const inside = insidePackage(t.split("/"));
|
|
115
115
|
const inBin = inside[0] === "bin" && inside.length === 2;
|
|
116
|
-
const name =
|
|
117
|
-
if (
|
|
118
|
-
(inBin || basename(t) === PACKAGE_NAME) &&
|
|
119
|
-
tokens[i + 1] === "hook" &&
|
|
120
|
-
name
|
|
121
|
-
)
|
|
116
|
+
const [verb, name] = after;
|
|
117
|
+
if ((inBin || basename(t) === PACKAGE_NAME) && verb === "hook" && name)
|
|
122
118
|
return { name, ours: t === MANAGED_BY, path: t };
|
|
123
119
|
if (inside[0] === "hooks" && inside[1]?.endsWith(".hook.mjs"))
|
|
124
120
|
return { name: basename(inside[1], ".hook.mjs"), ours: false, path: t };
|
|
@@ -171,22 +167,33 @@ export function shippedWiring(file: string = WIRING_FILE): Wiring {
|
|
|
171
167
|
return { compiled, names: [...new Set(names)] };
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
/** How many commands run
|
|
170
|
+
/** How many commands run one shipped hook, split by spelling. */
|
|
171
|
+
export interface HookCount {
|
|
172
|
+
readonly name: string;
|
|
173
|
+
readonly ours: number;
|
|
174
|
+
readonly other: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** One count per name in `names`, in that order — a name nothing runs has zeros, never a gap. */
|
|
175
178
|
export function wiredCounts(
|
|
176
179
|
settings: Settings,
|
|
177
180
|
names: readonly string[],
|
|
178
|
-
):
|
|
179
|
-
const
|
|
180
|
-
for (const { command } of commandsIn(settings)) {
|
|
181
|
+
): readonly HookCount[] {
|
|
182
|
+
const runs = commandsIn(settings).flatMap(({ command }) => {
|
|
181
183
|
const run = hookRun(command);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
return run ? [run] : [];
|
|
185
|
+
});
|
|
186
|
+
const count = (name: string, ours: boolean): number =>
|
|
187
|
+
runs.filter((r) => r.name === name && r.ours === ours).length;
|
|
188
|
+
return names.map((name) => ({
|
|
189
|
+
name,
|
|
190
|
+
ours: count(name, true),
|
|
191
|
+
other: count(name, false),
|
|
192
|
+
}));
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
const total = (c: HookCount): number => c.ours + c.other;
|
|
196
|
+
|
|
190
197
|
export type SettingsRead =
|
|
191
198
|
| { readonly status: "absent"; readonly path: string; readonly settings: {} }
|
|
192
199
|
| {
|
|
@@ -258,15 +265,16 @@ export function wireHooks(
|
|
|
258
265
|
const read = readSettings(root);
|
|
259
266
|
if (read.status === "unparsable") return read;
|
|
260
267
|
const { path, settings } = read;
|
|
261
|
-
const found = commandsIn(settings)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
268
|
+
const found = commandsIn(settings).flatMap(({ command }) => {
|
|
269
|
+
const run = hookRun(command);
|
|
270
|
+
return run && !run.ours && wiring.names.includes(run.name)
|
|
271
|
+
? [{ name: run.name, command }]
|
|
272
|
+
: [];
|
|
273
|
+
});
|
|
265
274
|
if (found.length > 0) {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
(
|
|
269
|
-
);
|
|
275
|
+
const missing = wiredCounts(settings, wiring.names)
|
|
276
|
+
.filter((c) => total(c) === 0)
|
|
277
|
+
.map((c) => c.name);
|
|
270
278
|
return { status: "foreign", path, names: wiring.names, found, missing };
|
|
271
279
|
}
|
|
272
280
|
|
|
@@ -297,19 +305,17 @@ export function doctorHooks(
|
|
|
297
305
|
return out;
|
|
298
306
|
}
|
|
299
307
|
const counts = wiredCounts(read.settings, wiring.names);
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
const missing = wiring.names.filter((n) => total(n) === 0);
|
|
303
|
-
const twice = wiring.names.filter((n) => total(n) > 1);
|
|
308
|
+
const missing = counts.filter((c) => total(c) === 0).map((c) => c.name);
|
|
309
|
+
const twice = counts.filter((c) => total(c) > 1);
|
|
304
310
|
// `init` refuses to write while any hook is wired under another spelling, so its remedy must
|
|
305
311
|
// not be offered then — the remedy is to pick ONE form.
|
|
306
|
-
const handWired =
|
|
312
|
+
const handWired = counts.some((c) => c.other > 0);
|
|
307
313
|
const remedy = handWired
|
|
308
314
|
? `some paperlint hooks are wired by hand under another spelling, and \`npx paperlint init\` writes nothing then — add the missing ones in that same form, or delete the hand-written ones and run \`npx paperlint init\``
|
|
309
315
|
: `\`npx paperlint init\` adds them`;
|
|
310
316
|
if (twice.length > 0) {
|
|
311
317
|
out.push(` ⚠ wired TWICE — each of these runs more than once per event:`);
|
|
312
|
-
for (const
|
|
318
|
+
for (const c of twice) out.push(` ${c.name} ×${String(total(c))}`);
|
|
313
319
|
out.push(
|
|
314
320
|
` keep one command per hook; \`npx paperlint init\` writes the ${MANAGED_BY} form`,
|
|
315
321
|
);
|