@descent-vtt/spec-graph 0.17.0 → 0.18.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.
package/dist/cli.js CHANGED
@@ -10,10 +10,11 @@
10
10
  * itself could not run. A usage mistake never masquerades as a passing build,
11
11
  * and an error nothing here expected never as findings.
12
12
  */
13
+ import { resolve } from 'node:path';
13
14
  import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline, } from './baseline.js';
14
15
  import { discoverConfig, loadConfig } from './config.js';
15
16
  import { DiffInputError, diffExports, formatDiffJson, formatDiffMarkdown, formatDiffText, parseGraphExport, } from './diff.js';
16
- import { isGlob, rebasePattern, underRoot } from './glob.js';
17
+ import { createGlobMatcher, isGlob, rebasePattern, underRoot, walkFiles } from './glob.js';
17
18
  import { isFamilyName } from './identity.js';
18
19
  import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
19
20
  import { formatGithub, formatGitlab, formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
@@ -83,13 +84,16 @@ OPTIONS
83
84
  --history <glob> Files that log what was decided rather than deciding
84
85
  it - journals, changelogs, minutes. Their links are
85
86
  still checked; their obligations are not. Repeatable.
87
+ A pattern whose files an ignore leaves out is
88
+ refused: no journal it names would be read.
86
89
  --baseline <file> Accept the findings recorded in this file and report
87
90
  only what is new since. The file has to be there: a
88
91
  path that reads nothing accepts nothing and says so
89
92
  nowhere. A "baseline" in the configuration may name a
90
93
  file not recorded yet; a path typed here may not.
91
94
  --record-baseline <f> Write today's findings to this file as accepted debt,
92
- and exit 0 without judging them.
95
+ and exit 0 without judging them. With --format json
96
+ the recording is a JSON document.
93
97
  --ratchet Also fail when a baseline entry no longer occurs, so
94
98
  a paid-off exemption cannot outlive the defect.
95
99
  --no-config Ignore .spec-graph.json and the package.json key.
@@ -147,18 +151,31 @@ OPTIONS
147
151
  An option that takes one value is refused given two, as in --root a
148
152
  --root b: the later would be read and the earlier go unread. That is each
149
153
  option above with a value that is not marked Repeatable, and one rule
150
- given two severities by --rule. The same value twice is that value once.
154
+ given two severities by --rule. Two values are compared as each is read,
155
+ so one value twice is that value once however it is spelled: --root .
156
+ beside --root ./ names one directory.
151
157
 
152
158
  A baseline to read is refused beside one to record, --baseline beside
153
159
  --record-baseline: a recording writes every finding and opens no
154
- baseline. So is one family given to both --family and --ignore-family,
155
- whose citations are passed over whatever asked for them. One family asked
156
- for beside another ignored is taken.
160
+ baseline. So is each option a recording does not read, since it judges,
161
+ shows and colours no finding: --ratchet, --strict, --max, --max-warnings,
162
+ --color, --no-ascii, and --format sarif, github or gitlab. So is one
163
+ family given to both --family and --ignore-family, whose citations are
164
+ passed over whatever asked for them. One family asked for beside another
165
+ ignored is taken.
157
166
 
158
167
  A value that begins with -- reads as an option, so the option before it is
159
168
  refused as missing its value. A name that does begin so is written ./--name
160
169
  for a file, [-]-name in a pattern and \\--name for a reference target.
161
170
 
171
+ A line that is wrong is refused with --help or --version on it too, an
172
+ option a recording does not read among them, and --help beside --version
173
+ is refused: one of the two answers would go unread. What a command still
174
+ needs, a selector or two exports, is what --help is for: query --help and
175
+ diff --help print it. The help reads no disk, so what only the run finds,
176
+ a --root that is not there or a --history an ignore leaves out, is
177
+ answered by it.
178
+
162
179
  SELECTORS
163
180
  A selector is a path through the graph.
164
181
 
@@ -190,7 +207,9 @@ EXIT CODES
190
207
  directory, a second word after rules, an option of another command. So
191
208
  are two things of which one would go unread: an option beside its own
192
209
  opposite, an option that takes one value given two, --baseline beside
193
- --record-baseline, a family both asked for and ignored.
210
+ --record-baseline, an option a recording does not read, a family both
211
+ asked for and ignored, a --history whose files an ignore leaves out,
212
+ --help beside --version.
194
213
 
195
214
  CONFIGURATION
196
215
  Anything repeated on every run belongs in the repository rather than in the
@@ -209,12 +228,13 @@ CONFIGURATION
209
228
  "maxRelated": 3 }
