@rdjperron/lint 0.5.0 → 0.7.0

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/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # @rdjperron/lint
2
2
 
3
3
  One Oxlint JS plugin, `rdjperron`, with twenty rules against low-evidence code, the config data a
4
- project spreads into its own `vite.config.ts`, and the check that proves every rule still fires.
4
+ project spreads into its own `vite.config.ts`, the check that proves every rule still fires, and
5
+ the repository checks that hold a repo's documents to their own conventions.
5
6
 
6
7
  | Entry | What it gives you |
7
8
  | ------------------------ | ---------------------------------------------------------------------------------- |
8
9
  | `@rdjperron/lint` | The plugin. One `jsPlugins` entry, one prefix, twenty rules. |
9
10
  | `@rdjperron/lint/config` | `recommended`, the override builders, and the `fmt`, `ignore` and `staged` blocks. |
11
+ | `@rdjperron/lint/repo` | `checkRepo`, three checks on a repo's Markdown links and research names. |
10
12
  | `rdjperron-lint-verify` | The command that proves every rule still fires. No config, no arguments. |
11
13
 
12
14
  Seventeen of the rules are a copy of [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop),
@@ -184,6 +186,123 @@ one right after you move `vite-plus`, and in CI.
184
186
  One thing not to do: do not delete the fixtures. They are the only thing that catches a rule going
185
187
  dead.
186
188
 
