@scientific-method/standard-checker 0.1.0 → 0.2.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 (77) 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 +130 -0
  14. package/dist/checks/evidence-entries.d.ts +6 -0
  15. package/dist/checks/evidence-entries.js +167 -0
  16. package/dist/checks/formats.d.ts +13 -0
  17. package/dist/checks/formats.js +194 -0
  18. package/dist/checks/kaitai.d.ts +6 -0
  19. package/dist/checks/kaitai.js +103 -0
  20. package/dist/checks/parity.d.ts +30 -0
  21. package/dist/checks/parity.js +189 -0
  22. package/dist/checks/references.d.ts +4 -0
  23. package/dist/checks/references.js +37 -0
  24. package/dist/checks/rules.d.ts +4 -0
  25. package/dist/checks/rules.js +259 -0
  26. package/dist/checks/screens.d.ts +4 -0
  27. package/dist/checks/screens.js +44 -0
  28. package/dist/checks/validation.d.ts +7 -0
  29. package/dist/checks/validation.js +116 -0
  30. package/dist/code-comments.d.ts +3 -0
  31. package/dist/code-comments.js +150 -0
  32. package/dist/code-files.d.ts +11 -0
  33. package/dist/code-files.js +41 -0
  34. package/dist/context.d.ts +34 -0
  35. package/dist/context.js +2 -0
  36. package/dist/evidence.d.ts +37 -0
  37. package/dist/evidence.js +91 -0
  38. package/dist/files.d.ts +11 -0
  39. package/dist/files.js +35 -0
  40. package/dist/generate/indexes.d.ts +3 -0
  41. package/dist/generate/indexes.js +135 -0
  42. package/dist/generate/layout.d.ts +18 -0
  43. package/dist/generate/layout.js +49 -0
  44. package/dist/generate/parity-md.d.ts +7 -0
  45. package/dist/generate/parity-md.js +44 -0
  46. package/dist/generate/write.d.ts +8 -0
  47. package/dist/generate/write.js +75 -0
  48. package/dist/ids.d.ts +13 -0
  49. package/dist/ids.js +22 -0
  50. package/dist/load/builds.d.ts +9 -0
  51. package/dist/load/builds.js +124 -0
  52. package/dist/load/code-ranges.d.ts +4 -0
  53. package/dist/load/code-ranges.js +79 -0
  54. package/dist/load/entries.d.ts +7 -0
  55. package/dist/load/entries.js +57 -0
  56. package/dist/load/glossary.d.ts +12 -0
  57. package/dist/load/glossary.js +70 -0
  58. package/dist/load/readme.d.ts +3 -0
  59. package/dist/load/readme.js +36 -0
  60. package/dist/load/spec.d.ts +6 -0
  61. package/dist/load/spec.js +25 -0
  62. package/dist/locations.d.ts +19 -0
  63. package/dist/locations.js +57 -0
  64. package/dist/markdown.d.ts +27 -0
  65. package/dist/markdown.js +162 -0
  66. package/dist/options.d.ts +34 -0
  67. package/dist/options.js +87 -0
  68. package/dist/problems.d.ts +27 -0
  69. package/dist/problems.js +27 -0
  70. package/dist/standard-checker.js +51 -3033
  71. package/dist/standard.d.ts +65 -0
  72. package/dist/standard.js +173 -0
  73. package/dist/types.d.ts +91 -0
  74. package/dist/types.js +2 -0
  75. package/dist/yaml.d.ts +6 -0
  76. package/dist/yaml.js +173 -0
  77. package/package.json +1 -1