210
229
 
211
230
  A flag always wins over the file, and list flags add to it rather than
212
- replacing it. maxRelated has no flag: it caps how many related locations one
231
+ replacing it: --family RFC reads the citations "ignoreFamilies" passes
232
+ over. maxRelated has no flag: it caps how many related locations one
213
233
  finding lists, 8 unless set, and 0 lists none.
214
234
 
215
235
  A problem in that file stops the run with exit 2, named on stderr: invalid
216
- JSON, an unknown key, a rule that does not compile, a file that is there and
217
- cannot be read. A configuration that did not load checks a different
236
+ JSON, an unknown key, a rule that does not compile, one family in both
237
+ "families" and "ignoreFamilies", a file that is there and cannot be read. A configuration that did not load checks a different
218
238
  repository than the one configured, and would report that one as consistent.
219
239
  --no-config runs on defaults instead.
220
240
 
@@ -361,6 +381,31 @@ function unreadBeside(option) {
361
381
  }
362
382
  return undefined;
363
383
  }
384
+ /**
385
+ * Whether a recording leaves this option of a check unread.
386
+ *
387
+ * A recording writes every finding of the run down and judges none, shows
388
+ * none and colours nothing: `--record-baseline debt.json --strict` recorded
389
+ * what the line without `--strict` records, and said it was strict. Each of
390
+ * these is refused beside it, as `--baseline` is (spec-core's ADR-0005,
391
+ * 2026-10-11). The options a recording does read are not here: the ones that
392
+ * choose what is found, `--rule` turning a rule off among them, `--verbose`,
393
+ * which names the configuration read, and `--no-color` and `--ascii`, which
394
+ * ask for what is already so. A function and not a table, for the reason
395
+ * `readersOf` is one.
396
+ */
397
+ function unreadByRecording(option) {
398
+ switch (option) {
399
+ case '--ratchet':
400
+ case '--strict':
401
+ case '--max':
402
+ case '--max-warnings':
403
+ case '--color':
404
+ case '--no-ascii':
405
+ return true;
406
+ }
407
+ return false;
408
+ }
364
409
  /** `a`, `a or b`, `a, b or c`: one name or more in a sentence, with the word that joins the last. */
