@scientific-method/standard-checker 0.2.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.
@@ -40,6 +40,7 @@ export function checkEntries(ctx) {
40
40
  const names = {
41
41
  enumNames: new Map(), // name -> format IDs
42
42
  fieldNames: new Map(), // format ID -> Set of names
43
+ layouts: new Map(), // format ID -> layout Name -> Type
43
44
  };
44
45
  for (const [id, e] of entries) {
45
46
  const { file, meta, kind } = e;
@@ -0,0 +1,4 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { FormatNames } from "./formats.ts";
3
+ /** Checks the field names of every live rule's procedure. Runs after checkRules, which sets Entry.code. */
4
+ export declare function checkFieldNames(ctx: Context, { layouts }: FormatNames): void;
@@ -0,0 +1,314 @@
1
+ // Field names in procedures: every name after a dot, on a structure whose type is known, is a Name in
2
+ // the layout of that structure's format.
3
+ //
4
+ // A type is known only where it is written in the notation's type form: `FMT-DATA-005`, a pointer to
5
+ // one (`PTR32<FMT-DATA-005>`) or a list of either (`FMT-DATA-005[]`). The checker reads it from the
6
+ // places the standard's Notation lists:
7
+ //
8
+ // - a `let` with a type, `let x = new FMT-...`, or a `let` whose value has a known type: another
9
+ // name or field, `copy` of one, a function whose `define` gives its result type, or a rule's
10
+ // `call` whose Outputs open with "Returns" and a type;
11
+ // - the rule's Parameters section, as `name: type`, and the parameters of a `define`;
12
+ // - for a name that is not a local, its glossary entry, as `name: type`, or a location in the form
13
+ // "field `x` of FMT-..." whose layout row has a type;
14
+ // - a `for each` loop variable takes the element type of the list it visits.
15
+ //
16
+ // A field takes the type in its layout row, so a chain such as `world.occupancy.cell_count` is
17
+ // followed row by row. An index into a list gives its element, and an index on a pointer gives the
18
+ // structure it points at; an index on anything else, or a field of a list, ends what is known. A
19
+ // format's mention in prose gives no type, since prose names formats for many reasons. A name whose
20
+ // type is not written in one of these forms, or that two declarations give different types, stays
21
+ // unchecked. A declaration that writes no type, such as a parameter the Parameters section describes
22
+ // in prose or an untyped parameter of a define, gives none and so differs from no other.
23
+ import { asList } from "../ids.js";
24
+ import { parameterNames, withoutCommentsAndStrings } from "./rules.js";
25
+ const sameType = (a, b) => !!a && !!b && a.format.join(" ") === b.format.join(" ") && a.pointer === b.pointer && a.list === b.list;
26
+ const FMT_ID = String.raw `FMT-[A-Z][A-Z0-9]*-\d{3,}`;
27
+ // A type in the notation that holds a structure: a format ID, a pointer to one, or a list of either.
28
+ const STRUCTURE_TYPE = String.raw `(?:[A-Z][A-Z0-9]*<)?${FMT_ID}>?(?:\[[^\]\n]*\])?`;
29
+ const WHOLE_TYPE = new RegExp(String.raw `^([A-Z][A-Z0-9]*<)?(${FMT_ID})(>)?(\[[^\]\n]*\])?$`);
30
+ // `name: type`, with the backticks around the whole or around each part.
31
+ const TYPED_NAME = new RegExp(String.raw `\x60([a-z_][a-z0-9_]*)\x60?\s*:\s*\x60?(${STRUCTURE_TYPE})(?![\w-])`, "g");
32
+ // Where the glossary says the game keeps a value: a field of a structure.
33
+ const LOCATION = new RegExp(String.raw `\bfield\s+\x60([A-Za-z_][A-Za-z0-9_]*)\x60\s+of\s+(${FMT_ID})|\x60([A-Za-z_][A-Za-z0-9_]*)\x60\s+field\s+of\s+(${FMT_ID})`, "g");
34
+ const RETURNS = new RegExp(String.raw `^Returns\s+(?:an?\s+)?\x60?(${STRUCTURE_TYPE})\x60?(?=[\s,.;]|$)`);
35
+ const LOWER = /[a-z_][a-z0-9_]*/y;
36
+ const FIELD = /[A-Za-z_][A-Za-z0-9_]*/y;
37
+ // Whether the bracket that opens at text[open] closes at the end of text.
38
+ function closesAtEnd(text, open) {
39
+ let depth = 0;
40
+ for (let k = open; k < text.length; k++) {
41
+ if (text[k] === "(")
42
+ depth++;
43
+ else if (text[k] === ")" && --depth === 0)
44
+ return k === text.length - 1;
45
+ }
46
+ return false;
47
+ }
48
+ // Reads a chain that starts at i, or returns null when text[i] does not start a lower-case name.
49
+ function readChain(text, i) {
50
+ LOWER.lastIndex = i;
51
+ const m = LOWER.exec(text);
52
+ if (!m)
53
+ return null;
54
+ const steps = [];
55
+ const upTo = [];
56
+ let j = i + m[0].length;
57
+ for (;;) {
58
+ let k = j;
59
+ while (text[k] === " ")
60
+ k++;
61
+ if (text[k] === "[") {
62
+ let depth = 0;
63
+ for (; k < text.length; k++) {
64
+ if (text[k] === "[")
65
+ depth++;
66
+ else if (text[k] === "]" && --depth === 0)
67
+ break;
68
+ }
69
+ if (k >= text.length)
70
+ break;
71
+ j = k + 1;
72
+ steps.push(null);
73
+ upTo.push(text.slice(i, j));
74
+ continue;
75
+ }
76
+ if (text[j] !== ".")
77
+ break;
78
+ FIELD.lastIndex = j + 1;
79
+ const f = FIELD.exec(text);
80
+ if (!f)
81
+ break;
82
+ j += 1 + f[0].length;
83
+ steps.push(f[0]);
84
+ upTo.push(text.slice(i, j));
85
+ }
86
+ return { base: m[0], steps, upTo, end: j };
87
+ }
88
+ /** Checks the field names of every live rule's procedure. Runs after checkRules, which sets Entry.code. */
89
+ export function checkFieldNames(ctx, { layouts }) {
90
+ const { problem } = ctx;
91
+ const { entries, glossary } = ctx.spec;
92
+ // A format ID and the entries it is split with, which define the same fields.
93
+ const formatOf = (id) => {
94
+ const e = entries.get(id);
95
+ if (!e || e.kind !== "FMT")
96
+ return null;
97
+ return [id, ...asList(e.meta.split_with).filter((x) => entries.get(x)?.kind === "FMT")].sort();
98
+ };
99
+ // The type a type written in the notation gives, or null for a type that holds no structure.
100
+ const typeOf = (text) => {
101
+ const m = WHOLE_TYPE.exec((text ?? "").replaceAll("`", "").trim());
102
+ if (!m || !m[1] !== !m[3])
103
+ return null;
104
+ const format = formatOf(m[2]);
105
+ return format ? { format, pointer: !!m[1], list: !!m[4] } : null;
106
+ };
107
+ // The types text gives names in the form `name: type`; a name given two types has none.
108
+ const typedNames = (text) => {
109
+ const found = new Map();
110
+ for (const m of text.matchAll(TYPED_NAME)) {
111
+ const type = typeOf(m[2]);
112
+ found.set(m[1], found.has(m[1]) && !sameType(found.get(m[1]), type) ? null : type);
113
+ }
114
+ return found;
115
+ };
116
+ // The layout rows of a format, or null when one of its entries has a malformed layout table. A
117
+ // field whose entries give it different types has no type.
118
+ const formatRows = new Map();
119
+ const plain = (type) => type.replaceAll("`", "").trim();
120
+ const rowsOf = (format) => {
121
+ const key = format.join(" ");
122
+ if (formatRows.has(key))
123
+ return formatRows.get(key);
124
+ let rows = new Map();
125
+ for (const id of format) {
126
+ const layout = layouts.get(id);
127
+ if (!layout) {
128
+ rows = null;
129
+ break;
130
+ }
131
+ for (const [name, type] of layout)
132
+ rows.set(name, rows.has(name) && plain(rows.get(name)) !== plain(type) ? "" : type);
133
+ }
134
+ formatRows.set(key, rows);
135
+ return rows;
136
+ };
137
+ const termTypes = new Map();
138
+ const termType = (term) => {
139
+ if (termTypes.has(term))
140
+ return termTypes.get(term);
141
+ const text = glossary.get(term) ?? "";
142
+ const typed = typedNames(text);
143
+ let type = null;
144
+ if (typed.has(term))
145
+ type = typed.get(term);
146
+ else {
147
+ const locations = [...text.matchAll(LOCATION)];
148
+ if (locations.length === 1) {
149
+ const format = formatOf(locations[0][2] ?? locations[0][4]);
150
+ type = format ? typeOf(rowsOf(format)?.get(locations[0][1] ?? locations[0][3])) : null;
151
+ }
152
+ }
153
+ termTypes.set(term, type);
154
+ return type;
155
+ };
156
+ // The type of the value a rule returns, from the opening of its Outputs section.
157
+ const returnType = (id) => {
158
+ const outputs = entries
159
+ .get(id)
160
+ ?.sections.find((s) => s.title === "Outputs")
161
+ ?.text.trim() ?? "";
162
+ const m = RETURNS.exec(outputs);
163
+ return m ? typeOf(m[1]) : null;
164
+ };
165
+ const live = [...entries.values()].filter((e) => e.kind === "RULE" && e.meta.status !== "superseded" && e.code);
166
+ // Functions whose define gives a structure as the result: define name(...) -> FMT-...
167
+ const functionTypes = new Map();
168
+ for (const e of live)
169
+ for (const m of withoutCommentsAndStrings(e.code).matchAll(/\bdefine\s+([a-z_][a-z0-9_]*)\s*\([^)]*\)\s*->\s*([^:\n]+):/g)) {
170
+ const type = typeOf(m[2]);
171
+ // The entries of a split rule define the same function; if they disagree, its type is unknown.
172
+ const disagrees = functionTypes.has(m[1]) && !sameType(functionTypes.get(m[1]), type);
173
+ functionTypes.set(m[1], disagrees ? null : type);
174
+ }
175
+ for (const e of live)
176
+ checkRule(e);
177
+ function checkRule(e) {
178
+ const lines = withoutCommentsAndStrings(e.code).split("\n");
179
+ const params = e.sections.find((s) => s.title === "Parameters")?.text ?? "";
180
+ const declarations = [];
181
+ const declare = (name, type) => declarations.push({ name, type });
182
+ const fixed = (type, source) => () => type === undefined ? undefined : type ? { ...type, source } : null;
183
+ const typedParams = typedNames(params);
184
+ for (const name of parameterNames(params))
185
+ declare(name, fixed(typedParams.has(name) ? typedParams.get(name) : undefined, "the Parameters section"));
186
+ for (const line of lines) {
187
+ const d = /\bdefine\s+[a-z_][a-z0-9_]*\s*\(([^)]*)\)/.exec(line);
188
+ if (d)
189
+ for (const p of d[1].split(",")) {
190
+ const [name, type] = p.split(":");
191
+ if (/^[a-z_][a-z0-9_]*$/.test(name.trim()))
192
+ declare(name.trim(), fixed(type === undefined ? undefined : typeOf(type), "its define"));
193
+ }
194
+ const l = /^\s*let\s+([a-z_][a-z0-9_]*)\s*(?::\s*([^=]+?))?\s*=\s*(.+?)\s*$/.exec(line);
195
+ if (l) {
196
+ const [, name, type, value] = l;
197
+ if (type)
198
+ declare(name, fixed(typeOf(type), "its let"));
199
+ else
200
+ declare(name, (known) => valueType(value, known, "its let"));
201
+ }
202
+ const f = /^\s*for\s+(?:each\s+)?([a-z_][a-z0-9_]*)\s+in\s+(.+?)\s*:\s*$/.exec(line);
203
+ if (f)
204
+ declare(f[1], (known) => {
205
+ const list = f[2].includes("..") ? null : valueType(f[2], known, "the list its loop visits");
206
+ return list?.list ? { ...list, list: false } : null;
207
+ });
208
+ }
209
+ const locals = new Set(declarations.map((d) => d.name));
210
+ // A let can take its type from another local, so the types are worked out again until they
211
+ // stop changing.
212
+ let types = new Map();
213
+ const known = (name) => {
214
+ if (locals.has(name))
215
+ return types.get(name) ?? null;
216
+ const type = termType(name);
217
+ return type ? { ...type, source: `the glossary entry for ${name}` } : null;
218
+ };
219
+ const settledAs = (a, b) => a.size === b.size && [...a].every(([name, t]) => sameType(t, b.get(name) ?? null));
220
+ for (let pass = 0; pass <= declarations.length; pass++) {
221
+ const found = new Map();
222
+ for (const d of declarations) {
223
+ const t = d.type(known);
224
+ if (t === undefined)
225
+ continue;
226
+ found.set(d.name, found.has(d.name) && !sameType(found.get(d.name), t) ? null : t);
227
+ }
228
+ const next = new Map();
229
+ for (const [name, t] of found)
230
+ if (t)
231
+ next.set(name, t);
232
+ const settled = settledAs(next, types);
233
+ types = next;
234
+ if (settled)
235
+ break;
236
+ }
237
+ for (const line of lines)
238
+ for (let i = 0; i < line.length; i++) {
239
+ if (/[\w.]/.test(line[i - 1] ?? ""))
240
+ continue;
241
+ const chain = readChain(line, i);
242
+ if (!chain)
243
+ continue;
244
+ // Names inside the chain's indexes start chains of their own.
245
+ i += chain.base.length - 1;
246
+ if (chain.steps.some((s) => s !== null))
247
+ follow(chain, known, (message) => problem(e.file, message));
248
+ }
249
+ }
250
+ // The type of the value of a let, or of the list a for each loop visits, or null when it has none.
251
+ function valueType(value, known, source) {
252
+ const v = value.trim();
253
+ const made = new RegExp(String.raw `^new\s+(${FMT_ID})$`).exec(v);
254
+ if (made) {
255
+ const format = formatOf(made[1]);
256
+ return format ? { format, pointer: false, list: false, source } : null;
257
+ }
258
+ // A call is the whole value only when the bracket after its name closes at the end, so
259
+ // make(a) + other(b) is not a call of make.
260
+ const called = /^call\s+(RULE-[A-Z][A-Z0-9]*-\d{3,})\s*\(/.exec(v);
261
+ if (called && closesAtEnd(v, called[0].length - 1)) {
262
+ const type = returnType(called[1]);
263
+ return type ? { ...type, source: `the Outputs of ${called[1]}` } : null;
264
+ }
265
+ const copied = /^copy\s*\(/.exec(v);
266
+ if (copied && closesAtEnd(v, copied[0].length - 1))
267
+ return valueType(v.slice(copied[0].length, -1), known, source);
268
+ const fn = /^([a-z_][a-z0-9_]*)\s*\(/.exec(v);
269
+ if (fn && closesAtEnd(v, fn[0].length - 1)) {
270
+ const type = functionTypes.get(fn[1]);
271
+ return type ? { ...type, source: `the define of ${fn[1]}` } : null;
272
+ }
273
+ const chain = readChain(v, 0);
274
+ if (!chain || chain.end !== v.length)
275
+ return null;
276
+ const t = follow(chain, known);
277
+ return t ? { ...t, source } : null;
278
+ }
279
+ // Follows a chain access by access and returns the type it ends with. With report, a field missing
280
+ // from its structure's layout is reported.
281
+ function follow(chain, known, report) {
282
+ const base = known(chain.base);
283
+ if (!base)
284
+ return null;
285
+ let type = base;
286
+ for (const [n, field] of chain.steps.entries()) {
287
+ if (field === null) {
288
+ // list[i] is an element, and p[i] is the structure p + i points at.
289
+ if (type.list)
290
+ type = { ...type, list: false };
291
+ else if (type.pointer)
292
+ type = { ...type, pointer: false };
293
+ else
294
+ return null;
295
+ continue;
296
+ }
297
+ if (type.list)
298
+ return null;
299
+ const rows = rowsOf(type.format);
300
+ if (!rows)
301
+ return null;
302
+ const cell = rows.get(field);
303
+ if (cell === undefined) {
304
+ report?.(`${chain.upTo[n]} names ${field}, which is not in the layout of ${type.format.join(" or ")} (${chain.base} has its type from ${base.source})`);
305
+ return null;
306
+ }
307
+ const next = typeOf(cell);
308
+ if (!next)
309
+ return null;
310
+ type = next;
311
+ }
312
+ return { ...type, source: base.source };
313
+ }
314
+ }
@@ -6,6 +6,12 @@ export interface FormatNames {
6
6
  enumNames: Map<string, string[]>;
7
7
  /** Format ID -> the names of its layout and enumeration rows. */
8
8
  fieldNames: Map<string, Set<string>>;
9
+ /**
10
+ * Format ID -> the Name of each layout row -> its Type cell, for a format whose layout tables all
11
+ * have the right columns and whose rows all have one cell per column. A format without a Layout
12
+ * table has an empty map.
13
+ */
14
+ layouts: Map<string, Map<string, string>>;
9
15
  }
10
16
  /** The IDs cited in the last column of the tables of an entry's sections (of every section when sectionTitles is left out). */
11
17
  export declare function tableIds(e: Entry, sectionTitles?: string[]): Set<string>;
@@ -21,7 +21,7 @@ export function tableIds(e, sectionTitles) {
21
21
  export function checkFormat(ctx, e, formatNames) {
22
22
  const { problem } = ctx;
23
23
  const { entries, buildFiles } = ctx.spec;
24
- const { enumNames, fieldNames } = formatNames;
24
+ const { enumNames, fieldNames, layouts } = formatNames;
25
25
  const { file, meta } = e;
26
26
  const id = meta.id;
27
27
  const first = asList(meta.builds)[0];
@@ -88,18 +88,45 @@ export function checkFormat(ctx, e, formatNames) {
88
88
  names.add(row[nameCol].replaceAll("`", ""));
89
89
  }
90
90
  };
91
+ // A row whose status is wrong still names a field, so the field checks of rules read every row.
92
+ const layoutTypes = new Map();
93
+ let wellFormed = true;
91
94
  if (layout) {
92
95
  const ts = tables(layout.text);
93
96
  const wanted = meta.text === true ? TEXT_LAYOUT : BINARY_LAYOUT;
94
97
  if (meta.status !== "unknown" && ts.length === 0)
95
98
  problem(file, "Layout has no table");
96
99
  for (const t of ts) {
97
- if (t.header.join("|") !== wanted.join("|"))
100
+ if (t.header.join("|") !== wanted.join("|")) {
98
101
  problem(file, `a layout table has the columns ${wanted.join(" | ")}`);
99
- else
100
- visit(t, "layout");
102
+ wellFormed = false;
103
+ continue;
104
+ }
105
+ visit(t, "layout");
106
+ const nameCol = t.header.indexOf("Name");
107
+ const typeCol = t.header.indexOf("Type");
108
+ for (const row of t.rows) {
109
+ // A row with the wrong number of cells may hold a field whose Name cell cannot be found.
110
+ if (row.length !== t.header.length) {
111
+ wellFormed = false;
112
+ continue;
113
+ }
114
+ if (!row[nameCol])
115
+ continue;
116
+ // A cell that names more than one field, or a path into a field (`items[i].count`), still
117
+ // names the field it starts with, though not its type.
118
+ for (const name of (row[nameCol].match(/`[^`]+`/g) ?? [row[nameCol]]).map((n) => n.replaceAll("`", "").trim())) {
119
+ const lead = /^[A-Za-z_][A-Za-z0-9_]*/.exec(name)?.[0];
120
+ if (lead === name)
121
+ layoutTypes.set(name, row[typeCol]);
122
+ else if (lead && !layoutTypes.has(lead))
123
+ layoutTypes.set(lead, "");
124
+ }
125
+ }
101
126
  }
102
127
  }
128
+ if (wellFormed)
129
+ layouts.set(id, layoutTypes);
103
130
  // An enumeration table kept in a value file counts as one of the entry's tables.
104
131
  const enumTables = enums ? [...tables(enums.text), ...valueFileTables(ctx, e, enums.text)] : [];
105
132
  e.valueTables = enumTables.filter((t) => t.file);
@@ -1,4 +1,11 @@
1
1
  import type { Context } from "../context.ts";
2
2
  import type { FormatNames } from "./formats.ts";
3
+ /**
4
+ * A procedure with each string literal emptied and its comments dropped. Strings go first, so a `#`
5
+ * inside one does not start a comment, and a string ends on the line it starts on.
6
+ */
7
+ export declare const withoutCommentsAndStrings: (code: string) => string;
8
+ /** The names a rule's Parameters section declares: each lower-case name that opens a code span, as `n` or `n: type`. */
9
+ export declare const parameterNames: (params: string) => string[];
3
10
  /** Checks every rule's procedure. Sets Entry.code on every rule entry, superseded ones included. */
4
11
  export declare function checkRules(ctx: Context, { enumNames }: FormatNames): void;
@@ -6,6 +6,13 @@ import { mayBeInterrupted, onlyEmulatedRuns } from "../evidence.js";
6
6
  import { asList, idsIn, kindOf } from "../ids.js";
7
7
  import { readCsv } from "../markdown.js";
8
8
  import { KINDS, LIST_LIMIT } from "../standard.js";
9
+ /**
10
+ * A procedure with each string literal emptied and its comments dropped. Strings go first, so a `#`
11
+ * inside one does not start a comment, and a string ends on the line it starts on.
12
+ */
13
+ export const withoutCommentsAndStrings = (code) => code.replace(/"[^"\n]*"/g, '""').replace(/#.*$/gm, "");
14
+ /** The names a rule's Parameters section declares: each lower-case name that opens a code span, as `n` or `n: type`. */
15
+ export const parameterNames = (params) => [...params.matchAll(/`([a-z_][a-z0-9_]*)(?=`|\s*:)/g)].map((m) => m[1]);
9
16
  const BUILTINS = new Set([
10
17
  "min",
11
18
  "max",
@@ -102,10 +109,7 @@ export function checkRules(ctx, { enumNames }) {
102
109
  // Types in a define's signature (`define roll(n: UINT16) -> char[]:`) are not names the
103
110
  // procedure reads, so they are dropped before the name checks.
104
111
  const TYPE = String.raw `(?:[A-Za-z][A-Za-z0-9]*|FMT-[A-Z0-9]+-\d+)(?:\[[^\]]*\])?`;
105
- const code = e
106
- .code.replace(/#.*$/gm, "")
107
- .replace(/"[^"]*"/g, '""')
108
- .replace(new RegExp(String.raw `(\bdefine\s+[a-z_][a-z0-9_]*\s*\()([^)]*)\)(\s*->\s*${TYPE})?`, "g"), (_, head, params) => `${head}${params
112
+ const code = withoutCommentsAndStrings(e.code).replace(new RegExp(String.raw `(\bdefine\s+[a-z_][a-z0-9_]*\s*\()([^)]*)\)(\s*->\s*${TYPE})?`, "g"), (_, head, params) => `${head}${params
109
113
  .split(",")
110
114
  .map((p) => p.split(":")[0].trim())
111
115
  .join(", ")})`);
@@ -196,8 +200,8 @@ export function checkRules(ctx, { enumNames }) {
196
200
  for (const p of m[1].split(","))
197
201
  locals.add(p.split(":")[0].trim());
198
202
  const params = e.sections.find((s) => s.title === "Parameters")?.text ?? "";
199
- for (const m of params.matchAll(/`([a-z_][a-z0-9_]*)`/g))
200
- locals.add(m[1]);
203
+ for (const name of parameterNames(params))
204
+ locals.add(name);
201
205
  for (const m of code.matchAll(/(?<![.\w])([a-z_][a-z0-9_]*)\s*\(/g)) {
202
206
  const name = m[1];
203
207
  if (BUILTINS.has(name) || KEYWORDS.has(name) || locals.has(name))
@@ -42,10 +42,10 @@
42
42
  // scalars, flow lists, and block lists of flat maps.
43
43
  //
44
44
  // This file reads the options and runs the phases in order: load the spec, check the entries, the
45
- // rules and what crosses entries, compile the Kaitai definitions, check the deviations, parity,
46
- // VALIDATION.md, the code's references and comments and the base ref, then write or check the
47
- // generated files. Every phase reports into one collector, which prints the problems at the end in
48
- // the order they were found.
45
+ // rules, the field names in their procedures and what crosses entries, compile the Kaitai
46
+ // definitions, check the deviations, parity, VALIDATION.md, the code's references and comments and
47
+ // the base ref, then write or check the generated files. Every phase reports into one collector,
48
+ // which prints the problems at the end in the order they were found.
49
49
  import { readFileSync } from "node:fs";
50
50
  import { dirname } from "node:path";
51
51
  import { fileURLToPath } from "node:url";
@@ -54,6 +54,7 @@ import { checkCommentAddresses } from "./checks/comment-addresses.js";
54
54
  import { checkAcrossEntries } from "./checks/cross-entry.js";
55
55
  import { checkDeviations } from "./checks/deviations.js";
56
56
  import { checkEntries } from "./checks/entries.js";
57
+ import { checkFieldNames } from "./checks/fields.js";
57
58
  import { compileKaitai } from "./checks/kaitai.js";
58
59
  import { checkParity } from "./checks/parity.js";
59
60
  import { checkReferences } from "./checks/references.js";
@@ -84,6 +85,7 @@ const spec = loadSpec({ config, problem });
84
85
  const ctx = { config, problem, spec, codeFiles: createCodeFiles(config, dirname(selfPath)) };
85
86
  const formatNames = checkEntries(ctx);
86
87
  checkRules(ctx, formatNames);
88
+ checkFieldNames(ctx, formatNames);
87
89
  checkAcrossEntries(ctx, formatNames);
88
90
  compileKaitai(ctx);
89
91
  const deviations = checkDeviations(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scientific-method/standard-checker",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Checks a restoration's spec/, parity/ and deviations/ against version 1 of the dinorefurb documentation standard.",
5
5
  "type": "module",
6
6
  "license": "MIT",