@descent-vtt/spec-graph 0.13.1 → 0.14.1
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.
- package/CHANGELOG.md +110 -0
- package/README.md +108 -8
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +214 -25
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +35 -5
- package/dist/config.js.map +1 -1
- package/dist/glob.js +3 -2
- package/dist/glob.js.map +1 -1
- package/dist/identity.d.ts +12 -0
- package/dist/identity.js +15 -0
- package/dist/identity.js.map +1 -1
- package/dist/report.d.ts +18 -2
- package/dist/report.js +30 -10
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +9 -0
- package/dist/resolve.js +1 -1
- package/dist/resolve.js.map +1 -1
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +9 -0
- package/dist/runner.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline, } from '.
|
|
|
14
14
|
import { discoverConfig, loadConfig } from './config.js';
|
|
15
15
|
import { DiffInputError, diffExports, formatDiffJson, formatDiffMarkdown, formatDiffText, parseGraphExport, } from './diff.js';
|
|
16
16
|
import { isGlob, rebasePattern, underRoot } from './glob.js';
|
|
17
|
+
import { isFamilyName } from './identity.js';
|
|
17
18
|
import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
|
|
18
19
|
import { formatGithub, formatGitlab, formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
19
20
|
import { DEFAULT_SEVERITIES, resolveStrict, RULE_DECISIONS, decisionUrl, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES, } from './rules.js';
|
|
@@ -60,16 +61,22 @@ OPTIONS
|
|
|
60
61
|
and the directory holding it becomes the root - so a
|
|
61
62
|
run from a subdirectory reports what a run from the
|
|
62
63
|
top reports. Paths typed on the command line stay
|
|
63
|
-
relative to where they were typed.
|
|
64
|
+
relative to where they were typed. It has to be a
|
|
65
|
+
directory that is there.
|
|
64
66
|
--ignore <glob> Skip paths. Repeatable.
|
|
65
67
|
--ignore-ref <glob> Do not report these reference targets when they fail
|
|
66
68
|
to resolve, for repositories where [[...]] tags a
|
|
67
69
|
concept rather than naming a file. Repeatable.
|
|
68
70
|
Suppresses findings only, never edges.
|
|
69
71
|
--family <name> Families a bare identifier in prose may name. When
|
|
70
|
-
given, everything else stays prose. Repeatable
|
|
72
|
+
given, everything else stays prose. Repeatable: one
|
|
73
|
+
name each time, the prefix its identifiers are
|
|
74
|
+
written with, ADR for ADR-0007. A document here has
|
|
75
|
+
to belong to one of them, or the check stops: a list
|
|
76
|
+
that names none allows nothing.
|
|
71
77
|
--ignore-family <name> Families that are never citations - RFC when the repo
|
|
72
|
-
cites RFC 2119 and keeps its own RFCs. Repeatable
|
|
78
|
+
cites RFC 2119 and keeps its own RFCs. Repeatable,
|
|
79
|
+
one name each time.
|
|
73
80
|
--history <glob> Files that log what was decided rather than deciding
|
|
74
81
|
it - journals, changelogs, minutes. Their links are
|
|
75
82
|
still checked; their obligations are not. Repeatable.
|
|
@@ -83,14 +90,15 @@ OPTIONS
|
|
|
83
90
|
--ratchet Also fail when a baseline entry no longer occurs, so
|
|
84
91
|
a paid-off exemption cannot outlive the defect.
|
|
85
92
|
--no-config Ignore .spec-graph.json and the package.json key.
|
|
86
|
-
--format <fmt> human, json, sarif, github, gitlab or markdown.
|
|
87
|
-
is
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
pull-request comment
|
|
93
|
-
check or diff (default:
|
|
93
|
+
--format <fmt> human, json, sarif, github, gitlab or markdown. json
|
|
94
|
+
is for check, query or diff. sarif is the interchange
|
|
95
|
+
format GitHub code scanning and editors already read,
|
|
96
|
+
github the workflow commands a GitHub Actions log
|
|
97
|
+
turns into annotations, and gitlab the Code Quality
|
|
98
|
+
report a GitLab merge request reads, all three for
|
|
99
|
+
check; markdown is a table for a pull-request comment
|
|
100
|
+
or $GITHUB_STEP_SUMMARY, for check or diff (default:
|
|
101
|
+
human)
|
|
94
102
|
--graph-format <fmt> dot, mermaid or json (default: dot)
|
|
95
103
|
--documents-only Leave items out of the exported graph
|
|
96
104
|
--rule <id>=<severity> Override one rule: error, warn, info or off. A project
|
|
@@ -101,12 +109,32 @@ OPTIONS
|
|
|
101
109
|
--strict Raise every warning to an error. An explicit --rule
|
|
102
110
|
still wins, so --strict --rule x=warn exempts x.
|
|
103
111
|
--color / --no-color Force colour on or off
|
|
104
|
-
--ascii Use ASCII glyphs only
|
|
112
|
+
--ascii Use ASCII glyphs only. SPEC_GRAPH_ASCII asks for the
|
|
113
|
+
same, unless it is empty, 0 or false
|
|
105
114
|
--verbose Include parse problems, per-file detail, and every
|
|
106
115
|
reference --ignore-ref or --ignore-family silenced
|
|
116
|
+
--explain With rules: add each rule's selector and the ADR
|
|
117
|
+
that decided it
|
|
107
118
|
-h, --help Show this help
|
|
108
119
|
-v, --version Show the version
|
|
109
120
|
|
|
121
|
+
An option belongs to the commands that read it, and any other command
|
|
122
|
+
refuses it by name:
|
|
123
|
+
|
|
124
|
+
--root, --verbose check, query, graph, rules
|
|
125
|
+
--ignore, --history check, query, graph
|
|
126
|
+
--format check, query, diff
|
|
127
|
+
--graph-format, --documents-only graph
|
|
128
|
+
--explain rules
|
|
129
|
+
every other option check
|
|
130
|
+
|
|
131
|
+
--no-config, --no-color and --ascii are taken by every command: each is
|
|
132
|
+
true already of a command with nothing to read it for.
|
|
133
|
+
|
|
134
|
+
A value that begins with -- reads as an option, so the option before it is
|
|
135
|
+
refused as missing its value. A name that does begin so is written ./--name
|
|
136
|
+
for a file, [-]-name in a pattern and \\--name for a reference target.
|
|
137
|
+
|
|
110
138
|
SELECTORS
|
|
111
139
|
A selector is a path through the graph.
|
|
112
140
|
|
|
@@ -132,6 +160,11 @@ EXIT CODES
|
|
|
132
160
|
1 findings at error severity (or over --max-warnings)
|
|
133
161
|
2 the tool could not run
|
|
134
162
|
|
|
163
|
+
An input that is set and names nothing is a 2, named on stderr, and never
|
|
164
|
+
read as if it had not been given: an empty value, a family name no family
|
|
165
|
+
has, a list of families no document here belongs to, a --root that is no
|
|
166
|
+
directory, a second word after rules, an option of another command.
|
|
167
|
+
|
|
135
168
|
CONFIGURATION
|
|
136
169
|
Anything repeated on every run belongs in the repository rather than in the
|
|
137
170
|
command. spec-graph reads the first of these that exists:
|
|
@@ -153,9 +186,10 @@ CONFIGURATION
|
|
|
153
186
|
finding lists, 8 unless set, and 0 lists none.
|
|
154
187
|
|
|
155
188
|
A problem in that file stops the run with exit 2, named on stderr: invalid
|
|
156
|
-
JSON, an unknown key, a rule that does not compile
|
|
157
|
-
|
|
158
|
-
|
|
189
|
+
JSON, an unknown key, a rule that does not compile, a file that is there and
|
|
190
|
+
cannot be read. A configuration that did not load checks a different
|
|
191
|
+
repository than the one configured, and would report that one as consistent.
|
|
192
|
+
--no-config runs on defaults instead.
|
|
159
193
|
|
|
160
194
|
PROJECT RULES
|
|
161
195
|
A convention spec-graph never anticipated is a selector plus a sentence, and
|
|
@@ -213,6 +247,61 @@ EXAMPLES
|
|
|
213
247
|
spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
214
248
|
`;
|
|
215
249
|
const SEVERITIES = ['error', 'warn', 'info', 'off'];
|
|
250
|
+
/**
|
|
251
|
+
* The commands that read an option, or nothing for one every command takes.
|
|
252
|
+
*
|
|
253
|
+
* Any other command refuses the option, rather than run as if it were not
|
|
254
|
+
* there: `query --strict` taking the flag and doing nothing with it would
|
|
255
|
+
* tell someone their query was strict, `graph --record-baseline` wrote no
|
|
256
|
+
* file and said so nowhere, and `spec-graph --graph-format mermaid`, the
|
|
257
|
+
* command forgotten, wrote the check's report into what was meant to hold a
|
|
258
|
+
* graph. The three options about citations are a check's alone: they pass
|
|
259
|
+
* over findings and never an edge, so a graph or a query is the same with
|
|
260
|
+
* them as without.
|
|
261
|
+
*
|
|
262
|
+
* `--no-color`, `--ascii` and `--no-config` are taken by every command.
|
|
263
|
+
* Each is true already of what the commands that do not read it write - none
|
|
264
|
+
* of them colours or draws a glyph, and `diff` reads no configuration - and
|
|
265
|
+
* a script passes them to every command it runs. `--color` is not one of
|
|
266
|
+
* them: it would promise colour that only a check prints.
|
|
267
|
+
*
|
|
268
|
+
* A function and not a table, so that none of it runs when the module loads:
|
|
269
|
+
* a mutant of a table built at import pays for the whole suite (ADR-0007).
|
|
270
|
+
*/
|
|
271
|
+
function readersOf(option) {
|
|
272
|
+
switch (option) {
|
|
273
|
+
case '--root':
|
|
274
|
+
case '--verbose':
|
|
275
|
+
return ['check', 'query', 'graph', 'rules'];
|
|
276
|
+
case '--ignore':
|
|
277
|
+
case '--history':
|
|
278
|
+
return ['check', 'query', 'graph'];
|
|
279
|
+
case '--format':
|
|
280
|
+
return ['check', 'query', 'diff'];
|
|
281
|
+
case '--graph-format':
|
|
282
|
+
case '--documents-only':
|
|
283
|
+
return ['graph'];
|
|
284
|
+
case '--explain':
|
|
285
|
+
return ['rules'];
|
|
286
|
+
case '--ignore-ref':
|
|
287
|
+
case '--family':
|
|
288
|
+
case '--ignore-family':
|
|
289
|
+
case '--baseline':
|
|
290
|
+
case '--record-baseline':
|
|
291
|
+
case '--ratchet':
|
|
292
|
+
case '--rule':
|
|
293
|
+
case '--strict':
|
|
294
|
+
case '--max':
|
|
295
|
+
case '--max-warnings':
|
|
296
|
+
case '--color':
|
|
297
|
+
return ['check'];
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
/** `a`, `a or b`, `a, b or c`: one name or more in a sentence, with the word that joins the last. */
|
|
302
|
+
function listed(names, joint) {
|
|
303
|
+
return names.length < 2 ? names[0] : `${names.slice(0, -1).join(', ')} ${joint} ${names[names.length - 1]}`;
|
|
304
|
+
}
|
|
216
305
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
217
306
|
export function parseArgs(argv, cwd) {
|
|
218
307
|
const args = [...argv];
|
|
@@ -247,10 +336,42 @@ export function parseArgs(argv, cwd) {
|
|
|
247
336
|
let selector = null;
|
|
248
337
|
let help = false;
|
|
249
338
|
let version = false;
|
|
339
|
+
// In the order they were given, so the first that does not belong is the
|
|
340
|
+
// one named. A set keeps that order, and starts from no literal: a list
|
|
341
|
+
// seeded with a word no option is called would refuse nothing more, which
|
|
342
|
+
// is a mutant no test can tell from the code.
|
|
343
|
+
const given = new Set();
|
|
250
344
|
const next = (flag, index) => {
|
|
251
345
|
const value = args[index + 1];
|
|
252
346
|
if (value === undefined)
|
|
253
347
|
throw new UsageError(`${flag} needs a value`);
|
|
348
|
+
// The option after one whose value was forgotten. Taken as the value,
|
|
349
|
+
// `--ignore --strict` ignored a directory called `--strict` and the check
|
|
350
|
+
// was not strict, and `--record-baseline --verbose` wrote a file of that
|
|
351
|
+
// name. A name that does begin with two dashes has another spelling
|
|
352
|
+
// wherever one can be meant: `./--x` for a file, `[-]-x` in a pattern,
|
|
353
|
+
// `\--x` for a reference target.
|
|
354
|
+
if (value.startsWith('--') || value === '-h' || value === '-v') {
|
|
355
|
+
throw new UsageError(`${flag} needs a value, and ${value} reads as an option`);
|
|
356
|
+
}
|
|
357
|
+
return value;
|
|
358
|
+
};
|
|
359
|
+
// A blank value is what an unset variable leaves in `--root "$DIR"`. A path
|
|
360
|
+
// made of it names the directory beside it or a file called " ", and the run
|
|
361
|
+
// then answered for a place nobody named.
|
|
362
|
+
const filled = (flag, index, what) => {
|
|
363
|
+
const value = next(flag, index);
|
|
364
|
+
if (value.trim() === '')
|
|
365
|
+
throw new UsageError(`${flag} expects ${what}, got "${value}"`);
|
|
366
|
+
return value;
|
|
367
|
+
};
|
|
368
|
+
// A family is taken once per option. A blank, `ADR,RFC` or the option typed
|
|
369
|
+
// after it is a name no family has: alone on the allowlist it left every
|
|
370
|
+
// citation as prose, and the check passed over the ones that dangle.
|
|
371
|
+
const family = (flag, index) => {
|
|
372
|
+
const value = next(flag, index);
|
|
373
|
+
if (!isFamilyName(value))
|
|
374
|
+
throw new UsageError(`${flag} expects one family's name, such as ADR, got "${value}"`);
|
|
254
375
|
return value;
|
|
255
376
|
};
|
|
256
377
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -266,6 +387,7 @@ export function parseArgs(argv, cwd) {
|
|
|
266
387
|
patterns.push(arg);
|
|
267
388
|
continue;
|
|
268
389
|
}
|
|
390
|
+
given.add(arg);
|
|
269
391
|
switch (arg) {
|
|
270
392
|
case '-h':
|
|
271
393
|
case '--help':
|
|
@@ -300,7 +422,7 @@ export function parseArgs(argv, cwd) {
|
|
|
300
422
|
verbose = true;
|
|
301
423
|
break;
|
|
302
424
|
case '--root':
|
|
303
|
-
root =
|
|
425
|
+
root = filled(arg, i, 'a directory');
|
|
304
426
|
rootExplicit = true;
|
|
305
427
|
i += 1;
|
|
306
428
|
break;
|
|
@@ -313,11 +435,11 @@ export function parseArgs(argv, cwd) {
|
|
|
313
435
|
i += 1;
|
|
314
436
|
break;
|
|
315
437
|
case '--family':
|
|
316
|
-
families.push(
|
|
438
|
+
families.push(family(arg, i));
|
|
317
439
|
i += 1;
|
|
318
440
|
break;
|
|
319
441
|
case '--ignore-family':
|
|
320
|
-
ignoreFamilies.push(
|
|
442
|
+
ignoreFamilies.push(family(arg, i));
|
|
321
443
|
i += 1;
|
|
322
444
|
break;
|
|
323
445
|
case '--history':
|
|
@@ -325,11 +447,11 @@ export function parseArgs(argv, cwd) {
|
|
|
325
447
|
i += 1;
|
|
326
448
|
break;
|
|
327
449
|
case '--baseline':
|
|
328
|
-
baseline =
|
|
450
|
+
baseline = filled(arg, i, 'a file');
|
|
329
451
|
i += 1;
|
|
330
452
|
break;
|
|
331
453
|
case '--record-baseline':
|
|
332
|
-
recordBaseline =
|
|
454
|
+
recordBaseline = filled(arg, i, 'a file');
|
|
333
455
|
i += 1;
|
|
334
456
|
break;
|
|
335
457
|
case '--no-config':
|
|
@@ -404,9 +526,35 @@ export function parseArgs(argv, cwd) {
|
|
|
404
526
|
if (format === 'markdown' && command !== 'check' && command !== 'diff' && !help && !version) {
|
|
405
527
|
throw new UsageError(`--format markdown is a report for a pull request, so it belongs to check or diff, not to ${command}`);
|
|
406
528
|
}
|
|
529
|
+
// JSON is refused where the other reports are, and for the reason above:
|
|
530
|
+
// `graph --format json` wrote DOT and `rules --format json` a list, each
|
|
531
|
+
// with exit 0, to whatever was waiting for JSON.
|
|
532
|
+
if (format === 'json' && (command === 'graph' || command === 'rules') && !help && !version) {
|
|
533
|
+
const instead = command === 'graph' ? 'graph writes JSON with --graph-format json' : 'rules lists the rules as text';
|
|
534
|
+
throw new UsageError(`--format json belongs to check, query or diff, not to ${command}: ${instead}`);
|
|
535
|
+
}
|
|
536
|
+
if (!help && !version) {
|
|
537
|
+
for (const option of given) {
|
|
538
|
+
const readers = readersOf(option);
|
|
539
|
+
if (readers !== undefined && !readers.includes(command)) {
|
|
540
|
+
throw new UsageError(`${option} belongs to ${listed(readers, 'or')}, not to ${command}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// The second word given to `rules` was dropped, so `rules a b` listed `a`
|
|
545
|
+
// and said nothing of `b`.
|
|
546
|
+
if (command === 'rules' && patterns.length > 1 && !help && !version) {
|
|
547
|
+
throw new UsageError(`rules takes one rule id, got ${patterns.length}: ${patterns.join(', ')}`);
|
|
548
|
+
}
|
|
407
549
|
if (command === 'diff' && patterns.length !== 2 && !help && !version) {
|
|
408
550
|
throw new UsageError('diff compares two graph exports, for example:\n spec-graph diff base.json head.json\n (make each with spec-graph graph --graph-format json)');
|
|
409
551
|
}
|
|
552
|
+
// An empty name resolved to the directory the run was started in, and the
|
|
553
|
+
// refusal was the read's: `cannot read : EISDIR`.
|
|
554
|
+
const blank = patterns.findIndex((path) => path.trim() === '');
|
|
555
|
+
if (command === 'diff' && blank !== -1 && !help && !version) {
|
|
556
|
+
throw new UsageError(`diff expects a graph export on each side, got "${patterns[blank]}" for the ${blank === 0 ? 'first' : 'second'}`);
|
|
557
|
+
}
|
|
410
558
|
if (command === 'query' && selector === null && !help && !version) {
|
|
411
559
|
throw new UsageError('query needs a selector, for example:\n spec-graph query \'item[openness=open]\'');
|
|
412
560
|
}
|
|
@@ -540,6 +688,20 @@ async function run(io, err) {
|
|
|
540
688
|
// it can compare exports from a checkout it is not running in.
|
|
541
689
|
if (options.command === 'diff')
|
|
542
690
|
return runDiff(options.patterns, cwd, options.format, out, err);
|
|
691
|
+
// A root that was named is a directory, for every command that reads one.
|
|
692
|
+
// `rules --root` with a path that is not there listed the built-in rules and
|
|
693
|
+
// exited 0, as if the repository declared none, and the others said only
|
|
694
|
+
// that no specification matched under it.
|
|
695
|
+
if (options.rootExplicit) {
|
|
696
|
+
const { stat } = await import('node:fs/promises');
|
|
697
|
+
// A path that cannot be reached reads as one that is not there: either
|
|
698
|
+
// way there is nothing under it to read.
|
|
699
|
+
const found = await stat(options.root).catch(() => null);
|
|
700
|
+
if (found === null || !found.isDirectory()) {
|
|
701
|
+
err(`spec-graph: --root "${options.root}" is not ${found === null ? 'there' : 'a directory'}\n`);
|
|
702
|
+
return EXIT_ERROR;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
543
705
|
const color = options.color ?? shouldUseColor({ isTTY: io.isTTY, env });
|
|
544
706
|
const ascii = options.ascii ?? shouldUseAscii({ env });
|
|
545
707
|
// Configuration is what is true of the repository; a flag is somebody
|
|
@@ -625,16 +787,19 @@ async function run(io, err) {
|
|
|
625
787
|
const patterns = typed.patterns.length > 0 ? typed.patterns : (file.patterns ?? DEFAULT_PATTERNS);
|
|
626
788
|
const severityOverrides = { ...(file.severities ?? {}), ...options.severities };
|
|
627
789
|
const { severities, escalated } = resolveStrict(severityOverrides, options.strict || (file.strict ?? false), projectRules);
|
|
790
|
+
// The list the check is held against, once it has read the documents.
|
|
791
|
+
const families = [...(file.families ?? []), ...options.families];
|
|
628
792
|
const analyseOptions = {
|
|
629
793
|
root,
|
|
630
794
|
patterns,
|
|
631
|
-
// A stray entry in one of the empty fallbacks
|
|
632
|
-
// it matches a path
|
|
633
|
-
//
|
|
634
|
-
//
|
|
795
|
+
// A stray entry in one of the empty fallbacks here changes a run only if
|
|
796
|
+
// it matches a path or a target some corpus has, so the mutants that plant
|
|
797
|
+
// one survive every corpus but one built for them - except in the two
|
|
798
|
+
// lists of families, where what they plant is no family's name and is
|
|
799
|
+
// refused.
|
|
635
800
|
ignore: [...(file.ignore ?? []), ...typed.ignore],
|
|
636
801
|
ignoreReferences: [...(file.ignoreReferences ?? []), ...options.ignoreReferences],
|
|
637
|
-
families
|
|
802
|
+
families,
|
|
638
803
|
ignoreFamilies: [...(file.ignoreFamilies ?? []), ...options.ignoreFamilies],
|
|
639
804
|
historyPatterns: [...(file.historyPatterns ?? []), ...typed.history],
|
|
640
805
|
severities,
|
|
@@ -701,6 +866,30 @@ async function run(io, err) {
|
|
|
701
866
|
return matches.length > 0 ? EXIT_OK : EXIT_FAILED;
|
|
702
867
|
}
|
|
703
868
|
default: {
|
|
869
|
+
// An allowlist is held against the corpus it was written for. One that
|
|
870
|
+
// names no family a document here belongs to allows nothing the corpus
|
|
871
|
+
// has: every citation is then prose, and the run that failed on a
|
|
872
|
+
// dangling one reported a consistent graph - for `--family ADRS` as it
|
|
873
|
+
// did for `--family ""`. Nothing was measured, which is not clean
|
|
874
|
+
// (spec-core's ADR-0005), so the run stops before it says anything. A
|
|
875
|
+
// list that names such a family beside one the corpus has is a family
|
|
876
|
+
// the repository means to start, or a typo in half of it: said, and
|
|
877
|
+
// nothing fails.
|
|
878
|
+
const allowed = [...new Set(families.map((family) => family.trim().toUpperCase()))];
|
|
879
|
+
const absent = allowed.filter((family) => !result.corpus.families.includes(family));
|
|
880
|
+
if (absent.length > 0) {
|
|
881
|
+
const lists = [];
|
|
882
|
+
if (options.families.length > 0)
|
|
883
|
+
lists.push('--family');
|
|
884
|
+
if (file.families !== undefined && file.families.length > 0)
|
|
885
|
+
lists.push(`"families" in ${loaded.source}`);
|
|
886
|
+
const here = `the documents here belong to ${result.corpus.families.length > 0 ? listed(result.corpus.families, 'and') : 'no family'}`;
|
|
887
|
+
if (absent.length === allowed.length) {
|
|
888
|
+
err(`spec-graph: no document here belongs to a family named by ${lists.join(' and ')} (${allowed.join(', ')}); ${here}\n name a family the documents belong to, or leave the list out\n`);
|
|
889
|
+
return EXIT_ERROR;
|
|
890
|
+
}
|
|
891
|
+
err(`spec-graph: no document here belongs to ${listed(absent, 'or')}, named by ${lists.join(' and ')}; ${here}\n`);
|
|
892
|
+
}
|
|
704
893
|
// Recording is not checking. It writes down what is wrong today so that
|
|
705
894
|
// tomorrow can be compared against it, and says nothing about whether
|
|
706
895
|
// today is acceptable - so it reports what it wrote and exits clean.
|