@descent-vtt/spec-graph 0.2.2 → 0.3.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 +128 -0
- package/README.md +87 -2
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +34 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/extract.d.ts +49 -1
- package/dist/extract.js +156 -8
- package/dist/extract.js.map +1 -1
- package/dist/identity.js +14 -5
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +16 -2
- package/dist/markdown.js +89 -22
- package/dist/markdown.js.map +1 -1
- package/dist/report.d.ts +21 -4
- package/dist/report.js +123 -7
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +2 -1
- package/dist/resolve.js +56 -6
- package/dist/resolve.js.map +1 -1
- package/dist/rules.js +30 -0
- package/dist/rules.js.map +1 -1
- package/dist/types.d.ts +23 -3
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,134 @@
|
|
|
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.3.0
|
|
7
|
+
|
|
8
|
+
Found by writing a corpus designed to break the scanner and reading the edges it
|
|
9
|
+
produced, which is the practice [CLAUDE.md](CLAUDE.md) asks for and the reason
|
|
10
|
+
it is there. The mutation score could not have found any of it: a mutant is a
|
|
11
|
+
change to code that exists, and a construct nobody parsed has no code to mutate.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
**A reference-style link was read as a citation of its own label.**
|
|
16
|
+
|
|
17
|
+
```md
|
|
18
|
+
This depends on [ADR-0001][one].
|
|
19
|
+
|
|
20
|
+
[one]: 0001-target.md
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every renderer reads that as a link to `0001-target.md`. spec-graph read it as a
|
|
24
|
+
citation of the word `one`, which resolved to nothing - so those two lines
|
|
25
|
+
produced a false `broken-reference` **and** no `depends-on` edge at all. The
|
|
26
|
+
missing half is the worse one: an absent edge is invisible in a report that says
|
|
27
|
+
everything is consistent.
|
|
28
|
+
|
|
29
|
+
Definitions are now the destination table the labelled forms are read through. A
|
|
30
|
+
label nothing defines is prose, because `[design][one]` renders verbatim when
|
|
31
|
+
nothing says what `one` is. A broken one is reported at the definition, which is
|
|
32
|
+
the line that fixes it, however many places use the label.
|
|
33
|
+
|
|
34
|
+
**A `@spec-node` directive inside `<script>` or `<pre>` was honoured**, so a page
|
|
35
|
+
explaining how to annotate a document could annotate itself. The four elements
|
|
36
|
+
whose content is not Markdown are masked now, which is the CommonMark reading.
|
|
37
|
+
`<div>` and `<details>` keep their content: a decision written inside a collapsed
|
|
38
|
+
section is still a decision.
|
|
39
|
+
|
|
40
|
+
**A document could take an id that was not a name.** `id=\"ADR-9\"` inside a
|
|
41
|
+
JavaScript string parses its bare value as a lone backslash, and the document
|
|
42
|
+
then collided with itself, reported a duplicate id against its own file,
|
|
43
|
+
contained itself, and exported as invalid Mermaid. A declared id must carry a
|
|
44
|
+
letter or a digit.
|
|
45
|
+
|
|
46
|
+
**A percent-escaped destination resolved to nothing.**
|
|
47
|
+
`[Design](docs/my%20design.md)` is what a renderer, a documentation site and
|
|
48
|
+
GitHub's own copy-link all produce for a file with a space in its name. The
|
|
49
|
+
escapes are read as a fallback, so a filename that genuinely contains one keeps
|
|
50
|
+
resolving by the spelling it was written with, and a stray `100%` is left alone.
|
|
51
|
+
|
|
52
|
+
See [ADR-0013](docs/adr/0013-the-scanner-hands-back-prose.md).
|
|
53
|
+
|
|
54
|
+
**Front matter knew half of each relation.** `depends-on` was a key and
|
|
55
|
+
`depended-on-by` was not, so a repository whose filing convention records
|
|
56
|
+
dependents wrote the inverse spelling and got no edge, no warning, and a graph
|
|
57
|
+
that reported itself as consistent. Every directional kind is spelled both ways
|
|
58
|
+
now - twenty-five keys became fifty-six - and a test holds the invariant.
|
|
59
|
+
`relates-to` is symmetric and exempt.
|
|
60
|
+
|
|
61
|
+
Keys also fold on separators and case, so `depends-on`, `depends_on` and
|
|
62
|
+
`dependsOn` are one key rather than the two that happened to be written out.
|
|
63
|
+
See [ADR-0014](docs/adr/0014-a-relation-is-spelled-both-ways.md).
|
|
64
|
+
|
|
65
|
+
**`--verbose --format json` printed human lines onto stdout beside the JSON.**
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **`unknown-relation-key`** (warn). A front-matter key one edit from a relation,
|
|
70
|
+
carrying something that could name a document, is reported rather than
|
|
71
|
+
ignored. Both halves of that gate matter: `deprecated: true` is one edit from
|
|
72
|
+
`deprecates` and a boolean is not a citation. It says what is true - *"the key
|
|
73
|
+
declares no relation"* - and offers the near spellings rather than asserting
|
|
74
|
+
one. See [ADR-0014](docs/adr/0014-a-relation-is-spelled-both-ways.md).
|
|
75
|
+
- **`--ratchet`**, and `"ratchet": true` in the configuration. The other side of
|
|
76
|
+
a baseline: with it, a declared finding that no longer occurs fails the build,
|
|
77
|
+
named, with `--record-baseline` as the fix. Off by default, because
|
|
78
|
+
[ADR-0012](docs/adr/0012-a-baseline-is-a-ratchet.md) is right that failing a
|
|
79
|
+
build because somebody fixed something is a strange way to encourage them -
|
|
80
|
+
and on for a team that has decided its debt only moves one way, because a note
|
|
81
|
+
on a green build is a line that scrolls past.
|
|
82
|
+
- **`--format sarif`**. SARIF 2.1.0, for `check`, so
|
|
83
|
+
`github/codeql-action/upload-sarif` can turn findings into annotations on the
|
|
84
|
+
diff with no integration on either side. The `partialFingerprints` are the
|
|
85
|
+
same rule/specification/citation identity a baseline is keyed on, so an
|
|
86
|
+
annotation survives a reformat. Deterministic and timestamp-free like every
|
|
87
|
+
other output here. See
|
|
88
|
+
[ADR-0015](docs/adr/0015-feedback-goes-where-the-tools-already-look.md).
|
|
89
|
+
|
|
90
|
+
### Not added
|
|
91
|
+
|
|
92
|
+
**`--watch`.** A full run is 60 ms, so the loop is one line of shell in whatever
|
|
93
|
+
the developer already uses. A resident process, a debounce timer and `fs.watch`
|
|
94
|
+
semantics that differ on all three platforms would be the one part of this
|
|
95
|
+
program that could not honour byte-determinism, inside a codebase whose whole
|
|
96
|
+
test strategy rests on everything between the edges being a pure function of
|
|
97
|
+
text. Reasoning in
|
|
98
|
+
[ADR-0015](docs/adr/0015-feedback-goes-where-the-tools-already-look.md).
|
|
99
|
+
|
|
100
|
+
### Measured
|
|
101
|
+
|
|
102
|
+
684 tests, and a mutation score of **79.69% over 8,126 mutants** - up from
|
|
103
|
+
77.16% over 7,720. The guard stays at `break: 70`, because the same source
|
|
104
|
+
measured twice on the same machine, with only test cases added between the runs,
|
|
105
|
+
moved nine untouched files by more than a point each in both directions. See
|
|
106
|
+
[ADR-0007](docs/adr/0007-mutation-testing.md).
|
|
107
|
+
|
|
108
|
+
## 0.2.3
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
**A path spelled without naming a file exactly could resolve to the wrong file.**
|
|
113
|
+
`docs/A` is two things at once - `docs/A.md` with its extension dropped, and
|
|
114
|
+
`docs/A/README.md` standing for its directory - and the index answering such
|
|
115
|
+
spellings held one id per key. A second claimant therefore did not make the link
|
|
116
|
+
ambiguous; it overwrote the first, and the link went to whichever file was
|
|
117
|
+
indexed last.
|
|
118
|
+
|
|
119
|
+
[ADR-0004](docs/adr/0004-reference-resolution.md) has always said two candidates
|
|
120
|
+
is worse than none, and a reference matching several documents is reported as
|
|
121
|
+
ambiguous rather than bound to whichever was indexed first. That now holds for
|
|
122
|
+
paths too.
|
|
123
|
+
|
|
124
|
+
Naming a file exactly is never ambiguous, whatever else is spelled the same way,
|
|
125
|
+
and a spelling only one file answers to still resolves in silence - so
|
|
126
|
+
`docs/adr/0007` addressing `docs/adr/0007/README.md` is unchanged.
|
|
127
|
+
|
|
128
|
+
**This can surface a new warning on upgrade**, in the one situation where it is
|
|
129
|
+
earned: a deliberate link written as a path that genuinely names two files. It
|
|
130
|
+
is a warning rather than an error, it lists both candidates, and prose that
|
|
131
|
+
happens to mention such a path stays silent, because an ambiguity nobody wrote
|
|
132
|
+
down is not a mistake anybody made.
|
|
133
|
+
|
|
6
134
|
## 0.2.2
|
|
7
135
|
|
|
8
136
|
### Fixed
|
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ npx @descent-vtt/spec-graph "docs/**/*.md"
|
|
|
78
78
|
| `ambiguous-reference` | warn | A citation matching more than one document |
|
|
79
79
|
| `unreciprocated-supersession` | warn | A retired document that never says what replaced it |
|
|
80
80
|
| `state-conflict` | warn | An item whose two state signals disagree about whether work remains |
|
|
81
|
+
| `unknown-relation-key` | warn | A front-matter key one edit from a relation, carrying what looks like a citation |
|
|
81
82
|
| `self-reference` | info | A document delegating to, or depending on, itself |
|
|
82
83
|
|
|
83
84
|
Override any of them:
|
|
@@ -266,6 +267,38 @@ still a relation in the graph no matter what you exclude — not even
|
|
|
266
267
|
`--ignore-ref "*"` can delete one. See
|
|
267
268
|
[ADR-0008](docs/adr/0008-wiki-links-carry-no-path.md).
|
|
268
269
|
|
|
270
|
+
#### A gap that is deliberate
|
|
271
|
+
|
|
272
|
+
Real filing histories have holes: an ADR number reserved and then withdrawn, a
|
|
273
|
+
document that was folded into another before it was ever committed. Something
|
|
274
|
+
still cites it, and the citation is not a mistake.
|
|
275
|
+
|
|
276
|
+
There are two mechanisms and the difference is what you are declaring.
|
|
277
|
+
|
|
278
|
+
**The target is never going to exist** — `ADR-0006` was withdrawn before filing.
|
|
279
|
+
That is a fact about the repository, so declare it once in
|
|
280
|
+
`.spec-graph.json`:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{ "ignoreReferences": ["ADR-0006"] }
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Every citation of it stays quiet, wherever it is written, forever. A glob is
|
|
287
|
+
available and rarely what you want here: `ADR-0006` names one target exactly.
|
|
288
|
+
|
|
289
|
+
**This particular citation is accepted for now** — it should resolve, one day it
|
|
290
|
+
will, and until then the build should not stop. That is debt, not a fact, and it
|
|
291
|
+
belongs in the baseline, keyed to the one document and the one target:
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{ "rule": "broken-reference", "document": "ADR-0004", "subject": "docs/plans/x.md", "count": 1 }
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The same citation from a different document is still reported, and `--ratchet`
|
|
298
|
+
makes the entry disappear the day it is paid. Reach for the first when the
|
|
299
|
+
absence is permanent, the second when it is temporary — and if you cannot say
|
|
300
|
+
which, it is the second.
|
|
301
|
+
|
|
269
302
|
### Relations
|
|
270
303
|
|
|
271
304
|
Relations are read from front matter, from link context, and from explicit
|
|
@@ -426,7 +459,54 @@ A baseline that expires when somebody reformats a paragraph is worse than none,
|
|
|
426
459
|
so the entry survives edits, reordering, and the file being renamed — because
|
|
427
460
|
`ADR-0004` is the decision's name, not its location. Repeats are counted rather
|
|
428
461
|
than told apart, which is the deliberate cost of a key with no position in it.
|
|
429
|
-
|
|
462
|
+
|
|
463
|
+
By default that line is a note and the build stays green: failing because
|
|
464
|
+
somebody fixed something is a strange way to encourage them. But a note in CI
|
|
465
|
+
is a line that scrolls past, and an exemption nobody strikes outlives the defect
|
|
466
|
+
it was written for. A team that has decided its debt only moves one way turns on
|
|
467
|
+
the other side of the ratchet:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
spec-graph check --baseline .spec-graph-baseline.json --ratchet
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Now an undeclared finding fails **and** a declared one that no longer occurs
|
|
474
|
+
fails, each named, with `--record-baseline` as the fix — and the diff of that
|
|
475
|
+
file is the record of what was paid off. See
|
|
476
|
+
[ADR-0012](docs/adr/0012-a-baseline-is-a-ratchet.md).
|
|
477
|
+
|
|
478
|
+
## In CI
|
|
479
|
+
|
|
480
|
+
The exit code is the contract: `0` clean, `1` findings, `2` the tool could not
|
|
481
|
+
run. That is all most pipelines need.
|
|
482
|
+
|
|
483
|
+
For annotations on the diff rather than a line in a log, emit SARIF and hand it
|
|
484
|
+
to the uploader that already exists:
|
|
485
|
+
|
|
486
|
+
```yaml
|
|
487
|
+
- run: npx spec-graph --format sarif > spec-graph.sarif
|
|
488
|
+
continue-on-error: true
|
|
489
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
490
|
+
with:
|
|
491
|
+
sarif_file: spec-graph.sarif
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Findings land as comments on the changed lines, and they persist across commits
|
|
495
|
+
without drifting, because the `partialFingerprints` spec-graph writes are the
|
|
496
|
+
same identity a baseline is keyed on — the rule, the specification, and the
|
|
497
|
+
citation, with no line number in it (ADR-0012).
|
|
498
|
+
|
|
499
|
+
`--format json` is the other machine-readable output, and the one to parse if
|
|
500
|
+
you are building something of your own: it carries the baseline note, the
|
|
501
|
+
summary and the per-file detail that SARIF has nowhere to put.
|
|
502
|
+
|
|
503
|
+
**There is no `--watch`.** A full run on this repository takes 60 ms, so the
|
|
504
|
+
loop that would justify a resident process is one line of shell, and it belongs
|
|
505
|
+
to whatever the developer already uses rather than to this tool:
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
npx spec-graph --format json | jq -r '.summary'
|
|
509
|
+
```
|
|
430
510
|
|
|
431
511
|
## Configuration
|
|
432
512
|
|
|
@@ -441,6 +521,7 @@ command. spec-graph reads the first of `.spec-graph.json`,
|
|
|
441
521
|
"ignoreFamilies": ["RFC"],
|
|
442
522
|
"historyPatterns": ["**/JOURNAL_*.md"],
|
|
443
523
|
"baseline": ".spec-graph-baseline.json",
|
|
524
|
+
"ratchet": true,
|
|
444
525
|
"severities": { "self-reference": "off" },
|
|
445
526
|
"strict": true
|
|
446
527
|
}
|
|
@@ -493,8 +574,9 @@ spec-graph rules [--explain] List the diagnostics.
|
|
|
493
574
|
--history <glob> Files that log decisions rather than making them
|
|
494
575
|
--baseline <file> Accept these findings; report only what is new
|
|
495
576
|
--record-baseline <f> Write today's findings as accepted debt, exit 0
|
|
577
|
+
--ratchet Also fail when a baseline entry no longer occurs
|
|
496
578
|
--no-config Ignore .spec-graph.json and the package.json key
|
|
497
|
-
--format human|json
|
|
579
|
+
--format <fmt> human | json | sarif
|
|
498
580
|
--graph-format <fmt> dot | mermaid | json
|
|
499
581
|
--documents-only Hide items; their relations lift onto their documents
|
|
500
582
|
--rule <id>=<severity> error | warn | info | off (repeatable)
|
|
@@ -590,6 +672,9 @@ Twelve ADRs, which `spec-graph` validates on every CI run:
|
|
|
590
672
|
- [ADR-0010 — Configuration belongs to the repository](docs/adr/0010-configuration-belongs-to-the-repository.md)
|
|
591
673
|
- [ADR-0011 — A historical record is not a specification](docs/adr/0011-a-record-is-not-a-specification.md)
|
|
592
674
|
- [ADR-0012 — A baseline is a ratchet, keyed on identity](docs/adr/0012-a-baseline-is-a-ratchet.md)
|
|
675
|
+
- [ADR-0013 — When the scanner is unsure, it hands back prose](docs/adr/0013-the-scanner-hands-back-prose.md)
|
|
676
|
+
- [ADR-0014 — A relation is spelled in both directions](docs/adr/0014-a-relation-is-spelled-both-ways.md)
|
|
677
|
+
- [ADR-0015 — Feedback goes where the tools already look](docs/adr/0015-feedback-goes-where-the-tools-already-look.md)
|
|
593
678
|
|
|
594
679
|
**Zero runtime dependencies.** Node 22+, native ESM, TypeScript strict with
|
|
595
680
|
`noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`. The Markdown
|
package/dist/cli.d.ts
CHANGED
|
@@ -34,9 +34,18 @@ export interface CliOptions {
|
|
|
34
34
|
readonly baseline: string | null;
|
|
35
35
|
/** Where to write the current findings as accepted debt. */
|
|
36
36
|
readonly recordBaseline: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Fail when the baseline allows something that no longer happens.
|
|
39
|
+
*
|
|
40
|
+
* The other side of the ratchet, and off by default for the reason ADR-0012
|
|
41
|
+
* gives: failing a build because somebody fixed something is a strange way to
|
|
42
|
+
* encourage them. A team that has decided its debt only goes one way asks for
|
|
43
|
+
* it explicitly, and then a dead exemption cannot outlive the defect.
|
|
44
|
+
*/
|
|
45
|
+
readonly ratchet: boolean;
|
|
37
46
|
/** Skip the repository configuration file entirely. */
|
|
38
47
|
readonly noConfig: boolean;
|
|
39
|
-
readonly format: 'human' | 'json';
|
|
48
|
+
readonly format: 'human' | 'json' | 'sarif';
|
|
40
49
|
readonly graphFormat: GraphFormat;
|
|
41
50
|
readonly severities: Partial<Record<RuleId, Severity>>;
|
|
42
51
|
readonly color: boolean | null;
|
|
@@ -59,7 +68,7 @@ export interface CliIO {
|
|
|
59
68
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
60
69
|
readonly isTTY?: boolean | undefined;
|
|
61
70
|
}
|
|
62
|
-
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> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [--explain]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector and print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the built-in diagnostics.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against (default: cwd)\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. Missing file = accept nothing.\n --record-baseline <f> Write today's findings to this file as accepted debt,\n and exit 0 without judging them.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format human
|
|
71
|
+
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> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [--explain]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector and print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the built-in diagnostics.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against (default: cwd)\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. Missing file = accept nothing.\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, or sarif - the interchange format\n GitHub code scanning and editors already read\n (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. 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 and per-file detail\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 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\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";
|
|
63
72
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
64
73
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
65
74
|
/** Runs the CLI and returns the process exit code. Never throws. */
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline } from './
|
|
|
12
12
|
import { loadConfig } from './config.js';
|
|
13
13
|
import { underRoot } from './glob.js';
|
|
14
14
|
import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
|
|
15
|
-
import { formatGraph, formatJson, formatReport, shouldUseAscii, shouldUseColor } from './report.js';
|
|
15
|
+
import { formatGraph, formatJson, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
16
16
|
import { DEFAULT_SEVERITIES, resolveStrict, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES } from './rules.js';
|
|
17
17
|
import { formatRef } from './source.js';
|
|
18
18
|
import { execute, parseQuery, QueryError, renderMatch } from './select.js';
|
|
@@ -57,8 +57,12 @@ OPTIONS
|
|
|
57
57
|
only what is new since. Missing file = accept nothing.
|
|
58
58
|
--record-baseline <f> Write today's findings to this file as accepted debt,
|
|
59
59
|
and exit 0 without judging them.
|
|
60
|
+
--ratchet Also fail when a baseline entry no longer occurs, so
|
|
61
|
+
a paid-off exemption cannot outlive the defect.
|
|
60
62
|
--no-config Ignore .spec-graph.json and the package.json key.
|
|
61
|
-
--format human
|
|
63
|
+
--format <fmt> human, json, or sarif - the interchange format
|
|
64
|
+
GitHub code scanning and editors already read
|
|
65
|
+
(default: human)
|
|
62
66
|
--graph-format <fmt> dot, mermaid or json (default: dot)
|
|
63
67
|
--documents-only Leave items out of the exported graph
|
|
64
68
|
--rule <id>=<severity> Override one rule: error, warn, info or off. Repeatable.
|
|
@@ -160,6 +164,7 @@ export function parseArgs(argv, cwd) {
|
|
|
160
164
|
let maxWarnings = -1;
|
|
161
165
|
let strict = false;
|
|
162
166
|
let baseline = null;
|
|
167
|
+
let ratchet = false;
|
|
163
168
|
let recordBaseline = null;
|
|
164
169
|
let selector = null;
|
|
165
170
|
let help = false;
|
|
@@ -210,6 +215,9 @@ export function parseArgs(argv, cwd) {
|
|
|
210
215
|
case '--strict':
|
|
211
216
|
strict = true;
|
|
212
217
|
break;
|
|
218
|
+
case '--ratchet':
|
|
219
|
+
ratchet = true;
|
|
220
|
+
break;
|
|
213
221
|
case '--explain':
|
|
214
222
|
verbose = true;
|
|
215
223
|
break;
|
|
@@ -250,8 +258,8 @@ export function parseArgs(argv, cwd) {
|
|
|
250
258
|
break;
|
|
251
259
|
case '--format': {
|
|
252
260
|
const value = next(arg, i);
|
|
253
|
-
if (value !== 'human' && value !== 'json') {
|
|
254
|
-
throw new UsageError(`--format must be human or
|
|
261
|
+
if (value !== 'human' && value !== 'json' && value !== 'sarif') {
|
|
262
|
+
throw new UsageError(`--format must be human, json or sarif, got "${value}"`);
|
|
255
263
|
}
|
|
256
264
|
format = value;
|
|
257
265
|
i += 1;
|
|
@@ -297,6 +305,12 @@ export function parseArgs(argv, cwd) {
|
|
|
297
305
|
throw new UsageError(`unknown option "${arg}"\n run "spec-graph --help" to see the available options`);
|
|
298
306
|
}
|
|
299
307
|
}
|
|
308
|
+
// SARIF is a report about findings, and only `check` produces those. Falling
|
|
309
|
+
// back to JSON would hand a pipeline something its uploader rejects with a
|
|
310
|
+
// message about a schema rather than about the command that was run.
|
|
311
|
+
if (format === 'sarif' && command !== 'check' && !help && !version) {
|
|
312
|
+
throw new UsageError(`--format sarif reports findings, so it belongs to check, not to ${command}`);
|
|
313
|
+
}
|
|
300
314
|
if (command === 'query' && selector === null && !help && !version) {
|
|
301
315
|
throw new UsageError('query needs a selector, for example:\n spec-graph query \'item[openness=open]\'');
|
|
302
316
|
}
|
|
@@ -310,6 +324,7 @@ export function parseArgs(argv, cwd) {
|
|
|
310
324
|
ignoreFamilies,
|
|
311
325
|
historyPatterns,
|
|
312
326
|
baseline,
|
|
327
|
+
ratchet,
|
|
313
328
|
recordBaseline,
|
|
314
329
|
noConfig,
|
|
315
330
|
format,
|
|
@@ -433,7 +448,7 @@ export async function main(io = {}) {
|
|
|
433
448
|
case 'query': {
|
|
434
449
|
try {
|
|
435
450
|
const matches = execute(result.graph, parseQuery(options.selector));
|
|
436
|
-
out(renderMatches(matches, options.format, result.graph.nodes.size));
|
|
451
|
+
out(renderMatches(matches, options.format === 'json' ? 'json' : 'human', result.graph.nodes.size));
|
|
437
452
|
return matches.length > 0 ? EXIT_OK : EXIT_FAILED;
|
|
438
453
|
}
|
|
439
454
|
catch (error) {
|
|
@@ -463,6 +478,7 @@ export async function main(io = {}) {
|
|
|
463
478
|
return EXIT_OK;
|
|
464
479
|
}
|
|
465
480
|
const source = options.baseline ?? file.baseline ?? null;
|
|
481
|
+
const ratchet = options.ratchet || file.ratchet === true;
|
|
466
482
|
let reported = result;
|
|
467
483
|
let note;
|
|
468
484
|
if (source !== null) {
|
|
@@ -471,18 +487,25 @@ export async function main(io = {}) {
|
|
|
471
487
|
err(`spec-graph: ${problem}\n`);
|
|
472
488
|
const outcome = applyBaseline(result.graph, result.diagnostics, held.baseline);
|
|
473
489
|
reported = withDiagnostics(result, outcome.kept);
|
|
474
|
-
note = { source, suppressed: outcome.suppressed, stale: outcome.stale.length };
|
|
475
|
-
|
|
490
|
+
note = { source, suppressed: outcome.suppressed, stale: outcome.stale.length, ratchet };
|
|
491
|
+
// Listed rather than counted when the run turns on them: a number is
|
|
492
|
+
// enough to know the file has slack, and not enough to strike it. Never
|
|
493
|
+
// in JSON, where the entries are already in the report and a stray line
|
|
494
|
+
// on stdout is the difference between parsing and not.
|
|
495
|
+
if (options.format === 'human' && (options.verbose || (ratchet && outcome.stale.length > 0))) {
|
|
476
496
|
for (const entry of outcome.stale) {
|
|
477
497
|
out(` paid: ${entry.rule} ${entry.document}${entry.subject === '' ? '' : ` "${entry.subject}"`}\n`);
|
|
478
498
|
}
|
|
479
499
|
}
|
|
480
500
|
}
|
|
501
|
+
const looseBaseline = note !== undefined && note.ratchet && note.stale > 0;
|
|
481
502
|
const baselineNote = note === undefined ? {} : { baseline: note };
|
|
482
|
-
out(options.format === '
|
|
483
|
-
?
|
|
484
|
-
:
|
|
485
|
-
|
|
503
|
+
out(options.format === 'sarif'
|
|
504
|
+
? formatSarif(reported, reported.graph, { version: await readVersion(), escalated })
|
|
505
|
+
: options.format === 'json'
|
|
506
|
+
? formatJson(reported, { escalated, ...baselineNote })
|
|
507
|
+
: `${formatReport(reported, { color, ascii, verbose: options.verbose, max: options.max, escalated, ...baselineNote })}\n`);
|
|
508
|
+
if (!reported.ok || looseBaseline)
|
|
486
509
|
return EXIT_FAILED;
|
|
487
510
|
if (options.maxWarnings >= 0 && reported.summary.warnings > options.maxWarnings)
|
|
488
511
|
return EXIT_FAILED;
|