@descent-vtt/spec-graph 0.6.0 → 0.8.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 +151 -0
- package/README.md +32 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +60 -12
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +3 -3
- package/dist/config.js +4 -4
- package/dist/config.js.map +1 -1
- package/dist/glob.d.ts +7 -1
- package/dist/glob.js +10 -2
- package/dist/glob.js.map +1 -1
- package/dist/identity.d.ts +12 -4
- package/dist/identity.js +32 -8
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +13 -0
- package/dist/paths.js.map +1 -1
- package/dist/sections.js +11 -1
- package/dist/sections.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,157 @@
|
|
|
3
3
|
Notable changes, newest first. Versions follow [semver](https://semver.org):
|
|
4
4
|
a patch fixes behaviour without asking anything of a repository that upgrades.
|
|
5
5
|
|
|
6
|
+
## 0.8.0
|
|
7
|
+
|
|
8
|
+
A register row took its identifier from the first thing its *title* said, not
|
|
9
|
+
from its ID column. Found on the same 885-document repository as 0.7.0: 27 of
|
|
10
|
+
232 rows of one issues register were filed under the wrong entity.
|
|
11
|
+
|
|
12
|
+
A minor rather than a patch, and the baseline is the reason. A baseline is
|
|
13
|
+
fingerprinted on document ids, and the whole of this fix is that those ids move.
|
|
14
|
+
Measured on an unchanged corpus against a baseline recorded by 0.7.0: the
|
|
15
|
+
accepted finding came back as new, its entry was reported as no longer
|
|
16
|
+
occurring, and `check --baseline` went from exit `0` to exit `1`. One
|
|
17
|
+
`--record-baseline` settles it, and the entries it writes are the ones that were
|
|
18
|
+
meant all along. The rule at the top of this file says a patch asks nothing of a
|
|
19
|
+
repository that upgrades, and this asks for that run.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
**A title names nothing.** `| OI-V-05 | ADR-040's enforcement point has no
|
|
24
|
+
browser test |` was read as `ADR-040`. Eighteen rows collided with the ADR they
|
|
25
|
+
cite that way, and the collision does not merely mislabel a row: run against a
|
|
26
|
+
corpus holding both, the ADR keeps the id and the issue has no node in the graph
|
|
27
|
+
at all. Nine more opened with a noun phrase, inventing a `STAGE-1`, a
|
|
28
|
+
`GUARDRAIL-5` and a `CLOSED-2026` that nobody had written, each of them open -
|
|
29
|
+
which is what drew the false `ghost-handover` findings the register was reported
|
|
30
|
+
for; two rows collapsed onto one `STAGE-1`.
|
|
31
|
+
|
|
32
|
+
The identifier a title opens with almost always belongs to the document the
|
|
33
|
+
title is *about*, so a title is read as a name only where nothing else has given
|
|
34
|
+
one - and where it is not the name it registers no alias either, because an
|
|
35
|
+
alias sends every citation of the real document to whatever quoted it.
|
|
36
|
+
`# ADR-0040 considered harmful` at the top of `0007-sharding.md` was registering
|
|
37
|
+
`adr0040` against `ADR-0007`.
|
|
38
|
+
|
|
39
|
+
Two things count as having given one: a number, from a declaration or a file
|
|
40
|
+
name, and for a register row its id column, whatever shape the id is in. A whole
|
|
41
|
+
file keeps the looser reading and its H1 can still name it, because there
|
|
42
|
+
nothing separates a title about another document from a title about this one:
|
|
43
|
+
`slug:` holds a URL segment rather than an id, so `slug: sharding` in
|
|
44
|
+
`sharding.md` under `# ADR-0007: Sharding` has to stay `ADR-0007`. A declaration
|
|
45
|
+
and the file name are still weighed together rather than ranked, so
|
|
46
|
+
`slug: sharding-the-write-path` in `0007-sharding.md` is `ADR-0007` as before.
|
|
47
|
+
|
|
48
|
+
**A region no longer reads a number out of the file it sits in.** Every row of
|
|
49
|
+
a register kept in `0042-open-issues.md` answered to `ADR-0042`, and so did the
|
|
50
|
+
file. ADR-0009 already said a region does not claim the file's *path*, for the
|
|
51
|
+
reason that two nodes answering to one name make every link to it ambiguous; the
|
|
52
|
+
file's name is that same claim spelled differently, and the rule was written
|
|
53
|
+
down as two.
|
|
54
|
+
|
|
55
|
+
**A file's own title heading is matched by its number, not its spelling.**
|
|
56
|
+
`# ADR-040` at the top of `0040-enforce.md` was read as a region *inside* the
|
|
57
|
+
file it names: one decision, two nodes, a split lifecycle, and a citation
|
|
58
|
+
arriving at whichever padding it happened to use. Padding is not part of an
|
|
59
|
+
identity anywhere else - `ADR-40`, `ADR-040` and `ADR-0040` all resolve to one
|
|
60
|
+
document - and now it is not part of this comparison either.
|
|
61
|
+
|
|
62
|
+
### Verified
|
|
63
|
+
|
|
64
|
+
`npm run lint`, 950 tests and `npm run selfcheck`. Thirteen of the fifteen new
|
|
65
|
+
tests fail against the unfixed source; the other two are the guards on the
|
|
66
|
+
layouts that must not move, which have to pass both ways. `identity.ts` and
|
|
67
|
+
`sections.ts` were measured on their own before and after: 72.77% and 66.89%
|
|
68
|
+
became 75.18% and 71.93%, and no mutant on the new lines survived, so nothing
|
|
69
|
+
was masked. ADR-0004 carries the rule as a
|
|
70
|
+
fifth asymmetry, with the hyphenated-prefix question declined under it, and
|
|
71
|
+
ADR-0009 is amended in place.
|
|
72
|
+
|
|
73
|
+
## 0.7.0
|
|
74
|
+
|
|
75
|
+
Three ways a run handed back something other than what it said it was: a
|
|
76
|
+
configuration that did not load reporting a clean graph, a `--baseline` that
|
|
77
|
+
read nothing accepting nothing, and `--verbose` breaking the document it was
|
|
78
|
+
printed above. The first two were found on an 885-document repository, and the
|
|
79
|
+
third while fixing them.
|
|
80
|
+
|
|
81
|
+
A minor rather than a patch. Two of the three turn a build that was passing
|
|
82
|
+
into one that fails, which is the whole point of them and exactly what the rule
|
|
83
|
+
at the top of this file says a patch may not do.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
**A configuration that did not load stops the run, with exit `2`.** Invalid
|
|
88
|
+
JSON, an unknown key, a value of the wrong type, a project rule that does not
|
|
89
|
+
compile: each was printed to stderr, and the run then went on with defaults -
|
|
90
|
+
reporting `the specification graph is consistent`, exiting `0`, and writing
|
|
91
|
+
`"ok": true` into JSON, SARIF and Markdown. Under `--strict` as well. A
|
|
92
|
+
`{1.phse}` for `{1.phase}` in one rule's message was a green build over exactly
|
|
93
|
+
the files that rule fails.
|
|
94
|
+
|
|
95
|
+
It was the one place the exit codes disagreed with themselves, too:
|
|
96
|
+
`--rule not-a-rule=off` on the command line has always exited `2`, and the same
|
|
97
|
+
misspelling inside `"severities"` printed a line and passed. `--no-config`
|
|
98
|
+
checks on defaults, and the exit code then says which run it was. The old
|
|
99
|
+
behaviour was the decision in ADR-0010, and that ADR now carries why it moved.
|
|
100
|
+
|
|
101
|
+
**A `--baseline` that cannot be read stops the run.** Read as an empty baseline,
|
|
102
|
+
a typo in the path reports every accepted finding as new, passes `--ratchet`
|
|
103
|
+
with nothing left to be stale about, and looks like a regression nobody
|
|
104
|
+
introduced. A `"baseline"` named in configuration is unchanged and still
|
|
105
|
+
optional, because a repository declares the path before the first run records
|
|
106
|
+
the file - `--verbose` now says when it read nothing. Unreadable, as opposed to
|
|
107
|
+
absent, is an error wherever the path was written. A baseline that cannot be
|
|
108
|
+
*parsed* is still reported and ignored: it only ever suppresses, so a row that
|
|
109
|
+
is dropped is a finding reported. See
|
|
110
|
+
[ADR-0012](docs/adr/0012-a-baseline-is-a-ratchet.md).
|
|
111
|
+
|
|
112
|
+
### Fixed
|
|
113
|
+
|
|
114
|
+
**`--verbose` no longer breaks the format it was combined with.** The line
|
|
115
|
+
naming which configuration file was read, and the one naming a project rule's
|
|
116
|
+
selector under `query`, went to stdout above the report. With `--format json`
|
|
117
|
+
that made the output unparseable; with `--format sarif` it made a file the
|
|
118
|
+
code-scanning uploader rejects over a schema rather than over the command that
|
|
119
|
+
was run. Both now go to stderr, with everything else a run says about itself.
|
|
120
|
+
[ADR-0015](docs/adr/0015-feedback-goes-where-the-tools-already-look.md) already
|
|
121
|
+
had the rule - rows "cannot go on stdout beside a JSON document without
|
|
122
|
+
breaking the parse" - and these two lines were the places that did not follow
|
|
123
|
+
it.
|
|
124
|
+
|
|
125
|
+
**An absolute baseline path is no longer resolved under the root.**
|
|
126
|
+
`--baseline /tmp/b.json` read `<root>/tmp/b.json` and `--record-baseline`
|
|
127
|
+
wrote there: the recording landed inside the corpus, or failed naming a
|
|
128
|
+
directory nobody had typed, and the read found nothing and said nothing. Both
|
|
129
|
+
platforms' spellings are absolute on either platform, since one repository is
|
|
130
|
+
read on a Windows checkout and in Linux CI from the same file.
|
|
131
|
+
|
|
132
|
+
- **A `package.json` configuration problem names `package.json`.** It reported
|
|
133
|
+
`"spec-graph" must be an object` with no file in it, and a problem that names
|
|
134
|
+
no file sends the reader to a `.spec-graph.json` that is not there.
|
|
135
|
+
|
|
136
|
+
### Verified
|
|
137
|
+
|
|
138
|
+
| | |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `npm run lint` | pass |
|
|
141
|
+
| `npm test` | 935 passing, 23 files |
|
|
142
|
+
| `npm run selfcheck` | pass, over 23 documents and 184 relations |
|
|
143
|
+
| focused sweep, every changed region | 100% |
|
|
144
|
+
| `src/cli.ts` alone, before and after | 80.43% over 797 mutants, then 81.11% over 826 |
|
|
145
|
+
| `break` | unchanged at 70 |
|
|
146
|
+
|
|
147
|
+
Each behaviour was also run against the built binary rather than only through
|
|
148
|
+
`main()`, including the absolute paths, which is where a Windows drive letter
|
|
149
|
+
either survives `toPosix` or does not.
|
|
150
|
+
|
|
151
|
+
Moving two lines from stdout to stderr cost two mutants that a JSON parse used
|
|
152
|
+
to kill for free, and both were replaced with an assertion: a flag that is only
|
|
153
|
+
ever asserted when it is passed stops meaning anything. Both `cli.ts` figures
|
|
154
|
+
above are single-file runs on one machine with `--coverageAnalysis all`, which
|
|
155
|
+
is the comparison ADR-0007 asks for and not the hosted sweep that governs.
|
|
156
|
+
|
|
6
157
|
## 0.6.0
|
|
7
158
|
|
|
8
159
|
A diff of two graph exports that names only the changes it can tell apart,
|
package/README.md
CHANGED
|
@@ -227,6 +227,13 @@ header you wrote:
|
|
|
227
227
|
|
|
228
228
|
Findings point at the declaring cell, not at the file.
|
|
229
229
|
|
|
230
|
+
A row is named by its ID column, and a title is only a title. An identifier a
|
|
231
|
+
title opens with belongs to the document the row is *about* — an issue reading
|
|
232
|
+
`| OI-V-05 | ADR-040's enforcement point has no browser test |` is filed under
|
|
233
|
+
`OI-V-05`, and citations of `ADR-040` still reach `ADR-040`. The same holds for
|
|
234
|
+
a whole file: an explicit `id:`, then the file name, then the H1, and the H1 is
|
|
235
|
+
read as a name only when neither of the others gave one.
|
|
236
|
+
|
|
230
237
|
The register itself stays in the graph as the thing that holds them, so
|
|
231
238
|
`spec-graph query 'document[id=register] -contains-> document'` lists what is
|
|
232
239
|
inside it.
|
|
@@ -561,8 +568,15 @@ spec-graph check --baseline .spec-graph-baseline.json --ratchet
|
|
|
561
568
|
|
|
562
569
|
Now an undeclared finding fails **and** a declared one that no longer occurs
|
|
563
570
|
fails, each named, with `--record-baseline` as the fix — and the diff of that
|
|
564
|
-
file is the record of what was paid off.
|
|
565
|
-
|
|
571
|
+
file is the record of what was paid off.
|
|
572
|
+
|
|
573
|
+
**A `--baseline` you typed has to be there.** A path that reads nothing accepts
|
|
574
|
+
nothing, reports every accepted finding as new, and passes `--ratchet` with
|
|
575
|
+
nothing left to be stale about, so a typo in it looks exactly like a regression
|
|
576
|
+
nobody introduced: it exits `2` instead. A `"baseline"` in the configuration is
|
|
577
|
+
the other case and stays optional, because a repository declares the path before
|
|
578
|
+
the first run records the file — `--verbose` says when it read nothing. Either
|
|
579
|
+
may be absolute. See [ADR-0012](docs/adr/0012-a-baseline-is-a-ratchet.md).
|
|
566
580
|
|
|
567
581
|
## In CI
|
|
568
582
|
|
|
@@ -633,11 +647,19 @@ a path typed on the command line stays relative to where you typed it. See
|
|
|
633
647
|
A flag always wins over the file, and list flags **add** to it rather than
|
|
634
648
|
replacing it — a `--ignore-ref` on the command line is one more exclusion, not a
|
|
635
649
|
decision to discard what the repository already declared. `--verbose` prints
|
|
636
|
-
which file was read
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
650
|
+
which file was read, on stderr with everything else a run says about itself, so
|
|
651
|
+
it can be combined with any `--format` and leave the document on stdout intact.
|
|
652
|
+
`--no-config` skips the mechanism entirely.
|
|
653
|
+
|
|
654
|
+
A broken config stops the run with exit `2`, each problem named on stderr —
|
|
655
|
+
invalid JSON, a value of the wrong type, an unknown key, a rule that does not
|
|
656
|
+
compile. A configuration that did not load checks a different repository than
|
|
657
|
+
the one configured, and would report *that* one as consistent: a `{1.phse}` for
|
|
658
|
+
`{1.phase}` in one rule's message is a green build over the files the correctly
|
|
659
|
+
spelled rule fails. An unknown key counts, for the same reason it is reported at
|
|
660
|
+
all — a silently ignored `ignoreReference` is a configuration that looks applied
|
|
661
|
+
and is not. `--no-config` checks on defaults instead, and the exit code then
|
|
662
|
+
says which run it was.
|
|
641
663
|
|
|
642
664
|
### Family rules
|
|
643
665
|
|
|
@@ -711,8 +733,9 @@ spec-graph diff <before> <after> Compare two JSON graph exports.
|
|
|
711
733
|
```
|
|
712
734
|
|
|
713
735
|
Exit codes: `0` clean, `1` findings, `2` the tool could not run. A mistyped flag
|
|
714
|
-
never masquerades as a passing build, and a pattern
|
|
715
|
-
|
|
736
|
+
never masquerades as a passing build, and neither does a pattern that matches
|
|
737
|
+
nothing, a configuration file that did not load, or a `--baseline` that is not
|
|
738
|
+
there.
|
|
716
739
|
|
|
717
740
|
## Continuous integration
|
|
718
741
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface CliIO {
|
|
|
77
77
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
78
78
|
readonly isTTY?: boolean | undefined;
|
|
79
79
|
}
|
|
80
|
-
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.\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.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable.\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 --baseline <file> Accept the findings recorded in this file and report\n only what is new since.
|
|
80
|
+
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.\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.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable.\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 --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.\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 or markdown. sarif is the\n interchange format GitHub code scanning and editors\n already read, for check; markdown is a table for a\n pull-request comment or $GITHUB_STEP_SUMMARY, for check\n or diff (default: 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\n --ascii Use ASCII glyphs only\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n -h, --help Show this help\n -v, --version Show the version\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\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\n A flag always wins over the file, and list flags add to it rather than\n replacing it.\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. A configuration that did\n not load checks a different repository than the one configured, and would\n report that one as consistent. --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 diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
81
81
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
82
82
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
83
83
|
/** Runs the CLI and returns the process exit code. Never throws. */
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { isGlob, underRoot } from './glob.js';
|
|
|
16
16
|
import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
|
|
17
17
|
import { formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
18
18
|
import { DEFAULT_SEVERITIES, resolveStrict, RULE_DECISIONS, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES, } from './rules.js';
|
|
19
|
-
import { toPosix } from './paths.js';
|
|
19
|
+
import { isAbsolutePath, toPosix } from './paths.js';
|
|
20
20
|
import { formatRef } from './source.js';
|
|
21
21
|
import { execute, parseQuery, QueryError, renderMatch } from './select.js';
|
|
22
22
|
import { isProjectRule } from './types.js';
|
|
@@ -72,7 +72,10 @@ OPTIONS
|
|
|
72
72
|
it - journals, changelogs, minutes. Their links are
|
|
73
73
|
still checked; their obligations are not. Repeatable.
|
|
74
74
|
--baseline <file> Accept the findings recorded in this file and report
|
|
75
|
-
only what is new since.
|
|
75
|
+
only what is new since. The file has to be there: a
|
|
76
|
+
path that reads nothing accepts nothing and says so
|
|
77
|
+
nowhere. A "baseline" in the configuration may name a
|
|
78
|
+
file not recorded yet; a path typed here may not.
|
|
76
79
|
--record-baseline <f> Write today's findings to this file as accepted debt,
|
|
77
80
|
and exit 0 without judging them.
|
|
78
81
|
--ratchet Also fail when a baseline entry no longer occurs, so
|
|
@@ -142,6 +145,11 @@ CONFIGURATION
|
|
|
142
145
|
A flag always wins over the file, and list flags add to it rather than
|
|
143
146
|
replacing it.
|
|
144
147
|
|
|
148
|
+
A problem in that file stops the run with exit 2, named on stderr: invalid
|
|
149
|
+
JSON, an unknown key, a rule that does not compile. A configuration that did
|
|
150
|
+
not load checks a different repository than the one configured, and would
|
|
151
|
+
report that one as consistent. --no-config runs on defaults instead.
|
|
152
|
+
|
|
145
153
|
PROJECT RULES
|
|
146
154
|
A convention spec-graph never anticipated is a selector plus a sentence, and
|
|
147
155
|
belongs in the same file. Every rule declared here runs beside the built-ins
|
|
@@ -424,9 +432,11 @@ function count(n, word, many) {
|
|
|
424
432
|
/**
|
|
425
433
|
* Reads a baseline file.
|
|
426
434
|
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
* baseline
|
|
435
|
+
* Absent and unreadable are different answers, and the caller needs both. A
|
|
436
|
+
* repository that has not recorded a baseline yet declares the path before the
|
|
437
|
+
* file exists, and an empty baseline accepts nothing, which is what that run
|
|
438
|
+
* should report (ADR-0012). Anything else - a directory, a permission, a path
|
|
439
|
+
* that went somewhere unintended - is the tool failing to do what it was told.
|
|
430
440
|
*/
|
|
431
441
|
async function readBaseline(path, source) {
|
|
432
442
|
let raw;
|
|
@@ -434,10 +444,15 @@ async function readBaseline(path, source) {
|
|
|
434
444
|
const { readFile } = await import('node:fs/promises');
|
|
435
445
|
raw = await readFile(path, 'utf8');
|
|
436
446
|
}
|
|
437
|
-
catch {
|
|
438
|
-
|
|
447
|
+
catch (error) {
|
|
448
|
+
const failure = error;
|
|
449
|
+
return {
|
|
450
|
+
baseline: EMPTY_BASELINE,
|
|
451
|
+
problems: [],
|
|
452
|
+
unread: { missing: failure.code === 'ENOENT', message: failure.message },
|
|
453
|
+
};
|
|
439
454
|
}
|
|
440
|
-
return parseBaseline(raw, source);
|
|
455
|
+
return { ...parseBaseline(raw, source), unread: null };
|
|
441
456
|
}
|
|
442
457
|
function parseCount(flag, value) {
|
|
443
458
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -495,6 +510,17 @@ export async function main(io = {}) {
|
|
|
495
510
|
: discoverConfig(options.root);
|
|
496
511
|
for (const problem of loaded.problems)
|
|
497
512
|
err(`spec-graph: ${problem}\n`);
|
|
513
|
+
// A configuration that did not load means a run against a different
|
|
514
|
+
// repository than the one configured - other patterns, other rules, other
|
|
515
|
+
// severities - and the verdict printed is about that one. Reporting the
|
|
516
|
+
// problem and carrying on gave a green build with `ok: true` from a file with
|
|
517
|
+
// a typo in it, which is the one answer a checker must never reach by
|
|
518
|
+
// accident, and the same mistake in a `--rule` has always exited 2. The
|
|
519
|
+
// findings are one flag away, and the flag is named. Amends ADR-0010.
|
|
520
|
+
if (loaded.problems.length > 0) {
|
|
521
|
+
err(`spec-graph: ${count(loaded.problems.length, 'problem')} in the configuration, so nothing was checked\n fix it, or run again with --no-config to check on defaults\n`);
|
|
522
|
+
return EXIT_ERROR;
|
|
523
|
+
}
|
|
498
524
|
const file = loaded.config;
|
|
499
525
|
const projectRules = file.rules ?? [];
|
|
500
526
|
const root = loaded.root;
|
|
@@ -543,9 +569,14 @@ export async function main(io = {}) {
|
|
|
543
569
|
};
|
|
544
570
|
// Named the way the reader would have to type it, because a discovered
|
|
545
571
|
// configuration is often not the one in front of them.
|
|
572
|
+
//
|
|
573
|
+
// On stderr, with everything else this run says about itself. stdout is the
|
|
574
|
+
// report, and in three of the four formats it is a document: a line above it
|
|
575
|
+
// made `--verbose --format json` unparseable and handed `--format sarif` to a
|
|
576
|
+
// code-scanning uploader that rejects it over a schema.
|
|
546
577
|
if (options.verbose && loaded.source !== null) {
|
|
547
578
|
const depth = here === '' ? 0 : here.split('/').length;
|
|
548
|
-
|
|
579
|
+
err(`configuration: ${'../'.repeat(depth)}${loaded.source}\n`);
|
|
549
580
|
}
|
|
550
581
|
let result;
|
|
551
582
|
try {
|
|
@@ -580,9 +611,11 @@ export async function main(io = {}) {
|
|
|
580
611
|
return EXIT_ERROR;
|
|
581
612
|
}
|
|
582
613
|
queries = rule.queries;
|
|
614
|
+
// Beside the report rather than inside it, for the reason the
|
|
615
|
+
// configuration line above is: `--format json` is a document.
|
|
583
616
|
if (options.verbose)
|
|
584
617
|
for (const source of rule.sources)
|
|
585
|
-
|
|
618
|
+
err(`${rule.id}: ${source}\n`);
|
|
586
619
|
}
|
|
587
620
|
else {
|
|
588
621
|
try {
|
|
@@ -624,6 +657,22 @@ export async function main(io = {}) {
|
|
|
624
657
|
let note;
|
|
625
658
|
if (source !== null) {
|
|
626
659
|
const held = await readBaseline(underRoot(root, source), source);
|
|
660
|
+
// A path typed on the command line says the file is there for this run.
|
|
661
|
+
// Read as an empty baseline instead, a typo in it reports every accepted
|
|
662
|
+
// finding as new, and passes --ratchet with nothing left to be stale.
|
|
663
|
+
// A path in the configuration is the case ADR-0012 argued for, and only
|
|
664
|
+
// where the file is absent: unreadable is unreadable wherever the path
|
|
665
|
+
// was written.
|
|
666
|
+
if (held.unread !== null && (options.baseline !== null || !held.unread.missing)) {
|
|
667
|
+
err(`spec-graph: cannot read the baseline ${source}: ${held.unread.message}\n`);
|
|
668
|
+
return EXIT_ERROR;
|
|
669
|
+
}
|
|
670
|
+
// Quiet by default, because a configured path legitimately precedes the
|
|
671
|
+
// first --record-baseline. Under --verbose, where parse problems already
|
|
672
|
+
// go, a path that reads nothing says so rather than accepting nothing.
|
|
673
|
+
if (held.unread !== null && options.verbose) {
|
|
674
|
+
err(`spec-graph: baseline ${source} is not there, so nothing is accepted\n`);
|
|
675
|
+
}
|
|
627
676
|
for (const problem of held.problems)
|
|
628
677
|
err(`spec-graph: ${problem}\n`);
|
|
629
678
|
const outcome = applyBaseline(result.graph, result.diagnostics, held.baseline);
|
|
@@ -687,11 +736,10 @@ function below(root, from) {
|
|
|
687
736
|
* the root cannot change what it means.
|
|
688
737
|
*/
|
|
689
738
|
function anchor(prefix, value) {
|
|
690
|
-
if (prefix === '' ||
|
|
739
|
+
if (prefix === '' || isAbsolutePath(value))
|
|
691
740
|
return value;
|
|
692
741
|
return value.startsWith('!') ? `!${prefix}/${value.slice(1)}` : `${prefix}/${value}`;
|
|
693
742
|
}
|
|
694
|
-
const ABSOLUTE = /^(?:[/\\]|[A-Za-z]:)/;
|
|
695
743
|
/** The same, for an ignore, where a bare name is a directory at any depth. */
|
|
696
744
|
function anchorPath(prefix, pattern) {
|
|
697
745
|
return isGlob(pattern) || pattern.includes('/') ? anchor(prefix, pattern) : pattern;
|