189
+ ## Repository checks
190
+
191
+ `@rdjperron/lint/repo` is for a repo that keeps living documents at its root and under `docs/`, and
192
+ dated research in `_research/`. Prose can state those conventions and cannot enforce them.
193
+ `checkRepo` reads the tree and returns what breaks them, so a test fails on a dead link or on a
194
+ research entry with no date in its name.
195
+
196
+ It is plain Node and imports no test runner. You call it from a test of your own:
197
+
198
+ ```ts
199
+ // lib/repo.test.ts
200
+ import { fileURLToPath, URL } from "node:url";
201
+
202
+ import { CHECKS, checkRepo, HINTS, TITLES } from "@rdjperron/lint/repo";
203
+ import { describe, expect, it } from "vitest";
204
+
205
+ const faults = checkRepo({ root: fileURLToPath(new URL("..", import.meta.url)) });
206
+
207
+ describe("the tree", () => {
208
+ for (const check of CHECKS) {
209
+ it(TITLES[check], () => {
210
+ expect(faults[check], HINTS[check]).toEqual([]);
211
+ });
212
+ }
213
+ });
214
+ ```
215
+
216
+ Loop over `CHECKS` rather than listing the checks by hand. A check added in a later version then
217
+ runs in your repo the day you move the pin, and nobody has to remember to assert it.
218
+
219
+ `checkRepo` returns one array of faults per check, empty where the check holds. A fault is one
220
+ line that opens with a path relative to the root, with the line number when the fault has one:
221
+
222
+ ```
223
+ README.md:63 points at docs/not-there.md, which is not there
224
+ _research/notes.md is not named YYYYMMDD.HHMM.topic.md or YYYYMMDD.topic/
225
+ e2e/README.md, named by `linked`, is not there
226
+ ```
227
+
228
+ `TITLES` holds what each check promises, for the test's name. `HINTS` holds one sentence per check
229
+ saying how to fix it, which the example passes to `expect` as the failure message. `CHECKS` is the
230
+ three names: `scans`, `links`, `research`.
231
+
232
+ `checkRepo` does not throw on a tree or a config it cannot read. A linked document that is a
233
+ directory, a symbolic link that leads nowhere and a root that does not exist are each a fault in
234
+ `scans`, and a link whose percent-escapes do not decode is a fault in `links`.
235
+
236
+ ### What each check holds
237
+
238
+ | Check | What it holds |
239
+ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
240
+ | `links` | Every relative Markdown link in the living documents points at a path that exists. So does every link in a research folder's `README.md` that stays inside the research directory. A URL with a scheme, a `//host` and a bare `#anchor` are skipped, and an anchor after a path is dropped before the path is tested. |
241
+ | `research` | Each entry of `_research/` is a file `YYYYMMDD.HHMM.topic.md` or a folder `YYYYMMDD.topic/` that holds a `README.md`. The topic is lowercase letters, digits and hyphens. |
242
+ | `scans` | Every path the config names is there and is the right kind, a file or a directory. A default that is there is the right kind too. No symbolic link at one of those paths, or that a walk meets, leads nowhere. |
243
+
244
+ `scans` exists because a check that reads nothing passes. Move `e2e/README.md` without touching
245
+ the config and its links would stop being read while the test stays green. With `scans` the
246
+ missing document is a fault.
247
+
248
+ `scans` holds paths, not matches. A repo with no relative link yet is clean, so a repository can
249
+ turn the test on from its first commit, with its root alone for a config. A default that is absent
250
+ is not a fault: a repo may have no `BACKLOG.md`, no `docs` directory and no research yet. A path
251
+ the config names must be there, in `documents`, `docs` or `linked`. The research directory is the
252
+ one exception, and may be absent even when the config names it. Absent means nothing is at the
253
+ path: a symbolic link that leads nowhere is a fault, at a default path too.
254
+
255
+ The living documents are `README.md`, `AGENTS.md` and `BACKLOG.md` at the root, every `.md` file
256
+ under `docs/`, and the documents `linked` names. Every relative link in them is held, a link into
257
+ the research included.
258
+
259
+ The research is frozen once written, so the checks read little of it. The body of a research
260
+ entry is not checked for links, since it names paths as they stood on its date. Only a research
261
+ folder's `README.md` has its links held, and only the links that stay inside the research
262
+ directory. A link out of it, to `../../docs/product.md` say, pointed at a document of its day, and
263
+ the living tree has moved on since, so it is not held. This goes for the research directory
264
+ wherever it is, under `docs/` included, and for a document inside it that `linked` names.
265
+
266
+ ### The config
267
+
268
+ Every path is relative to the root, and `docs`, `./docs` and `docs/` name the same one.
269
+
270
+ | Field | Default | What it is |
271
+ | ----------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
272
+ | `root` | required | The repository root. A relative one is resolved against the working directory. |
273
+ | `skip` | `node_modules` `_generated` `dist` | Names the walk of `docs` does not enter, wherever they sit. Yours are added to the default list. |
274
+ | `documents` | `README.md` `AGENTS.md` `BACKLOG.md` | The living documents at the root. A default that is absent is passed over. Yours replace the default list, and each one you name must be there. |
275
+ | `docs` | `docs` | The directory whose every `.md` file is a living document. Left at its default, it may be absent. |
276
+ | `linked` | none | More living documents, such as `e2e/README.md`. Each must be there. |
277
+ | `research` | `_research` | The research directory. It may be absent. |
278
+
279
+ `skip` matches a name at any depth, so `skip: ["fixtures"]` leaves out every `fixtures` directory
280
+ under `docs`. It acts on what the walk finds under `docs`, and removes neither a document you name
281
+ in `documents` or `linked` nor an entry of the research directory. To stop holding a named
282
+ document, take it off its list.
283
+
284
+ `DEFAULT_DOCUMENTS` and `DEFAULT_SKIP` are exported, for a config that adds to a list the field
285
+ replaces.
286
+
287
+ ### What the checks do not do
288
+
289
+ They hold form, not truth. A link that resolves may point at the wrong document. A research entry
290
+ with a correct name may be empty.
291
+
292
+ They also leave these alone:
293
+
294
+ - An anchor is not checked against the headings of its target.
295
+ - A link written as bare text, as `<path>`, inside an HTML tag, with a title in single quotes or
296
+ with parentheses in its path is not read as a link, or is read wrong.
297
+ - A link that opens with `/` is tested against the filesystem root, not the repository's.
298
+ - A link out of the research is not tested at all, so one that was already dead on its day passes.
299
+ - The date in a research name is not checked to be a real date.
300
+ - Any file directly under the research directory that is not a dated note is a fault, a
301
+ `README.md` or a `.DS_Store` included.
302
+ - A linked directory is not entered, since it may lead back to one of its own parents.
303
+
304
+ Reading the documents against the code is still a review's job.
305
+
187
306
  ## The rules
188
307
 
189
308
  Each rule file opens with the reason the rule exists and the case where it is legitimate to
@@ -248,6 +367,44 @@ Land a new rule at `warn`, fix or scope what fires, then promote it. When a rule
248
367
  code or add a narrowly scoped override with the measurement that justifies it. Never weaken the
249
368
  rule, add a cast, or rename a symbol to dodge the check.
250
369
 
