paperlint 13.5.0 → 13.6.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/dist/skills/build-label-page/scripts/label-page-core.d.ts +25 -5
- package/dist/skills/build-label-page/scripts/rule-descriptions.d.ts +12 -0
- package/package.json +1 -1
- package/skills/assess-novelty/scripts/check_novelty.py +7 -2
- package/skills/assess-novelty/scripts/check_novelty_search.test.ts +279 -0
- package/skills/assess-novelty/scripts/fixtures/novelty/siblings/novelty.json +6 -3
- package/skills/assess-novelty/scripts/novelty_search.py +87 -0
- package/skills/build-label-page/references/closed-choice-generator.md +7 -1
- package/skills/build-label-page/scripts/fixtures/ruff-rule-TID253.json +23 -0
- package/skills/build-label-page/scripts/label-page-core.js +34 -4
- package/skills/build-label-page/scripts/label-page-core.test.ts +97 -59
- package/skills/build-label-page/scripts/label-page-core.ts +52 -4
- package/skills/build-label-page/scripts/label-page-html.test.ts +46 -20
- package/skills/build-label-page/scripts/label-page.js +3 -0
- package/skills/build-label-page/scripts/label-page.ts +3 -0
- package/skills/build-label-page/scripts/page-template.html +4 -0
- package/skills/build-label-page/scripts/rule-descriptions.js +82 -0
- package/skills/build-label-page/scripts/rule-descriptions.test.ts +118 -0
- package/skills/build-label-page/scripts/rule-descriptions.ts +98 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a linter rule does, in the linter's own words, for the card of an item that names one
|
|
3
|
+
* ("read as ruff `TID253`" means nothing to a labeller who has not met the code). Never invented: a
|
|
4
|
+
* rule the linter cannot describe has no description, and the build refuses it.
|
|
5
|
+
*
|
|
6
|
+
* - ruff: `ruff rule <code> --output-format json`, the paragraph under "## What it does".
|
|
7
|
+
* - ESLint: the rule's `meta.docs.description`, from the ESLint (and the plugin) installed where
|
|
8
|
+
* items.json is: a study's own install, found by Node's resolution from that folder upwards.
|
|
9
|
+
* A plugin rule is found by ESLint's naming convention (`@scope/rule` → `@scope/eslint-plugin`,
|
|
10
|
+
* `@scope/p/rule` → `@scope/eslint-plugin-p`, `p/rule` → `eslint-plugin-p`).
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
export interface RuleRef {
|
|
18
|
+
readonly linter: "eslint" | "ruff";
|
|
19
|
+
readonly id: string;
|
|
20
|
+
}
|
|
21
|
+
/** A process run to its end: the exit status and what it printed. */
|
|
22
|
+
export type RunProcess = (
|
|
23
|
+
cmd: string,
|
|
24
|
+
args: readonly string[],
|
|
25
|
+
) => { readonly status: number | null; readonly stdout: string };
|
|
26
|
+
|
|
27
|
+
export const runProcess: RunProcess = (cmd, args) => {
|
|
28
|
+
const r = spawnSync(cmd, args, { encoding: "utf8" });
|
|
29
|
+
return { status: r.status, stdout: r.stdout };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const RuffRule = z.object({ explanation: z.string() });
|
|
33
|
+
/** The paragraph under "## What it does", its lines joined. */
|
|
34
|
+
function ruffDescription(id: string, run: RunProcess): string | undefined {
|
|
35
|
+
const r = run("ruff", ["rule", id, "--output-format", "json"]);
|
|
36
|
+
if (r.status !== 0) return undefined;
|
|
37
|
+
const { explanation } = RuffRule.parse(JSON.parse(r.stdout));
|
|
38
|
+
const what = /## What it does\n([\s\S]*?)(?:\n\n|$)/.exec(explanation)?.[1];
|
|
39
|
+
return what?.split("\n").join(" ").trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const RuleModule = z.object({
|
|
43
|
+
meta: z.object({ docs: z.object({ description: z.string() }) }),
|
|
44
|
+
});
|
|
45
|
+
const Plugin = z.object({ rules: z.record(z.string(), z.unknown()) });
|
|
46
|
+
const Builtins = z.object({
|
|
47
|
+
builtinRules: z.map(z.string(), z.unknown()),
|
|
48
|
+
});
|
|
49
|
+
/** The package and the rule name a namespaced ESLint rule id points at. */
|
|
50
|
+
function pluginOf(id: string): readonly [string, string] {
|
|
51
|
+
const parts = id.split("/");
|
|
52
|
+
const name = id.slice(id.lastIndexOf("/") + 1);
|
|
53
|
+
const [scope, plugin] = parts;
|
|
54
|
+
if (id.startsWith("@"))
|
|
55
|
+
return [
|
|
56
|
+
parts.length === 2
|
|
57
|
+
? `${String(scope)}/eslint-plugin`
|
|
58
|
+
: `${String(scope)}/eslint-plugin-${String(plugin)}`,
|
|
59
|
+
name,
|
|
60
|
+
];
|
|
61
|
+
return [`eslint-plugin-${String(scope)}`, name];
|
|
62
|
+
}
|
|
63
|
+
// `require` of an ES-module plugin returns its namespace: the plugin object is then its `default`.
|
|
64
|
+
const EsmPlugin = z.object({ default: Plugin });
|
|
65
|
+
const pluginRules = (mod: unknown): Readonly<Record<string, unknown>> => {
|
|
66
|
+
const esm = EsmPlugin.safeParse(mod);
|
|
67
|
+
return esm.success ? esm.data.default.rules : Plugin.parse(mod).rules;
|
|
68
|
+
};
|
|
69
|
+
const description = (rule: unknown): string | undefined => {
|
|
70
|
+
const r = RuleModule.safeParse(rule);
|
|
71
|
+
return r.success ? r.data.meta.docs.description : undefined;
|
|
72
|
+
};
|
|
73
|
+
function eslintDescription(id: string, fromDir: string): string | undefined {
|
|
74
|
+
// createRequire takes an absolute path only; items.json is often named relative to the study folder.
|
|
75
|
+
const load = createRequire(resolve(fromDir, "items.json"));
|
|
76
|
+
try {
|
|
77
|
+
if (!id.includes("/")) {
|
|
78
|
+
const core = Builtins.parse(load("eslint/use-at-your-own-risk"));
|
|
79
|
+
return description(core.builtinRules.get(id));
|
|
80
|
+
}
|
|
81
|
+
const [pkg, name] = pluginOf(id);
|
|
82
|
+
return description(pluginRules(load(pkg))[name]);
|
|
83
|
+
} catch {
|
|
84
|
+
// ESLint or the plugin is not installed where items.json is: the linter cannot answer.
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The linter's own one-line description of `ref`, or undefined when it has none. */
|
|
90
|
+
export function describeRule(
|
|
91
|
+
ref: RuleRef,
|
|
92
|
+
fromDir: string,
|
|
93
|
+
run: RunProcess = runProcess,
|
|
94
|
+
): string | undefined {
|
|
95
|
+
return ref.linter === "ruff"
|
|
96
|
+
? ruffDescription(ref.id, run)
|
|
97
|
+
: eslintDescription(ref.id, fromDir);
|
|
98
|
+
}
|