@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,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>;
@@ -0,0 +1,135 @@
1
+ // The four indexes in spec/index/: by kind, by area, by status, and what cites each entry.
2
+ import { dirname, join, relative } from "node:path";
3
+ import { evidenceFacts } from "../evidence.js";
4
+ import { toSlash } from "../files.js";
5
+ import { areaOf, asList, compareIds, idsIn, isAlias, kindOf } from "../ids.js";
6
+ import { CLAIM_STATUSES, EVIDENCE_STATUSES, KINDS } from "../standard.js";
7
+ import { GENERATED, layout } from "./layout.js";
8
+ /** Renders the four indexes, split where the line limit requires. Returns absolute path -> text. */
9
+ export function generateIndexes(ctx) {
10
+ const { entries, areas, glossary, glossaryFiles } = ctx.spec;
11
+ const { specDir } = ctx.config;
12
+ const esc = (s) => String(s ?? "").replaceAll("|", "\\|");
13
+ const sortedIds = [...entries.keys()].sort(compareIds);
14
+ const indexDir = join(specDir, "index");
15
+ const linkFrom = (path) => (id) => `[${id}](${toSlash(relative(dirname(join(indexDir, `${path}.md`)), entries.get(id).file))})`;
16
+ const opening = (path, what) => [`# ${path}`, "", GENERATED, "", what, ""];
17
+ // A file of the full index lists every group, empty ones too, so its counts read as a progress
18
+ // report. A file of a split index lists only the groups it has entries in.
19
+ const isWhole = (path) => !path.includes("/");
20
+ function renderByKind(ids, path) {
21
+ const link = linkFrom(path);
22
+ const out = opening(path, "Entries by kind.");
23
+ for (const [kind, { dir }] of Object.entries(KINDS)) {
24
+ const kindIds = ids.filter((x) => kindOf(x) === kind);
25
+ if (!kindIds.length && !isWhole(path))
26
+ continue;
27
+ out.push(`## ${dir}`, "", `${kindIds.length} entries.`, "");
28
+ if (kindIds.length)
29
+ out.push("| ID | Title | Status |", "|---|---|---|", ...kindIds.map((x) => `| ${link(x)} | ${esc(entries.get(x).meta.title)} | ${entries.get(x).meta.status ?? "None"} |`), "");
30
+ }
31
+ return out.join("\n");
32
+ }
33
+ function renderByArea(ids, path) {
34
+ const link = linkFrom(path);
35
+ const out = opening(path, "Entries by area.");
36
+ for (const a of areas) {
37
+ const areaIds = ids.filter((x) => areaOf(x) === a);
38
+ if (!areaIds.length && !isWhole(path))
39
+ continue;
40
+ out.push(`## ${a}`, "");
41
+ if (!areaIds.length)
42
+ out.push("None.", "");
43
+ else
44
+ out.push("| ID | Title | Status |", "|---|---|---|", ...areaIds.map((x) => `| ${link(x)} | ${esc(entries.get(x).meta.title)} | ${entries.get(x).meta.status} |`), "");
45
+ }
46
+ return out.join("\n");
47
+ }
48
+ function renderByStatus(ids, path) {
49
+ const link = linkFrom(path);
50
+ const out = opening(path, "Entries by status.");
51
+ const section = (title, intro, list, withStatus) => {
52
+ if (!list.length && !isWhole(path))
53
+ return;
54
+ out.push(`## ${title}`, "");
55
+ if (intro)
56
+ out.push(intro, "");
57
+ if (!list.length) {
58
+ out.push(intro ? "None." : "0 entries.", "");
59
+ return;
60
+ }
61
+ if (!intro)
62
+ out.push(`${list.length} entries.`, "");
63
+ out.push(withStatus ? "| ID | Title | Status |" : "| ID | Title |", withStatus ? "|---|---|---|" : "|---|---|", ...list.map((x) => `| ${link(x)} | ${esc(entries.get(x).meta.title)} |${withStatus ? ` ${entries.get(x).meta.status} |` : ""}`), "");
64
+ };
65
+ for (const st of [...CLAIM_STATUSES, ...EVIDENCE_STATUSES.filter((x) => x !== "superseded")])
66
+ section(st, null, ids.filter((x) => entries.get(x).meta.status === st), false);
67
+ section("Established on unreproduced evidence", "Entries whose status is established and whose findings and experiments are all only recorded.", ids.filter((x) => KINDS[kindOf(x)].statuses === "claim" &&
68
+ entries.get(x).meta.status === "established" &&
69
+ asList(entries.get(x).meta.evidence)
70
+ .filter((y) => ["FND", "EXP"].includes(kindOf(y)))
71
+ .every((y) => entries.get(y)?.meta.status !== "reproduced")), false);
72
+ // Listed only when there are any, so that indexes written before complete readings stay fresh.
73
+ const byReading = ids.filter((x) => KINDS[kindOf(x)].statuses === "claim" &&
74
+ entries.get(x).meta.status === "established" &&
75
+ evidenceFacts(entries, entries.get(x)).dynamic === 0);
76
+ if (byReading.length)
77
+ section("Established by a complete reading alone", "Entries whose status is established and that no dynamic finding or experiment confirms.", byReading, false);
78
+ section("Open questions", "Entries whose Open questions section says more than None known.", ids.filter((x) => {
79
+ const s = entries.get(x).sections.find((y) => y.title === "Open questions");
80
+ return s && !/^\s*None( known)?\.\s*$/.test(s.text);
81
+ }), true);
82
+ return out.join("\n");
83
+ }
84
+ const refs = new Map(sortedIds.map((x) => [x, new Map()]));
85
+ const addRef = (to, from, how) => {
86
+ if (refs.has(to) && to !== from) {
87
+ const m = refs.get(to);
88
+ if (!m.has(from))
89
+ m.set(from, new Set());
90
+ m.get(from).add(how);
91
+ }
92
+ };
93
+ for (const [id, e] of entries) {
94
+ for (const f of ["evidence", "conflicting", "related", "split_with", "superseded_by", "builds"])
95
+ for (const x of asList(e.meta[f]))
96
+ addRef(x, id, f);
97
+ for (const loc of asList(e.meta.locations))
98
+ if (loc?.build)
99
+ addRef(loc.build, id, "locations");
100
+ for (const x of idsIn(e.body))
101
+ addRef(x, id, "body");
102
+ }
103
+ for (const [term, text] of glossary)
104
+ for (const x of idsIn(text))
105
+ addRef(x, `glossary:${term}`, "glossary");
106
+ // One row per entry: everything that cites it goes in one cell.
107
+ function renderReferences(ids, path) {
108
+ const link = linkFrom(path);
109
+ const termLink = (term) => glossaryFiles.has(term)
110
+ ? `[${term}](${toSlash(relative(dirname(join(indexDir, `${path}.md`)), glossaryFiles.get(term)))})`
111
+ : esc(term);
112
+ const out = opening(path, "For each entry, the entries and glossary terms that cite or relate to it, and the field they do it in.");
113
+ out.push("| ID | Cited by |", "|---|---|");
114
+ for (const x of ids) {
115
+ const cited = [...refs.get(x)]
116
+ .sort((a, b) => compareIds(a[0], b[0]))
117
+ .map(([from, hows]) => `${from.startsWith("glossary:") ? termLink(from.slice(9)) : link(from)} (${[...hows].sort().join(", ")})`);
118
+ out.push(`| ${link(x)} | ${cited.join(", ") || "None"} |`);
119
+ }
120
+ out.push("");
121
+ return out.join("\n");
122
+ }
123
+ const generated = new Map(); // absolute path -> text
124
+ const areaIds = sortedIds.filter((x) => !isAlias(x));
125
+ const indexes = {
126
+ "by-kind": [renderByKind, sortedIds],
127
+ "by-area": [renderByArea, areaIds],
128
+ "by-status": [renderByStatus, sortedIds],
129
+ references: [renderReferences, sortedIds],
130
+ };
131
+ for (const [name, [render, ids]] of Object.entries(indexes))
132
+ for (const [path, { text }] of layout(ctx, name, ids, render))
133
+ generated.set(join(indexDir, `${path}.md`), text);
134
+ return generated;
135
+ }
@@ -0,0 +1,18 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { Render } from "../types.ts";
3
+ /** The block of an ID in a split file: its hundred, such as 100, or the first character of an alias. */
4
+ export declare const blockOf: (id: string) => string;
5
+ /**
6
+ * Lays out one generated file: `${name}.md` when render's text fits, otherwise a directory split only
7
+ * as far as the limit requires. render(ids, path) gives the text of the file at path, which has no
8
+ * .md and is relative to the generated tree. Returns a Map of path -> { ids, text }.
9
+ */
10
+ export declare function layout(ctx: Context, name: string, ids: string[], render: Render, level?: number, out?: Map<string, {
11
+ ids: string[];
12
+ text: string;
13
+ }>): Map<string, {
14
+ ids: string[];
15
+ text: string;
16
+ }>;
17
+ /** The comment every generated file carries under its heading. */
18
+ export declare const GENERATED = "<!-- Generated by the documentation standard check. Do not edit. -->";
@@ -0,0 +1,49 @@
1
+ // Splitting generated files. A generated file that would pass the line limit becomes a directory of
2
+ // the same name, split by area (BLD-SRC for builds and sources, which have no area), then by kind,
3
+ // then by a block of 100 numbers, or by the first character of a build's or source's alias.
4
+ import { areaOf, isAlias, kindOf } from "../ids.js";
5
+ import { lineCount } from "../markdown.js";
6
+ import { KINDS, LINE_LIMIT } from "../standard.js";
7
+ const groupOf = (id) => (isAlias(id) ? "BLD-SRC" : areaOf(id));
8
+ /** The block of an ID in a split file: its hundred, such as 100, or the first character of an alias. */
9
+ export const blockOf = (id) => {
10
+ const kind = kindOf(id);
11
+ if (kind === "BLD" || kind === "SRC")
12
+ return id.charAt(kind.length + 1);
13
+ return String(Math.floor(Number(id.split("-")[2]) / 100) * 100).padStart(3, "0");
14
+ };
15
+ const SPLIT_LEVELS = [groupOf, kindOf, blockOf];
16
+ function orderKeys(areas, level, keys) {
17
+ if (level === 0)
18
+ return [...areas.filter((a) => keys.includes(a)), ...keys.filter((k) => !areas.includes(k)).sort()];
19
+ if (level === 1)
20
+ return Object.keys(KINDS).filter((k) => keys.includes(k));
21
+ return keys.sort((a, b) => (/^\d+$/.test(a) && /^\d+$/.test(b) ? Number(a) - Number(b) : a < b ? -1 : a > b ? 1 : 0));
22
+ }
23
+ /**
24
+ * Lays out one generated file: `${name}.md` when render's text fits, otherwise a directory split only
25
+ * as far as the limit requires. render(ids, path) gives the text of the file at path, which has no
26
+ * .md and is relative to the generated tree. Returns a Map of path -> { ids, text }.
27
+ */
28
+ export function layout(ctx, name, ids, render, level = 0, out = new Map()) {
29
+ const text = render(ids, name);
30
+ const { problem } = ctx;
31
+ if (lineCount(text) <= LINE_LIMIT || level === SPLIT_LEVELS.length) {
32
+ if (lineCount(text) > LINE_LIMIT)
33
+ problem(null, `${name}.md would pass ${LINE_LIMIT} lines even split by block`);
34
+ out.set(name, { ids, text });
35
+ return out;
36
+ }
37
+ const groups = new Map();
38
+ for (const id of ids) {
39
+ const key = SPLIT_LEVELS[level](id);
40
+ if (!groups.has(key))
41
+ groups.set(key, []);
42
+ groups.get(key).push(id);
43
+ }
44
+ for (const key of orderKeys(ctx.spec.areas, level, [...groups.keys()]))
45
+ layout(ctx, `${name}/${key}`, groups.get(key), render, level + 1, out);
46
+ return out;
47
+ }
48
+ /** The comment every generated file carries under its heading. */
49
+ export const GENERATED = "<!-- Generated by the documentation standard check. Do not edit. -->";
@@ -0,0 +1,7 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { Parity } from "../checks/parity.ts";
3
+ /**
4
+ * Adds the text of PARITY.md to generated, unless PARITY.md still holds the rows (which the parity
5
+ * check reports).
6
+ */
7
+ export declare function generateParity(ctx: Context, parity: Parity, generated: Map<string, string>): void;
@@ -0,0 +1,44 @@
1
+ // PARITY.md: the totals of the parity rows, and a link to each area's rows.
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { areaOf } from "../ids.js";
5
+ import { GENERATED } from "./layout.js";
6
+ /**
7
+ * Adds the text of PARITY.md to generated, unless PARITY.md still holds the rows (which the parity
8
+ * check reports).
9
+ */
10
+ export function generateParity(ctx, parity, generated) {
11
+ const { areas } = ctx.spec;
12
+ const { repoDir } = ctx.config;
13
+ const parityDir = join(repoDir, "parity");
14
+ const { counts: parityCounts, rows: parityRows, legacy: legacyParity } = parity;
15
+ const out = [
16
+ "# Parity matrix",
17
+ "",
18
+ GENERATED,
19
+ "",
20
+ "How much of the spec in `spec/` the rebuild does. The rows are in `parity/`, one file per area.",
21
+ "",
22
+ "| Status | Rows |",
23
+ "|---|---|",
24
+ ...["unknown", "sourced", "supported", "established", "disputed", "implemented", "validated"].map((k) => `| ${k} | ${parityCounts.status[k] ?? 0} |`),
25
+ "",
26
+ "| Code | Rows |",
27
+ "|---|---|",
28
+ ...["missing", "partial", "complete"].map((k) => `| ${k} | ${parityCounts.code[k] ?? 0} |`),
29
+ "",
30
+ "## Areas",
31
+ "",
32
+ ];
33
+ const withRows = areas.filter((a) => existsSync(join(parityDir, `${a}.md`)) || existsSync(join(parityDir, a)));
34
+ if (!withRows.length)
35
+ out.push("None yet.");
36
+ else
37
+ out.push("| Area | Rows |", "|---|---|", ...withRows.map((a) => {
38
+ const target = existsSync(join(parityDir, `${a}.md`)) ? `parity/${a}.md` : `parity/${a}/`;
39
+ return `| [${a}](${target}) | ${[...parityRows.keys()].filter((x) => areaOf(x) === a).length} |`;
40
+ }));
41
+ out.push("");
42
+ if (!legacyParity)
43
+ generated.set(join(repoDir, "PARITY.md"), out.join("\n"));
44
+ }
@@ -0,0 +1,8 @@
1
+ import type { Context } from "../context.ts";
2
+ /**
3
+ * Writes each generated file whose text has changed and removes any other file in spec/index/,
4
+ * printing what it did. With --check it reports them as problems instead.
5
+ */
6
+ export declare function writeGenerated(ctx: Context, generated: Map<string, string>): void;
7
+ /** Reports every Markdown file the standard defines that is longer than LINE_LIMIT lines. */
8
+ export declare function checkLineLimits(ctx: Context): void;
@@ -0,0 +1,75 @@
1
+ // Writing the generated files, or with --check reporting the stale ones, and the line limit of
2
+ // every Markdown file the standard defines.
3
+ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, relative } from "node:path";
5
+ import { markdownTree, toSlash, walk } from "../files.js";
6
+ import { lineCount, readText } from "../markdown.js";
7
+ import { LINE_LIMIT } from "../standard.js";
8
+ /**
9
+ * Writes each generated file whose text has changed and removes any other file in spec/index/,
10
+ * printing what it did. With --check it reports them as problems instead.
11
+ */
12
+ export function writeGenerated(ctx, generated) {
13
+ const { problem } = ctx;
14
+ const { repoDir, specDir, checkOnly } = ctx.config;
15
+ const indexDir = join(specDir, "index");
16
+ const stale = [];
17
+ for (const [p, content] of generated) {
18
+ const current = existsSync(p) ? readText(p) : null;
19
+ if (current !== content)
20
+ stale.push(p);
21
+ }
22
+ const extra = [...(existsSync(indexDir) ? markdownTree(indexDir).values() : [])].filter((f) => !generated.has(f));
23
+ if (checkOnly) {
24
+ for (const p of stale)
25
+ problem(p, existsSync(p)
26
+ ? "is stale; run the check without --check to rewrite it"
27
+ : "is missing; run the check without --check to write it");
28
+ for (const f of extra)
29
+ problem(f, "is not a file the check writes; run the check without --check to remove it");
30
+ }
31
+ else {
32
+ for (const p of stale) {
33
+ mkdirSync(dirname(p), { recursive: true });
34
+ writeFileSync(p, generated.get(p));
35
+ console.log(`wrote ${toSlash(relative(repoDir, p))}`);
36
+ }
37
+ for (const f of extra) {
38
+ rmSync(f);
39
+ console.log(`removed ${toSlash(relative(repoDir, f))}`);
40
+ }
41
+ // Directories left empty by a file that moved.
42
+ const prune = (dir) => {
43
+ for (const name of readdirSync(dir)) {
44
+ const p = join(dir, name);
45
+ if (statSync(p).isDirectory())
46
+ prune(p);
47
+ }
48
+ if (dir !== indexDir && readdirSync(dir).length === 0)
49
+ rmSync(dir, { recursive: true });
50
+ };
51
+ if (existsSync(indexDir))
52
+ prune(indexDir);
53
+ }
54
+ }
55
+ /** Reports every Markdown file the standard defines that is longer than LINE_LIMIT lines. */
56
+ export function checkLineLimits(ctx) {
57
+ const { problem } = ctx;
58
+ const { repoDir, specDir } = ctx.config;
59
+ const validationPath = join(repoDir, "VALIDATION.md");
60
+ const parityDir = join(repoDir, "parity");
61
+ const devDir = join(repoDir, "deviations");
62
+ const files = [join(repoDir, "PARITY.md"), validationPath];
63
+ for (const dir of [specDir, parityDir, devDir])
64
+ walk(dir, (f) => {
65
+ if (f.endsWith(".md"))
66
+ files.push(f);
67
+ });
68
+ for (const f of files) {
69
+ if (!existsSync(f))
70
+ continue;
71
+ const lines = lineCount(readText(f));
72
+ if (lines > LINE_LIMIT)
73
+ problem(f, `has ${lines} lines; the documentation standard allows ${LINE_LIMIT}. Split it as the standard's File size section describes`);
74
+ }
75
+ }
package/dist/ids.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Yaml } from "./types.ts";
2
+ /** The distinct spec IDs in text, in the order they first appear. */
3
+ export declare const idsIn: (text: unknown) => string[];
4
+ /** The kind of an ID, such as `RULE`. */
5
+ export declare const kindOf: (id: string) => string;
6
+ /** The area of an ID, such as `COMBAT` (for a build or source, the start of its alias). */
7
+ export declare const areaOf: (id: string) => string;
8
+ /** Builds and sources have an alias in place of an area and a number. */
9
+ export declare const isAlias: (id: string) => boolean;
10
+ /** Compares two IDs for sorting: by number within one kind and area, otherwise by UTF-16 code unit. */
11
+ export declare const compareIds: (a: string, b: string) => -1 | 0 | 1;
12
+ /** A front matter value as a list: a list as is, nothing for null, undefined or "", else a list of one. */
13
+ export declare const asList: (v: Yaml) => Yaml[];
package/dist/ids.js ADDED
@@ -0,0 +1,22 @@
1
+ // Spec IDs: finding them in text, taking them apart and ordering them.
2
+ import { ID_RE } from "./standard.js";
3
+ /** The distinct spec IDs in text, in the order they first appear. */
4
+ export const idsIn = (text) => [...new Set(String(text ?? "").match(ID_RE) ?? [])];
5
+ /** The kind of an ID, such as `RULE`. */
6
+ export const kindOf = (id) => id.split("-")[0];
7
+ /** The area of an ID, such as `COMBAT` (for a build or source, the start of its alias). */
8
+ export const areaOf = (id) => id.split("-")[1];
9
+ /** Builds and sources have an alias in place of an area and a number. */
10
+ export const isAlias = (id) => ["BLD", "SRC"].includes(kindOf(id));
11
+ // Orders IDs of one kind and area by number, so RULE-A-999 comes before RULE-A-1000. Anything else
12
+ // compares by UTF-16 code unit, as Array.prototype.sort does, so the order does not depend on the
13
+ // machine's locale.
14
+ const idSortKey = (id) => id.replace(/^((?:FMT|RULE|FND|EXP|BUG|SCR|DEV)-[A-Z][A-Z0-9]*-)(\d+)$/, (_, head, n) => head + n.padStart(12, "0"));
15
+ /** Compares two IDs for sorting: by number within one kind and area, otherwise by UTF-16 code unit. */
16
+ export const compareIds = (a, b) => {
17
+ const x = idSortKey(a);
18
+ const y = idSortKey(b);
19
+ return x < y ? -1 : x > y ? 1 : 0;
20
+ };
21
+ /** A front matter value as a list: a list as is, nothing for null, undefined or "", else a list of one. */
22
+ export const asList = (v) => Array.isArray(v) ? v : v === null || v === undefined || v === "" ? [] : [v];
@@ -0,0 +1,9 @@
1
+ import type { LoadContext } from "../context.ts";
2
+ import type { Entry, Meta } from "../types.ts";
3
+ /**
4
+ * Reads and checks the manifest of every build entry, and reports a manifest or list of other files
5
+ * that belongs to no build. Returns build ID -> the items of its files list that are maps.
6
+ */
7
+ export declare function loadBuildFiles({ config, problem }: LoadContext, entries: Map<string, Entry>): Map<string, Meta[]>;
8
+ /** Checks each build's list of other files against its manifest and its Other files section. */
9
+ export declare function checkOtherFiles({ problem }: LoadContext, entries: Map<string, Entry>, buildFiles: Map<string, Meta[]>): void;