@descent-vtt/spec-graph 0.5.0 → 0.6.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 +106 -3
- package/README.md +37 -3
- package/dist/cli.d.ts +5 -4
- package/dist/cli.js +67 -9
- package/dist/cli.js.map +1 -1
- package/dist/diff.d.ts +120 -0
- package/dist/diff.js +404 -0
- package/dist/diff.js.map +1 -0
- package/dist/directives.d.ts +18 -1
- package/dist/directives.js +79 -0
- package/dist/directives.js.map +1 -1
- package/dist/extract.js +4 -2
- package/dist/extract.js.map +1 -1
- package/dist/glob.d.ts +34 -0
- package/dist/glob.js +51 -4
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/regex.d.ts +18 -6
- package/dist/regex.js +185 -71
- package/dist/regex.js.map +1 -1
- package/dist/report.d.ts +10 -0
- package/dist/report.js +2 -0
- package/dist/report.js.map +1 -1
- package/dist/resolve.js +11 -0
- package/dist/resolve.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,106 @@
|
|
|
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.6.0
|
|
7
|
+
|
|
8
|
+
A diff of two graph exports that names only the changes it can tell apart,
|
|
9
|
+
every glob moved onto the automaton `~=` already ran on, and a directive that
|
|
10
|
+
annotates one item where it used to annotate every item in reach. The mutation
|
|
11
|
+
figure is lower than 0.5.0's, and it is the first a release has carried from
|
|
12
|
+
tests that no longer race each other on disk.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
**A glob can no longer keep a run busy for minutes.** 0.5.0 moved `~=` onto an
|
|
17
|
+
automaton that cannot backtrack and left globs on `RegExp`, on the argument
|
|
18
|
+
that a glob has no nested quantifiers. Nobody had timed it. Stars that are not
|
|
19
|
+
nested still divide a failing subject between them every possible way:
|
|
20
|
+
|
|
21
|
+
| glob | subject | `RegExp` | now |
|
|
22
|
+
| --- | --- | ---: | ---: |
|
|
23
|
+
| `**/*-*-*-*.md` | a 643-character hyphenated file name | 2.3s | 0.2ms |
|
|
24
|
+
| `*-*-*-x` | a 10,000-character reference target | 120s | 0.9ms |
|
|
25
|
+
|
|
26
|
+
The second is the one that matters: `--ignore-ref` and `ignoreReferences` are
|
|
27
|
+
matched against targets read out of documents, which nothing limits the length
|
|
28
|
+
of. Every glob - the patterns to check, `--ignore`, `--ignore-ref`, `--history` -
|
|
29
|
+
now runs on the same automaton as `~=`, verified against the `RegExp` it
|
|
30
|
+
replaces on every glob and path in a differential corpus. See
|
|
31
|
+
[ADR-0017](docs/adr/0017-a-predicate-must-finish.md).
|
|
32
|
+
|
|
33
|
+
- **An invalid glob names itself.** `docs/{a` used to report
|
|
34
|
+
`Invalid regular expression: /^docs\/(?:a$/i: Unterminated group`, about a
|
|
35
|
+
parenthesis nobody typed. It now reports `invalid glob "docs/{a": unclosed "{"`.
|
|
36
|
+
- **`--ignore-ref` reads case the same way on every platform.** It lower-cased
|
|
37
|
+
both sides, and on Windows alone also folded case the way the `i` flag does,
|
|
38
|
+
which made a handful of distinct characters - the micro sign and the Greek
|
|
39
|
+
mu - one target there and two everywhere else.
|
|
40
|
+
- **A `@spec-item` directive annotates one item.** Each item used to look for
|
|
41
|
+
its own, so a directive was found by every item within 200 characters below
|
|
42
|
+
it and by every item enclosing it. The question after a declared one took the
|
|
43
|
+
same id and vanished from the graph without a word, and a parent could take
|
|
44
|
+
its child's directive in place of its own. A directive now binds to the item
|
|
45
|
+
directly below it, with nothing but blank lines and comments between, or else
|
|
46
|
+
to the innermost item it is written on or indented under. An id two items
|
|
47
|
+
still claim, by declaring it twice or by declaring another item's number, is
|
|
48
|
+
a parse problem, and the first item keeps it. `bindItemDirectives` is
|
|
49
|
+
exported; `directiveFor` is unchanged and still exported.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
**`spec-graph diff <before.json> <after.json>` says what a change did to the
|
|
54
|
+
decisions:** documents added, removed, moved or accepted, relations added or
|
|
55
|
+
removed, and obligations resolved or reopened. It names only what it can tell
|
|
56
|
+
apart. An obligation's id is its position in its section, so one inserted
|
|
57
|
+
question renumbers every question below it, and a diff keyed on those ids would
|
|
58
|
+
report a closed question as reopened when nobody touched it. An obligation is
|
|
59
|
+
paired only by a declared id, or by its document, section and title when nothing
|
|
60
|
+
else shares them; anything unpaired is reported as having appeared or
|
|
61
|
+
disappeared. Human, `--format json` or `--format markdown`, and exit 0 either
|
|
62
|
+
way. See [ADR-0020](docs/adr/0020-a-diff-names-what-it-can-tell-apart.md).
|
|
63
|
+
|
|
64
|
+
- The JSON graph export names its `generator`, so a diff can warn when two
|
|
65
|
+
exports came from different versions, and each item says whether its id was
|
|
66
|
+
`declared`. Both are additions; the export is still version 1.
|
|
67
|
+
- `compileGlob(pattern, { ignoreCase })`, the matcher the walk uses, and an
|
|
68
|
+
`ignoreCase` option on `compilePattern` for a pattern that must respect case.
|
|
69
|
+
`globToRegExp` is unchanged and still exported.
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
One behaviour moves, and it can turn a run that passed into one that fails.
|
|
74
|
+
|
|
75
|
+
- **A `@spec-item` directive with prose, a heading or code between it and the
|
|
76
|
+
item below binds to nothing.** It used to reach any item starting within 200
|
|
77
|
+
characters. Its id, state and title no longer apply: the item takes its
|
|
78
|
+
numbered id, a `state="moot"` stops closing it, and a link to the declared id
|
|
79
|
+
is a `broken-reference`. Move the directive down to the line above the item.
|
|
80
|
+
|
|
81
|
+
### Verified
|
|
82
|
+
|
|
83
|
+
| | |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `npm run lint` | pass |
|
|
86
|
+
| `npm test` | 921 passing, 23 files |
|
|
87
|
+
| `npm run selfcheck` | pass, over 23 documents and 184 relations |
|
|
88
|
+
| hosted full sweep, which governs | **75.25%** over 10,519 mutants, four shards of 22 to 33 minutes |
|
|
89
|
+
| `break` | unchanged at 70 |
|
|
90
|
+
|
|
91
|
+
0.5.0's 76.83% is not the figure to compare with. About three points of it were
|
|
92
|
+
kills no assertion made: tests writing to fixed paths deleted each other's files
|
|
93
|
+
under Stryker's parallel workers. Race-free, the sweep read 73.42% before
|
|
94
|
+
`spec-graph diff` and 75.25% with this release: `diff.ts` at 98.35%, and
|
|
95
|
+
`directives.ts` from 66.88% to 78.01%. Lose every timeout and it reads 71.89%.
|
|
96
|
+
See [ADR-0007](docs/adr/0007-mutation-testing.md) and
|
|
97
|
+
[ADR-0019](docs/adr/0019-the-sweep-runs-in-shards.md).
|
|
98
|
+
|
|
99
|
+
The sweep after the directive fix moved `extract.ts` down 0.18 points, among the
|
|
100
|
+
usual movement in files nobody touched, and this one was real. Four mutants
|
|
101
|
+
that hand an item to every specification in a register had gone from killed to
|
|
102
|
+
surviving: resolution now drops the duplicates they create, and the test that
|
|
103
|
+
caught them only counted items. It now also asserts that no problem is
|
|
104
|
+
reported.
|
|
105
|
+
|
|
6
106
|
## 0.5.0
|
|
7
107
|
|
|
8
108
|
One shipped feature turned out to have shipped a false positive, and the
|
|
@@ -72,7 +172,7 @@ now a usage error rather than a pattern that silently matches nothing.
|
|
|
72
172
|
Verified against the engine it replaces: **1.33 million pattern-subject pairs,
|
|
73
173
|
zero disagreements**, 19,000 of them committed as a gate. It earned that twice
|
|
74
174
|
over, on one clause of the language specification read backwards in two
|
|
75
|
-
different places - the word characters
|
|
175
|
+
different places - the word characters `\b` recognises, and whether a character
|
|
76
176
|
whose upper case is ASCII may match `[A-Z]`. The second was found by the
|
|
77
177
|
mutation score pointing at a corpus with a blind spot in it rather than at a
|
|
78
178
|
test. See [ADR-0017](docs/adr/0017-a-predicate-must-finish.md) and
|
|
@@ -245,8 +345,11 @@ points.
|
|
|
245
345
|
The incremental run on the same commit read 0.40 points low - the third
|
|
246
346
|
observation after 3.18 high at 0.3.0 and 1.29 low at 0.4.0, and the reason
|
|
247
347
|
[ADR-0007](docs/adr/0007-mutation-testing.md) calls it a signal rather than a
|
|
248
|
-
figure. The tag's run also
|
|
249
|
-
|
|
348
|
+
figure. The tag's run also logged a cache save, which this entry first read as
|
|
349
|
+
the cache-key fix from 0.4.0 doing its job. *Corrected 2026-09-13:* it saved a
|
|
350
|
+
copy of the stale report it had restored, because the full step never wrote one,
|
|
351
|
+
and an entry saved under a tag cannot be restored on `main` in any case. See
|
|
352
|
+
[ADR-0007](docs/adr/0007-mutation-testing.md).
|
|
250
353
|
|
|
251
354
|
**The rebuild took 168 of its 180 minutes.** 13% more mutants than 0.4.0, and 63%
|
|
252
355
|
longer on the hosted runner against 15% longer locally. At 0.4.0's old cap of
|
package/README.md
CHANGED
|
@@ -391,7 +391,11 @@ predicate is linear in the subject whatever the pattern — `^([A-Za-z0-9_]+[ ]?
|
|
|
391
391
|
against a fifty-four-character title takes `RegExp` 103 seconds and this 13
|
|
392
392
|
microseconds. It reads the usual syntax minus backreferences and lookaround,
|
|
393
393
|
which are not regular; both are refused when the selector is read, with the
|
|
394
|
-
character pointed at.
|
|
394
|
+
character pointed at. Globs - the patterns to check, `--ignore`, `--ignore-ref`,
|
|
395
|
+
`--history` - are matched by the same automaton, because a glob with three stars
|
|
396
|
+
in it is
|
|
397
|
+
enough to keep `RegExp` busy for two minutes over a long reference target. See
|
|
398
|
+
[ADR-0017](docs/adr/0017-a-predicate-must-finish.md).
|
|
395
399
|
|
|
396
400
|
On a register, a region answers its file's front matter for every descriptive
|
|
397
401
|
key — `fm.owner` on a decision inside a register is the register's owner — while
|
|
@@ -479,6 +483,11 @@ Directives are ordinary HTML comments — invisible in every Markdown renderer:
|
|
|
479
483
|
A directive always wins, and the report says the state came from a directive, so
|
|
480
484
|
an override is visible rather than mysterious.
|
|
481
485
|
|
|
486
|
+
A `@spec-item` annotates one item: the one directly below it, with nothing but
|
|
487
|
+
blank lines or other comments between, or else the one it is written on or
|
|
488
|
+
indented under. An id that two items end up with is a parse problem, listed by
|
|
489
|
+
`--verbose`, and the first item keeps it.
|
|
490
|
+
|
|
482
491
|
## Journals, changelogs and minutes
|
|
483
492
|
|
|
484
493
|
A 2024 journal noting *"decision deferred to ADR-002"* is not delegating
|
|
@@ -675,6 +684,7 @@ spec-graph query project:<rule> Run a registered rule by its id.
|
|
|
675
684
|
spec-graph graph [patterns...] Export for Graphviz, Mermaid or JSON.
|
|
676
685
|
spec-graph rules [rule-id] List the diagnostics; --explain adds
|
|
677
686
|
each selector and the ADR behind it.
|
|
687
|
+
spec-graph diff <before> <after> Compare two JSON graph exports.
|
|
678
688
|
|
|
679
689
|
--root <dir> Directory patterns resolve against, and the root of the
|
|
680
690
|
run. Without it the config file is discovered upward and
|
|
@@ -688,7 +698,7 @@ spec-graph rules [rule-id] List the diagnostics; --explain adds
|
|
|
688
698
|
--record-baseline <f> Write today's findings as accepted debt, exit 0
|
|
689
699
|
--ratchet Also fail when a baseline entry no longer occurs
|
|
690
700
|
--no-config Ignore .spec-graph.json and the package.json key
|
|
691
|
-
--format <fmt> human | json | sarif | markdown
|
|
701
|
+
--format <fmt> human | json | sarif (check) | markdown (check, diff)
|
|
692
702
|
--graph-format <fmt> dot | mermaid | json
|
|
693
703
|
--documents-only Hide items; their relations lift onto their documents
|
|
694
704
|
--rule <id>=<severity> error | warn | info | off (repeatable)
|
|
@@ -748,6 +758,28 @@ every finding, so a bot can annotate a diff:
|
|
|
748
758
|
}
|
|
749
759
|
```
|
|
750
760
|
|
|
761
|
+
A check says what is wrong. `spec-graph diff` says what a pull request changed:
|
|
762
|
+
documents added, removed, moved or accepted, relations added or removed, and
|
|
763
|
+
obligations resolved or reopened. It compares two exports made by the same
|
|
764
|
+
binary, so CI makes one from the base branch and one from the pull request:
|
|
765
|
+
|
|
766
|
+
```yaml
|
|
767
|
+
- run: git fetch --depth 1 origin "$GITHUB_BASE_REF" && git worktree add ../base FETCH_HEAD
|
|
768
|
+
- run: npx spec-graph graph --root ../base --graph-format json > base.json
|
|
769
|
+
- run: npx spec-graph graph --graph-format json > head.json
|
|
770
|
+
- run: npx spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
It names only what it can tell apart. A document keeps its id through a rename,
|
|
774
|
+
so a moved document reads as moved. An obligation's id is its position in its
|
|
775
|
+
section, so inserting a question renumbers every question below it; an
|
|
776
|
+
obligation is therefore paired only by an id its author declared, or by its
|
|
777
|
+
document, section and title when nothing else shares them. Anything unpaired
|
|
778
|
+
is reported as having appeared or disappeared, never as resolved or reopened.
|
|
779
|
+
Findings are left to `--baseline`, which already reports what is new. The exit
|
|
780
|
+
code is `0` whether anything changed or not: a diff describes, and `check`
|
|
781
|
+
gates. See [ADR-0020](docs/adr/0020-a-diff-names-what-it-can-tell-apart.md).
|
|
782
|
+
|
|
751
783
|
## Visualising
|
|
752
784
|
|
|
753
785
|
```bash
|
|
@@ -783,7 +815,7 @@ exported. Reporters, editor extensions and custom rules are all first-class.
|
|
|
783
815
|
|
|
784
816
|
## Design
|
|
785
817
|
|
|
786
|
-
|
|
818
|
+
Twenty ADRs, which `spec-graph` validates on every CI run:
|
|
787
819
|
|
|
788
820
|
- [ADR-0001 — A hand-written Markdown scanner](docs/adr/0001-hand-written-markdown-scanner.md)
|
|
789
821
|
- [ADR-0002 — A four-phase lifecycle lattice](docs/adr/0002-lifecycle-lattice.md)
|
|
@@ -803,6 +835,8 @@ Eighteen ADRs, which `spec-graph` validates on every CI run:
|
|
|
803
835
|
- [ADR-0016 — A query needs a sentence before it is a rule](docs/adr/0016-a-query-needs-a-sentence.md)
|
|
804
836
|
- [ADR-0017 — A predicate must finish](docs/adr/0017-a-predicate-must-finish.md)
|
|
805
837
|
- [ADR-0018 — The configuration file is the root](docs/adr/0018-the-configuration-file-is-the-root.md)
|
|
838
|
+
- [ADR-0019 — The sweep runs in shards](docs/adr/0019-the-sweep-runs-in-shards.md)
|
|
839
|
+
- [ADR-0020 — A diff names only what it can tell apart](docs/adr/0020-a-diff-names-what-it-can-tell-apart.md)
|
|
806
840
|
|
|
807
841
|
**Zero runtime dependencies.** Node 22+, native ESM, TypeScript strict with
|
|
808
842
|
`noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`. The Markdown
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The command line.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Five verbs, because there are five questions worth asking of a specification
|
|
5
5
|
* graph: is it consistent (`check`), what does it contain (`query`), what does
|
|
6
|
-
* it look like (`graph`),
|
|
6
|
+
* it look like (`graph`), what will you check for me (`rules`), and what did a
|
|
7
|
+
* change do to it (`diff`).
|
|
7
8
|
*
|
|
8
9
|
* Exit codes are the contract with CI: `0` clean, `1` findings, `2` the tool
|
|
9
10
|
* itself could not run. A usage mistake never masquerades as a passing build.
|
|
@@ -16,7 +17,7 @@ export declare const EXIT_ERROR = 2;
|
|
|
16
17
|
export declare class UsageError extends Error {
|
|
17
18
|
constructor(message: string);
|
|
18
19
|
}
|
|
19
|
-
export type Command = 'check' | 'query' | 'graph' | 'rules';
|
|
20
|
+
export type Command = 'check' | 'query' | 'graph' | 'rules' | 'diff';
|
|
20
21
|
export interface CliOptions {
|
|
21
22
|
readonly command: Command;
|
|
22
23
|
readonly patterns: readonly string[];
|
|
@@ -76,7 +77,7 @@ export interface CliIO {
|
|
|
76
77
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
77
78
|
readonly isTTY?: boolean | undefined;
|
|
78
79
|
}
|
|
79
|
-
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\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\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. 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, sarif or markdown. sarif is the\n interchange format GitHub code scanning and editors\n already read; markdown is a table for a
|
|
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. 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, 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\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";
|
|
80
81
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
81
82
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
82
83
|
/** Runs the CLI and returns the process exit code. Never throws. */
|
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The command line.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Five verbs, because there are five questions worth asking of a specification
|
|
5
5
|
* graph: is it consistent (`check`), what does it contain (`query`), what does
|
|
6
|
-
* it look like (`graph`),
|
|
6
|
+
* it look like (`graph`), what will you check for me (`rules`), and what did a
|
|
7
|
+
* change do to it (`diff`).
|
|
7
8
|
*
|
|
8
9
|
* Exit codes are the contract with CI: `0` clean, `1` findings, `2` the tool
|
|
9
10
|
* itself could not run. A usage mistake never masquerades as a passing build.
|
|
10
11
|
*/
|
|
11
12
|
import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline, } from './baseline.js';
|
|
12
13
|
import { discoverConfig, loadConfig } from './config.js';
|
|
14
|
+
import { DiffInputError, diffExports, formatDiffJson, formatDiffMarkdown, formatDiffText, parseGraphExport, } from './diff.js';
|
|
13
15
|
import { isGlob, underRoot } from './glob.js';
|
|
14
16
|
import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
|
|
15
17
|
import { formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
@@ -34,6 +36,7 @@ USAGE
|
|
|
34
36
|
spec-graph query <selector|project:rule> [patterns...] [options]
|
|
35
37
|
spec-graph graph [patterns...] [--graph-format dot|mermaid|json]
|
|
36
38
|
spec-graph rules [rule-id] [--explain]
|
|
39
|
+
spec-graph diff <before.json> <after.json> [--format human|json|markdown]
|
|
37
40
|
|
|
38
41
|
COMMANDS
|
|
39
42
|
check Validate the specification graph. The default.
|
|
@@ -43,6 +46,10 @@ COMMANDS
|
|
|
43
46
|
rules List the diagnostics that will run, built in and project. Name one
|
|
44
47
|
to see only that one; --explain adds its selector and the ADR that
|
|
45
48
|
decided it.
|
|
49
|
+
diff Compare two graph exports: documents added, removed, moved or
|
|
50
|
+
accepted; relations added or removed; and obligations resolved or
|
|
51
|
+
reopened where they can be told apart. Make both exports with the
|
|
52
|
+
same spec-graph. Exits 0 whether anything changed or not.
|
|
46
53
|
|
|
47
54
|
OPTIONS
|
|
48
55
|
--root <dir> Directory the patterns resolve against. Without it,
|
|
@@ -73,8 +80,9 @@ OPTIONS
|
|
|
73
80
|
--no-config Ignore .spec-graph.json and the package.json key.
|
|
74
81
|
--format <fmt> human, json, sarif or markdown. sarif is the
|
|
75
82
|
interchange format GitHub code scanning and editors
|
|
76
|
-
already read; markdown is a table for a
|
|
77
|
-
comment or $GITHUB_STEP_SUMMARY
|
|
83
|
+
already read, for check; markdown is a table for a
|
|
84
|
+
pull-request comment or $GITHUB_STEP_SUMMARY, for check
|
|
85
|
+
or diff (default: human)
|
|
78
86
|
--graph-format <fmt> dot, mermaid or json (default: dot)
|
|
79
87
|
--documents-only Leave items out of the exported graph
|
|
80
88
|
--rule <id>=<severity> Override one rule: error, warn, info or off. A project
|
|
@@ -185,6 +193,7 @@ EXAMPLES
|
|
|
185
193
|
spec-graph check --rule project:no-draft-dependency=off
|
|
186
194
|
spec-graph query project:no-draft-dependency # what does that rule match?
|
|
187
195
|
spec-graph check --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
196
|
+
spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
188
197
|
`;
|
|
189
198
|
const SEVERITIES = ['error', 'warn', 'info', 'off'];
|
|
190
199
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
@@ -193,7 +202,7 @@ export function parseArgs(argv, cwd) {
|
|
|
193
202
|
let command = 'check';
|
|
194
203
|
if (args.length > 0 && !(args[0].startsWith('-'))) {
|
|
195
204
|
const first = args[0];
|
|
196
|
-
if (first === 'check' || first === 'query' || first === 'graph' || first === 'rules') {
|
|
205
|
+
if (first === 'check' || first === 'query' || first === 'graph' || first === 'rules' || first === 'diff') {
|
|
197
206
|
command = first;
|
|
198
207
|
args.shift();
|
|
199
208
|
}
|
|
@@ -366,9 +375,16 @@ export function parseArgs(argv, cwd) {
|
|
|
366
375
|
}
|
|
367
376
|
// SARIF is a report about findings, and only `check` produces those. Falling
|
|
368
377
|
// back to JSON would hand a pipeline something its uploader rejects with a
|
|
369
|
-
// message about a schema rather than about the command that was run.
|
|
370
|
-
|
|
371
|
-
|
|
378
|
+
// message about a schema rather than about the command that was run. Markdown
|
|
379
|
+
// is for a pull request, which reads a check or a diff.
|
|
380
|
+
if (format === 'sarif' && command !== 'check' && !help && !version) {
|
|
381
|
+
throw new UsageError(`--format sarif reports findings, so it belongs to check, not to ${command}`);
|
|
382
|
+
}
|
|
383
|
+
if (format === 'markdown' && command !== 'check' && command !== 'diff' && !help && !version) {
|
|
384
|
+
throw new UsageError(`--format markdown is a report for a pull request, so it belongs to check or diff, not to ${command}`);
|
|
385
|
+
}
|
|
386
|
+
if (command === 'diff' && patterns.length !== 2 && !help && !version) {
|
|
387
|
+
throw new UsageError('diff compares two graph exports, for example:\n spec-graph diff base.json head.json\n (make each with spec-graph graph --graph-format json)');
|
|
372
388
|
}
|
|
373
389
|
if (command === 'query' && selector === null && !help && !version) {
|
|
374
390
|
throw new UsageError('query needs a selector, for example:\n spec-graph query \'item[openness=open]\'');
|
|
@@ -459,6 +475,10 @@ export async function main(io = {}) {
|
|
|
459
475
|
out(`${await readVersion()}\n`);
|
|
460
476
|
return EXIT_OK;
|
|
461
477
|
}
|
|
478
|
+
// A diff reads two files and nothing else: no configuration and no corpus, so
|
|
479
|
+
// it can compare exports from a checkout it is not running in.
|
|
480
|
+
if (options.command === 'diff')
|
|
481
|
+
return runDiff(options.patterns, cwd, options.format, out, err);
|
|
462
482
|
const color = options.color ?? shouldUseColor({ isTTY: io.isTTY, env });
|
|
463
483
|
const ascii = options.ascii ?? shouldUseAscii({ env });
|
|
464
484
|
// Configuration is what is true of the repository; a flag is somebody
|
|
@@ -541,7 +561,10 @@ export async function main(io = {}) {
|
|
|
541
561
|
}
|
|
542
562
|
switch (options.command) {
|
|
543
563
|
case 'graph':
|
|
544
|
-
out(formatGraph(result.graph, options.graphFormat, {
|
|
564
|
+
out(formatGraph(result.graph, options.graphFormat, {
|
|
565
|
+
documentsOnly: options.documentsOnly,
|
|
566
|
+
generator: { name: 'spec-graph', version: await readVersion() },
|
|
567
|
+
}));
|
|
545
568
|
return EXIT_OK;
|
|
546
569
|
case 'query': {
|
|
547
570
|
const selector = options.selector;
|
|
@@ -779,6 +802,41 @@ function renderQueryError(selector, error) {
|
|
|
779
802
|
const caret = `${' '.repeat(Math.max(0, error.offset))}^`;
|
|
780
803
|
return `spec-graph: ${error.message}\n ${selector}\n ${caret}\n`;
|
|
781
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* Compares two exports.
|
|
807
|
+
*
|
|
808
|
+
* Exit 0 whether anything changed or not: a diff describes, and `check` gates. A
|
|
809
|
+
* diff that failed on a removed relation would fail every legitimate
|
|
810
|
+
* supersession (ADR-0020).
|
|
811
|
+
*/
|
|
812
|
+
async function runDiff(paths, cwd, format, out, err) {
|
|
813
|
+
const { readFile } = await import('node:fs/promises');
|
|
814
|
+
const { resolve } = await import('node:path');
|
|
815
|
+
const sides = [];
|
|
816
|
+
for (const path of paths) {
|
|
817
|
+
let raw;
|
|
818
|
+
try {
|
|
819
|
+
raw = await readFile(resolve(cwd, path), 'utf8');
|
|
820
|
+
}
|
|
821
|
+
catch (error) {
|
|
822
|
+
err(`spec-graph: cannot read ${path}: ${error.message}\n`);
|
|
823
|
+
return EXIT_ERROR;
|
|
824
|
+
}
|
|
825
|
+
try {
|
|
826
|
+
sides.push(parseGraphExport(raw, path));
|
|
827
|
+
}
|
|
828
|
+
catch (error) {
|
|
829
|
+
if (!(error instanceof DiffInputError))
|
|
830
|
+
throw error;
|
|
831
|
+
err(`spec-graph: ${error.message}\n`);
|
|
832
|
+
return EXIT_ERROR;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const [before, after] = sides;
|
|
836
|
+
const diff = diffExports(before, after);
|
|
837
|
+
out(format === 'json' ? formatDiffJson(diff) : format === 'markdown' ? formatDiffMarkdown(diff) : formatDiffText(diff));
|
|
838
|
+
return EXIT_OK;
|
|
839
|
+
}
|
|
782
840
|
async function readVersion() {
|
|
783
841
|
try {
|
|
784
842
|
const { readFile } = await import('node:fs/promises');
|