@rdjperron/lint 0.6.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
@@ -8,7 +8,7 @@ the repository checks that hold a repo's documents to their own conventions.
8
8
  | ------------------------ | ---------------------------------------------------------------------------------- |
9
9
  | `@rdjperron/lint` | The plugin. One `jsPlugins` entry, one prefix, twenty rules. |
10
10
  | `@rdjperron/lint/config` | `recommended`, the override builders, and the `fmt`, `ignore` and `staged` blocks. |
11
- | `@rdjperron/lint/repo` | `checkRepo`, five checks on a repo's links, decision register and research names. |
11
+ | `@rdjperron/lint/repo` | `checkRepo`, three checks on a repo's Markdown links and research names. |
12
12
  | `rdjperron-lint-verify` | The command that proves every rule still fires. No config, no arguments. |
13
13
 
14
14
  Seventeen of the rules are a copy of [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop),
@@ -188,25 +188,21 @@ dead.
188
188
 
189
189
  ## Repository checks
190
190
 
191
- `@rdjperron/lint/repo` is for a repo that keeps a decision register in `docs/decisions.md` and
192
- dated research in `_research/`. Prose can state those conventions and cannot enforce them. `checkRepo`
193
- reads the tree and returns what breaks them, so a test fails on a dead link or on a citation of a
194
- retired decision.
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
195
 
196
196
  It is plain Node and imports no test runner. You call it from a test of your own:
197
197
 
198
198
  ```ts
199
199
  // lib/repo.test.ts
200
- import { fileURLToPath } from "node:url";
200
+ import { fileURLToPath, URL } from "node:url";
201
201
 
202
202
  import { CHECKS, checkRepo, HINTS, TITLES } from "@rdjperron/lint/repo";
203
203
  import { describe, expect, it } from "vitest";
204
204
 
205
- const faults = checkRepo({
206
- root: fileURLToPath(new URL("..", import.meta.url)),
207
- codeRoots: ["app", "components", "lib", "convex", "e2e"],
208
- linked: ["e2e/README.md"],
209
- });
205
+ const faults = checkRepo({ root: fileURLToPath(new URL("..", import.meta.url)) });
210
206
 
211
207
  describe("the tree", () => {
212
208
  for (const check of CHECKS) {
@@ -225,92 +221,73 @@ line that opens with a path relative to the root, with the line number when the
225
221
 
226
222
  ```
227
223
  README.md:63 points at docs/not-there.md, which is not there
228
- src/main.tsx:43 cites D16, which is retired
229
224
  _research/notes.md is not named YYYYMMDD.HHMM.topic.md or YYYYMMDD.topic/
230
- src, named by `codeRoots`, is not there
225
+ e2e/README.md, named by `linked`, is not there
231
226
  ```
232
227
 
233
228
  `TITLES` holds what each check promises, for the test's name. `HINTS` holds one sentence per check
234
229
  saying how to fix it, which the example passes to `expect` as the failure message. `CHECKS` is the
235
- five names: `scans`, `links`, `register`, `citations`, `research`.
230
+ three names: `scans`, `links`, `research`.
236
231
 
237
- `checkRepo` does not throw on a tree or a config it cannot read. A code root that is a file, a
238
- register that is a directory, a symbolic link that leads nowhere and a root that does not exist
239
- are each a fault in `scans`, and a link whose percent-escapes do not decode is a fault in `links`.
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`.
240
235
 
241
236
  ### What each check holds
242
237
 
243
- | Check | What it holds |
244
- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
245
- | `links` | Every relative Markdown link in the living documents and in each research folder's `README.md` points at a path that exists. 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. |
246
- | `register` | Every row is one line, `- **Dn** Sentence. (YYYY-MM-DD)` or `- **Dn** Sentence. (YYYY-MM-DD, source)`. Each number is given once and is not also in the retired list. No number is missing up to the highest ever given. No other line names a number in bold. A `## Retired` section exists, and it may name no number yet. |
247
- | `citations` | Every `D<n>` in the scanned files, and in the register's own rows after their number, is a live row. A retired number fails as retired and a number never given fails as unknown. |
248
- | `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. |
249
- | `scans` | Every path the config names is there and is the right kind, a file or a directory, defaults included. Every code root yields a file to scan. The register has a row. No symbolic link a walk meets leads nowhere. |
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. |
250
243
 
251
- `scans` exists because a check that reads nothing passes. Rename `lib/` to `src/` without touching
252
- the config and the citation scan would cover one directory fewer and stay green. With `scans` the
253
- missing root is a fault.
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.
254
247
 
255
- `scans` holds paths and files, not matches. A repo with no research directory, an empty one, no
256
- relative link or no citation yet is clean, so a repository can turn the test on from its first
257
- commit. The one path that may be absent is the research directory.
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.
258
254
 
259
255
  The living documents are `README.md`, `AGENTS.md` and `BACKLOG.md` at the root, every `.md` file
260
- under `docs/`, and the documents `linked` names. Their links are held and they are scanned for
261
- citations, the register excepted. The citation scan also reads every file under a code root with a
262
- scanned extension, and every file at the root with one. A lockfile is never scanned, meaning a
263
- name that ends in `-lock.json`, `-lock.yaml` or the like.
256
+ under `docs/`, and the documents `linked` names. Every relative link in them is held, a link into
257
+ the research included.
264
258
 
265
259
  The research is frozen once written, so the checks read little of it. The body of a research
266
- entry is checked for neither links nor citations, since it names paths and numbers as they stood on
267
- its date. Only a research folder's `README.md` has its links held, and nothing under the research
268
- directory is scanned for citations, even if you name it as a code root or keep it under `docs/`.
269
- The retired numbers are every `D<n>` in the `## Retired` section, up to the next `##` heading. The
270
- prose around the register's rows may name any number.
271
-
272
- When the register cannot be read, `scans` says so once, and `register` and `citations` stay empty
273
- rather than failing every citation in the repo.
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.
274
265
 
275
266
  ### The config
276
267
 