365
410
  function listed(names, joint) {
366
411
  return names.length < 2 ? names[0] : `${names.slice(0, -1).join(', ')} ${joint} ${names[names.length - 1]}`;
@@ -437,24 +482,20 @@ export function parseArgs(argv, cwd) {
437
482
  throw new UsageError(`${flag} expects one family's name, such as ADR, got "${value}"`);
438
483
  return value;
439
484
  };
440
- // What an option that takes one value was first given, and the line for
441
- // the first such option given another. The later was read and the earlier
485
+ // What each option that takes one value was given, in the order given, and
486
+ // what the run reads it as. Given two, the later was read and the earlier
442
487
  // went unread: `--root nowhere --root .` checked `.` and never said that
443
488
  // `nowhere` is not there, and `query --format sarif --format json` wrote
444
- // JSON where the two the other way round are refused. The same value
445
- // again is that value once, as an option that takes none is. Values are
446
- // compared as they were typed: a path spelled two ways is two values to
447
- // whoever reads the line. The line is thrown after the loop, with what
448
- // else help and the version are answered before.
449
- const taken = new Map();
450
- let second = null;
451
- const once = (flag, value, key = flag, takes = 'takes one value') => {
452
- const held = taken.get(key);
453
- if (held === undefined)
454
- taken.set(key, value);
455
- else if (held !== value)
456
- second ??= `${flag} is given "${held}" and "${value}", and ${takes}: give one of the two`;
457
- return value;
489
+ // JSON where the two the other way round are refused. Two values are
490
+ // compared as each is read, not as it is written, so `--root .` beside
491
+ // `--root ./` names one directory and is taken once (spec-core's ADR-0005,
492
+ // 2026-10-18). Each is read once the whole line has been, since a
493
+ // baseline typed beside `--root` is read from the root, wherever on the
494
+ // line the root stands. A mutant that seeds this list with a string holds
495
+ // it under no option's key, where no value is compared with it: equivalent.
496
+ const valued = [];
497
+ const once = (flag, typed, read, key = flag, takes = 'takes one value') => {
498
+ valued.push({ flag, key, typed, read, takes });
458
499
  };
459
500
  for (let i = 0; i < args.length; i += 1) {
460
501
  const arg = args[i];
@@ -506,11 +547,17 @@ export function parseArgs(argv, cwd) {
506
547
  case '--explain':
507
548
  verbose = true;
508
549
  break;
509
- case '--root':
510
- root = once(arg, filled(arg, i, 'a directory'));
550
+ case '--root': {
551
+ // Resolved from where the line was typed and compared as text: the
552
+ // disk is not asked, so a link and its target are two values, and
553
+ // no case is folded.
554
+ const value = filled(arg, i, 'a directory');
555
+ once(arg, value, () => resolve(cwd, value));
556
+ root = value;
511
557
  rootExplicit = true;
512
558
  i += 1;
513
559
  break;
560
+ }
514
561
  case '--ignore':
515
562
  ignore.push(next(arg, i));
516
563
  i += 1;
@@ -531,14 +578,22 @@ export function parseArgs(argv, cwd) {
531
578
  historyPatterns.push(next(arg, i));
532
579
  i += 1;
533
580
  break;
534
- case '--baseline':
535
- baseline = once(arg, filled(arg, i, 'a file'));
581
+ // A baseline typed here is read from the root where `--root` names one,
582
+ // and from where it was typed otherwise, which is the root's default.
583
+ case '--baseline': {
584
+ const value = filled(arg, i, 'a file');
585
+ once(arg, value, () => resolve(cwd, root, value));
586
+ baseline = value;
536
587
  i += 1;
537
588
  break;
538
- case '--record-baseline':
539
- recordBaseline = once(arg, filled(arg, i, 'a file'));
589
+ }
590
+ case '--record-baseline': {
591
+ const value = filled(arg, i, 'a file');
592
+ once(arg, value, () => resolve(cwd, root, value));
593
+ recordBaseline = value;
540
594
  i += 1;
541
595
  break;
596
+ }
542
597
  case '--no-config':
543
598
  noConfig = true;
544
599
  break;
@@ -553,7 +608,7 @@ export function parseArgs(argv, cwd) {
553
608
  throw new UsageError(`--format must be human, json, sarif, markdown, gitlab or github, got "${value}"`);
554
609
  }
555
610
  format = value;
556
- once(arg, value);
611
+ once(arg, value, () => value);
557
612
  i += 1;
558
613
  break;
559
614
  }
@@ -563,7 +618,7 @@ export function parseArgs(argv, cwd) {
563
618
  throw new UsageError(`--graph-format must be dot, mermaid or json, got "${value}"`);
564
619
  }
565
620
  graphFormat = value;
566
- once(arg, value);
621
+ once(arg, value, () => value);
567
622
  i += 1;
568
623
  break;
569
624
  }
@@ -587,17 +642,27 @@ export function parseArgs(argv, cwd) {
587
642
  throw new UsageError(`unknown severity "${level}", expected one of: ${SEVERITIES.join(', ')}`);
588
643
  }
589
644
  severities[id] = level;
590
- once(arg, value, `${arg} ${id}`, 'a rule has one severity');
645
+ // The id is the key and is read as written, so the severity alone
646
+ // tells two values apart, and it is read as written too.
647
+ once(arg, value, () => value, `${arg} ${id}`, 'a rule has one severity');
591
648
  i += 1;
592
649
  break;
593
650
  }
651
+ // A count is read past the spaces a shell variable brings, so ` 1` is
652
+ // the `1` it reads as.
594
653
  case '--max': {
595
- max = parseCount(arg, once(arg, next(arg, i)));
654
+ const value = next(arg, i);
655
+ const count = parseCount(arg, value);
656
+ once(arg, value, () => count);
657
+ max = count;
596
658
  i += 1;
597
659
  break;
598
660
  }
599
661
  case '--max-warnings': {
600
- maxWarnings = parseCount(arg, once(arg, next(arg, i)));
662
+ const value = next(arg, i);
663
+ const count = parseCount(arg, value);
664
+ once(arg, value, () => count);
665
+ maxWarnings = count;
601
666
  i += 1;
602
667
  break;
603
668
  }
@@ -605,67 +670,98 @@ export function parseArgs(argv, cwd) {
605
670
  throw new UsageError(`unknown option "${arg}"\n run "spec-graph --help" to see the available options`);
606
671
  }
607
672
  }
673
+ // A line that is wrong is refused with `--help` or `--version` on it, as an
674
+ // option nobody knows always was: answered first, the help told a script
675
+ // nothing of the line it had written, and the version answered it as if
676
+ // the line were right (spec-core's ADR-0005, 2026-10-18). What a command
677
+ // still lacks, a selector or an export, is what the help is asked for, and
678
+ // is refused only where neither is asked.
608
679
  // SARIF, GitHub's workflow commands and GitLab's Code Quality are reports
609
680
  // about findings, and only `check` produces those. Falling back to JSON would
610
681
  // hand a pipeline something its uploader rejects with a message about a
611
682
  // schema rather than about the command that was run. Markdown is for a pull
612
683
  // request, which reads a check or a diff.
613
- if ((format === 'sarif' || format === 'gitlab' || format === 'github') && command !== 'check' && !help && !version) {
684
+ if ((format === 'sarif' || format === 'gitlab' || format === 'github') && command !== 'check') {
614
685
  throw new UsageError(`--format ${format} reports findings, so it belongs to check, not to ${command}`);
615
686
  }
616
- if (format === 'markdown' && command !== 'check' && command !== 'diff' && !help && !version) {
687
+ if (format === 'markdown' && command !== 'check' && command !== 'diff') {
617
688
  throw new UsageError(`--format markdown is a report for a pull request, so it belongs to check or diff, not to ${command}`);
618
689
  }
619
690
  // JSON is refused where the other reports are, and for the reason above:
620
691
  // `graph --format json` wrote DOT and `rules --format json` a list, each
621
692
  // with exit 0, to whatever was waiting for JSON.
622
- if (format === 'json' && (command === 'graph' || command === 'rules') && !help && !version) {
693
+ if (format === 'json' && (command === 'graph' || command === 'rules')) {
623
694
  const instead = command === 'graph' ? 'graph writes JSON with --graph-format json' : 'rules lists the rules as text';
624
695
  throw new UsageError(`--format json belongs to check, query or diff, not to ${command}: ${instead}`);
625
696
  }
626
- if (!help && !version) {
627
- for (const option of given) {
628
- const readers = readersOf(option);
629
- if (readers !== undefined && !readers.includes(command)) {
630
- throw new UsageError(`${option} belongs to ${listed(readers, 'or')}, not to ${command}`);
631
- }
697
+ for (const option of given) {
698
+ const readers = readersOf(option);
699
+ if (readers !== undefined && !readers.includes(command)) {
700
+ throw new UsageError(`${option} belongs to ${listed(readers, 'or')}, not to ${command}`);
632
701
  }
633
- // After the loop above, so that an option of another command is named
634
- // before the pair it is half of: `query --color --no-color` is told
635
- // that `--color` is a check's, which is the line to act on first.
636
- for (const option of given) {
637
- const other = unreadBeside(option);
638
- if (other !== undefined && given.has(other.option)) {
639
- throw new UsageError(`${option} is given beside ${other.option}, and ${other.decided}: give one of the two`);
640
- }
702
+ }
703
+ // Two answers, of which one would go unread: the help was printed and the
704
+ // version was not, whichever came first.
705
+ if (help && version) {
706
+ throw new UsageError('--help is given beside --version, and a run prints the help or the version: give one of the two');
707
+ }
708
+ // After the loop above, so that an option of another command is named
709
+ // before the pair it is half of: `query --color --no-color` is told that
710
+ // `--color` is a check's, which is the line to act on first.
711
+ for (const option of given) {
712
+ const other = unreadBeside(option);
713
+ if (other !== undefined && given.has(other.option)) {
714
+ throw new UsageError(`${option} is given beside ${other.option}, and ${other.decided}: give one of the two`);
641
715
  }
642
- if (second !== null)
643
- throw new UsageError(second);
644
- // A family on both lists is passed over, since the list that ignores is
645
- // asked first, and its name on the other says nothing: `--family ADR
646
- // --family RFC --ignore-family RFC` is `--family ADR`. Where it is the
647
- // only name there, every citation in prose is passed over, and the check
648
- // that fails without the two reported a consistent graph. Two families,
649
- // one asked for and another ignored, are both read, and are taken.
650
- for (const asked of families) {
651
- const ignored = ignoreFamilies.find((name) => name.trim().toUpperCase() === asked.trim().toUpperCase());
652
- if (ignored !== undefined) {
653
- throw new UsageError(`--family ${asked} is given beside --ignore-family ${ignored}, and a family's citations are read or they are passed over: give one of the two`);
716
+ }
717
+ // Each value is held to the first its option was given, so the line names
718
+ // the first two that read as two, wherever on it they stand.
719
+ const firstOf = new Map();
720
+ for (const value of valued) {
721
+ const held = firstOf.get(value.key);
722
+ if (held === undefined)
723
+ firstOf.set(value.key, value);
724
+ else if (held.read() !== value.read()) {
725
+ throw new UsageError(`${value.flag} is given "${held.typed}" and "${value.typed}", and ${value.takes}: give one of the two`);
726
+ }
727
+ }
728
+ // A family on both lists is passed over, since the list that ignores is
729
+ // asked first, and its name on the other says nothing: `--family ADR
730
+ // --family RFC --ignore-family RFC` is `--family ADR`. Where it is the
731
+ // only name there, every citation in prose is passed over, and the check
732
+ // that fails without the two reported a consistent graph. Two families,
733
+ // one asked for and another ignored, are both read, and are taken.
734
+ for (const asked of families) {
735
+ const ignored = ignoreFamilies.find((name) => name.trim().toUpperCase() === asked.trim().toUpperCase());
736
+ if (ignored !== undefined) {
737
+ throw new UsageError(`--family ${asked} is given beside --ignore-family ${ignored}, and a family's citations are read or they are passed over: give one of the two`);
738
+ }
739
+ }
740
+ // Three formats are reports of findings, and a recording reports none:
741
+ // what it wrote in their place was the line for a person, which is none
742
+ // of the three. JSON is written as a document of what was recorded, and
743
+ // Markdown is the line, which reads as Markdown already. Refused beside
744
+ // the help and the version as every other wrong line is.
745
+ if (recordBaseline !== null) {
746
+ for (const option of given) {
747
+ const unread = unreadByRecording(option) ? option : option === '--format' && (format === 'sarif' || format === 'github' || format === 'gitlab') ? `--format ${format}` : null;
748
+ if (unread !== null) {
749
+ throw new UsageError(`${unread} is given beside --record-baseline, and a recording writes the findings down and reports none: give one of the two`);
654
750
  }
655
751
  }
656
752
  }
657
753
  // The second word given to `rules` was dropped, so `rules a b` listed `a`
658
754
  // and said nothing of `b`.
659
- if (command === 'rules' && patterns.length > 1 && !help && !version) {
755
+ if (command === 'rules' && patterns.length > 1) {
660
756
  throw new UsageError(`rules takes one rule id, got ${patterns.length}: ${patterns.join(', ')}`);
661
757
  }
662
- if (command === 'diff' && patterns.length !== 2 && !help && !version) {
758
+ if (command === 'diff' && (patterns.length > 2 || (patterns.length < 2 && !help && !version))) {
663
759
  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)');
664
760
  }
665
761
  // An empty name resolved to the directory the run was started in, and the
666
762
  // refusal was the read's: `cannot read : EISDIR`.
667
763
  const blank = patterns.findIndex((path) => path.trim() === '');
668
- if (command === 'diff' && blank !== -1 && !help && !version) {
764
+ if (command === 'diff' && blank !== -1) {
669
765
  throw new UsageError(`diff expects a graph export on each side, got "${patterns[blank]}" for the ${blank === 0 ? 'first' : 'second'}`);
670
766
  }
671
767
  if (command === 'query' && selector === null && !help && !version) {
@@ -902,8 +998,20 @@ async function run(io, err) {
902
998
  const { severities, escalated } = resolveStrict(severityOverrides, options.strict || (file.strict ?? false), projectRules);
903
999
  // The list the check is held against, once it has read the documents, and
904
1000
  // the families taken out on purpose, which that list is not held to.
1001
+ //
1002
+ // A flag says what the run does where the file says otherwise. The list
1003
+ // that ignores is asked first, so `--family RFC` over "ignoreFamilies":
1004
+ // ["RFC"] passed over every RFC it asked for, and the run reported a
1005
+ // consistent graph where the file alone fails it: a family a flag asks for
1006
+ // is taken off the file's list of those to pass over. The other way needs
1007
+ // nothing: `--ignore-family ADR` over "families": ["ADR"] passes over the
1008
+ // ADRs, as the flag says (ADR-0010, 2026-10-18).
1009
+ const asked = options.families.map((family) => family.trim().toUpperCase());
905
1010
  const families = [...(file.families ?? []), ...options.families];
906
- const ignoreFamilies = [...(file.ignoreFamilies ?? []), ...options.ignoreFamilies];
1011
+ const ignoreFamilies = [
1012
+ ...(file.ignoreFamilies ?? []).filter((family) => !asked.includes(family.trim().toUpperCase())),
1013
+ ...options.ignoreFamilies,
1014
+ ];
907
1015
  const analyseOptions = {
908
1016
  root,
909
1017
  patterns,
@@ -933,6 +1041,30 @@ async function run(io, err) {
933
1041
  err(`spec-graph: no specifications matched ${patterns.map((p) => `"${p}"`).join(', ')}\n looked under ${root}\n`);
934
1042
  return EXIT_ERROR;
935
1043
  }
1044
+ // A journal named on the line and never read. A `--history` pattern marks
1045
+ // the files the run reads, so one whose files an ignore leaves out marked
1046
+ // nothing and said nothing: `--ignore archive --history "docs/archive/**"`
1047
+ // read no journal, and the archive's links went unchecked with the rest of
1048
+ // it. Beside an `--ignore` one of the two goes unread, and beside the
1049
+ // file's "ignore" the flag cannot say what the run does: either way the run
1050
+ // stops, naming the ignores there are. The walk that finds it is made once,
1051
+ // and only for a pattern that matched nothing read; one that matches no
1052
+ // file at all is as it was. With no ignore that walk is the run's own, so
1053
+ // it finds nothing more and names no ignore (ADR-0010, 2026-10-18).
1054
+ const ignores = [...(typed.ignore.length > 0 ? ['--ignore'] : []), ...((file.ignore ?? []).length > 0 ? [`"ignore" in ${loaded.source}`] : [])];
1055
+ let unignored;
1056
+ for (const [at, pattern] of typed.history.entries()) {
1057
+ const marks = createGlobMatcher([pattern]);
1058
+ if (result.files.some(marks))
1059
+ continue;
1060
+ // A mutant that plants a name in this list prunes a directory of that
1061
+ // name, which no repository has: equivalent, and left untested.
1062
+ unignored ??= (await walkFiles({ root, patterns, ignore: [] })).map((walk) => walk.path);
1063
+ if (unignored.some(marks)) {
1064
+ err(`spec-graph: --history "${options.historyPatterns[at]}" names only files ${listed(ignores, 'or')} leaves out, and a journal is read or it is left out: give one of the two\n`);
1065
+ return EXIT_ERROR;
1066
+ }
1067
+ }
936
1068
  switch (options.command) {
937
1069
  case 'graph':
938
1070
  out(formatGraph(result.graph, options.graphFormat, {
@@ -1038,7 +1170,13 @@ async function run(io, err) {
1038
1170
  return EXIT_ERROR;
1039
1171
  }
1040
1172
  const entries = JSON.parse(text).findings.length;
1041
- out(`recorded ${count(result.diagnostics.length, 'finding')} as ${count(entries, 'entry', 'entries')} in ${options.recordBaseline}\n`);
1173
+ // A document asked for is a document: the line in its place was what
1174
+ // a script handed `--format json` failed to parse. No `ok` in it,
1175
+ // since a recording judges nothing.
1176
+ const recorded = { file: options.recordBaseline, findings: result.diagnostics.length, entries };
1177
+ out(options.format === 'json'
1178
+ ? `${JSON.stringify({ version: 1, recorded }, null, 2)}\n`
1179
+ : `recorded ${count(recorded.findings, 'finding')} as ${count(entries, 'entry', 'entries')} in ${recorded.file}\n`);
1042
1180
  return EXIT_OK;
1043
1181
  }
1044
1182
  const source = options.baseline === null ? (file.baseline ?? null) : anchorFile(here, options.baseline);
@@ -1296,7 +1434,6 @@ function renderQueryError(selector, error) {
1296
1434
  */
1297
1435
  async function runDiff(paths, cwd, format, out, err) {
1298
1436
  const { readFile } = await import('node:fs/promises');
1299
- const { resolve } = await import('node:path');
1300
1437
  const sides = [];
1301
1438
  for (const path of paths) {
1302
1439
  let raw;