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
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exclusions in `.c8rc.json` are CLAIMS about the excluded files, and each claim is checked
|
|
3
|
+
* here — otherwise an exclusion is the quiet way to make coverage go up.
|
|
4
|
+
*
|
|
5
|
+
* - `typeOnly`: files with no runtime code. c8's `all` counts every line of a file no test
|
|
6
|
+
* loaded, and a types-only module compiles to nothing, so it can never be "covered". The
|
|
7
|
+
* claim is checked on the syntax tree: a file listed here that grows a function or a constant
|
|
8
|
+
* fails this test and must leave the list.
|
|
9
|
+
* - `evalOnly`: support for the PAID `*.eval.mjs` evaluations, which `npm test` never runs. The
|
|
10
|
+
* claim is that nothing but an eval (or another eval-only module) imports them.
|
|
11
|
+
*
|
|
12
|
+
* The floor itself is pinned: lines, statements, functions and branches are all 100, `npm run
|
|
13
|
+
* coverage` checks them, and CI runs that script — lowering any of the three links fails here.
|
|
14
|
+
*
|
|
15
|
+
* Two more gates close the ways around a 100% threshold (owner's rule for #52):
|
|
16
|
+
* - no measured source carries a coverage-ignore comment (`c8`/`v8`/`istanbul`/`node:coverage
|
|
17
|
+
* ignore`) — an effect a test cannot reach is made injectable instead;
|
|
18
|
+
* - the exclude list is PINNED: a new pattern fails here until this file names it, with its
|
|
19
|
+
* reason, on purpose.
|
|
20
|
+
*/
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { globSync, readFileSync } from "node:fs";
|
|
24
|
+
import { dirname, join, matchesGlob, resolve } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
// @ts-expect-error — js-yaml ships no types, and the package does not depend on @types/js-yaml
|
|
27
|
+
import yaml from "js-yaml";
|
|
28
|
+
import ts from "typescript";
|
|
29
|
+
import { test } from "vitest";
|
|
30
|
+
|
|
31
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
32
|
+
const CONFIG = JSON.parse(readFileSync(join(ROOT, ".c8rc.json"), "utf8")) as {
|
|
33
|
+
lines: number;
|
|
34
|
+
statements: number;
|
|
35
|
+
functions: number;
|
|
36
|
+
branches: number;
|
|
37
|
+
excludeAfterRemap: boolean;
|
|
38
|
+
include: string[];
|
|
39
|
+
exclude: string[];
|
|
40
|
+
extension: string[];
|
|
41
|
+
typeOnly: string[];
|
|
42
|
+
evalOnly: string[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const expand = (patterns: readonly string[]): string[] =>
|
|
46
|
+
patterns.flatMap((p) => globSync(p, { cwd: ROOT })).sort();
|
|
47
|
+
|
|
48
|
+
/** Statements that emit JavaScript — anything but interfaces, type aliases and type-only imports/exports. */
|
|
49
|
+
export function runtimeStatements(fileName: string, source: string): string[] {
|
|
50
|
+
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest);
|
|
51
|
+
return sf.statements
|
|
52
|
+
.filter(
|
|
53
|
+
(s) =>
|
|
54
|
+
!ts.isInterfaceDeclaration(s) &&
|
|
55
|
+
!ts.isTypeAliasDeclaration(s) &&
|
|
56
|
+
!(ts.isImportDeclaration(s) && s.importClause?.isTypeOnly) &&
|
|
57
|
+
!(ts.isExportDeclaration(s) && s.isTypeOnly),
|
|
58
|
+
)
|
|
59
|
+
.map((s) => ts.SyntaxKind[s.kind]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Every relative module specifier a source imports, statically or with `import("…")`. */
|
|
63
|
+
export function importedPaths(fileName: string, source: string): string[] {
|
|
64
|
+
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest);
|
|
65
|
+
const found: string[] = [];
|
|
66
|
+
const visit = (node: ts.Node): void => {
|
|
67
|
+
const spec = ts.isImportDeclaration(node)
|
|
68
|
+
? node.moduleSpecifier
|
|
69
|
+
: ts.isCallExpression(node) &&
|
|
70
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
71
|
+
? node.arguments[0]
|
|
72
|
+
: undefined;
|
|
73
|
+
if (spec && ts.isStringLiteral(spec) && spec.text.startsWith("."))
|
|
74
|
+
found.push(resolve(dirname(fileName), spec.text));
|
|
75
|
+
ts.forEachChild(node, visit);
|
|
76
|
+
};
|
|
77
|
+
visit(sf);
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test("the named exclusion lists are part of c8's exclude, and match files", () => {
|
|
82
|
+
for (const list of [CONFIG.typeOnly, CONFIG.evalOnly]) {
|
|
83
|
+
assert.deepEqual(
|
|
84
|
+
list.filter((p) => !CONFIG.exclude.includes(p)),
|
|
85
|
+
[],
|
|
86
|
+
);
|
|
87
|
+
assert.ok(expand(list).length >= list.length);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("runtimeStatements tells a types-only module from one with code", () => {
|
|
92
|
+
assert.deepEqual(
|
|
93
|
+
[
|
|
94
|
+
runtimeStatements(
|
|
95
|
+
"a.ts",
|
|
96
|
+
'import type { X } from "./x";\nexport type Y = X;\ninterface Z {}\n',
|
|
97
|
+
),
|
|
98
|
+
runtimeStatements("b.ts", "export type Y = 1;\nexport const y = 1;\n"),
|
|
99
|
+
],
|
|
100
|
+
[[], ["FirstStatement"]],
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("every typeOnly exclusion has no runtime code", () => {
|
|
105
|
+
const offenders = expand(CONFIG.typeOnly)
|
|
106
|
+
.map(
|
|
107
|
+
(f) =>
|
|
108
|
+
[f, runtimeStatements(f, readFileSync(join(ROOT, f), "utf8"))] as const,
|
|
109
|
+
)
|
|
110
|
+
.filter(([, rt]) => rt.length > 0);
|
|
111
|
+
assert.deepEqual(offenders, []);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("every evalOnly exclusion is imported only by an eval or another evalOnly module", () => {
|
|
115
|
+
const evalOnly = new Set(expand(CONFIG.evalOnly).map((f) => join(ROOT, f)));
|
|
116
|
+
const sources = globSync("**/*.{mjs,ts}", {
|
|
117
|
+
cwd: ROOT,
|
|
118
|
+
exclude: (p) => p === "node_modules" || p === "dist" || p === "coverage",
|
|
119
|
+
});
|
|
120
|
+
const importers = sources.flatMap((f) => {
|
|
121
|
+
const abs = join(ROOT, f);
|
|
122
|
+
return importedPaths(abs, readFileSync(abs, "utf8"))
|
|
123
|
+
.filter((target) => evalOnly.has(target))
|
|
124
|
+
.map(() => f);
|
|
125
|
+
});
|
|
126
|
+
const outsiders = importers.filter(
|
|
127
|
+
(f) => !f.endsWith(".eval.mjs") && !evalOnly.has(join(ROOT, f)),
|
|
128
|
+
);
|
|
129
|
+
assert.deepEqual(
|
|
130
|
+
{ outsiders, anyImporter: importers.length > 0 },
|
|
131
|
+
{
|
|
132
|
+
outsiders: [],
|
|
133
|
+
anyImporter: true,
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/** Every tracked file c8 could load as code — the extensions it is configured to measure. */
|
|
139
|
+
function trackedSources(): string[] {
|
|
140
|
+
const r = spawnSync("git", ["ls-files", "-z"], {
|
|
141
|
+
cwd: ROOT,
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
});
|
|
144
|
+
assert.equal(r.status, 0, r.stderr);
|
|
145
|
+
return r.stdout
|
|
146
|
+
.split("\0")
|
|
147
|
+
.filter((f) => CONFIG.extension.some((x) => f.endsWith(x)))
|
|
148
|
+
.sort();
|
|
149
|
+
}
|
|
150
|
+
const isIncluded = (f: string): boolean =>
|
|
151
|
+
CONFIG.include.some((p) => matchesGlob(f, p));
|
|
152
|
+
const isExcluded = (f: string): boolean =>
|
|
153
|
+
CONFIG.exclude.some((p) => matchesGlob(f, p));
|
|
154
|
+
const isMeasured = (f: string): boolean => isIncluded(f) && !isExcluded(f);
|
|
155
|
+
|
|
156
|
+
/** The files c8 measures: `include` globs, the configured extensions, minus every `exclude`. */
|
|
157
|
+
function measuredFiles(): string[] {
|
|
158
|
+
return [...new Set(CONFIG.include.flatMap((p) => globSync(p, { cwd: ROOT })))]
|
|
159
|
+
.filter((f) => CONFIG.extension.some((x) => f.endsWith(x)))
|
|
160
|
+
.filter((f) => !CONFIG.exclude.some((x) => matchesGlob(f, x)))
|
|
161
|
+
.sort();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The text of every comment in a JS/TS source, read off the parsed tree — not by grep, which
|
|
166
|
+
* cannot tell a comment from a string or a regex. Every comment is trivia around a token — leading
|
|
167
|
+
* the token after it, or trailing the one before it on the same line — so the comment ranges
|
|
168
|
+
* around every token (end-of-file included) are all of them.
|
|
169
|
+
*/
|
|
170
|
+
export function comments(fileName: string, source: string): string[] {
|
|
171
|
+
const sf = ts.createSourceFile(
|
|
172
|
+
fileName,
|
|
173
|
+
source,
|
|
174
|
+
ts.ScriptTarget.Latest,
|
|
175
|
+
true,
|
|
176
|
+
fileName.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS,
|
|
177
|
+
);
|
|
178
|
+
const found = new Map<number, string>();
|
|
179
|
+
const visit = (node: ts.Node): void => {
|
|
180
|
+
if (ts.isJSDoc(node)) return; // its text is the comment already collected before its node
|
|
181
|
+
const kids = node.getChildren(sf);
|
|
182
|
+
if (kids.length > 0) return kids.forEach(visit);
|
|
183
|
+
for (const r of [
|
|
184
|
+
...(ts.getLeadingCommentRanges(source, node.pos) ?? []),
|
|
185
|
+
...(ts.getTrailingCommentRanges(source, node.end) ?? []),
|
|
186
|
+
])
|
|
187
|
+
found.set(r.pos, source.slice(r.pos, r.end));
|
|
188
|
+
};
|
|
189
|
+
visit(sf);
|
|
190
|
+
return [...found.entries()].sort(([a], [b]) => a - b).map(([, text]) => text);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const IGNORE = /\b(?:c8|v8|istanbul|node:coverage)\s+ignore\b/;
|
|
194
|
+
|
|
195
|
+
test("comments() finds comments and only comments — before a closing brace, in a template, at the end", () => {
|
|
196
|
+
assert.deepEqual(
|
|
197
|
+
comments(
|
|
198
|
+
"a.ts",
|
|
199
|
+
[
|
|
200
|
+
'const s = "/* c8 ignore next */";',
|
|
201
|
+
"const r = /x\\/y/; // v8 ignore",
|
|
202
|
+
"const t = `${1 /* in a template */}`;",
|
|
203
|
+
"try {",
|
|
204
|
+
" f();",
|
|
205
|
+
" /* c8 ignore start */",
|
|
206
|
+
"} catch {}",
|
|
207
|
+
"/* at the end */",
|
|
208
|
+
].join("\n"),
|
|
209
|
+
),
|
|
210
|
+
[
|
|
211
|
+
"// v8 ignore",
|
|
212
|
+
"/* in a template */",
|
|
213
|
+
"/* c8 ignore start */",
|
|
214
|
+
"/* at the end */",
|
|
215
|
+
],
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("no measured source carries a coverage-ignore comment", () => {
|
|
220
|
+
const files = measuredFiles();
|
|
221
|
+
// The scan covers exactly what c8 measures: the same set, derived from the tracked files.
|
|
222
|
+
assert.deepEqual(files, trackedSources().filter(isMeasured));
|
|
223
|
+
assert.ok(files.length > 0);
|
|
224
|
+
const offenders = files.filter((f) =>
|
|
225
|
+
comments(f, readFileSync(join(ROOT, f), "utf8")).some((c) =>
|
|
226
|
+
IGNORE.test(c),
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
assert.deepEqual(offenders, []);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("the floor is 100 on all four measures, and the script that checks it cannot loosen it", () => {
|
|
233
|
+
assert.deepEqual(
|
|
234
|
+
[CONFIG.lines, CONFIG.statements, CONFIG.functions, CONFIG.branches],
|
|
235
|
+
[100, 100, 100, 100],
|
|
236
|
+
);
|
|
237
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")) as {
|
|
238
|
+
scripts: Record<string, string>;
|
|
239
|
+
};
|
|
240
|
+
// Pinned whole: c8's command-line flags override .c8rc.json, so `--lines 50` or `--exclude "**"`
|
|
241
|
+
// added here would bypass every threshold and the pinned exclude list below.
|
|
242
|
+
assert.equal(
|
|
243
|
+
pkg.scripts["coverage"],
|
|
244
|
+
'NODE_OPTIONS="--import=./test/coverage-src.mjs $NODE_OPTIONS" c8 --check-coverage npm test --',
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
/** The gates job and the one step of it that runs coverage, read from the parsed workflow. */
|
|
249
|
+
interface Step {
|
|
250
|
+
readonly run?: string;
|
|
251
|
+
readonly if?: unknown;
|
|
252
|
+
readonly "continue-on-error"?: unknown;
|
|
253
|
+
}
|
|
254
|
+
interface Job {
|
|
255
|
+
readonly if?: unknown;
|
|
256
|
+
readonly "continue-on-error"?: unknown;
|
|
257
|
+
readonly steps?: readonly Step[];
|
|
258
|
+
}
|
|
259
|
+
export function coverageSteps(
|
|
260
|
+
workflow: string,
|
|
261
|
+
): { job: string; definition: Job; step: Step }[] {
|
|
262
|
+
const ci = yaml.load(workflow) as { jobs: Record<string, Job> };
|
|
263
|
+
return Object.entries(ci.jobs).flatMap(([job, definition]) =>
|
|
264
|
+
(definition.steps ?? [])
|
|
265
|
+
.filter((step) => (step.run ?? "").trim().startsWith("npm run coverage"))
|
|
266
|
+
.map((step) => ({ job, definition, step })),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const DRAFT_GATE =
|
|
271
|
+
"github.event_name != 'pull_request' || github.event.pull_request.draft == false";
|
|
272
|
+
|
|
273
|
+
/** Why CI's coverage step would not fail the run on a coverage drop — empty when it would. */
|
|
274
|
+
export function coverageStepHoles(workflow: string): string[] {
|
|
275
|
+
const found = coverageSteps(workflow);
|
|
276
|
+
if (found.length !== 1)
|
|
277
|
+
return [
|
|
278
|
+
`expected one step running \`npm run coverage\`, found ${String(found.length)}`,
|
|
279
|
+
];
|
|
280
|
+
const [{ job, definition, step }] = found as [(typeof found)[number]];
|
|
281
|
+
return [
|
|
282
|
+
...(job === "gates" ? [] : [`the step is in job ${job}, not gates`]),
|
|
283
|
+
...(definition.if === DRAFT_GATE
|
|
284
|
+
? []
|
|
285
|
+
: [
|
|
286
|
+
`job ${job} runs if ${JSON.stringify(definition.if)}, not only the draft gate`,
|
|
287
|
+
]),
|
|
288
|
+
...("continue-on-error" in definition
|
|
289
|
+
? [`job ${job} has continue-on-error`]
|
|
290
|
+
: []),
|
|
291
|
+
...(step.if === undefined
|
|
292
|
+
? []
|
|
293
|
+
: [`the step has if: ${JSON.stringify(step.if)}`]),
|
|
294
|
+
...("continue-on-error" in step ? ["the step has continue-on-error"] : []),
|
|
295
|
+
...(step.run?.trim() === "npm run coverage -- --no-skip"
|
|
296
|
+
? []
|
|
297
|
+
: [
|
|
298
|
+
`the step runs ${JSON.stringify(step.run)}, not \`npm run coverage -- --no-skip\``,
|
|
299
|
+
]),
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
test("CI runs the coverage step in the gates job, unconditionally past the draft gate, and fails on it", () => {
|
|
304
|
+
const ci = readFileSync(join(ROOT, ".github", "workflows", "ci.yml"), "utf8");
|
|
305
|
+
assert.deepEqual(coverageStepHoles(ci), []);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("coverageStepHoles names every way to keep CI green over a coverage drop", () => {
|
|
309
|
+
const wf = (step: string, jobIf = DRAFT_GATE, job = "gates") =>
|
|
310
|
+
[
|
|
311
|
+
"jobs:",
|
|
312
|
+
` ${job}:`,
|
|
313
|
+
` if: ${JSON.stringify(jobIf)}`,
|
|
314
|
+
" steps:",
|
|
315
|
+
" - name: coverage",
|
|
316
|
+
` run: npm run coverage -- --no-skip${step}`,
|
|
317
|
+
].join("\n");
|
|
318
|
+
assert.deepEqual(
|
|
319
|
+
[
|
|
320
|
+
coverageStepHoles(wf("")),
|
|
321
|
+
coverageStepHoles(wf("\n continue-on-error: true")),
|
|
322
|
+
coverageStepHoles(wf("\n if: false")),
|
|
323
|
+
coverageStepHoles(wf("", "false")),
|
|
324
|
+
coverageStepHoles(wf("", DRAFT_GATE, "other")),
|
|
325
|
+
coverageStepHoles("jobs:\n gates:\n steps: []\n"),
|
|
326
|
+
],
|
|
327
|
+
[
|
|
328
|
+
[],
|
|
329
|
+
["the step has continue-on-error"],
|
|
330
|
+
["the step has if: false"],
|
|
331
|
+
['job gates runs if "false", not only the draft gate'],
|
|
332
|
+
["the step is in job other, not gates"],
|
|
333
|
+
["expected one step running `npm run coverage`, found 0"],
|
|
334
|
+
],
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("dist/ is excluded BEFORE source maps: a direct load is not a second copy of src/", () => {
|
|
339
|
+
// With `true`, a dist/ file remapped through its source map lands under src/ and escapes the
|
|
340
|
+
// `dist/**` exclude — measured: the extract-pdf-facts shim, spawned without NODE_OPTIONS, added a
|
|
341
|
+
// column-shifted duplicate of every function in the src/ modules it imported.
|
|
342
|
+
assert.equal(CONFIG.excludeAfterRemap, false);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("the exclude list is exactly the justified set", () => {
|
|
346
|
+
assert.deepEqual(CONFIG.exclude, [
|
|
347
|
+
// tests themselves
|
|
348
|
+
"**/*.harness.mjs",
|
|
349
|
+
"**/*.test.*",
|
|
350
|
+
// PAID LLM evaluations, never run by `npm test`
|
|
351
|
+
"**/*.eval.mjs",
|
|
352
|
+
// skill sources vigiles compiles into SKILL.md
|
|
353
|
+
"**/*.spec.ts",
|
|
354
|
+
// type declarations
|
|
355
|
+
"**/*.d.ts",
|
|
356
|
+
"**/*.d.mts",
|
|
357
|
+
// inputs the tests read
|
|
358
|
+
"**/fixtures/**",
|
|
359
|
+
// build output (the coverage run resolves it to src/ — test/coverage-src.mjs)
|
|
360
|
+
"dist/**",
|
|
361
|
+
// test support
|
|
362
|
+
"test/**",
|
|
363
|
+
// not sources: dependencies, the report itself
|
|
364
|
+
"node_modules/**",
|
|
365
|
+
"coverage/**",
|
|
366
|
+
// typeOnly — checked above to hold no runtime code
|
|
367
|
+
...CONFIG.typeOnly,
|
|
368
|
+
// evalOnly — checked above to be imported only by evals
|
|
369
|
+
...CONFIG.evalOnly,
|
|
370
|
+
// dated research records kept as evidence for the skills' prose and the prior-art notes
|
|
371
|
+
"skills/**/repro/**",
|
|
372
|
+
"skills/**/references/**",
|
|
373
|
+
"docs/prior-art/repro/**",
|
|
374
|
+
// the test runner's own configuration, read by vitest, not code the package runs
|
|
375
|
+
"vitest.config.ts",
|
|
376
|
+
]);
|
|
377
|
+
assert.deepEqual(CONFIG.include, [
|
|
378
|
+
"bin/**",
|
|
379
|
+
"src/**/*.ts",
|
|
380
|
+
"lib/**",
|
|
381
|
+
"eslint-rules/**",
|
|
382
|
+
"hooks/**",
|
|
383
|
+
"skills/**",
|
|
384
|
+
"scripts/**",
|
|
385
|
+
"eslint.config.mjs",
|
|
386
|
+
]);
|
|
387
|
+
assert.deepEqual(CONFIG.typeOnly, [
|
|
388
|
+
"src/types.ts",
|
|
389
|
+
"src/ports/*.ts",
|
|
390
|
+
"src/domain/page-layout.ts",
|
|
391
|
+
"src/domain/paths.ts",
|
|
392
|
+
]);
|
|
393
|
+
assert.deepEqual(CONFIG.evalOnly, [
|
|
394
|
+
"lib/skill-eval-kit.mjs",
|
|
395
|
+
"lib/skill-eval-fixture.mjs",
|
|
396
|
+
"lib/trigger-ledger.mjs",
|
|
397
|
+
]);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("every tracked source is either measured or excluded by a pattern named above", () => {
|
|
401
|
+
// A new directory outside `include` would otherwise be invisible to c8 and to this file alike.
|
|
402
|
+
assert.deepEqual(
|
|
403
|
+
trackedSources().filter((f) => !isIncluded(f) && !isExcluded(f)),
|
|
404
|
+
[],
|
|
405
|
+
);
|
|
406
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report guard's listing: a file entry without `messages`, a warning, and a message with no
|
|
3
|
+
* rule (ESLint's own parse errors carry `ruleId: null`). The zero-file refusal and the pass-through
|
|
4
|
+
* are driven end to end by `action.harness.mjs`.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { test } from "vitest";
|
|
10
|
+
import { useTempDir } from "../test/support.mjs";
|
|
11
|
+
import { guard } from "./eslint-report-guard.mjs";
|
|
12
|
+
|
|
13
|
+
test("warnings and rule-less messages are listed; a file with no messages counts as linted", () => {
|
|
14
|
+
const report = join(useTempDir("report-guard-"), "report.json");
|
|
15
|
+
writeFileSync(
|
|
16
|
+
report,
|
|
17
|
+
JSON.stringify([
|
|
18
|
+
{
|
|
19
|
+
filePath: "a.md",
|
|
20
|
+
messages: [
|
|
21
|
+
{
|
|
22
|
+
severity: 1,
|
|
23
|
+
line: 2,
|
|
24
|
+
column: 3,
|
|
25
|
+
ruleId: "paper/x",
|
|
26
|
+
message: "soft",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
severity: 2,
|
|
30
|
+
line: 1,
|
|
31
|
+
column: 1,
|
|
32
|
+
ruleId: null,
|
|
33
|
+
message: "Parsing error",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{ filePath: "b.md" },
|
|
38
|
+
]),
|
|
39
|
+
);
|
|
40
|
+
assert.deepEqual(guard(report, 1), {
|
|
41
|
+
code: 1,
|
|
42
|
+
lines: [
|
|
43
|
+
"warning a.md:2:3 paper/x soft",
|
|
44
|
+
"error a.md:1:1 (no rule) Parsing error",
|
|
45
|
+
"\nlinted 2 file(s) · 2 finding(s) · 1 error(s) · 1 warning(s)",
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -13,11 +13,9 @@
|
|
|
13
13
|
"eslint-rules/tex-build.harness.mjs",
|
|
14
14
|
"fixtures/real-markdown-paper/real-markdown-paper.harness.mjs",
|
|
15
15
|
"lib/agent-cli-version.harness.mjs",
|
|
16
|
-
"lib/mutation-driver.harness.mjs",
|
|
17
16
|
"lib/paper-config.harness.mjs",
|
|
18
17
|
"lib/skill-trigger-cases.harness.mjs",
|
|
19
18
|
"scripts/check.harness.mjs",
|
|
20
|
-
"scripts/mutation-batteries-frozen.harness.mjs",
|
|
21
19
|
"scripts/rules-are-content-only.harness.mjs",
|
|
22
20
|
"scripts/rules-see-files.harness.mjs",
|
|
23
21
|
"skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs",
|
|
@@ -21,8 +21,16 @@ export declare function judge(o: {
|
|
|
21
21
|
unexplained: readonly string[];
|
|
22
22
|
frozen: Counts;
|
|
23
23
|
}): string[];
|
|
24
|
-
export declare function checkFrozen(
|
|
24
|
+
export declare function checkFrozen(
|
|
25
|
+
root?: string,
|
|
26
|
+
options?: { lint?: (cwd: string) => Promise<unknown[]> },
|
|
27
|
+
): Promise<{
|
|
25
28
|
problems: string[];
|
|
26
29
|
frozen: Counts;
|
|
27
30
|
counts: Counts;
|
|
28
31
|
}>;
|
|
32
|
+
export declare function main(options?: {
|
|
33
|
+
check?: () => Promise<{ problems: string[]; frozen: Counts; counts: Counts }>;
|
|
34
|
+
log?: (line: string) => void;
|
|
35
|
+
err?: (line: string) => void;
|
|
36
|
+
}): Promise<number>;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `reportUnusedDisableDirectives: "error"` already makes a directive that suppresses nothing a lint
|
|
13
13
|
* error, so a fixed site must drop its comment. What that cannot stop is a NEW directive: a new file,
|
|
14
14
|
* or a new import in an old one, silenced the same way. This check freezes the per-file counts, in
|
|
15
|
-
*
|
|
15
|
+
* a list that may only shrink:
|
|
16
16
|
*
|
|
17
17
|
* 1. a file carrying a legacy suppression that is not in the frozen list fails — a new exemption;
|
|
18
18
|
* 2. a listed file with no legacy suppression left fails — the list must shrink with the code;
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
*
|
|
35
35
|
* Run: `node scripts/layer-legacy-frozen.mjs` (also part of `npm run check`)
|
|
36
36
|
*/
|
|
37
|
+
import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
37
38
|
import { readFileSync } from "node:fs";
|
|
38
39
|
import { dirname, join, relative, sep } from "node:path";
|
|
39
40
|
import { fileURLToPath } from "node:url";
|
|
@@ -114,12 +115,16 @@ export function judge({ counts, unexplained, frozen }) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/** Lint `src/` with the repository's configuration and run the whole check. */
|
|
117
|
-
export async function checkFrozen(
|
|
118
|
+
export async function checkFrozen(
|
|
119
|
+
root = ROOT,
|
|
120
|
+
{ lint = (cwd) => new ESLint({ cwd }).lintFiles(["src/**/*.ts"]) } = {},
|
|
121
|
+
) {
|
|
118
122
|
const data = JSON.parse(readFileSync(join(root, FROZEN_FILE), "utf8"));
|
|
119
123
|
const frozen = data.files ?? {};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
124
|
+
// ESLint itself THROWS when the glob matches nothing ("No files matching 'src/**/*.ts' were
|
|
125
|
+
// found") or only ignored files — measured. An empty result from any other linter (or a
|
|
126
|
+
// configuration that lets it through) must not tally nothing and report every frozen file fixed.
|
|
127
|
+
const raw = await lint(root);
|
|
123
128
|
if (raw.length === 0)
|
|
124
129
|
return {
|
|
125
130
|
problems: ["src/**/*.ts matched no file — nothing was counted."],
|
|
@@ -134,19 +139,31 @@ export async function checkFrozen(root = ROOT) {
|
|
|
134
139
|
return { problems: judge({ ...t, frozen }), frozen, counts: t.counts };
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
/**
|
|
143
|
+
* The gate: exit 1 naming every problem, or 0 with the count of what stays frozen. `check` is
|
|
144
|
+
* injected so a test can hand it problems without planting suppressions in real source.
|
|
145
|
+
*/
|
|
146
|
+
export async function main({
|
|
147
|
+
check = () => checkFrozen(ROOT),
|
|
148
|
+
log = console.log,
|
|
149
|
+
err = console.error,
|
|
150
|
+
} = {}) {
|
|
151
|
+
const { problems, counts } = await check();
|
|
139
152
|
if (problems.length) {
|
|
140
|
-
|
|
153
|
+
err(
|
|
141
154
|
`🔴 legacy layer exemptions are frozen and may only shrink (#76) — ${String(problems.length)} problem(s):`,
|
|
142
155
|
);
|
|
143
|
-
for (const p of problems)
|
|
144
|
-
|
|
156
|
+
for (const p of problems) err(` ${p}`);
|
|
157
|
+
return 1;
|
|
145
158
|
}
|
|
146
159
|
const n = Object.values(counts)
|
|
147
160
|
.flatMap((r) => Object.values(r))
|
|
148
161
|
.reduce((s, v) => s + v, 0);
|
|
149
|
-
|
|
162
|
+
log(
|
|
150
163
|
`✓ ${String(Object.keys(counts).length)} legacy files, ${String(n)} frozen layer exemptions, none new, none grown (#76)`,
|
|
151
164
|
);
|
|
165
|
+
return 0;
|
|
152
166
|
}
|
|
167
|
+
|
|
168
|
+
// `isMain`, not a comparison with `file://${argv[1]}`: that is false through a symlink (consumer.mjs).
|
|
169
|
+
if (isMain(import.meta.url)) process.exit(await main());
|