277
- Every path is relative to the root, and `lib`, `./lib` and `lib/` name the same one.
278
-
279
- | Field | Default | What it is |
280
- | ------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
281
- | `root` | required | The repository root. A relative one is resolved against the working directory. |
282
- | `codeRoots` | required | The directories scanned for citations. `["."]` scans the whole root. |
283
- | `extensions` | `ts` `tsx` `mts` `cts` `js` `jsx` `mjs` `cjs` `md` `json` `jsonc` `yml` `yaml` `html` | The extensions scanned for citations. Yours replace the default list. |
284
- | `skip` | `node_modules` `_generated` `dist` | Names no walk enters, wherever they sit. Yours are added to the default list. |
285
- | `leftOut` | none | Paths no walk enters, as `{ path: reason }`. The reason is for the reader. |
286
- | `documents` | `README.md` `AGENTS.md` `BACKLOG.md` | The living documents at the root. Yours replace the default list, so a repo with no backlog names the two it has. |
287
- | `docs` | `docs` | The directory whose every `.md` file is a living document. |
288
- | `linked` | none | More living documents, such as `e2e/README.md`. |
289
- | `register` | `decisions.md` in the `docs` directory | The decision register. It follows `docs`: with `docs: "handbook"` it is `handbook/decisions.md`. |
290
- | `research` | `_research` | The research directory. It may be absent. |
291
-
292
- `skip` matches a name at any depth, so `skip: ["fixtures"]` leaves out every `fixtures` directory.
293
- `leftOut` matches one path, for a directory whose name is legitimate elsewhere:
268
+ Every path is relative to the root, and `docs`, `./docs` and `docs/` name the same one.
294
269
 
295
- ```ts
296
- leftOut: { "site/.well-known": "certificate fingerprints read like decision numbers" },
297
- ```
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. |
298
278
 
299
- Both act on what a walk finds under `docs`, under a code root and at the root. Neither removes a
300
- document you name in `documents` or `linked`. To stop holding one of those, take it off its list.
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.
301
283
 
302
- A citation is any `D` followed by digits and standing as a word of its own, so recorded data full
303
- of ids such as `D12` is the usual reason to leave a path out. Leave it out with `skip` or
304
- `leftOut`, or keep its extension off the list, as with `csv`.
305
-
306
- `DEFAULT_DOCUMENTS`, `DEFAULT_EXTENSIONS` and `DEFAULT_SKIP` are exported, for a config that adds
307
- to a list the field replaces.
284
+ `DEFAULT_DOCUMENTS` and `DEFAULT_SKIP` are exported, for a config that adds to a list the field
285
+ replaces.
308
286
 
309
287
  ### What the checks do not do
310
288
 
311
- They hold form, not truth. A link that resolves may point at the wrong document. A row may be well
312
- formed and state a rule the code broke last week. A citation of a live number may cite the wrong
313
- rule. A research entry with a correct name may be empty.
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.
314
291
 
315
292
  They also leave these alone:
316
293
 
@@ -318,14 +295,13 @@ They also leave these alone:
318
295
  - A link written as bare text, as `<path>`, inside an HTML tag, with a title in single quotes or
319
296
  with parentheses in its path is not read as a link, or is read wrong.
320
297
  - A link that opens with `/` is tested against the filesystem root, not the repository's.
321
- - The date in a row or in a research name is not checked to be a real date.
322
- - `**D11.**` and `**D12a**` are neither a row nor row-like, so the register check does not see them.
323
- - `#D12` and `-D16` count as citations, since the `D` still opens a word.
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.
324
300
  - Any file directly under the research directory that is not a dated note is a fault, a
325
301
  `README.md` or a `.DS_Store` included.
326
302
  - A linked directory is not entered, since it may lead back to one of its own parents.
327
303
 
328
- Reading the rows against the code is still a review's job.
304
+ Reading the documents against the code is still a review's job.
329
305
 
330
306
  ## The rules
331
307
 
@@ -391,6 +367,38 @@ Land a new rule at `warn`, fix or scope what fires, then promote it. When a rule
391
367
  code or add a narrowly scoped override with the measurement that justifies it. Never weaken the
392
368
  rule, add a cast, or rename a symbol to dodge the check.
393
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
+
394
402
  ## Moving from 0.5
395
403
 
396
404
  Nothing is removed and nothing changes. 0.6.0 adds `@rdjperron/lint/repo`. A repo that carries the
package/dist/repo.d.mts CHANGED
@@ -3,46 +3,40 @@
3
3
  * The promises a repository's prose can only state, held as faults instead.
4
4
  *
5
5
  * - a relative Markdown link points at something that is there;
6
- * - the decision register is one well-formed line per row, each number given once;
7
- * - a decision number cited anywhere is a live row in the register, never a retired one;
8
6
  * - a research entry is named by its date, and a dated folder opens with a README;
9
- * - every path the config names is there and holds something to check.
7
+ * - every path the config names is there.
10
8
  *
11
9
  * Every path in a fault is relative to the root, so it reads as the path you would type to open
12
10
  * the offender. Nothing here imports a test runner: the consumer's test asserts on what
13
11
  * `checkRepo` returns. It never throws on a tree or a config it cannot read: that is a fault too.
14
12
  */
15
13
  export interface RepoConfig {
16
- /** The repository root, absolute. */
17
- root: string;
18
14
  /**
19
- * The directories scanned for decision numbers. Every path in this config is relative to the
20
- * root, and `lib`, `./lib` and `lib/` name the same one.
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.
21
17
  */
22
- codeRoots: readonly string[];
18
+ root: string;
23
19
  /**
24
- * The extensions scanned for decision numbers, in the code roots and at the root itself.
25
- * Replaces `DEFAULT_EXTENSIONS`.
20
+ * Directory and file names the walk of `docs` does not enter, wherever they sit. Added to
21
+ * `DEFAULT_SKIP`.
26
22
  */
27
- extensions?: readonly string[];
28
- /** Directory and file names no walk enters, wherever they sit. Added to `DEFAULT_SKIP`. */
29
23
  skip?: readonly string[];
30
- /** Paths no walk enters, each with the reason it is left out. */
31
- leftOut?: {
32
- readonly [path: string]: string;
33
- };
34
- /** The living documents at the root. Replaces `DEFAULT_DOCUMENTS`. */
24
+ /**
25
+ * The living documents at the root. Replaces `DEFAULT_DOCUMENTS`, any of which a repo may
26
+ * lack.
27
+ */
35
28
  documents?: readonly string[];
36
- /** The directory whose every Markdown file is a living document. `docs` by default. */
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
+ */
37
33
  docs?: string;
38
- /** More documents held as the living ones are, for their links and their citations. */
34
+ /** More documents whose links are held as the living ones' are. */
39
35
  linked?: readonly string[];
40
- /** The decision register. `decisions.md` in the `docs` directory by default. */
41
- register?: string;
42
36
  /** The research directory. `_research` by default, and a repo may have none yet. */
43
37
  research?: string;
44
38
  }
45
- export declare const CHECKS: readonly ["scans", "links", "register", "citations", "research"];
39
+ export declare const CHECKS: readonly ["scans", "links", "research"];
46
40
  export type Check = (typeof CHECKS)[number];
47
41
  /** The faults of each check, empty where the check holds. */
48
42
  export type RepoFaults = { readonly [check in Check]: readonly string[]; };
