@descent-vtt/spec-graph 0.19.0 → 0.20.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/CHANGELOG.md CHANGED
@@ -11,6 +11,74 @@ the family's
11
11
  [versioning policy](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md)
12
12
  records.
13
13
 
14
+ ## 0.20.0
15
+
16
+ A negation an aside sets apart from its phrase cancels it, and a
17
+ pointer such as `see` governs the reference after it, so neither is
18
+ read as the relation it denies or passes over. On Linux a `\` in an
19
+ export path `diff` is given is a separator, and a run does not start in
20
+ a directory whose name holds one. A relative `cwd` given to the library
21
+ is read once.
22
+
23
+ ### Changed
24
+
25
+ - **A negation an aside sets apart from the phrase cancels it.**
26
+ `Not, as it happens, blocked by [ADR-0002](0002.md)` and
27
+ `Never, ever, blocked by` were `blocked-by`, the relation each denies:
28
+ the comma after the negation ended its clause. An aside closed by a comma
29
+ of its own leaves the clause open now, and each is a citation, and no
30
+ phrase before such a negation governs the reference either, so
31
+ `Not, as decided in [ADR-0003](0003.md), blocked by [ADR-0002](0002.md)`
32
+ reads no premise from the second. The miss it trades is a negation that
33
+ ends a clause of its own before an aside, `If not, then, blocked by`,
34
+ which is a citation too
35
+ ([ADR-0006](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0006-false-positives-cost-more.md)).
36
+ A `ghost-handover`, `stale-premise` or `live-supersession` such a
37
+ sentence denies is no longer reported, and an open question it hands to
38
+ a live document is an `orphaned-obligation`, so a check can pass that
39
+ failed, and fail that passed. Upgrading: run `check`; where a hand-off
40
+ is written `Owner: nobody, for now, deferred to`, write it without the
41
+ aside.
42
+ - **A pointer governs the reference after it.**
43
+ `Per tick, see [ADR-0002](0002.md)` read the cited document as a
44
+ premise, and `Blocked by the cache, see [ADR-0002](0002.md)` as what
45
+ blocks: `see` between a phrase
46
+ and the reference was passed over. It governs the reference as a
47
+ citation now, as it does with no phrase before it; `the must-see list`
48
+ and `seen in` point at nothing
49
+ ([ADR-0006](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0006-false-positives-cost-more.md)).
50
+ A finding read from such a phrase is no longer reported. Upgrading: run
51
+ `check`; where the phrase does name the reference, name it before the
52
+ pointer, `Blocked by [ADR-0002](0002.md), see its table`.
53
+ - **A `\` in an export path is a separator.** `diff` read its two paths as
54
+ typed, so on Linux `diff 'sub\base.json' 'sub\head.json'` compared files
55
+ of those names, or found none, and `..\sub\base.json` typed one directory
56
+ down found none, where a baseline typed either way is `sub/base.json`.
57
+ Each path is read as a root and a baseline are now, from where it was
58
+ typed. Windows reads as before
59
+ ([ADR-0010](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0010-configuration-belongs-to-the-repository.md)).
60
+ Upgrading: on Linux, rename an export whose file name holds a `\`; a
61
+ diff that read it reads the path the `\` spells now.
62
+ - **A run does not start in a directory whose name holds a `\`.** Where the
63
+ host separates names with `/`, as Linux does, `check`, `query`, `graph`
64
+ and `rules` in `/work/a\b` read `/work/a/b`, the path the `\` spells: its
65
+ documents, its configuration and its rules, so a check could pass over
66
+ the tree it was typed in, and a `--record-baseline` was written there.
67
+ Each stops now with exit `2` and one line, before it reads or writes
68
+ anything; `diff`, `--help` and `--version` run there as before
69
+ ([ADR-0010](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0010-configuration-belongs-to-the-repository.md)).
70
+ Upgrading: run from a path whose names hold no `\`, or give `--root` one,
71
+ such as a link to the directory.
72
+ - **A relative `cwd` is read once.** The library's `main({ cwd: 'docs' })`
73
+ with no `--root` started the run in `docs/docs`, the directory joined to
74
+ itself: it checked `docs/docs/*.md` for `*.md` and recorded a baseline
75
+ there. It starts in `docs` now, and two values of a baseline are
76
+ compared from there. The `spec-graph` command gives an absolute
77
+ directory and is as it was
78
+ ([ADR-0010](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0010-configuration-belongs-to-the-repository.md)).
79
+ Upgrading: nothing to do; a caller that gave a relative `cwd` gets the
80
+ directory it named.
81
+
14
82
  ## 0.19.0
15
83
 
16
84
  A comma ends the clause a negation before a phrase is in, so
package/README.md CHANGED
@@ -475,7 +475,15 @@ it, in the same statement: a sentence, a paragraph, a list item or a table cell.
475
475
  `Not in this release: deferred to` each state the relation, and are read as
476
476
  they state it. So do `if not, blocked by` and `both or neither, because`: a
477
477
  comma ends the clause the negation is in, and counts toward no phrase's 40
478
- characters.
478
+ characters. An aside closed by a comma of its own leaves the clause open,
479
+ so `not, as it happens, blocked by` and `never, ever, blocked by` are
480
+ citations, and so is `if not, then, blocked by`, which is missed. No
481
+ phrase before such a negation governs the reference either:
482
+ `not, as decided in [ADR-0003](0003.md), blocked by [ADR-0002](0002.md)`
483
+ reads the first as a premise and the second as a citation.
484
+ - A pointer between the two, `see`, governs the reference as a citation:
485
+ `per tick, see [ADR-0002](0002.md)` and `blocked by the cache, see` read no
486
+ relation, and `the must-see list` and `seen in` point at nothing.
479
487
  - A sentence written in Chinese ends at `。`, `?`, `!` or `;`, with a space
480
488
  after it or none, so `Deferred to later。另見 ADR-0002` hands nothing to
481
489
  ADR-0002. A stop only ends a statement sooner: it reads less, never more.
@@ -894,9 +902,17 @@ reports, byte for byte, because every path here is relative to the root.
894
902
  - A path typed on the command line stays relative to where you typed it, brace
895
903
  alternative by brace alternative: `{/docs,x}` typed in `sub` is `/docs` or
896
904
  `sub/x`.
897
- - A `\` in a root or a baseline is a separator on every host, as in a pattern,
898
- so `--baseline sub\held.json` typed at the root and `..\sub\held.json`
899
- typed in `docs` are one file, `sub/held.json`, on Linux as on Windows.
905
+ - A `\` in a root, a baseline or an export `diff` reads is a separator on
906
+ every host, as in a pattern, so `--baseline sub\held.json` typed at the
907
+ root and `..\sub\held.json` typed in `docs` are one file, `sub/held.json`,
908
+ on Linux as on Windows.
909
+ - So a run does not start in a directory whose name holds a `\`, where the
910
+ host separates names with `/`, as Linux does: read as a separator, the
911
+ `\` would name another directory, and the run would check it and record a
912
+ baseline into it. The run stops with
913
+ exit `2` and one line before it reads anything; run it from a path whose
914
+ names hold none, or give `--root` one, such as a link to the directory.
915
+ `diff`, `--help` and `--version` run there.
900
916
 
901
917
  See [ADR-0018](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0018-the-configuration-file-is-the-root.md).
902
918
 
@@ -1339,7 +1355,7 @@ steps:
1339
1355
  - run: npm ci
1340
1356
  - run: npx --no-install @descent-vtt/spec-graph --format sarif > spec-graph.sarif
1341
1357
  continue-on-error: true
1342
- - uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
1358
+ - uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
1343
1359
  with:
1344
1360
  sarif_file: spec-graph.sarif
1345
1361
  ```
@@ -1483,6 +1499,9 @@ It names only what it can tell apart:
1483
1499
  resolved or reopened.
1484
1500
  - Findings are left to `--baseline`, which already reports what is new.
1485
1501
 
1502
+ Each export is read from where its path was typed, with a `\` a separator,
1503
+ as a baseline is.
1504
+
1486
1505
  The exit code is `0` whether anything changed or not: a diff describes, and
1487
1506
  `check` gates. See
1488
1507
  [ADR-0020](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0020-a-diff-names-what-it-can-tell-apart.md).
@@ -1524,7 +1543,8 @@ The command line is `main`, which takes the line and the directory it was
1524
1543
  typed in: `await main({ argv: ['check', '--root', 'docs'], cwd })` reads
1525
1544
  `docs`, its configuration and any baseline beside it from `cwd`, and walks
1526
1545
  up from `cwd` for a configuration where no `--root` is given. Left out,
1527
- `cwd` is the process's working directory, as for the `spec-graph` command.
1546
+ `cwd` is the process's working directory, as for the `spec-graph` command;
1547
+ a relative `cwd` is read from that directory.
1528
1548
 
1529
1549
  ## Design
1530
1550
 
@@ -1563,9 +1583,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
1563
1583
  than installed. So the whole package is auditable in an afternoon — and nothing
1564
1584
  in the pipeline can take longer than its input.
1565
1585
 
1566
- **Verified, not just covered.** 2,354 tests; 99.4% statement and 99.8% line
1586
+ **Verified, not just covered.** 2,381 tests; 99.4% statement and 99.8% line
1567
1587
  coverage. Coverage says a line ran, so the suite is also held to a mutation
1568
- score - 96.58% over 9,126 mutants, measured by CI's full sweep, and a build
1588
+ score - 96.59% over 9,149 mutants, measured by CI's full sweep, and a build
1569
1589
  fails under 93 - because a vocabulary entry or a boundary
1570
1590
  condition can be weakened by an ordinary-looking refactor without a single test
1571
1591
  going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@
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
+ import { resolve, sep } from 'node:path';
14
14
  import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline, } from './baseline.js';
15
15
  import { discoverConfig, loadConfig } from './config.js';
16
16
  import { DiffInputError, diffExports, formatDiffJson, formatDiffMarkdown, formatDiffText, parseGraphExport, } from './diff.js';
@@ -19,7 +19,7 @@ import { isFamilyName } from './identity.js';
19
19
  import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
20
20
  import { formatGithub, formatGitlab, formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
21
21
  import { DEFAULT_SEVERITIES, resolveStrict, RULE_DECISIONS, decisionUrl, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES, } from './rules.js';
22
- import { foldDriveLetter, isAbsolutePath, joinPosix, toPosix } from './paths.js';
22
+ import { foldDriveLetter, holdsBackslashInName, isAbsolutePath, joinPosix, toPosix } from './paths.js';
23
23
  import { formatRef } from './source.js';
24
24
  import { execute, parseQuery, QueryError, renderMatch } from './select.js';
25
25
  import { isProjectRule } from './types.js';
@@ -500,6 +500,11 @@ export function parseArgs(argv, cwd) {
500
500
  const once = (flag, typed, read, key = flag, takes = 'takes one value') => {
501
501
  valued.push({ flag, key, typed, read, takes });
502
502
  };
503
+ // A baseline typed here is read from the root where `--root` names one,
504
+ // and from where it was typed otherwise, which is the root's default. That
505
+ // directory is read once: joined to itself, a relative `cwd` such as
506
+ // `docs` read `docs/docs` (ADR-0010, 2026-10-24).
507
+ const fromRoot = (value) => foldDriveLetter(resolve(cwd, rootExplicit ? toPosix(root) : '', toPosix(value)));
503
508
  for (let i = 0; i < args.length; i += 1) {
504
509
  const arg = args[i];
505
510
  if (arg === '--') {
@@ -582,18 +587,16 @@ export function parseArgs(argv, cwd) {
582
587
  historyPatterns.push(next(arg, i));
583
588
  i += 1;
584
589
  break;
585
- // A baseline typed here is read from the root where `--root` names one,
586
- // and from where it was typed otherwise, which is the root's default.
587
590
  case '--baseline': {
588
591
  const value = filled(arg, i, 'a file');
589
- once(arg, value, () => foldDriveLetter(resolve(cwd, toPosix(root), toPosix(value))));
592
+ once(arg, value, () => fromRoot(value));
590
593
  baseline = value;
591
594
  i += 1;
592
595
  break;
593
596
  }
594
597
  case '--record-baseline': {
595
598
  const value = filled(arg, i, 'a file');
596
- once(arg, value, () => foldDriveLetter(resolve(cwd, toPosix(root), toPosix(value))));
599
+ once(arg, value, () => fromRoot(value));
597
600
  recordBaseline = value;
598
601
  i += 1;
599
602
  break;
@@ -906,8 +909,24 @@ async function run(io, err) {
906
909
  // the library names with `io.cwd`, and where two values of `--root` were
907
910
  // compared: left relative, the disk read it from Node's own directory, so
908
911
  // `--root docs` checked, and recorded a baseline into, another tree than
909
- // the caller's. A `\` is a separator, as the walk has always read it.
910
- const start = resolve(cwd, toPosix(options.root));
912
+ // the caller's. A `\` typed in the root is a separator, as the walk has
913
+ // always read it. The directory the line was typed in is not typed, and is
914
+ // read once, as the host gives it: joined to itself, a relative `cwd` such
915
+ // as `docs` started the run in `docs/docs`.
916
+ const start = options.rootExplicit ? resolve(cwd, toPosix(options.root)) : resolve(cwd);
917
+ // On a host whose separator is `/`, a `\` in where the run starts is a
918
+ // character of a name, from the directory the line was typed in, and the
919
+ // walk, the configuration and a baseline read every `\` as a separator: in
920
+ // `/t/a\b` they read `/t/a/b`, another directory, and recorded a baseline
921
+ // into it. No path handed on tells the one `\` from the other, so the run
922
+ // stops before it reads or writes anything. A diff reads its two files
923
+ // where they are, from that directory as the host gives it, and is not
924
+ // stopped (ADR-0010, 2026-10-24).
925
+ if (holdsBackslashInName(start, sep)) {
926
+ const named = options.rootExplicit ? `--root "${options.root}" is ${start}, which has` : `${start} has`;
927
+ err(`spec-graph: ${named} a "\\" in a name, and spec-graph reads each "\\" as a separator, so it would read ${toPosix(start)} instead: run it from a path with none, or give --root one\n`);
928
+ return EXIT_ERROR;
929
+ }
911
930
  // A root that was named is a directory, for every command that reads one.
912
931
  // `rules --root` with a path that is not there listed the built-in rules and
913
932
  // exited 0, as if the repository declared none, and the others said only
@@ -1456,9 +1475,15 @@ async function runDiff(paths, cwd, format, out, err) {
1456
1475
  for (const path of paths) {
1457
1476
  let raw;
1458
1477
  try {
1478
+ // A `\` is a separator, as it is in a root and a baseline: kept as a
1479
+ // character, `sub\a.json` typed at the top read a file of that name on
1480
+ // Linux, where the same baseline is `sub/a.json`, and `..\sub\a.json`
1481
+ // typed below it read nothing. The directory it was typed in is read
1482
+ // as the host gives it, so a diff reads its files there, `\` or none.
1483
+ //
1459
1484
  // Without the encoding this is a Buffer, which JSON.parse decodes as
1460
1485
  // UTF-8 all the same - so emptying the literal parses the same export.
1461
- raw = await readFile(resolve(cwd, path), 'utf8');
1486
+ raw = await readFile(resolve(cwd, toPosix(path)), 'utf8');
1462
1487
  }
1463
1488
  catch (error) {
1464
1489
  err(`spec-graph: cannot read ${path}: ${error.message}\n`);