@@ -0,0 +1,116 @@
1
+ // VALIDATION.md records the marked test files of the validated rows as they were when a maintainer ran
2
+ // them against the original's files, which CI never holds. A file is hashed with CRLF read as LF,
3
+ // so a Windows checkout and a Linux one give the same hash.
4
+ import { execFileSync } from "node:child_process";
5
+ import { createHash } from "node:crypto";
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { readText, tables } from "../markdown.js";
9
+ const VALIDATION_HEADER = ["Test file", "SHA-256"];
10
+ const testHash = (p) => createHash("sha256")
11
+ .update(Buffer.from(readFileSync(p).toString("latin1").replaceAll("\r\n", "\n"), "latin1"))
12
+ .digest("hex");
13
+ /**
14
+ * With --record-validation, writes VALIDATION.md (or prints why it cannot and exits with 2). Then
15
+ * checks VALIDATION.md against the marked test files of the validated rows.
16
+ */
17
+ export function checkValidation(ctx, { validatedTests }) {
18
+ const { problem } = ctx;
19
+ const { entries } = ctx.spec;
20
+ const { repoDir } = ctx.config;
21
+ const validationPath = join(repoDir, "VALIDATION.md");
22
+ if (ctx.config.recordValidation !== undefined) {
23
+ const builds = ctx.config.recordValidation;
24
+ if (!builds.length) {
25
+ console.error("--record-validation needs at least one build ID");
26
+ process.exit(2);
27
+ }
28
+ for (const b of builds)
29
+ if (entries.get(b)?.kind !== "BLD") {
30
+ console.error(`--record-validation: ${b} is not a build entry`);
31
+ process.exit(2);
32
+ }
33
+ let commit;
34
+ try {
35
+ commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: repoDir, encoding: "utf8" }).trim();
36
+ }
37
+ catch {
38
+ console.error("--record-validation: git rev-parse HEAD failed");
39
+ process.exit(2);
40
+ }
41
+ const files = [...validatedTests.keys()].sort();
42
+ if (!files.length) {
43
+ console.error('--record-validation: no validated row lists a test file with a "needs: GAME_DIR" comment, so there is nothing to record');
44
+ process.exit(2);
45
+ }
46
+ writeFileSync(validationPath, [
47
+ "# Validation record",
48
+ "",
49
+ "The test files of the validated parity rows that read the original's files, as they were when every test in them passed against those files.",
50
+ "",
51
+ `- Commit: ${commit}`,
52
+ `- Date: ${new Date().toISOString().slice(0, 10)}`,
53
+ `- Builds: ${builds.join(", ")}`,
54
+ "",
55
+ `| ${VALIDATION_HEADER.join(" | ")} |`,
56
+ `|${"---|".repeat(VALIDATION_HEADER.length)}`,
57
+ ...files.map((f) => `| \`${f}\` | \`${testHash(join(repoDir, f))}\` |`),
58
+ "",
59
+ ].join("\n"));
60
+ console.log("wrote VALIDATION.md");
61
+ }
62
+ {
63
+ const recorded = new Map(); // test file -> hash
64
+ if (existsSync(validationPath)) {
65
+ const text = readText(validationPath);
66
+ const item = (key, pattern) => {
67
+ const m = text.match(new RegExp(`^- ${key}: (.*)$`, "m"));
68
+ if (!m || !pattern.test(m[1].trim())) {
69
+ problem(validationPath, `needs a "- ${key}:" item in the form the standard gives`);
70
+ return null;
71
+ }
72
+ return m[1].trim();
73
+ };
74
+ item("Commit", /^[0-9a-f]{40}$/);
75
+ item("Date", /^\d{4}-\d{2}-\d{2}$/);
76
+ const builds = item("Builds", /^\S.*$/);
77
+ if (builds !== null)
78
+ for (const b of builds.split(",").map((x) => x.trim()))
79
+ if (entries.get(b)?.kind !== "BLD")
80
+ problem(validationPath, `Builds names ${b}, which is not a build entry`);
81
+ const ts = tables(text);
82
+ if (ts.length !== 1 || ts[0].header.join("|") !== VALIDATION_HEADER.join("|"))
83
+ problem(validationPath, `holds one table with the columns ${VALIDATION_HEADER.join(" | ")}`);
84
+ else {
85
+ let previous = "";
86
+ for (const row of ts[0].rows) {
87
+ const [path, hash] = row.map((c) => c.replaceAll("`", "").trim());
88
+ if (row.length !== VALIDATION_HEADER.length || !/^[0-9a-f]{64}$/.test(hash ?? "")) {
89
+ problem(validationPath, `the row ${row.join(" | ")} needs a test file and its SHA-256 in lowercase hex`);
90
+ continue;
91
+ }
92
+ if (recorded.has(path))
93
+ problem(validationPath, `${path} is listed twice`);
94
+ if (path < previous)
95
+ problem(validationPath, `${path} is out of order; the files are sorted by path`);
96
+ previous = path;
97
+ recorded.set(path, hash);
98
+ if (!validatedTests.has(path))
99
+ problem(validationPath, `${path} is not a test file with a "needs: GAME_DIR" comment in a validated row's Tests; run the check with --record-validation again`);
100
+ }
101
+ }
102
+ }
103
+ for (const [tf, rows] of validatedTests) {
104
+ const p = join(repoDir, tf);
105
+ if (!existsSync(p))
106
+ continue;
107
+ const hash = recorded.get(tf);
108
+ for (const { specId, file } of rows) {
109
+ if (hash === undefined)
110
+ problem(file, `${specId}: ${tf} is not in VALIDATION.md, so the row cannot be validated until its tests pass against the original's files and are recorded`);
111
+ else if (hash !== testHash(p))
112
+ problem(file, `${specId}: ${tf} has changed since VALIDATION.md recorded it; run its tests against the original's files and record them again`);
113
+ }
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,3 @@
1
+ import type { CodeLine } from "./types.ts";
2
+ /** Reads the comments of a C-family source file line by line, as the comment above describes. */
3
+ export declare function codeComments(source: string, javascript: boolean): CodeLine[];
@@ -0,0 +1,150 @@
1
+ // Reading the comments of C#, TypeScript and JavaScript source.
2
+ // The comments of a C-family source file (C#, TypeScript, JavaScript), line by line: for each
3
+ // line, whether it holds code and the comment text on it with the column where each piece starts.
4
+ // String and character literals are skipped (regular, verbatim, interpolated and raw in C#;
5
+ // template literals in JavaScript), so a `//` inside one starts no comment. An interpolation hole is
6
+ // read as part of its string, which is enough to find comments. A JavaScript regular expression
7
+ // literal is skipped too, so a quote or a `/*` inside one starts nothing; a `/` is read as one
8
+ // where a value can begin. The lines of a `/* … */` after its first are marked continued.
9
+ /** Reads the comments of a C-family source file line by line, as the comment above describes. */
10
+ export function codeComments(source, javascript) {
11
+ const text = source.replace(/\r\n?/g, "\n");
12
+ const lines = [{ code: false, comments: [] }];
13
+ const line = () => lines[lines.length - 1];
14
+ let i = 0, column = 0;
15
+ const advance = (to) => {
16
+ for (; i < to; i++) {
17
+ if (text[i] === "\n") {
18
+ lines.push({ code: false, comments: [] });
19
+ column = 0;
20
+ }
21
+ else
22
+ column++;
23
+ }
24
+ };
25
+ const addComment = (from, to, col, continued = false) => line().comments.push({ column: col, text: text.slice(from, to), continued });
26
+ let lastCode = -1; // the index of the last character read as code
27
+ // A JavaScript `/` starts a regular expression unless it follows a value, where it divides.
28
+ const regexMayStart = () => {
29
+ const before = text.slice(Math.max(0, lastCode - 11), lastCode + 1);
30
+ return (lastCode < 0 ||
31
+ !/[\w$)\]}]$/.test(before) ||
32
+ /(?<![\w$])(?:return|typeof|case|do|else|in|of|new|delete|void|throw|instanceof|yield|await)$/.test(before));
33
+ };
34
+ // Past the closing `/` of the regular expression literal at i, if it closes on its line.
35
+ const regexEnd = () => {
36
+ let inClass = false;
37
+ for (let j = i + 1; j < text.length && text[j] !== "\n"; j++) {
38
+ if (text[j] === "\\") {
39
+ if (text[j + 1] === "\n")
40
+ return -1;
41
+ j++;
42
+ }
43
+ else if (text[j] === "[")
44
+ inClass = true;
45
+ else if (text[j] === "]")
46
+ inClass = false;
47
+ else if (text[j] === "/" && !inClass)
48
+ return j + 1;
49
+ }
50
+ return -1;
51
+ };
52
+ // i is past the opening quote(s); stops past the closing one(s). A regular string ends at the
53
+ // line when it is not closed.
54
+ const skipString = (end, escapes, multiline) => {
55
+ while (i < text.length) {
56
+ if (escapes && text[i] === "\\") {
57
+ advance(i + 2);
58
+ continue;
59
+ }
60
+ if (text.startsWith(end, i)) {
61
+ if (!escapes && end === '"' && text[i + 1] === '"') {
62
+ advance(i + 2);
63
+ continue;
64
+ } // "" in a verbatim string
65
+ advance(i + end.length);
66
+ return;
67
+ }
68
+ if (text[i] === "\n" && !multiline) {
69
+ advance(i + 1);
70
+ return;
71
+ }
72
+ advance(i + 1);
73
+ }
74
+ };
75
+ while (i < text.length) {
76
+ const c = text[i];
77
+ if (c === "\n" || c === " " || c === "\t") {
78
+ advance(i + 1);
79
+ continue;
80
+ }
81
+ if (text.startsWith("//", i)) {
82
+ const nl = text.indexOf("\n", i);
83
+ const end = nl < 0 ? text.length : nl;
84
+ addComment(i, end, column);
85
+ advance(end);
86
+ continue;
87
+ }
88
+ if (text.startsWith("/*", i)) {
89
+ const close = text.indexOf("*/", i + 2);
90
+ const end = close < 0 ? text.length : close + 2;
91
+ let from = i, col = column, continued = false;
92
+ while (i < end) {
93
+ const nl = text.indexOf("\n", i);
94
+ if (nl < 0 || nl >= end) {
95
+ addComment(from, end, col, continued);
96
+ advance(end);
97
+ break;
98
+ }
99
+ addComment(from, nl, col, continued);
100
+ advance(nl + 1);
101
+ continued = true;
102
+ while (i < end && (text[i] === " " || text[i] === "\t"))
103
+ advance(i + 1);
104
+ from = i;
105
+ col = column;
106
+ }
107
+ continue;
108
+ }
109
+ line().code = true;
110
+ if (javascript) {
111
+ const regex = c === "/" && regexMayStart() ? regexEnd() : -1;
112
+ if (regex >= 0)
113
+ advance(regex);
114
+ else if (c === '"' || c === "'") {
115
+ advance(i + 1);
116
+ skipString(c, true, false);
117
+ }
118
+ else if (c === "`") {
119
+ advance(i + 1);
120
+ skipString("`", true, true);
121
+ }
122
+ else
123
+ advance(i + 1);
124
+ lastCode = i - 1;
125
+ continue;
126
+ }
127
+ const prefix = /^(?:\$+@?|@\$*)?(?=")/.exec(text.slice(i, i + 4))?.[0] ?? null;
128
+ if (prefix !== null) {
129
+ advance(i + prefix.length);
130
+ const quotes = /^"{3,}/.exec(text.slice(i, i + 64))?.[0];
131
+ if (quotes) {
132
+ advance(i + quotes.length);
133
+ skipString(quotes, false, true);
134
+ }
135
+ else {
136
+ const verbatim = prefix.includes("@");
137
+ advance(i + 1);
138
+ skipString('"', !verbatim, verbatim);
139
+ }
140
+ continue;
141
+ }
142
+ if (c === "'") {
143
+ advance(i + 1);
144
+ skipString("'", true, false);
145
+ continue;
146
+ }
147
+ advance(i + 1);
148
+ }
149
+ return lines;
150
+ }
@@ -0,0 +1,11 @@
1
+ import type { Config } from "./options.ts";
2
+ import type { CodeFile } from "./types.ts";
3
+ /**
4
+ * Returns a function that gives the files of the --code and --references directories, read on its
5
+ * first call. The checker's own source is left out, so a copy of the checker inside one of those
6
+ * directories does not flag the IDs its comments and messages give as examples. checkerDir is the
7
+ * directory that holds the checker's entry point and its modules, and nothing else.
8
+ */
9
+ export declare function createCodeFiles(config: Config, checkerDir: string): () => CodeFile[];
10
+ /** The spec IDs that PLACEHOLDER comments in the --code directories cite. */
11
+ export declare function collectPlaceholders(codeFiles: CodeFile[]): Set<string>;
@@ -0,0 +1,41 @@
1
+ // The --code and --references directories, walked and read once for the placeholder, the
2
+ // implementation-reference and the code comment checks.
3
+ import { readFileSync } from "node:fs";
4
+ import { join, sep } from "node:path";
5
+ import { walk } from "./files.js";
6
+ /**
7
+ * Returns a function that gives the files of the --code and --references directories, read on its
8
+ * first call. The checker's own source is left out, so a copy of the checker inside one of those
9
+ * directories does not flag the IDs its comments and messages give as examples. checkerDir is the
10
+ * directory that holds the checker's entry point and its modules, and nothing else.
11
+ */
12
+ export function createCodeFiles(config, checkerDir) {
13
+ let cache;
14
+ const own = (f) => f.startsWith(checkerDir + sep);
15
+ return () => {
16
+ if (cache)
17
+ return cache;
18
+ const files = [];
19
+ const roots = [
20
+ ...config.codeRoots.map((root) => ({ root, code: true })),
21
+ ...config.referenceRoots.map((root) => ({ root, code: false })),
22
+ ];
23
+ for (const { root, code } of roots)
24
+ walk(join(config.repoDir, root), (f) => {
25
+ if (!own(f) && /\.(cs|ts|mjs|js|ps1|fs|md|json)$/.test(f))
26
+ files.push({ code, file: f, text: readFileSync(f, "utf8") });
27
+ });
28
+ return (cache = files);
29
+ };
30
+ }
31
+ /** The spec IDs that PLACEHOLDER comments in the --code directories cite. */
32
+ export function collectPlaceholders(codeFiles) {
33
+ const found = new Set();
34
+ for (const { code, file, text } of codeFiles) {
35
+ if (!code || !/\.(cs|ts|mjs|js|ps1|fs)$/.test(file))
36
+ continue;
37
+ for (const m of text.matchAll(/PLACEHOLDER:\s*((?:FMT|RULE|SCR)-[A-Z0-9]+-\d+)/g))
38
+ found.add(m[1]);
39
+ }
40
+ return found;
41
+ }
@@ -0,0 +1,34 @@
1
+ import type { Config } from "./options.ts";
2
+ import type { Problem } from "./problems.ts";
3
+ import type { CodeFile, CodeRange, Entry, Meta } from "./types.ts";
4
+ /** The spec as the load phase read it. Later phases add to entries (Entry.code, Entry.valueTables). */
5
+ export interface Spec {
6
+ /** The areas of spec/README.md, in the order it lists them. */
7
+ areas: string[];
8
+ /** Every entry by ID, in the order the kind directories were read. */
9
+ entries: Map<string, Entry>;
10
+ /** spec/glossary/. */
11
+ glossaryDir: string;
12
+ /** Glossary term -> the text after its heading, including the --glossary drafts. */
13
+ glossary: Map<string, string>;
14
+ /** Glossary term -> its file in spec/glossary/ (drafts have none). */
15
+ glossaryFiles: Map<string, string>;
16
+ /** Build ID -> the files of its manifest that are maps. */
17
+ buildFiles: Map<string, Meta[]>;
18
+ /** Build ID -> its Code ranges, for a build whose section was read. */
19
+ codeRanges: Map<string, CodeRange[]>;
20
+ }
21
+ /** What the load phase needs: the options and the problem collector. */
22
+ export interface LoadContext {
23
+ /** The run's options. */
24
+ config: Config;
25
+ /** Records a problem. */
26
+ problem: Problem;
27
+ }
28
+ /** What every phase after the load needs. */
29
+ export interface Context extends LoadContext {
30
+ /** The spec as loaded. */
31
+ spec: Spec;
32
+ /** The files of the --code and --references directories, read on first use. */
33
+ codeFiles: () => CodeFile[];
34
+ }
@@ -0,0 +1,2 @@
1
+ // What the phases of a run share: the options, the problem collector and the spec as loaded.
2
+ export {};
@@ -0,0 +1,37 @@
1
+ import type { Context } from "./context.ts";
2
+ import type { Problem } from "./problems.ts";
3
+ import type { Entry, Facts, Yaml } from "./types.ts";
4
+ /** The rank of a claim status on the scale, or -1 for one off it. */
5
+ export declare const statusIndex: (s: string) => number;
6
+ /**
7
+ * Whether the entry id is superseded: its status says so, or it is a build or source that names a
8
+ * successor. Undefined or false when it is not, or does not exist.
9
+ */
10
+ export declare const isSuperseded: (entries: Map<string, Entry>, id: string) => boolean | undefined;
11
+ /** Reports each of ids that is not an entry, as cited by what in file. */
12
+ export declare function checkResolves(ctx: Context, file: string, ids: string[], what: string): void;
13
+ /**
14
+ * What the evidence of a claim covers for the first build.
15
+ * A whole entry counts as read completely when complete_reading holds any valid finding; a row of
16
+ * its tables only when every static finding the row cites is part of that reading.
17
+ */
18
+ export declare const evidenceFacts: (entries: Map<string, Entry>, e: Entry) => Facts;
19
+ /**
20
+ * The static findings of a complete reading: those in complete_reading that the entry cites in
21
+ * evidence and that list its first build. Anything else there is reported where the field is checked.
22
+ */
23
+ export declare function completeReading(entries: Map<string, Entry>, e: Entry): string[];
24
+ /**
25
+ * True when all of an entry's evidence from the original running is emulated calls of single
26
+ * functions, which model neither interrupts nor timing.
27
+ */
28
+ export declare function onlyEmulatedRuns(entries: Map<string, Entry>, e: Entry): boolean;
29
+ /** True for a rule whose procedure says another rule may run in the middle of it (`# may run:`). */
30
+ export declare const mayBeInterrupted: (e: Entry) => boolean;
31
+ /** Reports a status that the facts of its evidence do not reach. label names what has the status. */
32
+ export declare function checkStatusCitations(problem: Problem, file: string, status: Yaml, facts: Facts, conflicting: string[], label?: string): void;
33
+ /**
34
+ * What the evidence ids cover for the build first. complete is the entry's complete reading, and
35
+ * the cited evidence counts as part of it when it holds static findings and every one of them is in it.
36
+ */
37
+ export declare function rowFacts(entries: Map<string, Entry>, ids: string[], first: Yaml, complete?: string[]): Facts;
@@ -0,0 +1,91 @@
1
+ // What entries say about each other: supersession, citations that resolve, and what the evidence a
2
+ // claim cites covers.
3
+ import { asList } from "./ids.js";
4
+ import { SCALE } from "./standard.js";
5
+ /** The rank of a claim status on the scale, or -1 for one off it. */
6
+ export const statusIndex = (s) => SCALE.indexOf(s);
7
+ /**
8
+ * Whether the entry id is superseded: its status says so, or it is a build or source that names a
9
+ * successor. Undefined or false when it is not, or does not exist.
10
+ */
11
+ export const isSuperseded = (entries, id) => entries.get(id)?.meta.status === "superseded" ||
12
+ (entries.get(id) &&
13
+ asList(entries.get(id).meta.superseded_by).length > 0 &&
14
+ ["BLD", "SRC"].includes(entries.get(id).kind));
15
+ /** Reports each of ids that is not an entry, as cited by what in file. */
16
+ export function checkResolves(ctx, file, ids, what) {
17
+ for (const id of ids)
18
+ if (!ctx.spec.entries.has(id))
19
+ ctx.problem(file, `${what} cites ${id}, which does not exist`);
20
+ }
21
+ /**
22
+ * What the evidence of a claim covers for the first build.
23
+ * A whole entry counts as read completely when complete_reading holds any valid finding; a row of
24
+ * its tables only when every static finding the row cites is part of that reading.
25
+ */
26
+ export const evidenceFacts = (entries, e) => {
27
+ const reading = completeReading(entries, e);
28
+ return {
29
+ ...rowFacts(entries, asList(e.meta.evidence), asList(e.meta.builds)[0], reading),
30
+ completeReading: reading.length > 0,
31
+ };
32
+ };
33
+ /**
34
+ * The static findings of a complete reading: those in complete_reading that the entry cites in
35
+ * evidence and that list its first build. Anything else there is reported where the field is checked.
36
+ */
37
+ export function completeReading(entries, e) {
38
+ const first = asList(e.meta.builds)[0];
39
+ const evidence = asList(e.meta.evidence);
40
+ return asList(e.meta.complete_reading).filter((x) => {
41
+ const f = entries.get(x);
42
+ return (f?.kind === "FND" && f.meta.method === "static" && evidence.includes(x) && asList(f.meta.builds).includes(first));
43
+ });
44
+ }
45
+ /**
46
+ * True when all of an entry's evidence from the original running is emulated calls of single
47
+ * functions, which model neither interrupts nor timing.
48
+ */
49
+ export function onlyEmulatedRuns(entries, e) {
50
+ const runs = asList(e.meta.evidence)
51
+ .map((x) => entries.get(x))
52
+ .filter((x) => x !== undefined && (x.kind === "EXP" || (x.kind === "FND" && x.meta.method === "dynamic")));
53
+ return runs.length > 0 && runs.every((x) => x.kind === "EXP" && x.meta.starting_state === "emulated-call");
54
+ }
55
+ /** True for a rule whose procedure says another rule may run in the middle of it (`# may run:`). */
56
+ export const mayBeInterrupted = (e) => e.kind === "RULE" && /# may run: RULE-/.test(e.code ?? "");
57
+ /** Reports a status that the facts of its evidence do not reach. label names what has the status. */
58
+ export function checkStatusCitations(problem, file, status, facts, conflicting, label = "status") {
59
+ if (status === "sourced" && facts.sources === 0)
60
+ problem(file, `${label} sourced needs at least one source`, "STATUS-1");
61
+ if (status === "supported" && facts.staticF + facts.dynamic === 0)
62
+ problem(file, `${label} supported needs at least one finding or experiment that lists the first build`, "STATUS-1");
63
+ if (status === "established" && (facts.staticF === 0 || (facts.dynamic === 0 && !facts.completeReading)))
64
+ problem(file, `${label} established needs a static finding and either a dynamic finding or experiment that list the first build, or a complete reading in complete_reading`, "STATUS-1");
65
+ if (status === "disputed" && conflicting.length === 0)
66
+ problem(file, `${label} disputed needs at least one finding or experiment in conflicting`, "STATUS-1");
67
+ }
68
+ /**
69
+ * What the evidence ids cover for the build first. complete is the entry's complete reading, and
70
+ * the cited evidence counts as part of it when it holds static findings and every one of them is in it.
71
+ */
72
+ export function rowFacts(entries, ids, first, complete = []) {
73
+ let sources = 0, staticF = 0, dynamic = 0, outside = 0;
74
+ for (const id of ids) {
75
+ const ev = entries.get(id);
76
+ if (!ev)
77
+ continue;
78
+ if (ev.kind === "SRC")
79
+ sources++;
80
+ if (!["FND", "EXP"].includes(ev.kind) || !asList(ev.meta.builds).includes(first))
81
+ continue;
82
+ if (ev.kind === "EXP" || ev.meta.method === "dynamic")
83
+ dynamic++;
84
+ else if (ev.meta.method === "static") {
85
+ staticF++;
86
+ if (!complete.includes(id))
87
+ outside++;
88
+ }
89
+ }
90
+ return { sources, staticF, dynamic, completeReading: complete.length > 0 && staticF > 0 && outside === 0 };
91
+ }
@@ -0,0 +1,11 @@
1
+ /** A path with backslashes turned into forward slashes. */
2
+ export declare const toSlash: (p: string) => string;
3
+ /**
4
+ * Calls fn with every file under dir, depth first in directory order, skipping build output,
5
+ * dependencies and .git. A directory that does not exist has no files.
6
+ */
7
+ export declare function walk(dir: string, fn: (path: string) => void): void;
8
+ /** Markdown files under dir, by path relative to dir without .md. */
9
+ export declare function markdownTree(dir: string): Map<string, string>;
10
+ /** The paths of the files and directories in dir, apart from .gitkeep. */
11
+ export declare const termFiles: (dir: string) => string[];
package/dist/files.js ADDED
@@ -0,0 +1,35 @@
1
+ // Walking the repository's directories.
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ /** A path with backslashes turned into forward slashes. */
5
+ export const toSlash = (p) => p.replaceAll("\\", "/");
6
+ /**
7
+ * Calls fn with every file under dir, depth first in directory order, skipping build output,
8
+ * dependencies and .git. A directory that does not exist has no files.
9
+ */
10
+ export function walk(dir, fn) {
11
+ if (!existsSync(dir))
12
+ return;
13
+ for (const name of readdirSync(dir)) {
14
+ if (["bin", "obj", "node_modules", ".git", "artifacts"].includes(name))
15
+ continue;
16
+ const p = join(dir, name);
17
+ if (statSync(p).isDirectory())
18
+ walk(p, fn);
19
+ else
20
+ fn(p);
21
+ }
22
+ }
23
+ /** Markdown files under dir, by path relative to dir without .md. */
24
+ export function markdownTree(dir) {
25
+ const found = new Map();
26
+ walk(dir, (f) => {
27
+ if (f.endsWith(".md"))
28
+ found.set(toSlash(relative(dir, f)).replace(/\.md$/, ""), f);
29
+ });
30
+ return found;
31
+ }
32
+ /** The paths of the files and directories in dir, apart from .gitkeep. */
33
+ export const termFiles = (dir) => readdirSync(dir)
34
+ .filter((name) => name !== ".gitkeep")
35
+ .map((name) => join(dir, name));
@@ -0,0 +1,3 @@
1
+ import type { Context } from "../context.ts";
2
+ /** Renders the four indexes, split where the line limit requires. Returns absolute path -> text. */
3
+ export declare function generateIndexes(ctx: Context): Map<string, string>;