@scientific-method/standard-checker 0.1.0 → 0.3.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.
Files changed (79) hide show
  1. package/README.md +6 -0
  2. package/dist/checks/base.d.ts +7 -0
  3. package/dist/checks/base.js +72 -0
  4. package/dist/checks/claims.d.ts +4 -0
  5. package/dist/checks/claims.js +83 -0
  6. package/dist/checks/comment-addresses.d.ts +3 -0
  7. package/dist/checks/comment-addresses.js +111 -0
  8. package/dist/checks/cross-entry.d.ts +4 -0
  9. package/dist/checks/cross-entry.js +177 -0
  10. package/dist/checks/deviations.d.ts +12 -0
  11. package/dist/checks/deviations.js +101 -0
  12. package/dist/checks/entries.d.ts +7 -0
  13. package/dist/checks/entries.js +131 -0
  14. package/dist/checks/evidence-entries.d.ts +6 -0
  15. package/dist/checks/evidence-entries.js +167 -0
  16. package/dist/checks/fields.d.ts +4 -0
  17. package/dist/checks/fields.js +314 -0
  18. package/dist/checks/formats.d.ts +19 -0
  19. package/dist/checks/formats.js +221 -0
  20. package/dist/checks/kaitai.d.ts +6 -0
  21. package/dist/checks/kaitai.js +103 -0
  22. package/dist/checks/parity.d.ts +30 -0
  23. package/dist/checks/parity.js +189 -0
  24. package/dist/checks/references.d.ts +4 -0
  25. package/dist/checks/references.js +37 -0
  26. package/dist/checks/rules.d.ts +11 -0
  27. package/dist/checks/rules.js +263 -0
  28. package/dist/checks/screens.d.ts +4 -0
  29. package/dist/checks/screens.js +44 -0
  30. package/dist/checks/validation.d.ts +7 -0
  31. package/dist/checks/validation.js +116 -0
  32. package/dist/code-comments.d.ts +3 -0
  33. package/dist/code-comments.js +150 -0
  34. package/dist/code-files.d.ts +11 -0
  35. package/dist/code-files.js +41 -0
  36. package/dist/context.d.ts +34 -0
  37. package/dist/context.js +2 -0
  38. package/dist/evidence.d.ts +37 -0
  39. package/dist/evidence.js +91 -0
  40. package/dist/files.d.ts +11 -0
  41. package/dist/files.js +35 -0
  42. package/dist/generate/indexes.d.ts +3 -0
  43. package/dist/generate/indexes.js +135 -0
  44. package/dist/generate/layout.d.ts +18 -0
  45. package/dist/generate/layout.js +49 -0
  46. package/dist/generate/parity-md.d.ts +7 -0
  47. package/dist/generate/parity-md.js +44 -0
  48. package/dist/generate/write.d.ts +8 -0
  49. package/dist/generate/write.js +75 -0
  50. package/dist/ids.d.ts +13 -0
  51. package/dist/ids.js +22 -0
  52. package/dist/load/builds.d.ts +9 -0
  53. package/dist/load/builds.js +124 -0
  54. package/dist/load/code-ranges.d.ts +4 -0
  55. package/dist/load/code-ranges.js +79 -0
  56. package/dist/load/entries.d.ts +7 -0
  57. package/dist/load/entries.js +57 -0
  58. package/dist/load/glossary.d.ts +12 -0
  59. package/dist/load/glossary.js +70 -0
  60. package/dist/load/readme.d.ts +3 -0
  61. package/dist/load/readme.js +36 -0
  62. package/dist/load/spec.d.ts +6 -0
  63. package/dist/load/spec.js +25 -0
  64. package/dist/locations.d.ts +19 -0
  65. package/dist/locations.js +57 -0
  66. package/dist/markdown.d.ts +27 -0
  67. package/dist/markdown.js +162 -0
  68. package/dist/options.d.ts +34 -0
  69. package/dist/options.js +87 -0
  70. package/dist/problems.d.ts +27 -0
  71. package/dist/problems.js +27 -0
  72. package/dist/standard-checker.js +53 -3033
  73. package/dist/standard.d.ts +65 -0
  74. package/dist/standard.js +173 -0
  75. package/dist/types.d.ts +91 -0
  76. package/dist/types.js +2 -0
  77. package/dist/yaml.d.ts +6 -0
  78. package/dist/yaml.js +173 -0
  79. package/package.json +1 -1