370
+ ## Moving from 0.6
371
+
372
+ 0.7.0 takes the decision register out of the repository checks. `checkRepo` no longer reads a
373
+ register, and no longer scans code for the `D<n>` numbers that cited its rows. A repo that keeps a
374
+ register stays on 0.6.0, which stays published.
375
+
376
+ | Gone | What it was for |
377
+ | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
378
+ | The `register` check | It held `docs/decisions.md` to one well-formed line per row, each number given once. |
379
+ | The `citations` check | It held every `D<n>` in the scanned files to a live row of the register. |
380
+ | `codeRoots` | The directories scanned for citations. It was required, so delete it from your config. |
381
+ | `extensions` and `DEFAULT_EXTENSIONS` | The extensions the citation scan read. |
382
+ | `register` | The path of the register. |
383
+ | `leftOut` | Paths left out of the citation scan, for files whose contents read like `D<n>` numbers. Under `docs`, `skip` does the same by name. |
384
+
385
+ `CHECKS` is now `scans`, `links`, `research`, and `TITLES`, `HINTS` and the returned faults carry
386
+ those three keys. A test that loops over `CHECKS` needs no other edit. A test that filtered the
387
+ faults by hand to live without a register deletes the filter.
388
+
389
+ Three things change in what stays:
390
+
391
+ - `scans` no longer asks for a register or for code roots, and a default that is absent is not a
392
+ fault: a `docs` directory the config does not name, and any of `README.md`, `AGENTS.md` and
393
+ `BACKLOG.md` when the config leaves `documents` alone. A path the config names must still be
394
+ there.
395
+ - A research folder's `README.md` is held only for the links that stay inside the research
396
+ directory. A link from it to a living document that has since moved or gone no longer fails.
397
+ The same goes for a document inside the research directory that `linked` names: its links out
398
+ of the research are no longer held.
399
+ - `skip` acts on the walk of `docs` alone, since no code root and no file at the root is walked
400
+ any more.
401
+
402
+ ## Moving from 0.5
403
+
404
+ Nothing is removed and nothing changes. 0.6.0 adds `@rdjperron/lint/repo`. A repo that carries the
405
+ checks as a test file of its own replaces that file with the example under "Repository checks",
406
+ and keeps only its config.
407
+
251
408
  ## Moving from 0.4
252
409
 
253
410
  Nothing is removed. One builder changed what it does, and three are new.