@@ -50,20 +44,15 @@ export type RepoFaults = { readonly [check in Check]: readonly string[]; };
50
44
  export declare const TITLES: {
51
45
  scans: string;
52
46
  links: string;
53
- register: string;
54
- citations: string;
55
47
  research: string;
56
48
  };
57
49
  /** What to do about a failing check, for the message of the consumer's assertion. */
58
50
  export declare const HINTS: {
59
51
  scans: string;
60
52
  links: string;
61
- register: string;
62
- citations: string;
63
53
  research: string;
64
54
  };
65
55
  export declare const DEFAULT_DOCUMENTS: readonly string[];
66
- export declare const DEFAULT_EXTENSIONS: readonly string[];
67
56
  /** Vendored, generated and built trees. */
68
57
  export declare const DEFAULT_SKIP: readonly string[];
69
58
  export declare function checkRepo(config: RepoConfig): RepoFaults;
package/dist/repo.mjs CHANGED
@@ -1,14 +1,12 @@
1
1
  import { lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
2
- import { dirname, join, posix, relative, resolve } from "node:path";
2
+ import { dirname, join, posix, relative, resolve, sep } from "node:path";
3
3
  //#region src/repo.ts
4
4
  /**
5
5
  * The promises a repository's prose can only state, held as faults instead.
6
6
  *
7
7
  * - a relative Markdown link points at something that is there;
8
- * - the decision register is one well-formed line per row, each number given once;
9
- * - a decision number cited anywhere is a live row in the register, never a retired one;
10
8
  * - a research entry is named by its date, and a dated folder opens with a README;
11
- * - every path the config names is there and holds something to check.
9
+ * - every path the config names is there.
12
10
  *
13
11
  * Every path in a fault is relative to the root, so it reads as the path you would type to open
14
12
  * the offender. Nothing here imports a test runner: the consumer's test asserts on what
@@ -17,24 +15,18 @@ import { dirname, join, posix, relative, resolve } from "node:path";
17
15
  const CHECKS = [
18
16
  "scans",
19
17
  "links",
20
- "register",
21
- "citations",
22
18
  "research"
23
19
  ];
24
20
  /** What each check promises, for the title of the consumer's test. */
25
21
  const TITLES = {
26
- scans: "every path the config names is there and holds something to check",
22
+ scans: "every path the config names is there, and every symbolic link leads somewhere",
27
23
  links: "every relative Markdown link resolves",
28
- register: "the register is one well-formed line per row, each number given once",
29
- citations: "every decision number cited anywhere is a live row in the register",
30
24
  research: "every research entry is named by its date, and every dated folder has a README"
31
25
  };
32
26
  /** What to do about a failing check, for the message of the consumer's assertion. */
33
27
  const HINTS = {
34
28
  scans: "A check that reads nothing passes on nothing. Correct the path in the config, or remove it with the thing it named.",
35
29
  links: "Fix the path, or delete the link with the thing it pointed at.",
36
- register: "A row is one line, its number given once and never after it was retired, and a number that leaves the rows joins the retired list. Rewrite a rule in place under its own number.",
37
- citations: "A citation of a retired number, or of one never given, sends the reader nowhere. Cite the row that carries the rule now. `git log -S \"D<n>\"` on the register shows what a retired row said.",
38
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."
39
31
  };
40
32
  const DEFAULT_DOCUMENTS = [
@@ -42,22 +34,6 @@ const DEFAULT_DOCUMENTS = [
42
34
  "AGENTS.md",
43
35
  "BACKLOG.md"
44
36
  ];
45
- const DEFAULT_EXTENSIONS = [
46
- "ts",
47
- "tsx",
48
- "mts",
49
- "cts",
50
- "js",
51
- "jsx",
52
- "mjs",
53
- "cjs",
54
- "md",
55
- "json",
56
- "jsonc",
57
- "yml",
58
- "yaml",
59
- "html"
60
- ];
61
37
  /** Vendored, generated and built trees. */
62
38
  const DEFAULT_SKIP = [
63
39
  "node_modules",
@@ -78,9 +54,6 @@ function isSymbolicLink(tree, path) {
78
54
  return false;
79
55
  }
80
56
  }
81
- function read(tree, path) {
82
- return readFileSync(join(tree.root, path), "utf8").replaceAll("\r\n", "\n");
83
- }
84
57
  function names(tree, path) {
85
58
  try {
86
59
  return readdirSync(join(tree.root, path)).toSorted();
@@ -94,21 +67,18 @@ function child(base, name) {
94
67
  function walk(tree, base) {
95
68
  return names(tree, base).flatMap((name) => {
96
69
  const path = child(base, name);
97
- if (tree.skip.has(name) || tree.leftOut.has(path)) return [];
70
+ if (tree.skip.has(name)) return [];
98
71
  const kind = kindOf(tree, path);
99
72
  if (kind === "file") return [path];
100
73
  if (kind === "absent") tree.broken.add(path);
101
74
  return kind === "directory" && !isSymbolicLink(tree, path) ? walk(tree, path) : [];
102
75
  });
103
76
  }
104
- /** `lib`, `./lib` and `lib/` are one path, and the root itself is the empty one. */
77
+ /** `docs`, `./docs` and `docs/` are one path, and the root itself is the empty one. */
105
78
  function normal(path) {
106
79
  const clean = posix.normalize(path).replace(/\/+$/, "");
107
80
  return clean === "." ? "" : clean;
108
81
  }
109
- function hasExtension(name, extensions) {
110
- return extensions.some((extension) => name.endsWith(`.${extension}`));
111
- }
112
82
  function lineOf(source, index) {
113
83
  return source.slice(0, index).split("\n").length;
114
84
  }
@@ -134,12 +104,12 @@ function decoded(target) {
134
104
  }
135
105
  }
136
106
  function linksIn(tree, file) {
137
- const source = read(tree, file);
107
+ const source = readFileSync(join(tree.root, file), "utf8");
138
108
  return [...source.matchAll(LINK)].flatMap((match) => {
139
109
  const target = match[1] ?? "";
140
110
  const path = decoded(target);
141
- if (OUTSIDE.test(target) || path === "") return [];
142
- const points = path === void 0 ? void 0 : relative(tree.root, resolve(tree.root, dirname(file), path));
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, "/");
143
113
  return [{
144
114
  from: file,
145
115
  target,
@@ -155,94 +125,9 @@ function linkFaults(tree, links) {
155
125
  return kindOf(tree, link.points) === "absent" ? [`${at}, which is not there`] : [];
156
126
  });
157
127
  }
158
- /** A line that opens as a row does: a list item whose first word is a bold decision number. */
159
- const ROW_START = /^- \*\*D(\d+)\*\*/gm;
160
- /** A whole row: the number, one sentence or more, then the date and an optional source. */
161
- const ROW = /^- \*\*D\d+\*\* \S.*[.?!] \(\d{4}-\d{2}-\d{2}(?:, \S.*)?\)$/;
162
- /** A line that names a number in bold as a row does, whatever it opens with. */
163
- const ROW_LIKE = /\*\*D\d+\*\*/;
164
- const RETIRED_SECTION = /^## Retired[ \t]*$([\s\S]*?)(?=^## |(?![\s\S]))/m;
165
- /** A mistyped number in the retired list would otherwise print a gap per number below it. */
166
- const GAPS_SHOWN = 20;
167
- const CITATION = /\bD(\d+)\b/g;
168
- function numbersIn(text) {
169
- return [...text.matchAll(CITATION)].map((match) => Number(match[1]));
170
- }
171
- function readRegister(tree, path) {
172
- const text = kindOf(tree, path) === "file" ? read(tree, path) : "";
173
- const rows = [...text.matchAll(ROW_START)].map((match) => ({
174
- number: Number(match[1]),
175
- line: lineOf(text, match.index),
176
- text: text.slice(match.index).split("\n", 1)[0] ?? ""
177
- }));
178
- const section = RETIRED_SECTION.exec(text);
179
- return {
180
- path,
181
- text,
182
- rows,
183
- live: new Set(rows.map((row) => row.number)),
184
- retired: new Set(numbersIn(section?.[1] ?? "")),
185
- hasRetiredSection: section !== null
186
- };
187
- }
188
- function rowFaults(register) {
189
- const seen = /* @__PURE__ */ new Set();
190
- return register.rows.flatMap((row) => {
191
- const at = `${register.path}:${row.line} D${row.number}`;
192
- const found = [
193
- ...ROW.test(row.text) ? [] : [`${at} is not "- **Dn** Sentence. (date, source)"`],
194
- ...seen.has(row.number) ? [`${at} is a second row`] : [],
195
- ...register.retired.has(row.number) ? [`${at} is retired`] : []
196
- ];
197
- seen.add(row.number);
198
- return found;
199
- });
200
- }
201
- function gapFaults(register) {
202
- const highest = Math.max(0, ...register.live, ...register.retired);
203
- const gaps = [];
204
- let more = 0;
205
- for (let number = 1; number <= highest; number++) {
206
- if (register.live.has(number) || register.retired.has(number)) continue;
207
- if (gaps.length < GAPS_SHOWN) gaps.push(number);
208
- else more++;
209
- }
210
- return [...gaps.map((number) => `${register.path} D${number} is neither a row nor retired`), ...more === 0 ? [] : [`${register.path} ${more} more numbers up to D${highest} are neither a row nor retired`]];
211
- }
212
- function registerFaults(register) {
213
- const rowLike = register.text.split("\n").flatMap((text, index) => ROW_LIKE.test(text) && !text.startsWith("- **D") ? [`${register.path}:${index + 1} names a row but is not one: "${text.slice(0, 40)}"`] : []);
214
- return [
215
- ...rowFaults(register),
216
- ...rowLike,
217
- ...register.hasRetiredSection ? [] : [`${register.path} has no "## Retired" section`],
218
- ...gapFaults(register)
219
- ];
220
- }
221
- function citationsIn(tree, file) {
222
- const source = read(tree, file);
223
- return [...source.matchAll(CITATION)].map((match) => ({
224
- where: `${file}:${lineOf(source, match.index)}`,
225
- number: Number(match[1])
226
- }));
227
- }
228
- /** A row may cite another row. The retired list and the prose around the rows are not held. */
229
- function citationsInRows(register) {
230
- return register.rows.flatMap((row) => numbersIn(row.text.slice(row.text.indexOf("**", 4) + 2)).map((number) => ({
231
- where: `${register.path}:${row.line}`,
232
- number
233
- })));
234
- }
235
- function citationFaults(register, citations) {
236
- return citations.flatMap(({ where, number }) => {
237
- if (register.live.has(number)) return [];
238
- return register.retired.has(number) ? [`${where} cites D${number}, which is retired`] : [`${where} cites D${number}, which the register has no row for`];
239
- });
240
- }
241
128
  function isUnder(path, base) {
242
129
  return base === "" || path === base || path.startsWith(`${base}/`);
243
130
  }
244
- /** `package-lock.json`, `pnpm-lock.yaml`: generated, and full of hashes that read like numbers. */
245
- const LOCKFILE = /[-_.]lock\.(?:json|jsonc|ya?ml)$/;
246
131
  function allNamed(by, paths, want) {
247
132
  return paths.map((path) => ({
248
133
  path: normal(path),
@@ -272,17 +157,12 @@ function usable(tree, named) {
272
157
  const NO_FAULTS = {
273
158
  scans: [],
274
159
  links: [],
275
- register: [],
276
- citations: [],
277
160
  research: []
278
161
  };
279
162
  function checkRepo(config) {
280
- const leftOut = Object.keys(config.leftOut ?? {}).map(normal);
281
163
  const tree = {
282
164
  root: resolve(config.root),
283
165
  skip: /* @__PURE__ */ new Set([...DEFAULT_SKIP, ...config.skip ?? []]),
284
- leftOut: new Set(leftOut),
285
- extensions: config.extensions ?? DEFAULT_EXTENSIONS,
286
166
  broken: /* @__PURE__ */ new Set()
287
167
  };
288
168
  if (kindOf(tree, "") !== "directory") return {
@@ -290,13 +170,10 @@ function checkRepo(config) {
290
170
  scans: [`${tree.root}, named by \`root\`, is not a directory`]
291
171
  };
292
172
  const docs = namedOr("docs", config.docs, "docs", "directory");
293
- const registerAt = namedOr("register", config.register, child(docs.path, "decisions.md"), "file");
294
173
  const research = namedOr("research", config.research, "_research", "directory");
295
- const codeRoots = allNamed("named by `codeRoots`", config.codeRoots, "directory");
296
174
  const documents = config.documents === void 0 ? allNamed("a default of `documents`", DEFAULT_DOCUMENTS, "file") : allNamed("named by `documents`", config.documents, "file");
297
175
  const linked = allNamed("named by `linked`", config.linked ?? [], "file");
298
- const researchKind = kindOf(tree, research.path);
299
- const entries = (researchKind === "directory" ? names(tree, research.path) : []).flatMap((name) => {
176
+ const entries = names(tree, research.path).flatMap((name) => {
300
177
  const path = child(research.path, name);
301
178
  const kind = kindOf(tree, path);
302
179
  if (kind === "absent") tree.broken.add(path);
@@ -307,6 +184,7 @@ function checkRepo(config) {
307
184
  }];
308
185
  });
309
186
  const frozen = (file) => isUnder(file, research.path);
187
+ const held = (link) => !frozen(link.from) || link.points === void 0 || frozen(link.points);
310
188
  const living = [
311
189
  ...usable(tree, documents),
312
190
  ...walk(tree, docs.path).filter((file) => file.endsWith(".md") && !frozen(file)),
@@ -316,42 +194,28 @@ function checkRepo(config) {
316
194
  const readme = `${entry.path}/README.md`;
317
195
  return entry.folder && kindOf(tree, readme) === "file" ? [readme] : [];
318
196
  })]);
319
- const scanned = usable(tree, codeRoots).map((base) => ({
320
- base,
321
- files: walk(tree, base).filter((file) => hasExtension(file, tree.extensions))
322
- }));
323
- const atRoot = names(tree, "").filter((name) => hasExtension(name, tree.extensions) && !tree.skip.has(name) && !tree.leftOut.has(name) && kindOf(tree, name) === "file");
324
- const citing = [.../* @__PURE__ */ new Set([
325
- ...living,
326
- ...scanned.flatMap((scan) => scan.files),
327
- ...atRoot
328
- ])].filter((file) => file !== registerAt.path && !frozen(file) && !LOCKFILE.test(file));
329
- const held = kindOf(tree, registerAt.path) === "file";
330
- const register = readRegister(tree, registerAt.path);
331
- const wrongKind = researchKind === "file" ? namedFault(tree, research) : [];
332
- const leftOutFaults = leftOut.flatMap((path) => kindOf(tree, path) === "absent" ? [`${path}, named by \`leftOut\`, is not there`] : []);
197
+ const mayBeAbsent = /* @__PURE__ */ new Set([
198
+ ...config.documents === void 0 ? documents : [],
199
+ ...config.docs === void 0 ? [docs] : [],
200
+ research
201
+ ]);
333
202
  return {
334
- scans: [
335
- ...[
336
- ...codeRoots,
337
- ...documents,
338
- docs,
339
- registerAt,
340
- ...linked
341
- ].flatMap((one) => namedFault(tree, one)),
342
- ...wrongKind,
343
- ...leftOutFaults,
344
- ...scanned.flatMap((scan) => scan.files.length === 0 ? [`${scan.base === "" ? "." : scan.base} holds no file to scan for decision numbers`] : []),
345
- ...held && register.rows.length === 0 ? [`${registerAt.path} holds no row`] : [],
346
- ...[...tree.broken].toSorted().map((path) => `${path} is a symbolic link that leads nowhere`)
347
- ],
348
- links: linkFaults(tree, [...linking].flatMap((file) => linksIn(tree, file))),
349
- register: held ? registerFaults(register) : [],
350
- citations: held ? citationFaults(register, [...citing.flatMap((file) => citationsIn(tree, file)), ...citationsInRows(register)]) : [],
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)),
351
215
  research: researchFaults(tree, entries)
352
216
  };
353
217
  }
354
218
  //#endregion
355
- export { CHECKS, DEFAULT_DOCUMENTS, DEFAULT_EXTENSIONS, DEFAULT_SKIP, HINTS, TITLES, checkRepo };
219
+ export { CHECKS, DEFAULT_DOCUMENTS, DEFAULT_SKIP, HINTS, TITLES, checkRepo };
356
220
 
357
221
  //# sourceMappingURL=repo.mjs.map
package/dist/repo.mjs.map CHANGED
@@ -1 +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 * - the decision register is one well-formed line per row, each number given once;\n * - a decision number cited anywhere is a live row in the register, never a retired one;\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 and holds something to check.\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 } from \"node:path\";\n\nexport interface RepoConfig {\n /** The repository root, absolute. */\n root: string;\n /**\n * The directories scanned for decision numbers. Every path in this config is relative to the\n * root, and `lib`, `./lib` and `lib/` name the same one.\n */\n codeRoots: readonly string[];\n /**\n * The extensions scanned for decision numbers, in the code roots and at the root itself.\n * Replaces `DEFAULT_EXTENSIONS`.\n */\n extensions?: readonly string[];\n /** Directory and file names no walk enters, wherever they sit. Added to `DEFAULT_SKIP`. */\n skip?: readonly string[];\n /** Paths no walk enters, each with the reason it is left out. */\n leftOut?: { readonly [path: string]: string };\n /** The living documents at the root. Replaces `DEFAULT_DOCUMENTS`. */\n documents?: readonly string[];\n /** The directory whose every Markdown file is a living document. `docs` by default. */\n docs?: string;\n /** More documents held as the living ones are, for their links and their citations. */\n linked?: readonly string[];\n /** The decision register. `decisions.md` in the `docs` directory by default. */\n register?: 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\", \"register\", \"citations\", \"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 holds something to check\",\n links: \"every relative Markdown link resolves\",\n register: \"the register is one well-formed line per row, each number given once\",\n citations: \"every decision number cited anywhere is a live row in the register\",\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 register:\n \"A row is one line, its number given once and never after it was retired, and a number that \" +\n \"leaves the rows joins the retired list. Rewrite a rule in place under its own number.\",\n citations:\n \"A citation of a retired number, or of one never given, sends the reader nowhere. Cite the \" +\n 'row that carries the rule now. `git log -S \"D<n>\"` on the register shows what a retired ' +\n \"row said.\",\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\nexport const DEFAULT_EXTENSIONS: readonly string[] = [\n \"ts\",\n \"tsx\",\n \"mts\",\n \"cts\",\n \"js\",\n \"jsx\",\n \"mjs\",\n \"cjs\",\n \"md\",\n \"json\",\n \"jsonc\",\n \"yml\",\n \"yaml\",\n \"html\",\n];\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 leftOut: ReadonlySet<string>;\n extensions: readonly 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\n// A Windows checkout may hand back CRLF, and every pattern here ends its line on `\\n`.\nfunction read(tree: Tree, path: string): string {\n return readFileSync(join(tree.root, path), \"utf8\").replaceAll(\"\\r\\n\", \"\\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) || tree.leftOut.has(path)) 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/** `lib`, `./lib` and `lib/` 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 hasExtension(name: string, extensions: readonly string[]): boolean {\n return extensions.some((extension) => name.endsWith(`.${extension}`));\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 = read(tree, file);\n return [...source.matchAll(LINK)].flatMap((match) => {\n const target = match[1] ?? \"\";\n const path = decoded(target);\n if (OUTSIDE.test(target) || path === \"\") return [];\n const points =\n path === undefined ? undefined : relative(tree.root, resolve(tree.root, dirname(file), path));\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\n/** A line that opens as a row does: a list item whose first word is a bold decision number. */\nconst ROW_START = /^- \\*\\*D(\\d+)\\*\\*/gm;\n\n/** A whole row: the number, one sentence or more, then the date and an optional source. */\nconst ROW = /^- \\*\\*D\\d+\\*\\* \\S.*[.?!] \\(\\d{4}-\\d{2}-\\d{2}(?:, \\S.*)?\\)$/;\n\n/** A line that names a number in bold as a row does, whatever it opens with. */\nconst ROW_LIKE = /\\*\\*D\\d+\\*\\*/;\n\nconst RETIRED_SECTION = /^## Retired[ \\t]*$([\\s\\S]*?)(?=^## |(?![\\s\\S]))/m;\n\n/** A mistyped number in the retired list would otherwise print a gap per number below it. */\nconst GAPS_SHOWN = 20;\n\nconst CITATION = /\\bD(\\d+)\\b/g;\n\ninterface Row {\n number: number;\n line: number;\n text: string;\n}\n\ninterface Register {\n path: string;\n text: string;\n rows: readonly Row[];\n live: ReadonlySet<number>;\n /** Every number the `## Retired` section names, however its lines wrap. */\n retired: ReadonlySet<number>;\n hasRetiredSection: boolean;\n}\n\nfunction numbersIn(text: string): number[] {\n return [...text.matchAll(CITATION)].map((match) => Number(match[1]));\n}\n\nfunction readRegister(tree: Tree, path: string): Register {\n const text = kindOf(tree, path) === \"file\" ? read(tree, path) : \"\";\n const rows = [...text.matchAll(ROW_START)].map((match) => ({\n number: Number(match[1]),\n line: lineOf(text, match.index),\n text: text.slice(match.index).split(\"\\n\", 1)[0] ?? \"\",\n }));\n const section = RETIRED_SECTION.exec(text);\n return {\n path,\n text,\n rows,\n live: new Set(rows.map((row) => row.number)),\n retired: new Set(numbersIn(section?.[1] ?? \"\")),\n hasRetiredSection: section !== null,\n };\n}\n\nfunction rowFaults(register: Register): string[] {\n const seen = new Set<number>();\n return register.rows.flatMap((row) => {\n const at = `${register.path}:${row.line} D${row.number}`;\n const found = [\n ...(ROW.test(row.text) ? [] : [`${at} is not \"- **Dn** Sentence. (date, source)\"`]),\n ...(seen.has(row.number) ? [`${at} is a second row`] : []),\n ...(register.retired.has(row.number) ? [`${at} is retired`] : []),\n ];\n seen.add(row.number);\n return found;\n });\n}\n\nfunction gapFaults(register: Register): string[] {\n const highest = Math.max(0, ...register.live, ...register.retired);\n const gaps: number[] = [];\n let more = 0;\n for (let number = 1; number <= highest; number++) {\n if (register.live.has(number) || register.retired.has(number)) continue;\n if (gaps.length < GAPS_SHOWN) gaps.push(number);\n else more++;\n }\n return [\n ...gaps.map((number) => `${register.path} D${number} is neither a row nor retired`),\n ...(more === 0\n ? []\n : [`${register.path} ${more} more numbers up to D${highest} are neither a row nor retired`]),\n ];\n}\n\nfunction registerFaults(register: Register): string[] {\n const rowLike = register.text\n .split(\"\\n\")\n .flatMap((text, index) =>\n ROW_LIKE.test(text) && !text.startsWith(\"- **D\")\n ? [`${register.path}:${index + 1} names a row but is not one: \"${text.slice(0, 40)}\"`]\n : [],\n );\n return [\n ...rowFaults(register),\n ...rowLike,\n ...(register.hasRetiredSection ? [] : [`${register.path} has no \"## Retired\" section`]),\n ...gapFaults(register),\n ];\n}\n\ninterface Citation {\n where: string;\n number: number;\n}\n\nfunction citationsIn(tree: Tree, file: string): Citation[] {\n const source = read(tree, file);\n return [...source.matchAll(CITATION)].map((match) => ({\n where: `${file}:${lineOf(source, match.index)}`,\n number: Number(match[1]),\n }));\n}\n\n/** A row may cite another row. The retired list and the prose around the rows are not held. */\nfunction citationsInRows(register: Register): Citation[] {\n return register.rows.flatMap((row) =>\n numbersIn(row.text.slice(row.text.indexOf(\"**\", 4) + 2)).map((number) => ({\n where: `${register.path}:${row.line}`,\n number,\n })),\n );\n}\n\nfunction citationFaults(register: Register, citations: readonly Citation[]): string[] {\n return citations.flatMap(({ where, number }) => {\n if (register.live.has(number)) return [];\n return register.retired.has(number)\n ? [`${where} cites D${number}, which is retired`]\n : [`${where} cites D${number}, which the register has no row for`];\n });\n}\n\nfunction isUnder(path: string, base: string): boolean {\n return base === \"\" || path === base || path.startsWith(`${base}/`);\n}\n\n/** `package-lock.json`, `pnpm-lock.yaml`: generated, and full of hashes that read like numbers. */\nconst LOCKFILE = /[-_.]lock\\.(?:json|jsonc|ya?ml)$/;\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: [], register: [], citations: [], research: [] };\n\nexport function checkRepo(config: RepoConfig): RepoFaults {\n const leftOut = Object.keys(config.leftOut ?? {}).map(normal);\n const tree: Tree = {\n root: resolve(config.root),\n skip: new Set([...DEFAULT_SKIP, ...(config.skip ?? [])]),\n leftOut: new Set(leftOut),\n extensions: config.extensions ?? DEFAULT_EXTENSIONS,\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 registerAt = namedOr(\"register\", config.register, child(docs.path, \"decisions.md\"), \"file\");\n const research = namedOr(\"research\", config.research, \"_research\", \"directory\");\n const codeRoots = allNamed(\"named by `codeRoots`\", config.codeRoots, \"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 // A repo has no research until its first, so an absent directory is not a fault.\n const researchKind = kindOf(tree, research.path);\n const entries = (researchKind === \"directory\" ? names(tree, research.path) : []).flatMap(\n (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\n // Inside the research only a folder's README is held: an entry is frozen once it is on the\n // main branch, so its body names paths and numbers as they stood, and some have moved since.\n const frozen = (file: string): boolean => isUnder(file, research.path);\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 const scanned = usable(tree, codeRoots).map((base) => ({\n base,\n files: walk(tree, base).filter((file) => hasExtension(file, tree.extensions)),\n }));\n const atRoot = names(tree, \"\").filter(\n (name) =>\n hasExtension(name, tree.extensions) &&\n !tree.skip.has(name) &&\n !tree.leftOut.has(name) &&\n kindOf(tree, name) === \"file\",\n );\n const citing = [\n ...new Set([...living, ...scanned.flatMap((scan) => scan.files), ...atRoot]),\n ].filter((file) => file !== registerAt.path && !frozen(file) && !LOCKFILE.test(file));\n\n // With no register to read, every citation would fail: `scans` says so once instead.\n const held = kindOf(tree, registerAt.path) === \"file\";\n const register = readRegister(tree, registerAt.path);\n const wrongKind = researchKind === \"file\" ? namedFault(tree, research) : [];\n const leftOutFaults = leftOut.flatMap((path) =>\n kindOf(tree, path) === \"absent\" ? [`${path}, named by \\`leftOut\\`, is not there`] : [],\n );\n\n return {\n scans: [\n ...[...codeRoots, ...documents, docs, registerAt, ...linked].flatMap((one) =>\n namedFault(tree, one),\n ),\n ...wrongKind,\n ...leftOutFaults,\n ...scanned.flatMap((scan) =>\n scan.files.length === 0\n ? [`${scan.base === \"\" ? \".\" : scan.base} holds no file to scan for decision numbers`]\n : [],\n ),\n ...(held && register.rows.length === 0 ? [`${registerAt.path} holds no row`] : []),\n ...[...tree.broken].toSorted().map((path) => `${path} is a symbolic link that leads nowhere`),\n ],\n links: linkFaults(\n tree,\n [...linking].flatMap((file) => linksIn(tree, file)),\n ),\n register: held ? registerFaults(register) : [],\n citations: held\n ? citationFaults(register, [\n ...citing.flatMap((file) => citationsIn(tree, file)),\n ...citationsInRows(register),\n ])\n : [],\n research: researchFaults(tree, entries),\n };\n}\n"],"mappings":"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"}
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":";;;;;;;;;;;;;;AA0CA,MAAa,SAAS;CAAC;CAAS;CAAS;AAAU;;AAQnD,MAAa,SAAS;CACpB,OAAO;CACP,OAAO;CACP,UAAU;AACZ;;AAGA,MAAa,QAAQ;CACnB,OACE;CAEF,OAAO;CACP,UACE;AAEJ;AAEA,MAAa,oBAAuC;CAAC;CAAa;CAAa;AAAY;;AAG3F,MAAa,eAAkC;CAAC;CAAgB;CAAc;AAAM;AAWpF,SAAS,OAAO,MAAY,MAAoB;CAC9C,IAAI;EACF,OAAO,SAAS,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,YAAY,IAAI,cAAc;CACvE,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,eAAe,MAAY,MAAuB;CACzD,IAAI;EACF,OAAO,UAAU,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,eAAe;CACzD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,MAAM,MAAY,MAAwB;CACjD,IAAI;EACF,OAAO,YAAY,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,SAAS;CACrD,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,MAAM,MAAc,MAAsB;CACjD,OAAO,SAAS,KAAK,OAAO,GAAG,KAAK,GAAG;AACzC;AAGA,SAAS,KAAK,MAAY,MAAwB;CAChD,OAAO,MAAM,MAAM,IAAI,CAAC,CAAC,SAAS,SAAS;EACzC,MAAM,OAAO,MAAM,MAAM,IAAI;EAC7B,IAAI,KAAK,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC;EACjC,MAAM,OAAO,OAAO,MAAM,IAAI;EAC9B,IAAI,SAAS,QAAQ,OAAO,CAAC,IAAI;EACjC,IAAI,SAAS,UAAU,KAAK,OAAO,IAAI,IAAI;EAC3C,OAAO,SAAS,eAAe,CAAC,eAAe,MAAM,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC;CACnF,CAAC;AACH;;AAGA,SAAS,OAAO,MAAsB;CACpC,MAAM,QAAQ,MAAM,UAAU,IAAI,CAAC,CAAC,QAAQ,QAAQ,EAAE;CACtD,OAAO,UAAU,MAAM,KAAK;AAC9B;AAEA,SAAS,OAAO,QAAgB,OAAuB;CACrD,OAAO,OAAO,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;AAC5C;;AAGA,MAAM,aAAa;;AAGnB,MAAM,eAAe;AAQrB,SAAS,eAAe,MAAY,SAAqC;CACvE,OAAO,QAAQ,SAAS,UAAU;EAChC,IAAI,EAAE,MAAM,SAAS,eAAe,WAAA,CAAY,KAAK,MAAM,IAAI,GAC7D,OAAO,CAAC,GAAG,MAAM,KAAK,wDAAwD;EAEhF,OAAO,MAAM,UAAU,OAAO,MAAM,GAAG,MAAM,KAAK,WAAW,MAAM,SAC/D,CAAC,GAAG,MAAM,KAAK,mBAAmB,IAClC,CAAC;CACP,CAAC;AACH;;AAGA,MAAM,OAAO;;AAGb,MAAM,UAAU;AAUhB,SAAS,QAAQ,QAAoC;CACnD,IAAI;EACF,OAAO,UAAU,OAAO,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE;CAC7C,QAAQ;EACN;CACF;AACF;AAEA,SAAS,QAAQ,MAAY,MAAsB;CACjD,MAAM,SAAS,aAAa,KAAK,KAAK,MAAM,IAAI,GAAG,MAAM;CACzD,OAAO,CAAC,GAAG,OAAO,SAAS,IAAI,CAAC,CAAC,CAAC,SAAS,UAAU;EACnD,MAAM,SAAS,MAAM,MAAM;EAC3B,MAAM,OAAO,QAAQ,MAAM;EAC3B,IAAI,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC;EAElC,MAAM,SACJ,SAAS,KAAA,IACL,KAAA,IACA,SAAS,KAAK,MAAM,QAAQ,KAAK,MAAM,QAAQ,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,WAAW,KAAK,GAAG;EACtF,OAAO,CAAC;GAAE,MAAM;GAAM;GAAQ,MAAM,OAAO,QAAQ,MAAM,KAAK;GAAG;EAAO,CAAC;CAC3E,CAAC;AACH;AAEA,SAAS,WAAW,MAAY,OAAkC;CAChE,OAAO,MAAM,SAAS,SAAS;EAC7B,MAAM,KAAK,GAAG,KAAK,KAAK,GAAG,KAAK,KAAK,aAAa,KAAK;EACvD,IAAI,KAAK,WAAW,KAAA,GAAW,OAAO,CAAC,GAAG,GAAG,sCAAsC;EACnF,OAAO,OAAO,MAAM,KAAK,MAAM,MAAM,WAAW,CAAC,GAAG,GAAG,qBAAqB,IAAI,CAAC;CACnF,CAAC;AACH;AAEA,SAAS,QAAQ,MAAc,MAAuB;CACpD,OAAO,SAAS,MAAM,SAAS,QAAQ,KAAK,WAAW,GAAG,KAAK,EAAE;AACnE;AASA,SAAS,SAAS,IAAY,OAA0B,MAAqB;CAC3E,OAAO,MAAM,KAAK,UAAU;EAAE,MAAM,OAAO,IAAI;EAAG;EAAI;CAAK,EAAE;AAC/D;AAEA,SAAS,QAAQ,QAAgB,MAA0B,UAAkB,MAAmB;CAC9F,OAAO,SAAS,KAAA,IACZ;EAAE,MAAM;EAAU,IAAI,oBAAoB,OAAO;EAAK;CAAK,IAC3D;EAAE,MAAM,OAAO,IAAI;EAAG,IAAI,cAAc,OAAO;EAAK;CAAK;AAC/D;AAEA,SAAS,WAAW,MAAY,OAAwB;CACtD,MAAM,OAAO,OAAO,MAAM,MAAM,IAAI;CACpC,IAAI,SAAS,MAAM,MAAM,OAAO,CAAC;CAEjC,OAAO,CAAC,GADM,MAAM,SAAS,KAAK,MAAM,MAAM,KAC7B,IAAI,MAAM,GAAG,OAAO,SAAS,WAAW,cAAc,SAAS,MAAM,QAAQ;AAChG;AAEA,SAAS,OAAO,MAAY,OAAmC;CAC7D,OAAO,MAAM,SAAS,QAAS,OAAO,MAAM,IAAI,IAAI,MAAM,IAAI,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,CAAE;AACvF;AAEA,MAAM,YAAwB;CAAE,OAAO,CAAC;CAAG,OAAO,CAAC;CAAG,UAAU,CAAC;AAAE;AAEnE,SAAgB,UAAU,QAAgC;CACxD,MAAM,OAAa;EACjB,MAAM,QAAQ,OAAO,IAAI;EACzB,sBAAM,IAAI,IAAI,CAAC,GAAG,cAAc,GAAI,OAAO,QAAQ,CAAC,CAAE,CAAC;EACvD,wBAAQ,IAAI,IAAI;CAClB;CACA,IAAI,OAAO,MAAM,EAAE,MAAM,aACvB,OAAO;EAAE,GAAG;EAAW,OAAO,CAAC,GAAG,KAAK,KAAK,wCAAwC;CAAE;CAGxF,MAAM,OAAO,QAAQ,QAAQ,OAAO,MAAM,QAAQ,WAAW;CAC7D,MAAM,WAAW,QAAQ,YAAY,OAAO,UAAU,aAAa,WAAW;CAC9E,MAAM,YACJ,OAAO,cAAc,KAAA,IACjB,SAAS,4BAA4B,mBAAmB,MAAM,IAC9D,SAAS,wBAAwB,OAAO,WAAW,MAAM;CAC/D,MAAM,SAAS,SAAS,qBAAqB,OAAO,UAAU,CAAC,GAAG,MAAM;CAExE,MAAM,UAAU,MAAM,MAAM,SAAS,IAAI,CAAC,CAAC,SAAS,SAAS;EAC3D,MAAM,OAAO,MAAM,SAAS,MAAM,IAAI;EACtC,MAAM,OAAO,OAAO,MAAM,IAAI;EAC9B,IAAI,SAAS,UAAU,KAAK,OAAO,IAAI,IAAI;EAC3C,OAAO,SAAS,WAAW,CAAC,IAAI,CAAC;GAAE;GAAM;GAAM,QAAQ,SAAS;EAAY,CAAC;CAC/E,CAAC;CAID,MAAM,UAAU,SAA0B,QAAQ,MAAM,SAAS,IAAI;CACrE,MAAM,QAAQ,SACZ,CAAC,OAAO,KAAK,IAAI,KAAK,KAAK,WAAW,KAAA,KAAa,OAAO,KAAK,MAAM;CAEvE,MAAM,SAAS;EACb,GAAG,OAAO,MAAM,SAAS;EACzB,GAAG,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,QAAQ,SAAS,KAAK,SAAS,KAAK,KAAK,CAAC,OAAO,IAAI,CAAC;EAC/E,GAAG,OAAO,MAAM,MAAM;CACxB;CACA,MAAM,0BAAU,IAAI,IAAI,CACtB,GAAG,QACH,GAAG,QAAQ,SAAS,UAAU;EAC5B,MAAM,SAAS,GAAG,MAAM,KAAK;EAC7B,OAAO,MAAM,UAAU,OAAO,MAAM,MAAM,MAAM,SAAS,CAAC,MAAM,IAAI,CAAC;CACvE,CAAC,CACH,CAAC;CAID,MAAM,8BAAc,IAAI,IAAI;EAC1B,GAAI,OAAO,cAAc,KAAA,IAAY,YAAY,CAAC;EAClD,GAAI,OAAO,SAAS,KAAA,IAAY,CAAC,IAAI,IAAI,CAAC;EAC1C;CACF,CAAC;CASD,OAAO;EACL,OAAO,CACL,GATa;GAAC,GAAG;GAAW;GAAM,GAAG;GAAQ;EAAQ,CAAC,CAAC,SAAS,QAAQ;GAC1E,IAAI,OAAO,MAAM,IAAI,IAAI,MAAM,UAAU,OAAO,CAAC,GAAG;GACpD,IAAI,CAAC,eAAe,MAAM,IAAI,IAAI,GAAG,OAAO,YAAY,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG;GAC5E,KAAK,OAAO,IAAI,IAAI,IAAI;GACxB,OAAO,CAAC;EACV,CAIc,CAAC,CAAC,SAAS,QAAQ,WAAW,MAAM,GAAG,CAAC,GAClD,GAAG,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,SAAS,GAAG,KAAK,uCAAuC,CAC9F;EACA,OAAO,WAAW,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,SAAS,SAAS,QAAQ,MAAM,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC;EACxF,UAAU,eAAe,MAAM,OAAO;CACxC;AACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rdjperron/lint",
3
- "version": "0.6.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 links, decision register and research names.",
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",
@@ -32,6 +32,16 @@
32
32
  },
33
33
  "./package.json": "./package.json"
34
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
+ },
35
45
  "dependencies": {
36
46
  "@oxlint/plugins": "^1.79.0"
37
47
  },
@@ -52,12 +62,5 @@
52
62
  "engines": {
53
63
  "node": ">=24"
54
64
  },
55
- "scripts": {
56
- "build": "vp pack",
57
- "test": "vp test",
58
- "check": "vp check",
59
- "typecheck": "tsc --noEmit",
60
- "verify:lint-fixtures": "node src/verify.ts --plugin ./src/index.ts",
61
- "ready": "vp check && vp test && vp run typecheck && vp run build && vp run verify:lint-fixtures && vp pm pack -- --dry-run"
62
- }
63
- }
65
+ "packageManager": "pnpm@12.4.2"
66
+ }