@@ -0,0 +1,162 @@
1
+ // Markdown and CSV: entries with front matter, sections, tables and value files.
2
+ import { readFileSync } from "node:fs";
3
+ import { parseYaml } from "./yaml.js";
4
+ /** The number of lines of text, not counting the empty one after a final newline. */
5
+ export const lineCount = (text) => (text === "" ? 0 : text.split("\n").length - (text.endsWith("\n") ? 1 : 0));
6
+ /** Reads a text file with CRLF line ends read as LF. */
7
+ export const readText = (file) => readFileSync(file, "utf8").replace(/\r\n/g, "\n");
8
+ /** Reads an entry's front matter, body and sections, or reports why it cannot and returns null. */
9
+ export function readEntry(file, problem) {
10
+ const text = readFileSync(file, "utf8").replace(/\r\n/g, "\n");
11
+ if (!text.startsWith("---\n")) {
12
+ problem(file, "has no front matter");
13
+ return null;
14
+ }
15
+ const end = text.indexOf("\n---\n", 4);
16
+ if (end < 0) {
17
+ problem(file, "front matter is not closed");
18
+ return null;
19
+ }
20
+ const meta = parseYaml(text.slice(4, end), file, problem);
21
+ const body = text.slice(end + 5);
22
+ return { file, meta, body, sections: splitSections(body) };
23
+ }
24
+ /** The `##` sections of a Markdown body, skipping fenced code. */
25
+ export function splitSections(body) {
26
+ const sections = [];
27
+ let current = null;
28
+ let fence = false;
29
+ for (const line of body.split("\n")) {
30
+ if (/^(```|~~~)/.test(line))
31
+ fence = !fence;
32
+ const m = !fence && /^## (.+)$/.exec(line);
33
+ if (m) {
34
+ current = { title: m[1].trim(), lines: [] };
35
+ sections.push(current);
36
+ }
37
+ else if (current)
38
+ current.lines.push(line);
39
+ }
40
+ return sections.map((s) => ({ title: s.title, text: s.lines.join("\n") }));
41
+ }
42
+ /** Every Markdown table in a section, with the `###` heading above it. */
43
+ export function tables(text) {
44
+ const out = [];
45
+ const lines = text.split("\n");
46
+ let heading = null;
47
+ let fence = false;
48
+ for (let i = 0; i < lines.length; i++) {
49
+ if (/^(```|~~~)/.test(lines[i]))
50
+ fence = !fence;
51
+ if (fence)
52
+ continue;
53
+ const h = /^### (.+)$/.exec(lines[i]);
54
+ if (h)
55
+ heading = h[1].trim();
56
+ if (lines[i].startsWith("|") && i + 1 < lines.length && /^\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
57
+ const header = cells(lines[i]);
58
+ const rows = [];
59
+ let j = i + 2;
60
+ while (j < lines.length && lines[j].startsWith("|")) {
61
+ rows.push(cells(lines[j]));
62
+ j++;
63
+ }
64
+ out.push({ heading, header, rows, line: i });
65
+ i = j - 1;
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ /** The cells of a table row, with `\|` read as a pipe and pipes inside code spans kept. */
71
+ export function cells(line) {
72
+ const parts = [];
73
+ let cur = "";
74
+ let code = false;
75
+ const s = line.trim().replace(/^\|/, "").replace(/\|$/, "");
76
+ for (let k = 0; k < s.length; k++) {
77
+ const ch = s[k];
78
+ if (ch === "`")
79
+ code = !code;
80
+ if (ch === "\\" && s[k + 1] === "|") {
81
+ cur += "|";
82
+ k++;
83
+ continue;
84
+ }
85
+ if (ch === "|" && !code) {
86
+ parts.push(cur.trim());
87
+ cur = "";
88
+ continue;
89
+ }
90
+ cur += ch;
91
+ }
92
+ parts.push(cur.trim());
93
+ return parts;
94
+ }
95
+ /**
96
+ * A value file: CSV as RFC 4180 defines it, with a header row. Returns { header, rows }, or null
97
+ * after reporting a problem.
98
+ */
99
+ export function readCsv(file, problem) {
100
+ // Spreadsheet programs start a UTF-8 CSV with a byte order mark, which would join the first header.
101
+ const text = readFileSync(file, "utf8").replace(/^/, "");
102
+ const records = [];
103
+ let record = [];
104
+ let field = "";
105
+ let quoted = false;
106
+ let wasQuoted = false;
107
+ for (let i = 0; i < text.length; i++) {
108
+ const ch = text[i];
109
+ if (quoted) {
110
+ if (ch === '"' && text[i + 1] === '"') {
111
+ field += '"';
112
+ i++;
113
+ }
114
+ else if (ch === '"')
115
+ quoted = false;
116
+ else
117
+ field += ch;
118
+ }
119
+ else if (ch === '"' && field === "" && !wasQuoted) {
120
+ quoted = true;
121
+ wasQuoted = true;
122
+ }
123
+ else if (ch === ",") {
124
+ record.push(field);
125
+ field = "";
126
+ wasQuoted = false;
127
+ }
128
+ else if (ch === "\n" || ch === "\r") {
129
+ if (ch === "\r" && text[i + 1] === "\n")
130
+ i++;
131
+ record.push(field);
132
+ records.push(record);
133
+ record = [];
134
+ field = "";
135
+ wasQuoted = false;
136
+ }
137
+ else
138
+ field += ch;
139
+ }
140
+ if (quoted) {
141
+ problem(file, "has a quoted field that is never closed");
142
+ return null;
143
+ }
144
+ if (field !== "" || record.length > 0) {
145
+ record.push(field);
146
+ records.push(record);
147
+ }
148
+ // Blank lines at the end of the file are not rows.
149
+ while (records.length > 0 && records.at(-1).length === 1 && records.at(-1)[0] === "")
150
+ records.pop();
151
+ if (records.length === 0) {
152
+ problem(file, "has no header row");
153
+ return null;
154
+ }
155
+ const [header, ...rows] = records;
156
+ for (const row of rows)
157
+ if (row.length !== header.length) {
158
+ problem(file, `the row ${row.join(",")} has ${row.length} fields, not ${header.length}`);
159
+ return null;
160
+ }
161
+ return { header, rows };
162
+ }
@@ -0,0 +1,34 @@
1
+ /** What one run of the checker was asked to do, from its command line. */
2
+ export interface Config {
3
+ /** The repository being checked (--root), absolute. */
4
+ repoDir: string;
5
+ /** Its spec/ directory. */
6
+ specDir: string;
7
+ /** --check: report stale generated files instead of rewriting them. */
8
+ checkOnly: boolean;
9
+ /** --no-ksy: skip compiling the Kaitai definitions. */
10
+ skipKsy: boolean;
11
+ /** --base, or null when it was not given. */
12
+ baseArg: string | null;
13
+ /** Every --glossary path, in order. */
14
+ glossaryDrafts: string[];
15
+ /** The --code directories, relative to repoDir. */
16
+ codeRoots: string[];
17
+ /** The --references directories, relative to repoDir. */
18
+ referenceRoots: string[];
19
+ /** The --images ranges, as half-open [low, high). */
20
+ images: Array<[number, number]>;
21
+ /** --max-range in bytes. */
22
+ maxRange: number;
23
+ /** The --data-dirs directories, or undefined when the option was not given. */
24
+ dataDirs: string[] | undefined;
25
+ /** The --record-validation build IDs, or undefined when the option was not given. */
26
+ recordValidation: string[] | undefined;
27
+ }
28
+ /**
29
+ * Splits a comma-separated option into its trimmed, non-empty parts, or returns fallback when the
30
+ * option was not given.
31
+ */
32
+ export declare const dirList: (value: string | undefined, fallback: string[]) => string[];
33
+ /** Reads the arguments after the script's path. An invalid one prints why and exits with 2. */
34
+ export declare function parseOptions(argv: string[]): Config;
@@ -0,0 +1,87 @@
1
+ // The command line, read into a Config. An invalid option prints why and exits with 2.
2
+ import { join, resolve } from "node:path";
3
+ /**
4
+ * Splits a comma-separated option into its trimmed, non-empty parts, or returns fallback when the
5
+ * option was not given.
6
+ */
7
+ export const dirList = (value, fallback) => value === undefined
8
+ ? fallback
9
+ : value
10
+ .split(",")
11
+ .map((x) => x.trim())
12
+ .filter(Boolean);
13
+ const FLAGS = ["--check", "--no-ksy"];
14
+ const VALUED = [
15
+ "--root",
16
+ "--base",
17
+ "--glossary",
18
+ "--code",
19
+ "--references",
20
+ "--images",
21
+ "--max-range",
22
+ "--data-dirs",
23
+ "--record-validation",
24
+ ];
25
+ /** Reads the arguments after the script's path. An invalid one prints why and exits with 2. */
26
+ export function parseOptions(argv) {
27
+ const options = { glossary: [] };
28
+ for (let k = 0; k < argv.length; k++) {
29
+ const arg = argv[k];
30
+ if (FLAGS.includes(arg))
31
+ options[arg.slice(2)] = true;
32
+ else if (VALUED.includes(arg)) {
33
+ if (k + 1 >= argv.length) {
34
+ console.error(`${arg} needs a value; see --help`);
35
+ process.exit(2);
36
+ }
37
+ if (arg === "--glossary")
38
+ options.glossary.push(argv[++k]);
39
+ else
40
+ options[arg.slice(2)] = argv[++k];
41
+ }
42
+ else {
43
+ console.error(`unknown option ${arg}; see --help`);
44
+ process.exit(2);
45
+ }
46
+ }
47
+ const repoDir = resolve(options.root ?? ".");
48
+ const specDir = join(repoDir, "spec");
49
+ const checkOnly = options.check === true;
50
+ if (checkOnly && options["record-validation"] !== undefined) {
51
+ console.error("--record-validation writes VALIDATION.md, so it cannot be combined with --check");
52
+ process.exit(2);
53
+ }
54
+ const skipKsy = options["no-ksy"] === true;
55
+ const baseArg = options.base ?? null;
56
+ const codeRoots = dirList(options.code, ["src", "tests", "tools"]);
57
+ const referenceRoots = dirList(options.references, []);
58
+ // Half-open [low, high) ranges, as numbers: flat 32-bit addresses fit exactly.
59
+ const images = dirList(options.images, []).map((range) => {
60
+ const m = /^0x([0-9A-Fa-f]{8})\.\.0x([0-9A-Fa-f]{8})$/.exec(range);
61
+ const [low, high] = m ? [parseInt(m[1], 16), parseInt(m[2], 16)] : [NaN, NaN];
62
+ if (!m || high <= low) {
63
+ console.error(`--images takes half-open ranges such as 0x00400000..0x004C9000, not ${range}`);
64
+ process.exit(2);
65
+ }
66
+ return [low, high];
67
+ });
68
+ const maxRange = options["max-range"] === undefined ? 0x10000 : Number(options["max-range"]);
69
+ if (!Number.isSafeInteger(maxRange) || maxRange < 1) {
70
+ console.error(`--max-range takes a positive number of bytes, such as 0x10000, not ${options["max-range"]}`);
71
+ process.exit(2);
72
+ }
73
+ return {
74
+ repoDir,
75
+ specDir,
76
+ checkOnly,
77
+ skipKsy,
78
+ baseArg,
79
+ glossaryDrafts: options.glossary,
80
+ codeRoots,
81
+ referenceRoots,
82
+ images,
83
+ maxRange,
84
+ dataDirs: options["data-dirs"] === undefined ? undefined : dirList(options["data-dirs"], []),
85
+ recordValidation: options["record-validation"] === undefined ? undefined : dirList(options["record-validation"], []),
86
+ };
87
+ }
@@ -0,0 +1,27 @@
1
+ /** The whole numbers from 1 to N. */
2
+ type UpTo<N extends number, Seen extends 0[] = [0]> = Seen["length"] | (Seen["length"] extends N ? never : UpTo<N, [...Seen, 0]>);
3
+ /**
4
+ * A numbered rule of the documentation standard, such as `STATUS-14`. The standard opens each rule
5
+ * with the heading `###### STATUS-14`, anchored at `#status-14` on the site and in vendored copies.
6
+ * Each count is the last rule the standard numbers in that section, so a label that names no rule
7
+ * does not compile. Raise a count, or add a section, when the standard numbers more rules.
8
+ */
9
+ export type Rule = `IDENTIFIERS-${UpTo<7>}` | `STATUS-${UpTo<41>}` | `ENTRY-TYPES-${UpTo<8>}`;
10
+ /**
11
+ * Records a problem with file (or with the spec as a whole when file is null). A problem that breaks
12
+ * a numbered rule names it.
13
+ */
14
+ export type Problem = (file: string | null, message: string, rule?: Rule) => void;
15
+ /** The problem collector of one run. */
16
+ export interface Problems {
17
+ /** Records a problem. */
18
+ problem: Problem;
19
+ /**
20
+ * Prints every distinct problem in the order found, with the summary, and exits with 1. Returns
21
+ * when there are none.
22
+ */
23
+ report(entryCount: number): void;
24
+ }
25
+ /** Creates the collector for a run over repoDir, against which problem paths are printed. */
26
+ export declare function createProblems(repoDir: string): Problems;
27
+ export {};
@@ -0,0 +1,27 @@
1
+ // The problems a run finds, kept in the order they are found and printed together at the end.
2
+ import { relative } from "node:path";
3
+ /** Creates the collector for a run over repoDir, against which problem paths are printed. */
4
+ export function createProblems(repoDir) {
5
+ const problems = [];
6
+ let citedRule = false;
7
+ // A problem that breaks a numbered rule ends with the rule's label, so whoever fixes it can read
8
+ // that one rule instead of the whole section.
9
+ const problem = (file, message, rule) => {
10
+ if (rule)
11
+ citedRule = true;
12
+ problems.push(`${file ? relative(repoDir, file).replaceAll("\\", "/") : "spec"}: ${message}${rule ? ` [${rule}]` : ""}`);
13
+ };
14
+ const report = (entryCount) => {
15
+ // The same problem can be found twice, such as a term that cites one finding in two places.
16
+ const unique = [...new Set(problems)];
17
+ if (unique.length) {
18
+ for (const p of unique)
19
+ console.error(p);
20
+ console.error(`\n${unique.length} problem(s) in ${entryCount} spec entries.`);
21
+ if (citedRule)
22
+ console.error("A label in brackets, such as [STATUS-14], names the rule of the documentation standard that the problem breaks. The standard opens it with the heading ###### STATUS-14, anchored at https://dinorefurb.com/documentation-standard/#status-14 and at #status-14 in a vendored copy.");
23
+ process.exit(1);
24
+ }
25
+ };
26
+ return { problem, report };
27
+ }