@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,124 @@
1
+ // Build manifests, which list a build's files, and the lists of the other files of its installation.
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { readText } from "../markdown.js";
5
+ import { locationRule, unlistedFormat } from "../standard.js";
6
+ import { parseYaml } from "../yaml.js";
7
+ /**
8
+ * Reads and checks the manifest of every build entry, and reports a manifest or list of other files
9
+ * that belongs to no build. Returns build ID -> the items of its files list that are maps.
10
+ */
11
+ export function loadBuildFiles({ config, problem }, entries) {
12
+ const { specDir } = config;
13
+ // Build manifests: builds/<ID>.files.yaml holds a build's files list and nothing else.
14
+ const buildFiles = new Map();
15
+ for (const [id, e] of entries) {
16
+ if (e.kind !== "BLD")
17
+ continue;
18
+ const expected = `${id}.files.yaml`;
19
+ if ("files" in e.meta)
20
+ problem(e.file, `the files list belongs in the manifest ${expected}, not in the entry`);
21
+ if (e.meta.manifest !== expected) {
22
+ problem(e.file, `manifest must be ${expected}`);
23
+ continue;
24
+ }
25
+ const path = join(dirname(e.file), expected);
26
+ if (!existsSync(path)) {
27
+ problem(e.file, `manifest ${expected} does not exist`);
28
+ continue;
29
+ }
30
+ const manifest = parseYaml(readText(path), path, problem);
31
+ for (const key of Object.keys(manifest))
32
+ if (key !== "files")
33
+ problem(path, `a manifest has only the key files, not ${key}`);
34
+ if (!Array.isArray(manifest.files)) {
35
+ problem(path, "files must be a list");
36
+ continue;
37
+ }
38
+ // Later checks read f.path, so an item that is not a map is reported and left out.
39
+ if (manifest.files.some((f) => !f || typeof f !== "object"))
40
+ problem(path, "every item of files is a map of path, format, size and xxh3");
41
+ const files = manifest.files.filter((f) => f && typeof f === "object");
42
+ buildFiles.set(id, files);
43
+ for (const f of files) {
44
+ if (!f.path)
45
+ problem(path, "every file has a path");
46
+ if (f.format === undefined || f.format === null || f.format === "")
47
+ problem(path, `${f.path}: every file has a format`);
48
+ else if (!locationRule(f.format))
49
+ problem(path, `${f.path}: ${unlistedFormat(f.format)}`);
50
+ const unpackedFormat = f.unpacked?.format;
51
+ if (unpackedFormat !== undefined &&
52
+ unpackedFormat !== null &&
53
+ unpackedFormat !== "" &&
54
+ !locationRule(unpackedFormat))
55
+ problem(path, `${f.path}: unpacked ${unlistedFormat(unpackedFormat)}`);
56
+ if (!/^[0-9a-f]{32}$/.test(String(f.xxh3)))
57
+ problem(path, `${f.path}: xxh3 must be 32 lower-case hex digits`);
58
+ if (typeof f.size !== "number")
59
+ problem(path, `${f.path}: size must be a number`);
60
+ if (f.packer && !(f.unpacked && f.unpacked.size && f.unpacked.xxh3 && f.unpacked.format && f.unpacked.tool))
61
+ problem(path, `${f.path}: a packed file gives the size, xxh3, format and tool of its unpacked form`);
62
+ if (String(f.path).includes("\\"))
63
+ problem(path, `${f.path}: paths use forward slashes`);
64
+ }
65
+ }
66
+ if (existsSync(join(specDir, "builds")))
67
+ for (const name of readdirSync(join(specDir, "builds"))) {
68
+ const m = /^(.+)\.(?:other-)?files\.yaml$/.exec(name);
69
+ if (m && entries.get(m[1])?.kind !== "BLD")
70
+ problem(join(specDir, "builds", name), `belongs to no build entry (${m[1]})`);
71
+ }
72
+ return buildFiles;
73
+ }
74
+ /** Checks each build's list of other files against its manifest and its Other files section. */
75
+ export function checkOtherFiles({ problem }, entries, buildFiles) {
76
+ // Other files: every path of the installation's listing that the manifest leaves out, each with
77
+ // its reason, in the build entry's Other files section or, for a long list, in
78
+ // builds/<ID>.other-files.yaml, which that section names.
79
+ for (const [id, e] of entries) {
80
+ if (e.kind !== "BLD")
81
+ continue;
82
+ const name = `${id}.other-files.yaml`;
83
+ const path = join(dirname(e.file), name);
84
+ const named = (e.sections.find((s) => s.title === "Other files")?.text ?? "").includes(name);
85
+ if (!existsSync(path)) {
86
+ if (named)
87
+ problem(e.file, `Other files names ${name}, which does not exist`);
88
+ continue;
89
+ }
90
+ if (!named)
91
+ problem(e.file, `the Other files section names ${name}, which lists the paths the manifest leaves out`);
92
+ const list = parseYaml(readText(path), path, problem);
93
+ for (const key of Object.keys(list))
94
+ if (key !== "other_files")
95
+ problem(path, `a list of other files has only the key other_files, not ${key}`);
96
+ if (!Array.isArray(list.other_files)) {
97
+ problem(path, "other_files must be a list");
98
+ continue;
99
+ }
100
+ // The front matter reader turns a bare name such as 1990 into a number, so paths compare as text.
101
+ const inManifest = new Set((buildFiles.get(id) ?? []).map((f) => String(f.path)));
102
+ const seen = new Set();
103
+ for (const item of list.other_files) {
104
+ if (!item || typeof item !== "object" || Object.keys(item).sort().join(",") !== "path,reason") {
105
+ problem(path, "every item of other_files is a map of path and reason");
106
+ continue;
107
+ }
108
+ if (item.path === null || item.path === undefined || item.path === "") {
109
+ problem(path, "every other file has a path");
110
+ continue;
111
+ }
112
+ const other = String(item.path);
113
+ if (item.reason === null || String(item.reason).trim() === "")
114
+ problem(path, `${other}: every other file gives the reason the manifest leaves it out`);
115
+ if (other.includes("\\"))
116
+ problem(path, `${other}: paths use forward slashes`);
117
+ if (inManifest.has(other))
118
+ problem(path, `${other} is in the manifest, so it is not one of the other files`);
119
+ if (seen.has(other))
120
+ problem(path, `${other} is listed twice`);
121
+ seen.add(other);
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,4 @@
1
+ import type { LoadContext } from "../context.ts";
2
+ import type { CodeRange, Entry, Meta } from "../types.ts";
3
+ /** Reads and checks the Code ranges section of every build entry. Returns build ID -> its ranges. */
4
+ export declare function loadCodeRanges({ problem }: LoadContext, entries: Map<string, Entry>, buildFiles: Map<string, Meta[]>): Map<string, CodeRange[]>;
@@ -0,0 +1,79 @@
1
+ // Code ranges: the half-open ranges of each file that hold code located by offset, each with the
2
+ // finding that shows it. A table File | Range | Overlay | Finding, or None.
3
+ import { asList, idsIn, kindOf } from "../ids.js";
4
+ import { checkOffset, parseOffset } from "../locations.js";
5
+ import { tables } from "../markdown.js";
6
+ import { locationRule } from "../standard.js";
7
+ const CODE_RANGES = ["File", "Range", "Overlay", "Finding"];
8
+ const unticked = (cell) => cell.replace(/^`(.*)`$/, "$1").trim();
9
+ /** Reads and checks the Code ranges section of every build entry. Returns build ID -> its ranges. */
10
+ export function loadCodeRanges({ problem }, entries, buildFiles) {
11
+ const codeRanges = new Map(); // build ID -> [{ file, start, end }]
12
+ for (const [id, e] of entries) {
13
+ if (e.kind !== "BLD")
14
+ continue;
15
+ const section = e.sections.find((s) => s.title === "Code ranges");
16
+ // A missing section is reported with the other sections.
17
+ if (!section)
18
+ continue;
19
+ const ranges = [];
20
+ if (/^\s*None\.\s*$/.test(section.text)) {
21
+ codeRanges.set(id, ranges);
22
+ continue;
23
+ }
24
+ const found = tables(section.text);
25
+ // A malformed section is reported once here; offsets into the build are then not measured
26
+ // against it, as with a missing section, rather than each failing again.
27
+ if (found.length !== 1 || found[0].header.join("|") !== CODE_RANGES.join("|") || found[0].rows.length === 0) {
28
+ problem(e.file, `the Code ranges section is one table with the columns ${CODE_RANGES.join(" | ")}, or None.`);
29
+ continue;
30
+ }
31
+ codeRanges.set(id, ranges);
32
+ const files = buildFiles.get(id) ?? [];
33
+ for (const row of found[0].rows) {
34
+ if (row.length !== CODE_RANGES.length) {
35
+ problem(e.file, `Code ranges row ${row.join(" | ")}: a row has ${CODE_RANGES.length} cells, not ${row.length}`);
36
+ continue;
37
+ }
38
+ const [path, range, overlay, finding] = row.map(unticked);
39
+ const at = `Code ranges row ${path} ${range}`;
40
+ const bf = files.find((f) => f.path === path);
41
+ if (!bf) {
42
+ problem(e.file, `${at}: ${path} is not in the manifest`);
43
+ continue;
44
+ }
45
+ // Only overlay code is located by offset, so a row is for a file whose unpacked format takes
46
+ // both addresses and offsets (MZ). An unlisted format is already reported against its manifest.
47
+ const format = bf.unpacked?.format ?? bf.format;
48
+ const rule = locationRule(format);
49
+ if (rule && !(rule.offset && rule.address))
50
+ problem(e.file, `${at}: ${path} is a ${format} file, which holds no code located by offset`);
51
+ // The notation is parseOffset's; a row additionally needs both ends of the range.
52
+ if (!range.includes("..") || !parseOffset(range)) {
53
+ problem(e.file, `${at}: the range is one half-open offset range, 0x followed by upper-case hex digits on each side of ..`);
54
+ continue;
55
+ }
56
+ if (!/^(?:-|\d+|0x[0-9A-F]+)$/.test(overlay))
57
+ problem(e.file, `${at}: the overlay is its number, or - where there is none`);
58
+ // A finding that does not exist is reported with the other unresolved IDs of the body.
59
+ const ids = idsIn(finding);
60
+ const cited = entries.get(ids[0]);
61
+ if (ids.length !== 1 || kindOf(ids[0]) !== "FND" || finding !== ids[0])
62
+ problem(e.file, `${at}: the finding column holds the ID of one finding`);
63
+ else if (cited && !asList(cited.meta.builds).includes(id))
64
+ problem(e.file, `${at}: ${ids[0]} does not list ${id}`);
65
+ else if (cited?.meta.status === "superseded")
66
+ problem(e.file, `${at}: cites ${ids[0]}, which is superseded`, "STATUS-17");
67
+ // The finding shows code in this file of this build, so it has a location there that is not
68
+ // file data. One with no locations there, or only file data there, shows no code in the row.
69
+ else if (cited &&
70
+ !asList(cited.meta.locations).some((loc) => loc?.build === id && loc?.file === path && loc?.kind !== "file-data")) {
71
+ problem(e.file, `${at}: ${ids[0]} has no code location in ${path} of ${id}, so it cannot establish a code range there`);
72
+ }
73
+ const parsed = checkOffset(problem, e.file, range, bf);
74
+ if (parsed)
75
+ ranges.push({ file: path, start: parsed[0], end: parsed[1] });
76
+ }
77
+ }
78
+ return codeRanges;
79
+ }
@@ -0,0 +1,7 @@
1
+ import type { LoadContext } from "../context.ts";
2
+ import type { Entry } from "../types.ts";
3
+ /**
4
+ * Reads every entry under spec/, by ID, and reports a directory of spec/ the standard does not
5
+ * define. An entry without an ID or of an unknown kind is reported and left out.
6
+ */
7
+ export declare function loadEntries({ config, problem }: LoadContext): Map<string, Entry>;
@@ -0,0 +1,57 @@
1
+ // The entries: one Markdown file per ID in the directory of its kind.
2
+ import { readdirSync, statSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { isAlias, kindOf } from "../ids.js";
5
+ import { readEntry } from "../markdown.js";
6
+ import { KINDS } from "../standard.js";
7
+ /**
8
+ * Reads every entry under spec/, by ID, and reports a directory of spec/ the standard does not
9
+ * define. An entry without an ID or of an unknown kind is reported and left out.
10
+ */
11
+ export function loadEntries({ config, problem }) {
12
+ const { specDir } = config;
13
+ const entries = new Map();
14
+ for (const [kind, { dir }] of Object.entries(KINDS)) {
15
+ const d = join(specDir, dir);
16
+ if (!existsSync(d))
17
+ continue;
18
+ for (const name of readdirSync(d)) {
19
+ const file = join(d, name);
20
+ if (statSync(file).isDirectory() || !name.endsWith(".md"))
21
+ continue;
22
+ const read = readEntry(file, problem);
23
+ if (!read)
24
+ continue;
25
+ const entry = Object.assign(read, { kind });
26
+ const id = entry.meta.id;
27
+ if (typeof id !== "string") {
28
+ problem(file, "has no id", "IDENTIFIERS-1");
29
+ continue;
30
+ }
31
+ if (name !== `${id}.md`)
32
+ problem(file, `file name must be ${id}.md`);
33
+ // Later checks look the kind up in KINDS, so an entry of an unknown kind is reported and dropped.
34
+ if (!KINDS[kindOf(id)]) {
35
+ problem(file, `${id} is not an ID of a known kind`, "IDENTIFIERS-1");
36
+ continue;
37
+ }
38
+ if (kindOf(id) !== kind)
39
+ problem(file, `a ${kindOf(id)} entry does not belong in spec/${dir}/`);
40
+ // IDENTIFIERS-3 makes a number unique within its kind and area, and IDENTIFIERS-4 an alias.
41
+ if (entries.has(id))
42
+ problem(file, `ID ${id} is used twice`, isAlias(id) ? "IDENTIFIERS-4" : "IDENTIFIERS-3");
43
+ entries.set(id, entry);
44
+ }
45
+ }
46
+ // Stray Markdown anywhere else in spec/ that looks like an entry.
47
+ for (const dir of readdirSync(specDir)) {
48
+ const d = join(specDir, dir);
49
+ if (!statSync(d).isDirectory() ||
50
+ Object.values(KINDS).some((k) => k.dir === dir) ||
51
+ dir === "index" ||
52
+ dir === "glossary")
53
+ continue;
54
+ problem(d, "is not a directory the standard defines");
55
+ }
56
+ return entries;
57
+ }
@@ -0,0 +1,12 @@
1
+ import type { LoadContext } from "../context.ts";
2
+ /** The glossary as read from spec/glossary/ and the --glossary drafts. */
3
+ export interface Glossary {
4
+ /** spec/glossary/. */
5
+ glossaryDir: string;
6
+ /** Term -> the text after its heading. */
7
+ glossary: Map<string, string>;
8
+ /** Term -> its file in spec/glossary/. */
9
+ glossaryFiles: Map<string, string>;
10
+ }
11
+ /** Reads and checks spec/glossary/, then adds the terms of the --glossary drafts. */
12
+ export declare function loadGlossary({ config, problem }: LoadContext): Glossary;
@@ -0,0 +1,70 @@
1
+ // The glossary: one file per term in spec/glossary/, named after the term and opening with it as a
2
+ // # heading. A glossary file is not an entry, so it has no front matter.
3
+ import { existsSync, statSync } from "node:fs";
4
+ import { basename, join } from "node:path";
5
+ import { termFiles } from "../files.js";
6
+ import { readText } from "../markdown.js";
7
+ import { RESERVED_NAMES } from "../standard.js";
8
+ function readTerm(problem, file, report = true) {
9
+ const text = readText(file);
10
+ const say = (message) => {
11
+ if (report)
12
+ problem(file, message);
13
+ };
14
+ if (text.startsWith("---\n"))
15
+ say("a glossary file has no front matter");
16
+ const m = /^# (.+)\n?([\s\S]*)$/.exec(text.replace(/^---\n[\s\S]*?\n---\n/, "").replace(/^\s+/, ""));
17
+ if (!m) {
18
+ say("opens with the term as a # heading");
19
+ return null;
20
+ }
21
+ const term = m[1].trim().replaceAll("`", "");
22
+ if (basename(file) !== `${term}.md`)
23
+ say(`is named after its term, ${term}.md`);
24
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(term))
25
+ say(`${term} is not a name the pseudocode can use`);
26
+ if (RESERVED_NAMES.test(term))
27
+ say(`${term} is a name Windows reserves for a device, so no file can have it`);
28
+ return { term, text: m[2] };
29
+ }
30
+ /** Reads and checks spec/glossary/, then adds the terms of the --glossary drafts. */
31
+ export function loadGlossary({ config, problem }) {
32
+ const { specDir } = config;
33
+ const glossaryDir = join(specDir, "glossary");
34
+ const glossary = new Map(); // term -> text after the heading
35
+ const glossaryFiles = new Map(); // term -> file
36
+ if (existsSync(join(specDir, "glossary.md")))
37
+ problem(join(specDir, "glossary.md"), "the glossary is the directory spec/glossary/; move each ## term to spec/glossary/<term>.md with the term as its # heading");
38
+ if (!existsSync(glossaryDir))
39
+ problem(null, "spec/glossary/ is missing");
40
+ else {
41
+ const folded = new Map();
42
+ for (const file of termFiles(glossaryDir)) {
43
+ if (statSync(file).isDirectory() || !file.endsWith(".md")) {
44
+ problem(file, "is not a glossary file; spec/glossary/ holds one <term>.md per term");
45
+ continue;
46
+ }
47
+ const t = readTerm(problem, file);
48
+ if (!t)
49
+ continue;
50
+ const key = t.term.toLowerCase();
51
+ if (folded.has(key))
52
+ problem(file, `${t.term} differs only in case from ${folded.get(key)}`);
53
+ else
54
+ folded.set(key, t.term);
55
+ glossary.set(t.term, t.text);
56
+ glossaryFiles.set(t.term, file);
57
+ }
58
+ }
59
+ // --glossary <path> adds the terms of a draft term file, or of a directory of them, for checking
60
+ // entries before their terms are merged into spec/glossary/.
61
+ for (const draft of config.glossaryDrafts) {
62
+ const files = statSync(draft).isDirectory() ? termFiles(draft).filter((f) => f.endsWith(".md")) : [draft];
63
+ for (const file of files) {
64
+ const t = readTerm(problem, file, false);
65
+ if (t && !glossary.has(t.term))
66
+ glossary.set(t.term, t.text);
67
+ }
68
+ }
69
+ return { glossaryDir, glossary, glossaryFiles };
70
+ }
@@ -0,0 +1,3 @@
1
+ import type { LoadContext } from "../context.ts";
2
+ /** Checks spec/README.md and returns its areas, in the order it lists them. */
3
+ export declare function loadAreas({ config, problem }: LoadContext): string[];
@@ -0,0 +1,36 @@
1
+ // spec/README.md: its sections and the area list.
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { splitSections, tables } from "../markdown.js";
5
+ /** Checks spec/README.md and returns its areas, in the order it lists them. */
6
+ export function loadAreas({ config, problem }) {
7
+ const { specDir } = config;
8
+ const readme = existsSync(join(specDir, "README.md"))
9
+ ? readFileSync(join(specDir, "README.md"), "utf8").replace(/\r\n/g, "\n")
10
+ : "";
11
+ if (!readme)
12
+ problem(null, "spec/README.md is missing");
13
+ const areas = [];
14
+ const readmeSections = splitSections(readme);
15
+ const titles = readmeSections.map((s) => s.title);
16
+ const expected = ["Scope", "Standard version", "Areas"];
17
+ if (titles.join("|") !== expected.join("|"))
18
+ problem(join(specDir, "README.md"), `sections must be ${expected.join(", ")} in that order, found ${titles.join(", ")}`);
19
+ const areaSection = readmeSections.find((s) => s.title === "Areas");
20
+ const areaTable = areaSection && tables(areaSection.text)[0];
21
+ if (!areaTable || areaTable.header.join("|") !== "Area|Covers")
22
+ problem(join(specDir, "README.md"), "the area list must be a table with the columns Area | Covers");
23
+ else
24
+ for (const row of areaTable.rows) {
25
+ const a = row[0].replaceAll("`", "");
26
+ if (!/^[A-Z][A-Z0-9]*$/.test(a))
27
+ problem(join(specDir, "README.md"), `area ${a} must be upper-case letters and digits starting with a letter`, "IDENTIFIERS-2");
28
+ if (areas.includes(a))
29
+ problem(join(specDir, "README.md"), `area ${a} is listed twice`);
30
+ areas.push(a);
31
+ }
32
+ const version = readmeSections.find((s) => s.title === "Standard version");
33
+ if (version && !/version 1 of the/.test(version.text))
34
+ problem(join(specDir, "README.md"), "the Standard version section must say which version it follows (version 1)");
35
+ return areas;
36
+ }
@@ -0,0 +1,6 @@
1
+ import type { LoadContext, Spec } from "../context.ts";
2
+ /**
3
+ * Reads spec/ and reports what is wrong with its layout, the README, the glossary and the build
4
+ * manifests. Exits with 1 when there is no spec/ directory.
5
+ */
6
+ export declare function loadSpec(ctx: LoadContext): Spec;
@@ -0,0 +1,25 @@
1
+ // Reading the spec: the README's areas, the entries, the glossary and what the build entries name.
2
+ import { existsSync } from "node:fs";
3
+ import { checkOtherFiles, loadBuildFiles } from "./builds.js";
4
+ import { loadCodeRanges } from "./code-ranges.js";
5
+ import { loadEntries } from "./entries.js";
6
+ import { loadGlossary } from "./glossary.js";
7
+ import { loadAreas } from "./readme.js";
8
+ /**
9
+ * Reads spec/ and reports what is wrong with its layout, the README, the glossary and the build
10
+ * manifests. Exits with 1 when there is no spec/ directory.
11
+ */
12
+ export function loadSpec(ctx) {
13
+ const { repoDir, specDir } = ctx.config;
14
+ if (!existsSync(specDir)) {
15
+ console.error(`No spec/ directory in ${repoDir}.`);
16
+ process.exit(1);
17
+ }
18
+ const areas = loadAreas(ctx);
19
+ const entries = loadEntries(ctx);
20
+ const { glossaryDir, glossary, glossaryFiles } = loadGlossary(ctx);
21
+ const buildFiles = loadBuildFiles(ctx, entries);
22
+ checkOtherFiles(ctx, entries, buildFiles);
23
+ const codeRanges = loadCodeRanges(ctx, entries, buildFiles);
24
+ return { areas, entries, glossaryDir, glossary, glossaryFiles, buildFiles, codeRanges };
25
+ }
@@ -0,0 +1,19 @@
1
+ import type { Problem } from "./problems.ts";
2
+ import type { Meta, Yaml } from "./types.ts";
3
+ /**
4
+ * Reports an address that is not in the notation of format. An unlisted format is already reported
5
+ * against its manifest, so it is skipped here.
6
+ */
7
+ export declare function checkAddress(problem: Problem, file: string, value: Yaml, format: Yaml): void;
8
+ /**
9
+ * An offset names one byte, or a half-open range of two: 0x20..0x3C covers 0x20 up to but not
10
+ * including 0x3C. Returns [start, end) as BigInts, or null when the notation is wrong.
11
+ */
12
+ export declare function parseOffset(value: Yaml): [bigint, bigint] | null;
13
+ /**
14
+ * An offset is into the shipped file bf, so the bytes it covers lie within bf.size. Returns the
15
+ * parsed range when it is well formed.
16
+ * `bf` is the file the offset is into, given as { path, size }: the shipped file, or the unpacked
17
+ * form of a packed one.
18
+ */
19
+ export declare function checkOffset(problem: Problem, file: string, value: Yaml, bf: Meta, what?: string): [bigint, bigint] | null;
@@ -0,0 +1,57 @@
1
+ // Addresses and offsets, the two ways a location in a build's file is given.
2
+ import { locationRule } from "./standard.js";
3
+ /**
4
+ * Reports an address that is not in the notation of format. An unlisted format is already reported
5
+ * against its manifest, so it is skipped here.
6
+ */
7
+ export function checkAddress(problem, file, value, format) {
8
+ const rule = locationRule(format);
9
+ if (!rule)
10
+ return;
11
+ const re = rule.address;
12
+ if (!re) {
13
+ problem(file, `an address cannot be given in a file of format ${format}; use offset`);
14
+ return;
15
+ }
16
+ const parts = String(value).split("..");
17
+ if (parts.length > 2 || parts.some((p) => !re.test(p)))
18
+ problem(file, `address ${value} is not in the notation for a ${format} file`);
19
+ }
20
+ /**
21
+ * An offset names one byte, or a half-open range of two: 0x20..0x3C covers 0x20 up to but not
22
+ * including 0x3C. Returns [start, end) as BigInts, or null when the notation is wrong.
23
+ */
24
+ export function parseOffset(value) {
25
+ const parts = String(value).split("..");
26
+ if (parts.length > 2 || parts.some((p) => !/^0x[0-9A-F]{2,}$/.test(p)))
27
+ return null;
28
+ const [start, end] = parts.map((p) => BigInt(p));
29
+ return [start, end ?? start + 1n];
30
+ }
31
+ /**
32
+ * An offset is into the shipped file bf, so the bytes it covers lie within bf.size. Returns the
33
+ * parsed range when it is well formed.
34
+ * `bf` is the file the offset is into, given as { path, size }: the shipped file, or the unpacked
35
+ * form of a packed one.
36
+ */
37
+ export function checkOffset(problem, file, value, bf, what = "shipped file") {
38
+ const range = parseOffset(value);
39
+ if (!range) {
40
+ problem(file, `offset ${value} must be 0x followed by at least two upper-case hex digits, or a range of two`);
41
+ return null;
42
+ }
43
+ const [start, end] = range;
44
+ if (start > end) {
45
+ problem(file, "offset range is reversed");
46
+ return null;
47
+ }
48
+ if (start === end) {
49
+ problem(file, `offset range ${value} is empty; a range is half-open`);
50
+ return null;
51
+ }
52
+ if (Number.isSafeInteger(bf.size) && bf.size >= 0 && end > BigInt(bf.size)) {
53
+ problem(file, `offset ${value} is outside the ${what} ${bf.path} (${bf.size} bytes)`);
54
+ return null;
55
+ }
56
+ return range;
57
+ }
@@ -0,0 +1,27 @@
1
+ import type { Problem } from "./problems.ts";
2
+ import type { Section, Table } from "./types.ts";
3
+ /** The number of lines of text, not counting the empty one after a final newline. */
4
+ export declare const lineCount: (text: string) => number;
5
+ /** Reads a text file with CRLF line ends read as LF. */
6
+ export declare const readText: (file: string) => string;
7
+ /** Reads an entry's front matter, body and sections, or reports why it cannot and returns null. */
8
+ export declare function readEntry(file: string, problem: Problem): {
9
+ file: string;
10
+ meta: import("./types.ts").Meta;
11
+ body: string;
12
+ sections: Section[];
13
+ } | null;
14
+ /** The `##` sections of a Markdown body, skipping fenced code. */
15
+ export declare function splitSections(body: string): Section[];
16
+ /** Every Markdown table in a section, with the `###` heading above it. */
17
+ export declare function tables(text: string): Table[];
18
+ /** The cells of a table row, with `\|` read as a pipe and pipes inside code spans kept. */
19
+ export declare function cells(line: string): string[];
20
+ /**
21
+ * A value file: CSV as RFC 4180 defines it, with a header row. Returns { header, rows }, or null
22
+ * after reporting a problem.
23
+ */
24
+ export declare function readCsv(file: string, problem: Problem): {
25
+ header: string[];
26
+ rows: string[][];
27
+ } | null;