@@ -0,0 +1,60 @@
1
+ //#region src/repo.d.ts
2
+ /**
3
+ * The promises a repository's prose can only state, held as faults instead.
4
+ *
5
+ * - a relative Markdown link points at something that is there;
6
+ * - a research entry is named by its date, and a dated folder opens with a README;
7
+ * - every path the config names is there.
8
+ *
9
+ * Every path in a fault is relative to the root, so it reads as the path you would type to open
10
+ * the offender. Nothing here imports a test runner: the consumer's test asserts on what
11
+ * `checkRepo` returns. It never throws on a tree or a config it cannot read: that is a fault too.
12
+ */
13
+ export interface RepoConfig {
14
+ /**
15
+ * The repository root, absolute. Every other path in this config is relative to it, and
16
+ * `docs`, `./docs` and `docs/` name the same one.
17
+ */
18
+ root: string;
19
+ /**
20
+ * Directory and file names the walk of `docs` does not enter, wherever they sit. Added to
21
+ * `DEFAULT_SKIP`.
22
+ */
23
+ skip?: readonly string[];
24
+ /**
25
+ * The living documents at the root. Replaces `DEFAULT_DOCUMENTS`, any of which a repo may
26
+ * lack.
27
+ */
28
+ documents?: readonly string[];
29
+ /**
30
+ * The directory whose every Markdown file is a living document. `docs` by default, and a repo
31
+ * that names none may have none.
32
+ */
33
+ docs?: string;
34
+ /** More documents whose links are held as the living ones' are. */
35
+ linked?: readonly string[];
36
+ /** The research directory. `_research` by default, and a repo may have none yet. */
37
+ research?: string;
38
+ }
39
+ export declare const CHECKS: readonly ["scans", "links", "research"];
40
+ export type Check = (typeof CHECKS)[number];
41
+ /** The faults of each check, empty where the check holds. */
42
+ export type RepoFaults = { readonly [check in Check]: readonly string[]; };
43
+ /** What each check promises, for the title of the consumer's test. */
44
+ export declare const TITLES: {
45
+ scans: string;
46
+ links: string;
47
+ research: string;
48
+ };
49
+ /** What to do about a failing check, for the message of the consumer's assertion. */
50
+ export declare const HINTS: {
51
+ scans: string;
52
+ links: string;
53
+ research: string;
54
+ };
55
+ export declare const DEFAULT_DOCUMENTS: readonly string[];
56
+ /** Vendored, generated and built trees. */
57
+ export declare const DEFAULT_SKIP: readonly string[];
58
+ export declare function checkRepo(config: RepoConfig): RepoFaults;
59
+ //#endregion
60
+ //# sourceMappingURL=repo.d.mts.map
package/dist/repo.mjs ADDED
@@ -0,0 +1,221 @@
1
+ import { lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { dirname, join, posix, relative, resolve, sep } from "node:path";
3
+ //#region src/repo.ts
4
+ /**
5
+ * The promises a repository's prose can only state, held as faults instead.
6
+ *
7
+ * - a relative Markdown link points at something that is there;
8
+ * - a research entry is named by its date, and a dated folder opens with a README;
9
+ * - every path the config names is there.
10
+ *
11
+ * Every path in a fault is relative to the root, so it reads as the path you would type to open
12
+ * the offender. Nothing here imports a test runner: the consumer's test asserts on what
13
+ * `checkRepo` returns. It never throws on a tree or a config it cannot read: that is a fault too.
14
+ */
15
+ const CHECKS = [
16
+ "scans",
17
+ "links",
18
+ "research"
19
+ ];
20
+ /** What each check promises, for the title of the consumer's test. */
21
+ const TITLES = {
22
+ scans: "every path the config names is there, and every symbolic link leads somewhere",
23
+ links: "every relative Markdown link resolves",
24
+ research: "every research entry is named by its date, and every dated folder has a README"
25
+ };
26
+ /** What to do about a failing check, for the message of the consumer's assertion. */
27
+ const HINTS = {
28
+ scans: "A check that reads nothing passes on nothing. Correct the path in the config, or remove it with the thing it named.",
29
+ links: "Fix the path, or delete the link with the thing it pointed at.",
30
+ research: "The research sorts by name, so the name carries the date. Date an entry by the day it was opened, and give a folder a README that says what is in it."
31
+ };
32
+ const DEFAULT_DOCUMENTS = [
33
+ "README.md",
34
+ "AGENTS.md",
35
+ "BACKLOG.md"
36
+ ];
37
+ /** Vendored, generated and built trees. */
38
+ const DEFAULT_SKIP = [
39
+ "node_modules",
40
+ "_generated",
41
+ "dist"
42
+ ];
43
+ function kindOf(tree, path) {
44
+ try {
45
+ return statSync(join(tree.root, path)).isDirectory() ? "directory" : "file";
46
+ } catch {
47
+ return "absent";
48
+ }
49
+ }
50
+ function isSymbolicLink(tree, path) {
51
+ try {
52
+ return lstatSync(join(tree.root, path)).isSymbolicLink();
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+ function names(tree, path) {
58
+ try {
59
+ return readdirSync(join(tree.root, path)).toSorted();
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+ function child(base, name) {
65
+ return base === "" ? name : `${base}/${name}`;
66
+ }
67
+ function walk(tree, base) {
68
+ return names(tree, base).flatMap((name) => {
69
+ const path = child(base, name);
70
+ if (tree.skip.has(name)) return [];
71
+ const kind = kindOf(tree, path);
72
+ if (kind === "file") return [path];
73
+ if (kind === "absent") tree.broken.add(path);
74
+ return kind === "directory" && !isSymbolicLink(tree, path) ? walk(tree, path) : [];
75
+ });
76
+ }
77
+ /** `docs`, `./docs` and `docs/` are one path, and the root itself is the empty one. */
78
+ function normal(path) {
79
+ const clean = posix.normalize(path).replace(/\/+$/, "");
80
+ return clean === "." ? "" : clean;
81
+ }
82
+ function lineOf(source, index) {
83
+ return source.slice(0, index).split("\n").length;
84
+ }
85
+ /** A single note, `YYYYMMDD.HHMM.topic.md`. */
86
+ const DATED_NOTE = /^\d{8}\.\d{4}\.[a-z0-9-]+\.md$/;
87
+ /** Research of several files, `YYYYMMDD.topic/`. */
88
+ const DATED_FOLDER = /^\d{8}\.[a-z0-9-]+$/;
89
+ function researchFaults(tree, entries) {
90
+ return entries.flatMap((entry) => {
91
+ if (!(entry.folder ? DATED_FOLDER : DATED_NOTE).test(entry.name)) return [`${entry.path} is not named YYYYMMDD.HHMM.topic.md or YYYYMMDD.topic/`];
92
+ return entry.folder && kindOf(tree, `${entry.path}/README.md`) !== "file" ? [`${entry.path}/ has no README.md`] : [];
93
+ });
94
+ }
95
+ /** `](target)`, with an optional title. Bare `path.md` inside backticks is not a link. */
96
+ const LINK = /\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
97
+ /** A scheme, a protocol-relative host or a bare anchor: the target is outside the tree. */
98
+ const OUTSIDE = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
99
+ function decoded(target) {
100
+ try {
101
+ return decodeURI(target.split("#")[0] ?? "");
102
+ } catch {
103
+ return;
104
+ }
105
+ }
106
+ function linksIn(tree, file) {
107
+ const source = readFileSync(join(tree.root, file), "utf8");
108
+ return [...source.matchAll(LINK)].flatMap((match) => {
109
+ const target = match[1] ?? "";
110
+ const path = decoded(target);
111
+ if (OUTSIDE.test(target)) return [];
112
+ const points = path === void 0 ? void 0 : relative(tree.root, resolve(tree.root, dirname(file), path)).replaceAll(sep, "/");
113
+ return [{
114
+ from: file,
115
+ target,
116
+ line: lineOf(source, match.index),
117
+ points
118
+ }];
119
+ });
120
+ }
121
+ function linkFaults(tree, links) {
122
+ return links.flatMap((link) => {
123
+ const at = `${link.from}:${link.line} points at ${link.target}`;
124
+ if (link.points === void 0) return [`${at}, whose percent-escapes do not decode`];
125
+ return kindOf(tree, link.points) === "absent" ? [`${at}, which is not there`] : [];
126
+ });
127
+ }
128
+ function isUnder(path, base) {
129
+ return base === "" || path === base || path.startsWith(`${base}/`);
130
+ }
131
+ function allNamed(by, paths, want) {
132
+ return paths.map((path) => ({
133
+ path: normal(path),
134
+ by,
135
+ want
136
+ }));
137
+ }
138
+ function namedOr(option, path, fallback, want) {
139
+ return path === void 0 ? {
140
+ path: fallback,
141
+ by: `the default of \`${option}\``,
142
+ want
143
+ } : {
144
+ path: normal(path),
145
+ by: `named by \`${option}\``,
146
+ want
147
+ };
148
+ }
149
+ function namedFault(tree, named) {
150
+ const kind = kindOf(tree, named.path);
151
+ if (kind === named.want) return [];
152
+ return [`${named.path === "" ? "." : named.path}, ${named.by}, is ${kind === "absent" ? "not there" : `not a ${named.want}`}`];
153
+ }
154
+ function usable(tree, named) {
155
+ return named.flatMap((one) => kindOf(tree, one.path) === one.want ? [one.path] : []);
156
+ }
157
+ const NO_FAULTS = {
158
+ scans: [],
159
+ links: [],
160
+ research: []
161
+ };
162
+ function checkRepo(config) {
163
+ const tree = {
164
+ root: resolve(config.root),
165
+ skip: /* @__PURE__ */ new Set([...DEFAULT_SKIP, ...config.skip ?? []]),
166
+ broken: /* @__PURE__ */ new Set()
167
+ };
168
+ if (kindOf(tree, "") !== "directory") return {
169
+ ...NO_FAULTS,
170
+ scans: [`${tree.root}, named by \`root\`, is not a directory`]
171
+ };
172
+ const docs = namedOr("docs", config.docs, "docs", "directory");
173
+ const research = namedOr("research", config.research, "_research", "directory");
174
+ const documents = config.documents === void 0 ? allNamed("a default of `documents`", DEFAULT_DOCUMENTS, "file") : allNamed("named by `documents`", config.documents, "file");
175
+ const linked = allNamed("named by `linked`", config.linked ?? [], "file");
176
+ const entries = names(tree, research.path).flatMap((name) => {
177
+ const path = child(research.path, name);
178
+ const kind = kindOf(tree, path);
179
+ if (kind === "absent") tree.broken.add(path);
180
+ return kind === "absent" ? [] : [{
181
+ path,
182
+ name,
183
+ folder: kind === "directory"
184
+ }];
185
+ });
186
+ const frozen = (file) => isUnder(file, research.path);
187
+ const held = (link) => !frozen(link.from) || link.points === void 0 || frozen(link.points);
188
+ const living = [
189
+ ...usable(tree, documents),
190
+ ...walk(tree, docs.path).filter((file) => file.endsWith(".md") && !frozen(file)),
191
+ ...usable(tree, linked)
192
+ ];
193
+ const linking = /* @__PURE__ */ new Set([...living, ...entries.flatMap((entry) => {
194
+ const readme = `${entry.path}/README.md`;
195
+ return entry.folder && kindOf(tree, readme) === "file" ? [readme] : [];
196
+ })]);
197
+ const mayBeAbsent = /* @__PURE__ */ new Set([
198
+ ...config.documents === void 0 ? documents : [],
199
+ ...config.docs === void 0 ? [docs] : [],
200
+ research
201
+ ]);
202
+ return {
203
+ scans: [...[
204
+ ...documents,
205
+ docs,
206
+ ...linked,
207
+ research
208
+ ].flatMap((one) => {
209
+ if (kindOf(tree, one.path) !== "absent") return [one];
210
+ if (!isSymbolicLink(tree, one.path)) return mayBeAbsent.has(one) ? [] : [one];
211
+ tree.broken.add(one.path);
212
+ return [];
213
+ }).flatMap((one) => namedFault(tree, one)), ...[...tree.broken].toSorted().map((path) => `${path} is a symbolic link that leads nowhere`)],
214
+ links: linkFaults(tree, [...linking].flatMap((file) => linksIn(tree, file)).filter(held)),
215
+ research: researchFaults(tree, entries)
216
+ };
217
+ }
218
+ //#endregion
219
+ export { CHECKS, DEFAULT_DOCUMENTS, DEFAULT_SKIP, HINTS, TITLES, checkRepo };
220
+
221
+ //# sourceMappingURL=repo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo.mjs","names":[],"sources":["../src/repo.ts"],"sourcesContent":["/**\n * The promises a repository's prose can only state, held as faults instead.\n *\n * - a relative Markdown link points at something that is there;\n * - a research entry is named by its date, and a dated folder opens with a README;\n * - every path the config names is there.\n *\n * Every path in a fault is relative to the root, so it reads as the path you would type to open\n * the offender. Nothing here imports a test runner: the consumer's test asserts on what\n * `checkRepo` returns. It never throws on a tree or a config it cannot read: that is a fault too.\n */\n\nimport { lstatSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { dirname, join, posix, relative, resolve, sep } from \"node:path\";\n\nexport interface RepoConfig {\n /**\n * The repository root, absolute. Every other path in this config is relative to it, and\n * `docs`, `./docs` and `docs/` name the same one.\n */\n root: string;\n /**\n * Directory and file names the walk of `docs` does not enter, wherever they sit. Added to\n * `DEFAULT_SKIP`.\n */\n skip?: readonly string[];\n /**\n * The living documents at the root. Replaces `DEFAULT_DOCUMENTS`, any of which a repo may\n * lack.\n */\n documents?: readonly string[];\n /**\n * The directory whose every Markdown file is a living document. `docs` by default, and a repo\n * that names none may have none.\n */\n docs?: string;\n /** More documents whose links are held as the living ones' are. */\n linked?: readonly string[];\n /** The research directory. `_research` by default, and a repo may have none yet. */\n research?: string;\n}\n\nexport const CHECKS = [\"scans\", \"links\", \"research\"] as const;\n\nexport type Check = (typeof CHECKS)[number];\n\n/** The faults of each check, empty where the check holds. */\nexport type RepoFaults = { readonly [check in Check]: readonly string[] };\n\n/** What each check promises, for the title of the consumer's test. */\nexport const TITLES = {\n scans: \"every path the config names is there, and every symbolic link leads somewhere\",\n links: \"every relative Markdown link resolves\",\n research: \"every research entry is named by its date, and every dated folder has a README\",\n} satisfies { readonly [check in Check]: string };\n\n/** What to do about a failing check, for the message of the consumer's assertion. */\nexport const HINTS = {\n scans:\n \"A check that reads nothing passes on nothing. Correct the path in the config, or remove it \" +\n \"with the thing it named.\",\n links: \"Fix the path, or delete the link with the thing it pointed at.\",\n research:\n \"The research sorts by name, so the name carries the date. Date an entry by the day it was \" +\n \"opened, and give a folder a README that says what is in it.\",\n} satisfies { readonly [check in Check]: string };\n\nexport const DEFAULT_DOCUMENTS: readonly string[] = [\"README.md\", \"AGENTS.md\", \"BACKLOG.md\"];\n\n/** Vendored, generated and built trees. */\nexport const DEFAULT_SKIP: readonly string[] = [\"node_modules\", \"_generated\", \"dist\"];\n\ninterface Tree {\n root: string;\n skip: ReadonlySet<string>;\n /** Symbolic links a walk met that lead nowhere. */\n broken: Set<string>;\n}\n\ntype Kind = \"file\" | \"directory\" | \"absent\";\n\nfunction kindOf(tree: Tree, path: string): Kind {\n try {\n return statSync(join(tree.root, path)).isDirectory() ? \"directory\" : \"file\";\n } catch {\n return \"absent\";\n }\n}\n\nfunction isSymbolicLink(tree: Tree, path: string): boolean {\n try {\n return lstatSync(join(tree.root, path)).isSymbolicLink();\n } catch {\n return false;\n }\n}\n\nfunction names(tree: Tree, path: string): string[] {\n try {\n return readdirSync(join(tree.root, path)).toSorted();\n } catch {\n return [];\n }\n}\n\nfunction child(base: string, name: string): string {\n return base === \"\" ? name : `${base}/${name}`;\n}\n\n// A linked directory is not entered: it may lead back to one of its own parents.\nfunction walk(tree: Tree, base: string): string[] {\n return names(tree, base).flatMap((name) => {\n const path = child(base, name);\n if (tree.skip.has(name)) return [];\n const kind = kindOf(tree, path);\n if (kind === \"file\") return [path];\n if (kind === \"absent\") tree.broken.add(path);\n return kind === \"directory\" && !isSymbolicLink(tree, path) ? walk(tree, path) : [];\n });\n}\n\n/** `docs`, `./docs` and `docs/` are one path, and the root itself is the empty one. */\nfunction normal(path: string): string {\n const clean = posix.normalize(path).replace(/\\/+$/, \"\");\n return clean === \".\" ? \"\" : clean;\n}\n\nfunction lineOf(source: string, index: number): number {\n return source.slice(0, index).split(\"\\n\").length;\n}\n\n/** A single note, `YYYYMMDD.HHMM.topic.md`. */\nconst DATED_NOTE = /^\\d{8}\\.\\d{4}\\.[a-z0-9-]+\\.md$/;\n\n/** Research of several files, `YYYYMMDD.topic/`. */\nconst DATED_FOLDER = /^\\d{8}\\.[a-z0-9-]+$/;\n\ninterface Entry {\n path: string;\n name: string;\n folder: boolean;\n}\n\nfunction researchFaults(tree: Tree, entries: readonly Entry[]): string[] {\n return entries.flatMap((entry) => {\n if (!(entry.folder ? DATED_FOLDER : DATED_NOTE).test(entry.name)) {\n return [`${entry.path} is not named YYYYMMDD.HHMM.topic.md or YYYYMMDD.topic/`];\n }\n return entry.folder && kindOf(tree, `${entry.path}/README.md`) !== \"file\"\n ? [`${entry.path}/ has no README.md`]\n : [];\n });\n}\n\n/** `](target)`, with an optional title. Bare `path.md` inside backticks is not a link. */\nconst LINK = /\\]\\(([^)\\s]+)(?:\\s+\"[^\"]*\")?\\)/g;\n\n/** A scheme, a protocol-relative host or a bare anchor: the target is outside the tree. */\nconst OUTSIDE = /^(?:[a-z][a-z0-9+.-]*:|\\/\\/|#)/i;\n\ninterface Link {\n from: string;\n target: string;\n line: number;\n /** Where the target leads, relative to the root. Absent when its escapes do not decode. */\n points: string | undefined;\n}\n\nfunction decoded(target: string): string | undefined {\n try {\n return decodeURI(target.split(\"#\")[0] ?? \"\");\n } catch {\n return undefined;\n }\n}\n\nfunction linksIn(tree: Tree, file: string): Link[] {\n const source = readFileSync(join(tree.root, file), \"utf8\");\n return [...source.matchAll(LINK)].flatMap((match) => {\n const target = match[1] ?? \"\";\n const path = decoded(target);\n if (OUTSIDE.test(target)) return [];\n // `relative` answers with `\\` on Windows, and every path here has `/` between its names.\n const points =\n path === undefined\n ? undefined\n : relative(tree.root, resolve(tree.root, dirname(file), path)).replaceAll(sep, \"/\");\n return [{ from: file, target, line: lineOf(source, match.index), points }];\n });\n}\n\nfunction linkFaults(tree: Tree, links: readonly Link[]): string[] {\n return links.flatMap((link) => {\n const at = `${link.from}:${link.line} points at ${link.target}`;\n if (link.points === undefined) return [`${at}, whose percent-escapes do not decode`];\n return kindOf(tree, link.points) === \"absent\" ? [`${at}, which is not there`] : [];\n });\n}\n\nfunction isUnder(path: string, base: string): boolean {\n return base === \"\" || path === base || path.startsWith(`${base}/`);\n}\n\ninterface Named {\n path: string;\n /** How the path came to be named, for the fault. */\n by: string;\n want: Kind;\n}\n\nfunction allNamed(by: string, paths: readonly string[], want: Kind): Named[] {\n return paths.map((path) => ({ path: normal(path), by, want }));\n}\n\nfunction namedOr(option: string, path: string | undefined, fallback: string, want: Kind): Named {\n return path === undefined\n ? { path: fallback, by: `the default of \\`${option}\\``, want }\n : { path: normal(path), by: `named by \\`${option}\\``, want };\n}\n\nfunction namedFault(tree: Tree, named: Named): string[] {\n const kind = kindOf(tree, named.path);\n if (kind === named.want) return [];\n const shown = named.path === \"\" ? \".\" : named.path;\n return [`${shown}, ${named.by}, is ${kind === \"absent\" ? \"not there\" : `not a ${named.want}`}`];\n}\n\nfunction usable(tree: Tree, named: readonly Named[]): string[] {\n return named.flatMap((one) => (kindOf(tree, one.path) === one.want ? [one.path] : []));\n}\n\nconst NO_FAULTS: RepoFaults = { scans: [], links: [], research: [] };\n\nexport function checkRepo(config: RepoConfig): RepoFaults {\n const tree: Tree = {\n root: resolve(config.root),\n skip: new Set([...DEFAULT_SKIP, ...(config.skip ?? [])]),\n broken: new Set(),\n };\n if (kindOf(tree, \"\") !== \"directory\") {\n return { ...NO_FAULTS, scans: [`${tree.root}, named by \\`root\\`, is not a directory`] };\n }\n\n const docs = namedOr(\"docs\", config.docs, \"docs\", \"directory\");\n const research = namedOr(\"research\", config.research, \"_research\", \"directory\");\n const documents =\n config.documents === undefined\n ? allNamed(\"a default of `documents`\", DEFAULT_DOCUMENTS, \"file\")\n : allNamed(\"named by `documents`\", config.documents, \"file\");\n const linked = allNamed(\"named by `linked`\", config.linked ?? [], \"file\");\n\n const entries = names(tree, research.path).flatMap((name) => {\n const path = child(research.path, name);\n const kind = kindOf(tree, path);\n if (kind === \"absent\") tree.broken.add(path);\n return kind === \"absent\" ? [] : [{ path, name, folder: kind === \"directory\" }];\n });\n\n // Inside the research only a folder's README is held, and only for the links that stay inside\n // it: an entry is frozen once it is on the main branch, and the living tree moves on.\n const frozen = (file: string): boolean => isUnder(file, research.path);\n const held = (link: Link): boolean =>\n !frozen(link.from) || link.points === undefined || frozen(link.points);\n\n const living = [\n ...usable(tree, documents),\n ...walk(tree, docs.path).filter((file) => file.endsWith(\".md\") && !frozen(file)),\n ...usable(tree, linked),\n ];\n const linking = new Set([\n ...living,\n ...entries.flatMap((entry) => {\n const readme = `${entry.path}/README.md`;\n return entry.folder && kindOf(tree, readme) === \"file\" ? [readme] : [];\n }),\n ]);\n\n // A default names a place a repo may not have, and the research one it may not have yet. A\n // path the config names has to be there.\n const mayBeAbsent = new Set([\n ...(config.documents === undefined ? documents : []),\n ...(config.docs === undefined ? [docs] : []),\n research,\n ]);\n // Absent means nothing is at the path, and a link that leads nowhere is something.\n const expected = [...documents, docs, ...linked, research].flatMap((one) => {\n if (kindOf(tree, one.path) !== \"absent\") return [one];\n if (!isSymbolicLink(tree, one.path)) return mayBeAbsent.has(one) ? [] : [one];\n tree.broken.add(one.path);\n return [];\n });\n\n return {\n scans: [\n ...expected.flatMap((one) => namedFault(tree, one)),\n ...[...tree.broken].toSorted().map((path) => `${path} is a symbolic link that leads nowhere`),\n ],\n links: linkFaults(tree, [...linking].flatMap((file) => linksIn(tree, file)).filter(held)),\n research: researchFaults(tree, entries),\n };\n}\n"],"mappings":"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"}
package/dist/verify.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { s as plugin, t as RULE_IDS } from "./config-CD2iCM6L.mjs";
3
3
  import { createRequire } from "node:module";
4
- import { spawnSync } from "node:child_process";
5
4
  import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
6
- import { tmpdir } from "node:os";
7
5
  import { join, resolve } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { tmpdir } from "node:os";
8
8
  import { fileURLToPath } from "node:url";
9
9
  //#region src/verify.ts
10
10
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rdjperron/lint",
3
- "version": "0.5.0",
4
- "description": "One Oxlint JS plugin, `rdjperron`: 20 rules against low-evidence code, with the config data, the fixture that proves each rule fires, and the command that checks it.",
3
+ "version": "0.7.0",
4
+ "description": "One Oxlint JS plugin, `rdjperron`: 20 rules against low-evidence code, with the config data, the fixture that proves each rule fires, the command that checks it, and the repository checks that hold a repo's Markdown links and research names.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -26,8 +26,22 @@
26
26
  "types": "./dist/config.d.mts",
27
27
  "default": "./dist/config.mjs"
28
28
  },
29
+ "./repo": {
30
+ "types": "./dist/repo.d.mts",
31
+ "default": "./dist/repo.mjs"
32
+ },
29
33
  "./package.json": "./package.json"
30
34
  },
35
+ "scripts": {
36
+ "build": "vp pack",
37
+ "test": "vp test",
38
+ "check": "vp check",
39
+ "typecheck": "tsc --noEmit",
40
+ "verify:lint-fixtures": "node src/verify.ts --plugin ./src/index.ts",
41
+ "ready": "vp check && vp test && vp run typecheck && vp run build && vp run verify:lint-fixtures && vp pm pack -- --dry-run",
42
+ "prepare": "vp config",
43
+ "prepublishOnly": "vp run ready"
44
+ },
31
45
  "dependencies": {
32
46
  "@oxlint/plugins": "^1.79.0"
33
47
  },
@@ -48,12 +62,5 @@
48
62
  "engines": {
49
63
  "node": ">=24"
50
64
  },
51
- "scripts": {
52
- "build": "vp pack",
53
- "test": "vp test",
54
- "check": "vp check",
55
- "typecheck": "tsc --noEmit",
56
- "verify:lint-fixtures": "node src/verify.ts --plugin ./src/index.ts",
57
- "ready": "vp check && vp test && vp run typecheck && vp run build && vp run verify:lint-fixtures && vp pm pack -- --dry-run"
58
- }
59
- }
65
+ "packageManager": "pnpm@12.4.2"
66
+ }