@descent-vtt/spec-graph 0.18.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 +126 -0
- package/README.md +39 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +62 -19
- package/dist/cli.js.map +1 -1
- package/dist/extract.js +56 -12
- package/dist/extract.js.map +1 -1
- package/dist/glob.d.ts +5 -0
- package/dist/glob.js +6 -1
- package/dist/glob.js.map +1 -1
- package/dist/paths.d.ts +24 -0
- package/dist/paths.js +31 -0
- package/dist/paths.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,132 @@ 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
|
+
|
|
82
|
+
## 0.19.0
|
|
83
|
+
|
|
84
|
+
A comma ends the clause a negation before a phrase is in, so
|
|
85
|
+
`if not, blocked by [ADR-0002](0002.md)` is the relation it states. A
|
|
86
|
+
library caller's own directory is where a relative `--root` is read
|
|
87
|
+
from, a drive letter names its volume in either case on Windows, and a
|
|
88
|
+
`\` in a root or a baseline is a separator wherever the run starts.
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
|
|
92
|
+
- **A comma ends the clause a negation before a phrase is in.**
|
|
93
|
+
`if not, blocked by [ADR-0002](0002.md)` and
|
|
94
|
+
`both or neither, because [ADR-0002](0002.md) says so` were citations:
|
|
95
|
+
the comma was blanked, and the negation then stood against the phrase.
|
|
96
|
+
Each is the relation it states now, `blocked-by` and `assumes`, and so
|
|
97
|
+
are `No, blocked by`, `Not yet, deferred to` and
|
|
98
|
+
`Like it or not, superseded by`. A comma still counts toward no phrase's
|
|
99
|
+
40 characters, and a negation between the phrase and the reference
|
|
100
|
+
cancels it as before, whatever commas stand there. A `ghost-handover`,
|
|
101
|
+
`stale-premise` or supersession such a sentence states is reported, and
|
|
102
|
+
an open question it hands to a live document is no longer an
|
|
103
|
+
`orphaned-obligation`, so a check that passed can fail
|
|
104
|
+
([ADR-0006](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0006-false-positives-cost-more.md)).
|
|
105
|
+
Upgrading: run `check`; a finding it now reports from such a sentence is
|
|
106
|
+
one the sentence states. A negation set off by commas from the phrase it
|
|
107
|
+
denies, `Never, ever, blocked by`, is read as the relation too: write it
|
|
108
|
+
without the commas.
|
|
109
|
+
- **A relative `--root` is read from the directory `main` is given.** The
|
|
110
|
+
library's `main({ argv, cwd })` compared two values of `--root` from
|
|
111
|
+
`cwd`, and read the one it kept from the process's own directory: `check
|
|
112
|
+
--root docs` checked another tree's `docs`, a `--record-baseline` beside
|
|
113
|
+
it was written there, and where that directory had no `docs` the root was
|
|
114
|
+
not there. The root, its configuration and its baselines are read from
|
|
115
|
+
`cwd` now, and a relative `cwd` is read from the process's directory, so
|
|
116
|
+
the walk for a configuration from it goes up to the repository as it does
|
|
117
|
+
from that directory, where it stopped at the top of the relative path.
|
|
118
|
+
The `spec-graph` command is as it was
|
|
119
|
+
([ADR-0010](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0010-configuration-belongs-to-the-repository.md)).
|
|
120
|
+
Upgrading: a caller that passed a `cwd` other than the directory it meant
|
|
121
|
+
`--root` from passes that directory, or an absolute `--root`.
|
|
122
|
+
- **On Windows a drive letter names its volume in either case.** `--root
|
|
123
|
+
C:\x` beside `--root c:\x` was refused, exit `2`, as two directories, and
|
|
124
|
+
so were two baselines written so, and `C:docs` beside `c:docs`. Each is
|
|
125
|
+
one value now, taken once; past the letter two names are compared as
|
|
126
|
+
before, so `C:\x` beside `C:\X` is still refused (spec-core's ADR-0005,
|
|
127
|
+
2026-10-19). Upgrading: nothing to do; a line refused for this runs now.
|
|
128
|
+
- **A `\` in a baseline or a root is a separator wherever the run starts.**
|
|
129
|
+
On Linux `--baseline 'sub\held.json'` typed at the root read a file of
|
|
130
|
+
that name, and `'..\sub\held.json'` typed one directory down read
|
|
131
|
+
`sub/held.json`; a `"baseline"` in the configuration was read as the
|
|
132
|
+
first, and two baselines that differed only so were refused as two. Each
|
|
133
|
+
reads `sub/held.json` now, as an absolute path and a pattern did already,
|
|
134
|
+
and a `--root` with a `\` is asked of the disk as it is walked. Windows
|
|
135
|
+
reads every one as before
|
|
136
|
+
([ADR-0018](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0018-the-configuration-file-is-the-root.md)).
|
|
137
|
+
Upgrading: on Linux, rename a baseline whose file name holds a `\`; a
|
|
138
|
+
check that read it reads the path the `\` spells now.
|
|
139
|
+
|
|
14
140
|
## 0.18.0
|
|
15
141
|
|
|
16
142
|
Two values for an option that takes one are compared as each is read, a
|
package/README.md
CHANGED
|
@@ -473,7 +473,17 @@ it, in the same statement: a sentence, a paragraph, a list item or a table cell.
|
|
|
473
473
|
`yet`, `longer`, `necessarily` and a few more) stand between: `not only
|
|
474
474
|
blocked by`, `nothing but blocked by`, the option `"never" blocked by` and
|
|
475
475
|
`Not in this release: deferred to` each state the relation, and are read as
|
|
476
|
-
they state it.
|
|
476
|
+
they state it. So do `if not, blocked by` and `both or neither, because`: a
|
|
477
|
+
comma ends the clause the negation is in, and counts toward no phrase's 40
|
|
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.
|
|
477
487
|
- A sentence written in Chinese ends at `。`, `?`, `!` or `;`, with a space
|
|
478
488
|
after it or none, so `Deferred to later。另見 ADR-0002` hands nothing to
|
|
479
489
|
ADR-0002. A stop only ends a statement sooner: it reads less, never more.
|
|
@@ -892,6 +902,17 @@ reports, byte for byte, because every path here is relative to the root.
|
|
|
892
902
|
- A path typed on the command line stays relative to where you typed it, brace
|
|
893
903
|
alternative by brace alternative: `{/docs,x}` typed in `sub` is `/docs` or
|
|
894
904
|
`sub/x`.
|
|
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.
|
|
895
916
|
|
|
896
917
|
See [ADR-0018](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0018-the-configuration-file-is-the-root.md).
|
|
897
918
|
|
|
@@ -1187,8 +1208,10 @@ variable in `--family "$FAMILIES"` is the usual way to write one:
|
|
|
1187
1208
|
two`. Two values are compared as each is read, so one value twice is that
|
|
1188
1209
|
value once however it is spelled: `--root .` beside `--root ./` names one
|
|
1189
1210
|
directory, a baseline is a path resolved as the root is, and `--max 1`
|
|
1190
|
-
beside `--max " 1"` is one number.
|
|
1191
|
-
|
|
1211
|
+
beside `--max " 1"` is one number. On Windows a drive letter names its
|
|
1212
|
+
volume in either case, so `--root C:\x` beside `--root c:\x` is one
|
|
1213
|
+
directory; past the letter two names are two, `C:\x` beside `C:\X`. An
|
|
1214
|
+
option that is repeatable above takes each of its values;
|
|
1192
1215
|
- `--baseline` beside `--record-baseline`: a recording writes every finding
|
|
1193
1216
|
of the run and opens no baseline, so the one to read would go unread. A
|
|
1194
1217
|
`"baseline"` in the configuration is no option, and is recorded over as
|
|
@@ -1332,7 +1355,7 @@ steps:
|
|
|
1332
1355
|
- run: npm ci
|
|
1333
1356
|
- run: npx --no-install @descent-vtt/spec-graph --format sarif > spec-graph.sarif
|
|
1334
1357
|
continue-on-error: true
|
|
1335
|
-
- uses: github/codeql-action/upload-sarif@
|
|
1358
|
+
- uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
|
|
1336
1359
|
with:
|
|
1337
1360
|
sarif_file: spec-graph.sarif
|
|
1338
1361
|
```
|
|
@@ -1476,6 +1499,9 @@ It names only what it can tell apart:
|
|
|
1476
1499
|
resolved or reopened.
|
|
1477
1500
|
- Findings are left to `--baseline`, which already reports what is new.
|
|
1478
1501
|
|
|
1502
|
+
Each export is read from where its path was typed, with a `\` a separator,
|
|
1503
|
+
as a baseline is.
|
|
1504
|
+
|
|
1479
1505
|
The exit code is `0` whether anything changed or not: a diff describes, and
|
|
1480
1506
|
`check` gates. See
|
|
1481
1507
|
[ADR-0020](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0020-a-diff-names-what-it-can-tell-apart.md).
|
|
@@ -1513,6 +1539,13 @@ const ghosts = query(graph, 'item[openness=open] -delegates-to-> document[phase=
|
|
|
1513
1539
|
The graph, the scanner, the selector engine and every vocabulary table are
|
|
1514
1540
|
exported. Reporters, editor extensions and custom rules are all first-class.
|
|
1515
1541
|
|
|
1542
|
+
The command line is `main`, which takes the line and the directory it was
|
|
1543
|
+
typed in: `await main({ argv: ['check', '--root', 'docs'], cwd })` reads
|
|
1544
|
+
`docs`, its configuration and any baseline beside it from `cwd`, and walks
|
|
1545
|
+
up from `cwd` for a configuration where no `--root` is given. Left out,
|
|
1546
|
+
`cwd` is the process's working directory, as for the `spec-graph` command;
|
|
1547
|
+
a relative `cwd` is read from that directory.
|
|
1548
|
+
|
|
1516
1549
|
## Design
|
|
1517
1550
|
|
|
1518
1551
|
Twenty-three ADRs, which `spec-graph` validates on every CI run:
|
|
@@ -1550,9 +1583,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
|
|
|
1550
1583
|
than installed. So the whole package is auditable in an afternoon — and nothing
|
|
1551
1584
|
in the pipeline can take longer than its input.
|
|
1552
1585
|
|
|
1553
|
-
**Verified, not just covered.** 2,
|
|
1586
|
+
**Verified, not just covered.** 2,381 tests; 99.4% statement and 99.8% line
|
|
1554
1587
|
coverage. Coverage says a line ran, so the suite is also held to a mutation
|
|
1555
|
-
score - 96.
|
|
1588
|
+
score - 96.59% over 9,149 mutants, measured by CI's full sweep, and a build
|
|
1556
1589
|
fails under 93 - because a vocabulary entry or a boundary
|
|
1557
1590
|
condition can be weakened by an ordinary-looking refactor without a single test
|
|
1558
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.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface CliIO {
|
|
|
78
78
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
79
79
|
readonly isTTY?: boolean | undefined;
|
|
80
80
|
}
|
|
81
|
-
export declare const HELP = "spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n spec-graph diff <before.json> <after.json> [--format human|json|markdown]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed. It has to be a\n directory that is there.\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable: one\n name each time, the prefix its identifiers are\n written with, ADR for ADR-0007. A document here has\n to belong to one of them, or the check stops: a list\n that names none allows nothing. It stops too where\n a name has no document and the list leaves out a\n family the documents do belong to, as RFCS for RFC\n does: one is most likely meant for the other.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable,\n one name each time.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n A pattern whose files an ignore leaves out is\n refused: no journal it names would be read.\n --baseline <file> Accept the findings recorded in this file and report\n only what is new since. The file has to be there: a\n path that reads nothing accepts nothing and says so\n nowhere. A \"baseline\" in the configuration may name a\n file not recorded yet; a path typed here may not.\n --record-baseline <f> Write today's findings to this file as accepted debt,\n and exit 0 without judging them. With --format json\n the recording is a JSON document.\n --ratchet Also fail when a baseline entry no longer occurs, so\n a paid-off exemption cannot outlive the defect.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format <fmt> human, json, sarif, github, gitlab or markdown. json\n is for check, query or diff. sarif is the interchange\n format GitHub code scanning and editors already read,\n github the workflow commands a GitHub Actions log\n turns into annotations, and gitlab the Code Quality\n report a GitLab merge request reads, all three for\n check; markdown is a table for a pull-request comment\n or $GITHUB_STEP_SUMMARY, for check or diff (default:\n human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. A project\n rule is named in full: project:no-draft-dependency.\n Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off, whatever NO_COLOR, FORCE_COLOR\n and the terminal say. The two together are refused\n --ascii / --no-ascii ASCII glyphs or Unicode ones. SPEC_GRAPH_ASCII asks\n for ASCII, unless it is empty, 0 or false. Either\n option wins over it, and over what the terminal is\n taken for. The two together are refused\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n --explain With rules: add each rule's selector and the ADR\n that decided it\n -h, --help Show this help\n -v, --version Show the version\n\n An option belongs to the commands that read it, and any other command\n refuses it by name:\n\n --root, --verbose check, query, graph, rules\n --ignore, --history check, query, graph\n --format check, query, diff\n --graph-format, --documents-only graph\n --explain rules\n every other option check\n\n --no-config, --no-color and --ascii are taken by every command: each is\n true already of a command with nothing to read it for. --color and\n --no-ascii are not: each would say that a command drew what only a check\n draws.\n\n An option beside its own opposite is refused, whichever comes first:\n --color beside --no-color, and --ascii beside --no-ascii. The later would\n decide and the earlier go unread. Either alone wins over the environment,\n and the same option twice is that option once.\n\n An option that takes one value is refused given two, as in --root a\n --root b: the later would be read and the earlier go unread. That is each\n option above with a value that is not marked Repeatable, and one rule\n given two severities by --rule. Two values are compared as each is read,\n so one value twice is that value once however it is spelled: --root .\n beside --root ./ names one directory.\n\n A baseline to read is refused beside one to record, --baseline beside\n --record-baseline: a recording writes every finding and opens no\n baseline. So is each option a recording does not read, since it judges,\n shows and colours no finding: --ratchet, --strict, --max, --max-warnings,\n --color, --no-ascii, and --format sarif, github or gitlab. So is one\n family given to both --family and --ignore-family, whose citations are\n passed over whatever asked for them. One family asked for beside another\n ignored is taken.\n\n A value that begins with -- reads as an option, so the option before it is\n refused as missing its value. A name that does begin so is written ./--name\n for a file, [-]-name in a pattern and \\--name for a reference target.\n\n A line that is wrong is refused with --help or --version on it too, an\n option a recording does not read among them, and --help beside --version\n is refused: one of the two answers would go unread. What a command still\n needs, a selector or two exports, is what --help is for: query --help and\n diff --help print it. The help reads no disk, so what only the run finds,\n a --root that is not there or a --history an ignore leaves out, is\n answered by it.\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\n An input that is set and names nothing is a 2, named on stderr, and never\n read as if it had not been given: an empty value, a family name no family\n has, a list of families no document here belongs to, a --root that is no\n directory, a second word after rules, an option of another command. So\n are two things of which one would go unread: an option beside its own\n opposite, an option that takes one value given two, --baseline beside\n --record-baseline, an option a recording does not read, a family both\n asked for and ignored, a --history whose files an ignore leaves out,\n --help beside --version.\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true,\n \"maxRelated\": 3 }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it: --family RFC reads the citations \"ignoreFamilies\" passes\n over. maxRelated has no flag: it caps how many related locations one\n finding lists, 8 unless set, and 0 lists none.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile, one family in both\n \"families\" and \"ignoreFamilies\", a file that is there and cannot be read. A configuration that did not load checks a different\n repository than the one configured, and would report that one as consistent.\n --no-config runs on defaults instead.\n\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n spec-graph check --format github # annotations from an Actions log\n spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
81
|
+
export declare const HELP = "spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n spec-graph diff <before.json> <after.json> [--format human|json|markdown]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed. It has to be a\n directory that is there.\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable: one\n name each time, the prefix its identifiers are\n written with, ADR for ADR-0007. A document here has\n to belong to one of them, or the check stops: a list\n that names none allows nothing. It stops too where\n a name has no document and the list leaves out a\n family the documents do belong to, as RFCS for RFC\n does: one is most likely meant for the other.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable,\n one name each time.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n A pattern whose files an ignore leaves out is\n refused: no journal it names would be read.\n --baseline <file> Accept the findings recorded in this file and report\n only what is new since. The file has to be there: a\n path that reads nothing accepts nothing and says so\n nowhere. A \"baseline\" in the configuration may name a\n file not recorded yet; a path typed here may not.\n --record-baseline <f> Write today's findings to this file as accepted debt,\n and exit 0 without judging them. With --format json\n the recording is a JSON document.\n --ratchet Also fail when a baseline entry no longer occurs, so\n a paid-off exemption cannot outlive the defect.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format <fmt> human, json, sarif, github, gitlab or markdown. json\n is for check, query or diff. sarif is the interchange\n format GitHub code scanning and editors already read,\n github the workflow commands a GitHub Actions log\n turns into annotations, and gitlab the Code Quality\n report a GitLab merge request reads, all three for\n check; markdown is a table for a pull-request comment\n or $GITHUB_STEP_SUMMARY, for check or diff (default:\n human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. A project\n rule is named in full: project:no-draft-dependency.\n Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off, whatever NO_COLOR, FORCE_COLOR\n and the terminal say. The two together are refused\n --ascii / --no-ascii ASCII glyphs or Unicode ones. SPEC_GRAPH_ASCII asks\n for ASCII, unless it is empty, 0 or false. Either\n option wins over it, and over what the terminal is\n taken for. The two together are refused\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n --explain With rules: add each rule's selector and the ADR\n that decided it\n -h, --help Show this help\n -v, --version Show the version\n\n An option belongs to the commands that read it, and any other command\n refuses it by name:\n\n --root, --verbose check, query, graph, rules\n --ignore, --history check, query, graph\n --format check, query, diff\n --graph-format, --documents-only graph\n --explain rules\n every other option check\n\n --no-config, --no-color and --ascii are taken by every command: each is\n true already of a command with nothing to read it for. --color and\n --no-ascii are not: each would say that a command drew what only a check\n draws.\n\n An option beside its own opposite is refused, whichever comes first:\n --color beside --no-color, and --ascii beside --no-ascii. The later would\n decide and the earlier go unread. Either alone wins over the environment,\n and the same option twice is that option once.\n\n An option that takes one value is refused given two, as in --root a\n --root b: the later would be read and the earlier go unread. That is each\n option above with a value that is not marked Repeatable, and one rule\n given two severities by --rule. Two values are compared as each is read,\n so one value twice is that value once however it is spelled: --root .\n beside --root ./ names one directory, and so on Windows does C:\\x beside\n c:\\x, a drive letter naming its volume in either case.\n\n A baseline to read is refused beside one to record, --baseline beside\n --record-baseline: a recording writes every finding and opens no\n baseline. So is each option a recording does not read, since it judges,\n shows and colours no finding: --ratchet, --strict, --max, --max-warnings,\n --color, --no-ascii, and --format sarif, github or gitlab. So is one\n family given to both --family and --ignore-family, whose citations are\n passed over whatever asked for them. One family asked for beside another\n ignored is taken.\n\n A value that begins with -- reads as an option, so the option before it is\n refused as missing its value. A name that does begin so is written ./--name\n for a file, [-]-name in a pattern and \\--name for a reference target.\n\n A line that is wrong is refused with --help or --version on it too, an\n option a recording does not read among them, and --help beside --version\n is refused: one of the two answers would go unread. What a command still\n needs, a selector or two exports, is what --help is for: query --help and\n diff --help print it. The help reads no disk, so what only the run finds,\n a --root that is not there or a --history an ignore leaves out, is\n answered by it.\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\n An input that is set and names nothing is a 2, named on stderr, and never\n read as if it had not been given: an empty value, a family name no family\n has, a list of families no document here belongs to, a --root that is no\n directory, a second word after rules, an option of another command. So\n are two things of which one would go unread: an option beside its own\n opposite, an option that takes one value given two, --baseline beside\n --record-baseline, an option a recording does not read, a family both\n asked for and ignored, a --history whose files an ignore leaves out,\n --help beside --version.\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true,\n \"maxRelated\": 3 }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it: --family RFC reads the citations \"ignoreFamilies\" passes\n over. maxRelated has no flag: it caps how many related locations one\n finding lists, 8 unless set, and 0 lists none.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile, one family in both\n \"families\" and \"ignoreFamilies\", a file that is there and cannot be read. A configuration that did not load checks a different\n repository than the one configured, and would report that one as consistent.\n --no-config runs on defaults instead.\n\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n spec-graph check --format github # annotations from an Actions log\n spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
82
82
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
83
83
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
84
84
|
/**
|
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 { 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';
|
|
@@ -153,7 +153,8 @@ OPTIONS
|
|
|
153
153
|
option above with a value that is not marked Repeatable, and one rule
|
|
154
154
|
given two severities by --rule. Two values are compared as each is read,
|
|
155
155
|
so one value twice is that value once however it is spelled: --root .
|
|
156
|
-
beside --root ./ names one directory
|
|
156
|
+
beside --root ./ names one directory, and so on Windows does C:\\x beside
|
|
157
|
+
c:\\x, a drive letter naming its volume in either case.
|
|
157
158
|
|
|
158
159
|
A baseline to read is refused beside one to record, --baseline beside
|
|
159
160
|
--record-baseline: a recording writes every finding and opens no
|
|
@@ -489,7 +490,9 @@ export function parseArgs(argv, cwd) {
|
|
|
489
490
|
// JSON where the two the other way round are refused. Two values are
|
|
490
491
|
// compared as each is read, not as it is written, so `--root .` beside
|
|
491
492
|
// `--root ./` names one directory and is taken once (spec-core's ADR-0005,
|
|
492
|
-
// 2026-10-18)
|
|
493
|
+
// 2026-10-18), and so do `C:\x` and `c:\x` on Windows, where a drive letter
|
|
494
|
+
// names its volume in either case (the same, 2026-10-19). Each is read
|
|
495
|
+
// once the whole line has been, since a
|
|
493
496
|
// baseline typed beside `--root` is read from the root, wherever on the
|
|
494
497
|
// line the root stands. A mutant that seeds this list with a string holds
|
|
495
498
|
// it under no option's key, where no value is compared with it: equivalent.
|
|
@@ -497,6 +500,11 @@ export function parseArgs(argv, cwd) {
|
|
|
497
500
|
const once = (flag, typed, read, key = flag, takes = 'takes one value') => {
|
|
498
501
|
valued.push({ flag, key, typed, read, takes });
|
|
499
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)));
|
|
500
508
|
for (let i = 0; i < args.length; i += 1) {
|
|
501
509
|
const arg = args[i];
|
|
502
510
|
if (arg === '--') {
|
|
@@ -548,11 +556,12 @@ export function parseArgs(argv, cwd) {
|
|
|
548
556
|
verbose = true;
|
|
549
557
|
break;
|
|
550
558
|
case '--root': {
|
|
551
|
-
// Resolved from where the line was typed
|
|
552
|
-
//
|
|
553
|
-
//
|
|
559
|
+
// Resolved from where the line was typed, with a `\` read as the run
|
|
560
|
+
// reads it, and compared as text past the drive letter: the disk is
|
|
561
|
+
// not asked, so a link and its target are two values, and no other
|
|
562
|
+
// case is folded.
|
|
554
563
|
const value = filled(arg, i, 'a directory');
|
|
555
|
-
once(arg, value, () => resolve(cwd, value));
|
|
564
|
+
once(arg, value, () => foldDriveLetter(resolve(cwd, toPosix(value))));
|
|
556
565
|
root = value;
|
|
557
566
|
rootExplicit = true;
|
|
558
567
|
i += 1;
|
|
@@ -578,18 +587,16 @@ export function parseArgs(argv, cwd) {
|
|
|
578
587
|
historyPatterns.push(next(arg, i));
|
|
579
588
|
i += 1;
|
|
580
589
|
break;
|
|
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
590
|
case '--baseline': {
|
|
584
591
|
const value = filled(arg, i, 'a file');
|
|
585
|
-
once(arg, value, () =>
|
|
592
|
+
once(arg, value, () => fromRoot(value));
|
|
586
593
|
baseline = value;
|
|
587
594
|
i += 1;
|
|
588
595
|
break;
|
|
589
596
|
}
|
|
590
597
|
case '--record-baseline': {
|
|
591
598
|
const value = filled(arg, i, 'a file');
|
|
592
|
-
once(arg, value, () =>
|
|
599
|
+
once(arg, value, () => fromRoot(value));
|
|
593
600
|
recordBaseline = value;
|
|
594
601
|
i += 1;
|
|
595
602
|
break;
|
|
@@ -897,6 +904,29 @@ async function run(io, err) {
|
|
|
897
904
|
// it can compare exports from a checkout it is not running in.
|
|
898
905
|
if (options.command === 'diff')
|
|
899
906
|
return runDiff(options.patterns, cwd, options.format, out, err);
|
|
907
|
+
// Where the run starts: the root a `--root` names, or the directory the
|
|
908
|
+
// line was typed in. Either is read from that directory, which a caller of
|
|
909
|
+
// the library names with `io.cwd`, and where two values of `--root` were
|
|
910
|
+
// compared: left relative, the disk read it from Node's own directory, so
|
|
911
|
+
// `--root docs` checked, and recorded a baseline into, another tree than
|
|
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
|
+
}
|
|
900
930
|
// A root that was named is a directory, for every command that reads one.
|
|
901
931
|
// `rules --root` with a path that is not there listed the built-in rules and
|
|
902
932
|
// exited 0, as if the repository declared none, and the others said only
|
|
@@ -905,7 +935,7 @@ async function run(io, err) {
|
|
|
905
935
|
const { stat } = await import('node:fs/promises');
|
|
906
936
|
// A path that cannot be reached reads as one that is not there: either
|
|
907
937
|
// way there is nothing under it to read.
|
|
908
|
-
const found = await stat(
|
|
938
|
+
const found = await stat(start).catch(() => null);
|
|
909
939
|
if (found === null || !found.isDirectory()) {
|
|
910
940
|
err(`spec-graph: --root "${options.root}" is not ${found === null ? 'there' : 'a directory'}\n`);
|
|
911
941
|
return EXIT_ERROR;
|
|
@@ -921,10 +951,10 @@ async function run(io, err) {
|
|
|
921
951
|
// Read before `rules` prints anything, so that command lists the conventions
|
|
922
952
|
// this repository will actually check rather than the ones spec-graph ships.
|
|
923
953
|
const loaded = options.noConfig
|
|
924
|
-
? { config: {}, source: null, problems: [], root:
|
|
954
|
+
? { config: {}, source: null, problems: [], root: start }
|
|
925
955
|
: options.rootExplicit
|
|
926
|
-
? { ...loadConfig(
|
|
927
|
-
: discoverConfig(
|
|
956
|
+
? { ...loadConfig(start), root: start }
|
|
957
|
+
: discoverConfig(start);
|
|
928
958
|
for (const problem of loaded.problems)
|
|
929
959
|
err(`spec-graph: ${problem}\n`);
|
|
930
960
|
// A configuration that did not load means a run against a different
|
|
@@ -944,7 +974,7 @@ async function run(io, err) {
|
|
|
944
974
|
// How far the root moved up. Everything a repository declares about itself is
|
|
945
975
|
// relative to the root; everything typed on the command line is relative to
|
|
946
976
|
// where it was typed, and this is what keeps those two readings apart.
|
|
947
|
-
const here = below(root,
|
|
977
|
+
const here = below(root, start);
|
|
948
978
|
// A `--rule` naming a project rule that does not exist is a flag that
|
|
949
979
|
// silently does nothing, and nothing in the output would distinguish that
|
|
950
980
|
// from a rule that ran and found none.
|
|
@@ -1037,8 +1067,10 @@ async function run(io, err) {
|
|
|
1037
1067
|
err(`spec-graph: ${error.message}\n`);
|
|
1038
1068
|
return EXIT_ERROR;
|
|
1039
1069
|
}
|
|
1070
|
+
// A root that was named is named as it was typed, as every other refusal
|
|
1071
|
+
// of it is; the one discovery found is named as it was found.
|
|
1040
1072
|
if (result.files.length === 0) {
|
|
1041
|
-
err(`spec-graph: no specifications matched ${patterns.map((p) => `"${p}"`).join(', ')}\n looked under ${root}\n`);
|
|
1073
|
+
err(`spec-graph: no specifications matched ${patterns.map((p) => `"${p}"`).join(', ')}\n looked under ${options.rootExplicit ? options.root : root}\n`);
|
|
1042
1074
|
return EXIT_ERROR;
|
|
1043
1075
|
}
|
|
1044
1076
|
// A journal named on the line and never read. A `--history` pattern marks
|
|
@@ -1262,6 +1294,11 @@ async function run(io, err) {
|
|
|
1262
1294
|
* arithmetic.
|
|
1263
1295
|
*/
|
|
1264
1296
|
function below(root, from) {
|
|
1297
|
+
// `from` is where the run starts as `path.resolve` gives it, which ends in
|
|
1298
|
+
// no separator but at a filesystem root, `/` or `C:\`, and there `below` is
|
|
1299
|
+
// `''` whether the strip takes every slash, takes one, or writes a word in
|
|
1300
|
+
// their place: the mutants of this line are equivalent since the run
|
|
1301
|
+
// resolves where it starts. The strip stays for a path passed as typed.
|
|
1265
1302
|
const start = toPosix(from).replace(/[/]+$/, '');
|
|
1266
1303
|
// The prefix test only guards a root that is neither `start` nor above it,
|
|
1267
1304
|
// which nothing returns; for `start` itself, slicing past its end is `''`
|
|
@@ -1438,9 +1475,15 @@ async function runDiff(paths, cwd, format, out, err) {
|
|
|
1438
1475
|
for (const path of paths) {
|
|
1439
1476
|
let raw;
|
|
1440
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
|
+
//
|
|
1441
1484
|
// Without the encoding this is a Buffer, which JSON.parse decodes as
|
|
1442
1485
|
// UTF-8 all the same - so emptying the literal parses the same export.
|
|
1443
|
-
raw = await readFile(resolve(cwd, path), 'utf8');
|
|
1486
|
+
raw = await readFile(resolve(cwd, toPosix(path)), 'utf8');
|
|
1444
1487
|
}
|
|
1445
1488
|
catch (error) {
|
|
1446
1489
|
err(`spec-graph: cannot read ${path}: ${error.message}\n`);
|