@descent-vtt/spec-graph 0.15.0 → 0.17.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 +83 -0
- package/README.md +43 -7
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +103 -13
- package/dist/cli.js.map +1 -1
- package/dist/extract.js +9 -1
- package/dist/extract.js.map +1 -1
- package/dist/sections.js +8 -1
- package/dist/sections.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -1
- package/dist/vendor/spec-core/markdown/links.js.map +1 -1
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -1
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -1
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -1
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -1
- package/dist/vendor/spec-core/text/lines.js.map +1 -1
- package/dist/vendor/spec-core/text/width.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,89 @@ the family's
|
|
|
11
11
|
[versioning policy](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md)
|
|
12
12
|
records.
|
|
13
13
|
|
|
14
|
+
## 0.17.0
|
|
15
|
+
|
|
16
|
+
Two things of which one went unread are refused, exit 2: an option that
|
|
17
|
+
takes one value given two, `--baseline` beside `--record-baseline`, and a
|
|
18
|
+
family given to both `--family` and `--ignore-family`. And a negation is
|
|
19
|
+
read whatever mark stands after it: `Blocked by none: [ADR-0002](0002.md)`
|
|
20
|
+
was a `blocked-by` edge, the opposite of its sentence.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **An option that takes one value is refused given two.** The later was
|
|
25
|
+
read and the earlier went unread, with no word of it: `check --root
|
|
26
|
+
nowhere --root .` checked `.` and never said that `nowhere` is not there,
|
|
27
|
+
and `query --format sarif --format json` wrote JSON where the two the
|
|
28
|
+
other way round are refused. `--root`, `--baseline`, `--record-baseline`,
|
|
29
|
+
`--format`, `--graph-format`, `--max` and `--max-warnings` given two
|
|
30
|
+
values, and one rule given two severities by `--rule`, are exit `2` now,
|
|
31
|
+
with a line that names both: `--root is given "nowhere" and ".", and takes
|
|
32
|
+
one value: give one of the two`. The same value twice is that value once,
|
|
33
|
+
compared as typed, an option that is repeatable takes each of its values
|
|
34
|
+
as before, and `--help` and `--version` are answered first
|
|
35
|
+
([ADR-0010](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0010-configuration-belongs-to-the-repository.md)).
|
|
36
|
+
Upgrading: where a script adds such an option to a command line that may
|
|
37
|
+
already hold it, keep the value the run should have and drop the other.
|
|
38
|
+
- **`--baseline` beside `--record-baseline` is refused.** A recording writes
|
|
39
|
+
every finding of the run and opens no baseline, so the file `--baseline`
|
|
40
|
+
named went unread: `check --baseline nowhere.json --record-baseline
|
|
41
|
+
out.json` recorded and exited `0`, where `--baseline nowhere.json` alone
|
|
42
|
+
is exit `2`. The two together are exit `2` now, in either order, and
|
|
43
|
+
nothing is written. A `"baseline"` in the configuration is recorded over
|
|
44
|
+
as before. Upgrading: to record, give `--record-baseline` alone; to check
|
|
45
|
+
against a baseline, `--baseline` alone.
|
|
46
|
+
- **A family given to both `--family` and `--ignore-family` is refused.**
|
|
47
|
+
The list that ignores decided, and the family's name on the other said
|
|
48
|
+
nothing: `check --family ADR --ignore-family ADR` passed over every
|
|
49
|
+
citation in prose and printed `the specification graph is consistent`,
|
|
50
|
+
exit `0`, where either option alone exits `1` on a citation nobody wrote.
|
|
51
|
+
It is exit `2` now, in either order and in any case of the name, `--family
|
|
52
|
+
ADR is given beside --ignore-family ADR, and a family's citations are read
|
|
53
|
+
or they are passed over: give one of the two`. One family asked for beside
|
|
54
|
+
another ignored is taken as before. Upgrading: take the family off the
|
|
55
|
+
option that does not say what the run should do with it.
|
|
56
|
+
- **A negation is read whatever mark stands after it.** A governing phrase
|
|
57
|
+
is cancelled by a negation between it and the reference, and the word was
|
|
58
|
+
read only where a space or the reference followed it: `Blocked by none:
|
|
59
|
+
[ADR-0002](0002.md)` was a `blocked-by` edge and `Deferred to nothing: see
|
|
60
|
+
[ADR-0002](0002.md)` a delegation, each the opposite of its sentence. The
|
|
61
|
+
word ends now at anything but a letter, a digit or a hyphen, a colon and a
|
|
62
|
+
dash among them, and those are citations and no more. A hyphen joins the
|
|
63
|
+
word to the next, so `blocked by the no-op in` is read as it was. A
|
|
64
|
+
`ghost-handover` or `stale-premise` such a sentence caused goes; an open
|
|
65
|
+
question in a retired document that was read as handed to a live one by
|
|
66
|
+
such a sentence is an `orphaned-obligation`, so a check that passed can
|
|
67
|
+
fail
|
|
68
|
+
([ADR-0006](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0006-false-positives-cost-more.md)).
|
|
69
|
+
Upgrading: run `check`; where it now reports an `orphaned-obligation`, the
|
|
70
|
+
question is owed by nobody, as its sentence says: close it, or hand it to
|
|
71
|
+
a live document in so many words.
|
|
72
|
+
|
|
73
|
+
## 0.16.0
|
|
74
|
+
|
|
75
|
+
`--color` beside `--no-color` is refused, exit 2, where the later of the
|
|
76
|
+
two decided and the earlier went unread, as `--ascii` beside `--no-ascii`
|
|
77
|
+
already was.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- **`--color` beside `--no-color` is refused.** The later of the two
|
|
82
|
+
decided and the earlier went unread, with no word of it: `check --color
|
|
83
|
+
--no-color` printed a report without colour and `check --no-color --color`
|
|
84
|
+
one with it. Given together, in either order and in every format of a
|
|
85
|
+
check, they are exit `2` now, `--color is given beside --no-color, and a
|
|
86
|
+
report is coloured or it is not: give one of the two`, as `--ascii` beside
|
|
87
|
+
`--no-ascii` is since 0.15.0. Those are the two pairs the command line
|
|
88
|
+
has, so every option given beside its own opposite is refused. Either
|
|
89
|
+
alone wins over `NO_COLOR`, `FORCE_COLOR` and the terminal as before, the
|
|
90
|
+
same option twice is that option once, and `--help` and `--version` are
|
|
91
|
+
answered first
|
|
92
|
+
([ADR-0010](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0010-configuration-belongs-to-the-repository.md)).
|
|
93
|
+
Upgrading: where a script adds `--no-color` or `--color` to a command line
|
|
94
|
+
that may already hold the other, keep the one the run should have and drop
|
|
95
|
+
the other.
|
|
96
|
+
|
|
14
97
|
## 0.15.0
|
|
15
98
|
|
|
16
99
|
A list of families that names one no document belongs to, and leaves out
|
package/README.md
CHANGED
|
@@ -461,7 +461,10 @@ Blocked by [ADR-0009](0009-compliance.md). → blocked-by
|
|
|
461
461
|
A phrase governs a reference when it stands no more than 40 characters before
|
|
462
462
|
it, in the same statement: a sentence, a paragraph, a list item or a table cell.
|
|
463
463
|
|
|
464
|
-
- A negation between the two - `owned by nobody` - cancels it
|
|
464
|
+
- A negation between the two - `owned by nobody` - cancels it, whatever mark
|
|
465
|
+
stands after the word: `blocked by none: [ADR-0002](0002.md)` is a citation
|
|
466
|
+
and no more. A hyphen joins the word to the next, which names a thing, so
|
|
467
|
+
`blocked by the no-op in` cancels nothing.
|
|
465
468
|
- A sentence written in Chinese ends at `。`, `?`, `!` or `;`, with a space
|
|
466
469
|
after it or none, so `Deferred to later。另見 ADR-0002` hands nothing to
|
|
467
470
|
ADR-0002. A stop only ends a statement sooner: it reads less, never more.
|
|
@@ -1094,7 +1097,7 @@ spec-graph diff <before> <after> Compare two JSON graph exports.
|
|
|
1094
1097
|
--strict Raise every warning to an error
|
|
1095
1098
|
--max <n> Show at most n findings
|
|
1096
1099
|
--max-warnings <n> Fail when warnings exceed n
|
|
1097
|
-
--color / --no-color Force colour
|
|
1100
|
+
--color / --no-color Force colour on or off; one of the two, not both
|
|
1098
1101
|
--ascii / --no-ascii ASCII glyphs or Unicode ones; SPEC_GRAPH_ASCII=1 asks for ASCII
|
|
1099
1102
|
--verbose Include parse problems
|
|
1100
1103
|
```
|
|
@@ -1127,7 +1130,25 @@ variable in `--family "$FAMILIES"` is the usual way to write one:
|
|
|
1127
1130
|
- `--format json` asked of `graph`, whose JSON is `--graph-format json`, or
|
|
1128
1131
|
of `rules`, which has one format;
|
|
1129
1132
|
- an option given to a command that does not read it, in the table below;
|
|
1130
|
-
- `--
|
|
1133
|
+
- an option beside its own opposite, `--color` beside `--no-color` or
|
|
1134
|
+
`--ascii` beside `--no-ascii`, in either order: the later would decide and
|
|
1135
|
+
the earlier go unread ([Colour and glyphs](#colour-and-glyphs));
|
|
1136
|
+
- an option that takes one value, given two: `--root`, `--baseline`,
|
|
1137
|
+
`--record-baseline`, `--format`, `--graph-format`, `--max` or
|
|
1138
|
+
`--max-warnings`, and one rule given two severities by `--rule`. The later
|
|
1139
|
+
would be read and the earlier go unread: `--root nowhere --root .` would
|
|
1140
|
+
check `.` and never say that `nowhere` is not there. The line names both,
|
|
1141
|
+
`--root is given "nowhere" and ".", and takes one value: give one of the
|
|
1142
|
+
two`. The same value twice is that value once, as typed, and an option
|
|
1143
|
+
that is repeatable above takes each of its values;
|
|
1144
|
+
- `--baseline` beside `--record-baseline`: a recording writes every finding
|
|
1145
|
+
of the run and opens no baseline, so the one to read would go unread. A
|
|
1146
|
+
`"baseline"` in the configuration is no option, and is recorded over as
|
|
1147
|
+
before;
|
|
1148
|
+
- one family given to both `--family` and `--ignore-family`, whose citations
|
|
1149
|
+
are passed over whatever asked for them: alone on the allowlist it would
|
|
1150
|
+
leave every citation as prose. One family asked for beside another ignored
|
|
1151
|
+
is taken, since both are read;
|
|
1131
1152
|
- a value that begins with `--`, which reads as the option after one whose
|
|
1132
1153
|
value was forgotten: `--ignore --strict` would ignore a directory called
|
|
1133
1154
|
`--strict`, and `--record-baseline --verbose` write a file of that name.
|
|
@@ -1180,10 +1201,25 @@ the report is coloured when stdout is a terminal. `CI` is not read.
|
|
|
1180
1201
|
|
|
1181
1202
|
`--ascii` draws the report in ASCII glyphs and `--no-ascii` in the Unicode
|
|
1182
1203
|
ones, `✖`, `⚠`, `↳` and the like, on any platform and whatever the
|
|
1183
|
-
environment says.
|
|
1184
|
-
would go unread. `--no-ascii` belongs to `check`, as `--color` does; a format
|
|
1204
|
+
environment says. `--no-ascii` belongs to `check`, as `--color` does; a format
|
|
1185
1205
|
of a check that draws no glyph takes it all the same.
|
|
1186
1206
|
|
|
1207
|
+
An option given beside its own opposite is refused, exit `2`, whichever of
|
|
1208
|
+
the two comes first and in every format of a check:
|
|
1209
|
+
|
|
1210
|
+
```text
|
|
1211
|
+
spec-graph: --color is given beside --no-color, and a report is coloured or it is not: give one of the two
|
|
1212
|
+
spec-graph: --ascii is given beside --no-ascii, and the glyphs are drawn one way: give one of the two
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
The later of the two would decide and the earlier go unread, and a script
|
|
1216
|
+
that adds `--no-color` to a line already holding `--color` would be told
|
|
1217
|
+
nothing of which it got. Those are the two pairs the command line has; no
|
|
1218
|
+
other option has an opposite to be given beside. Either of a pair alone wins
|
|
1219
|
+
over the environment, which is what it is for, and the same option given
|
|
1220
|
+
twice is that option once. `--help` and `--version` are answered whatever
|
|
1221
|
+
else is on the line.
|
|
1222
|
+
|
|
1187
1223
|
Without either, `SPEC_GRAPH_ASCII` set to anything but `0`, `false` or nothing
|
|
1188
1224
|
asks for ASCII; and without that, a Windows console that sets neither
|
|
1189
1225
|
`WT_SESSION` nor `TERM_PROGRAM`, or sets them to nothing, gets ASCII and every
|
|
@@ -1450,9 +1486,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
|
|
|
1450
1486
|
than installed. So the whole package is auditable in an afternoon — and nothing
|
|
1451
1487
|
in the pipeline can take longer than its input.
|
|
1452
1488
|
|
|
1453
|
-
**Verified, not just covered.** 2,
|
|
1489
|
+
**Verified, not just covered.** 2,260 tests; 99.4% statement and 99.8% line
|
|
1454
1490
|
coverage. Coverage says a line ran, so the suite is also held to a mutation
|
|
1455
|
-
score - 96.
|
|
1491
|
+
score - 96.60% over 9,004 mutants, measured by CI's full sweep, and a build
|
|
1456
1492
|
fails under 93 - because a vocabulary entry or a boundary
|
|
1457
1493
|
condition can be weakened by an ordinary-looking refactor without a single test
|
|
1458
1494
|
going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
|
package/dist/cli.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface CliIO {
|
|
|
78
78
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
79
79
|
readonly isTTY?: boolean | undefined;
|
|
80
80
|
}
|
|
81
|
-
export declare const HELP = "spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n spec-graph diff <before.json> <after.json> [--format human|json|markdown]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed. It has to be a\n directory that is there.\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable: one\n name each time, the prefix its identifiers are\n written with, ADR for ADR-0007. A document here has\n to belong to one of them, or the check stops: a list\n that names none allows nothing. It stops too where\n a name has no document and the list leaves out a\n family the documents do belong to, as RFCS for RFC\n does: one is most likely meant for the other.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable,\n one name each time.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n --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, github, gitlab or markdown. json\n is for check, query or diff. sarif is the interchange\n format GitHub code scanning and editors already read,\n github the workflow commands a GitHub Actions log\n turns into annotations, and gitlab the Code Quality\n report a GitLab merge request reads, all three for\n check; markdown is a table for a pull-request comment\n or $GITHUB_STEP_SUMMARY, for check or diff (default:\n human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. A project\n rule is named in full: project:no-draft-dependency.\n Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off\n --ascii / --no-ascii ASCII glyphs or Unicode ones. SPEC_GRAPH_ASCII asks\n for ASCII, unless it is empty, 0 or false. Either\n option wins over it, and over what the terminal is\n taken for. The two together are refused\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n --explain With rules: add each rule's selector and the ADR\n that decided it\n -h, --help Show this help\n -v, --version Show the version\n\n An option belongs to the commands that read it, and any other command\n refuses it by name:\n\n --root, --verbose check, query, graph, rules\n --ignore, --history check, query, graph\n --format check, query, diff\n --graph-format, --documents-only graph\n --explain rules\n every other option check\n\n --no-config, --no-color and --ascii are taken by every command: each is\n true already of a command with nothing to read it for. --color and\n --no-ascii are not: each would say that a command drew what only a check\n draws.\n\n A value that begins with -- reads as an option, so the option before it is\n refused as missing its value. A name that does begin so is written ./--name\n for a file, [-]-name in a pattern and \\--name for a reference target.\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\n An input that is set and names nothing is a 2, named on stderr, and never\n read as if it had not been given: an empty value, a family name no family\n has, a list of families no document here belongs to, a --root that is no\n directory, a second word after rules, an option of another command.\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true,\n \"maxRelated\": 3 }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it. maxRelated has no flag: it caps how many related locations one\n finding lists, 8 unless set, and 0 lists none.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile, a file that is there and\n cannot be read. A configuration that did not load checks a different\n repository than the one configured, and would report that one as consistent.\n --no-config runs on defaults instead.\n\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n spec-graph check --format github # annotations from an Actions log\n spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
81
|
+
export declare const HELP = "spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n spec-graph diff <before.json> <after.json> [--format human|json|markdown]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed. It has to be a\n directory that is there.\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable: one\n name each time, the prefix its identifiers are\n written with, ADR for ADR-0007. A document here has\n to belong to one of them, or the check stops: a list\n that names none allows nothing. It stops too where\n a name has no document and the list leaves out a\n family the documents do belong to, as RFCS for RFC\n does: one is most likely meant for the other.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable,\n one name each time.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n --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, github, gitlab or markdown. json\n is for check, query or diff. sarif is the interchange\n format GitHub code scanning and editors already read,\n github the workflow commands a GitHub Actions log\n turns into annotations, and gitlab the Code Quality\n report a GitLab merge request reads, all three for\n check; markdown is a table for a pull-request comment\n or $GITHUB_STEP_SUMMARY, for check or diff (default:\n human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. A project\n rule is named in full: project:no-draft-dependency.\n Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off, whatever NO_COLOR, FORCE_COLOR\n and the terminal say. The two together are refused\n --ascii / --no-ascii ASCII glyphs or Unicode ones. SPEC_GRAPH_ASCII asks\n for ASCII, unless it is empty, 0 or false. Either\n option wins over it, and over what the terminal is\n taken for. The two together are refused\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n --explain With rules: add each rule's selector and the ADR\n that decided it\n -h, --help Show this help\n -v, --version Show the version\n\n An option belongs to the commands that read it, and any other command\n refuses it by name:\n\n --root, --verbose check, query, graph, rules\n --ignore, --history check, query, graph\n --format check, query, diff\n --graph-format, --documents-only graph\n --explain rules\n every other option check\n\n --no-config, --no-color and --ascii are taken by every command: each is\n true already of a command with nothing to read it for. --color and\n --no-ascii are not: each would say that a command drew what only a check\n draws.\n\n An option beside its own opposite is refused, whichever comes first:\n --color beside --no-color, and --ascii beside --no-ascii. The later would\n decide and the earlier go unread. Either alone wins over the environment,\n and the same option twice is that option once.\n\n An option that takes one value is refused given two, as in --root a\n --root b: the later would be read and the earlier go unread. That is each\n option above with a value that is not marked Repeatable, and one rule\n given two severities by --rule. The same value twice is that value once.\n\n A baseline to read is refused beside one to record, --baseline beside\n --record-baseline: a recording writes every finding and opens no\n baseline. So is one family given to both --family and --ignore-family,\n whose citations are passed over whatever asked for them. One family asked\n for beside another ignored is taken.\n\n A value that begins with -- reads as an option, so the option before it is\n refused as missing its value. A name that does begin so is written ./--name\n for a file, [-]-name in a pattern and \\--name for a reference target.\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\n An input that is set and names nothing is a 2, named on stderr, and never\n read as if it had not been given: an empty value, a family name no family\n has, a list of families no document here belongs to, a --root that is no\n directory, a second word after rules, an option of another command. So\n are two things of which one would go unread: an option beside its own\n opposite, an option that takes one value given two, --baseline beside\n --record-baseline, a family both asked for and ignored.\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true,\n \"maxRelated\": 3 }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it. maxRelated has no flag: it caps how many related locations one\n finding lists, 8 unless set, and 0 lists none.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile, a file that is there and\n cannot be read. A configuration that did not load checks a different\n repository than the one configured, and would report that one as consistent.\n --no-config runs on defaults instead.\n\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n spec-graph check --format github # annotations from an Actions log\n spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
82
82
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
83
83
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
84
84
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -111,7 +111,8 @@ OPTIONS
|
|
|
111
111
|
--max-warnings <n> Fail when warnings exceed n (default: no limit)
|
|
112
112
|
--strict Raise every warning to an error. An explicit --rule
|
|
113
113
|
still wins, so --strict --rule x=warn exempts x.
|
|
114
|
-
--color / --no-color Force colour on or off
|
|
114
|
+
--color / --no-color Force colour on or off, whatever NO_COLOR, FORCE_COLOR
|
|
115
|
+
and the terminal say. The two together are refused
|
|
115
116
|
--ascii / --no-ascii ASCII glyphs or Unicode ones. SPEC_GRAPH_ASCII asks
|
|
116
117
|
for ASCII, unless it is empty, 0 or false. Either
|
|
117
118
|
option wins over it, and over what the terminal is
|
|
@@ -138,6 +139,22 @@ OPTIONS
|
|
|
138
139
|
--no-ascii are not: each would say that a command drew what only a check
|
|
139
140
|
draws.
|
|
140
141
|
|
|
142
|
+
An option beside its own opposite is refused, whichever comes first:
|
|
143
|
+
--color beside --no-color, and --ascii beside --no-ascii. The later would
|
|
144
|
+
decide and the earlier go unread. Either alone wins over the environment,
|
|
145
|
+
and the same option twice is that option once.
|
|
146
|
+
|
|
147
|
+
An option that takes one value is refused given two, as in --root a
|
|
148
|
+
--root b: the later would be read and the earlier go unread. That is each
|
|
149
|
+
option above with a value that is not marked Repeatable, and one rule
|
|
150
|
+
given two severities by --rule. The same value twice is that value once.
|
|
151
|
+
|
|
152
|
+
A baseline to read is refused beside one to record, --baseline beside
|
|
153
|
+
--record-baseline: a recording writes every finding and opens no
|
|
154
|
+
baseline. So is one family given to both --family and --ignore-family,
|
|
155
|
+
whose citations are passed over whatever asked for them. One family asked
|
|
156
|
+
for beside another ignored is taken.
|
|
157
|
+
|
|
141
158
|
A value that begins with -- reads as an option, so the option before it is
|
|
142
159
|
refused as missing its value. A name that does begin so is written ./--name
|
|
143
160
|
for a file, [-]-name in a pattern and \\--name for a reference target.
|
|
@@ -170,7 +187,10 @@ EXIT CODES
|
|
|
170
187
|
An input that is set and names nothing is a 2, named on stderr, and never
|
|
171
188
|
read as if it had not been given: an empty value, a family name no family
|
|
172
189
|
has, a list of families no document here belongs to, a --root that is no
|
|
173
|
-
directory, a second word after rules, an option of another command.
|
|
190
|
+
directory, a second word after rules, an option of another command. So
|
|
191
|
+
are two things of which one would go unread: an option beside its own
|
|
192
|
+
opposite, an option that takes one value given two, --baseline beside
|
|
193
|
+
--record-baseline, a family both asked for and ignored.
|
|
174
194
|
|
|
175
195
|
CONFIGURATION
|
|
176
196
|
Anything repeated on every run belongs in the repository rather than in the
|
|
@@ -310,6 +330,37 @@ function readersOf(option) {
|
|
|
310
330
|
}
|
|
311
331
|
return undefined;
|
|
312
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* The option beside which this one goes unread, or nothing for an option
|
|
335
|
+
* that has none.
|
|
336
|
+
*
|
|
337
|
+
* Two of the three are an option and the one that undoes it. Given both,
|
|
338
|
+
* the later decided and the earlier went unread: a script that adds
|
|
339
|
+
* `--no-color` to a line already holding `--color` was told nothing of which
|
|
340
|
+
* it got, and two things of which one would go unread are refused together
|
|
341
|
+
* (spec-core's ADR-0005, 2026-10-11). Either alone still wins over the
|
|
342
|
+
* environment, which is what it is for.
|
|
343
|
+
*
|
|
344
|
+
* The third is a baseline to read beside one to record. A recording writes
|
|
345
|
+
* every finding of the run and opens no baseline, so `--baseline nowhere.json
|
|
346
|
+
* --record-baseline out.json` recorded and exited 0, where the first option
|
|
347
|
+
* alone is refused for a file that is not there.
|
|
348
|
+
*
|
|
349
|
+
* Asked of the first of each pair alone, so each pair is found once, by the
|
|
350
|
+
* option that names it, and each name here decides a refusal. A function and
|
|
351
|
+
* not a table, for the reason `readersOf` is one.
|
|
352
|
+
*/
|
|
353
|
+
function unreadBeside(option) {
|
|
354
|
+
switch (option) {
|
|
355
|
+
case '--color':
|
|
356
|
+
return { option: '--no-color', decided: 'a report is coloured or it is not' };
|
|
357
|
+
case '--ascii':
|
|
358
|
+
return { option: '--no-ascii', decided: 'the glyphs are drawn one way' };
|
|
359
|
+
case '--baseline':
|
|
360
|
+
return { option: '--record-baseline', decided: 'a check holds its findings against a baseline or records them as one' };
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
313
364
|
/** `a`, `a or b`, `a, b or c`: one name or more in a sentence, with the word that joins the last. */
|
|
314
365
|
function listed(names, joint) {
|
|
315
366
|
return names.length < 2 ? names[0] : `${names.slice(0, -1).join(', ')} ${joint} ${names[names.length - 1]}`;
|
|
@@ -386,6 +437,25 @@ export function parseArgs(argv, cwd) {
|
|
|
386
437
|
throw new UsageError(`${flag} expects one family's name, such as ADR, got "${value}"`);
|
|
387
438
|
return value;
|
|
388
439
|
};
|
|
440
|
+
// What an option that takes one value was first given, and the line for
|
|
441
|
+
// the first such option given another. The later was read and the earlier
|
|
442
|
+
// went unread: `--root nowhere --root .` checked `.` and never said that
|
|
443
|
+
// `nowhere` is not there, and `query --format sarif --format json` wrote
|
|
444
|
+
// JSON where the two the other way round are refused. The same value
|
|
445
|
+
// again is that value once, as an option that takes none is. Values are
|
|
446
|
+
// compared as they were typed: a path spelled two ways is two values to
|
|
447
|
+
// whoever reads the line. The line is thrown after the loop, with what
|
|
448
|
+
// else help and the version are answered before.
|
|
449
|
+
const taken = new Map();
|
|
450
|
+
let second = null;
|
|
451
|
+
const once = (flag, value, key = flag, takes = 'takes one value') => {
|
|
452
|
+
const held = taken.get(key);
|
|
453
|
+
if (held === undefined)
|
|
454
|
+
taken.set(key, value);
|
|
455
|
+
else if (held !== value)
|
|
456
|
+
second ??= `${flag} is given "${held}" and "${value}", and ${takes}: give one of the two`;
|
|
457
|
+
return value;
|
|
458
|
+
};
|
|
389
459
|
for (let i = 0; i < args.length; i += 1) {
|
|
390
460
|
const arg = args[i];
|
|
391
461
|
if (arg === '--') {
|
|
@@ -437,7 +507,7 @@ export function parseArgs(argv, cwd) {
|
|
|
437
507
|
verbose = true;
|
|
438
508
|
break;
|
|
439
509
|
case '--root':
|
|
440
|
-
root = filled(arg, i, 'a directory');
|
|
510
|
+
root = once(arg, filled(arg, i, 'a directory'));
|
|
441
511
|
rootExplicit = true;
|
|
442
512
|
i += 1;
|
|
443
513
|
break;
|
|
@@ -462,11 +532,11 @@ export function parseArgs(argv, cwd) {
|
|
|
462
532
|
i += 1;
|
|
463
533
|
break;
|
|
464
534
|
case '--baseline':
|
|
465
|
-
baseline = filled(arg, i, 'a file');
|
|
535
|
+
baseline = once(arg, filled(arg, i, 'a file'));
|
|
466
536
|
i += 1;
|
|
467
537
|
break;
|
|
468
538
|
case '--record-baseline':
|
|
469
|
-
recordBaseline = filled(arg, i, 'a file');
|
|
539
|
+
recordBaseline = once(arg, filled(arg, i, 'a file'));
|
|
470
540
|
i += 1;
|
|
471
541
|
break;
|
|
472
542
|
case '--no-config':
|
|
@@ -483,6 +553,7 @@ export function parseArgs(argv, cwd) {
|
|
|
483
553
|
throw new UsageError(`--format must be human, json, sarif, markdown, gitlab or github, got "${value}"`);
|
|
484
554
|
}
|
|
485
555
|
format = value;
|
|
556
|
+
once(arg, value);
|
|
486
557
|
i += 1;
|
|
487
558
|
break;
|
|
488
559
|
}
|
|
@@ -492,6 +563,7 @@ export function parseArgs(argv, cwd) {
|
|
|
492
563
|
throw new UsageError(`--graph-format must be dot, mermaid or json, got "${value}"`);
|
|
493
564
|
}
|
|
494
565
|
graphFormat = value;
|
|
566
|
+
once(arg, value);
|
|
495
567
|
i += 1;
|
|
496
568
|
break;
|
|
497
569
|
}
|
|
@@ -515,16 +587,17 @@ export function parseArgs(argv, cwd) {
|
|
|
515
587
|
throw new UsageError(`unknown severity "${level}", expected one of: ${SEVERITIES.join(', ')}`);
|
|
516
588
|
}
|
|
517
589
|
severities[id] = level;
|
|
590
|
+
once(arg, value, `${arg} ${id}`, 'a rule has one severity');
|
|
518
591
|
i += 1;
|
|
519
592
|
break;
|
|
520
593
|
}
|
|
521
594
|
case '--max': {
|
|
522
|
-
max = parseCount(arg, next(arg, i));
|
|
595
|
+
max = parseCount(arg, once(arg, next(arg, i)));
|
|
523
596
|
i += 1;
|
|
524
597
|
break;
|
|
525
598
|
}
|
|
526
599
|
case '--max-warnings': {
|
|
527
|
-
maxWarnings = parseCount(arg, next(arg, i));
|
|
600
|
+
maxWarnings = parseCount(arg, once(arg, next(arg, i)));
|
|
528
601
|
i += 1;
|
|
529
602
|
break;
|
|
530
603
|
}
|
|
@@ -557,12 +630,29 @@ export function parseArgs(argv, cwd) {
|
|
|
557
630
|
throw new UsageError(`${option} belongs to ${listed(readers, 'or')}, not to ${command}`);
|
|
558
631
|
}
|
|
559
632
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
633
|
+
// After the loop above, so that an option of another command is named
|
|
634
|
+
// before the pair it is half of: `query --color --no-color` is told
|
|
635
|
+
// that `--color` is a check's, which is the line to act on first.
|
|
636
|
+
for (const option of given) {
|
|
637
|
+
const other = unreadBeside(option);
|
|
638
|
+
if (other !== undefined && given.has(other.option)) {
|
|
639
|
+
throw new UsageError(`${option} is given beside ${other.option}, and ${other.decided}: give one of the two`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (second !== null)
|
|
643
|
+
throw new UsageError(second);
|
|
644
|
+
// A family on both lists is passed over, since the list that ignores is
|
|
645
|
+
// asked first, and its name on the other says nothing: `--family ADR
|
|
646
|
+
// --family RFC --ignore-family RFC` is `--family ADR`. Where it is the
|
|
647
|
+
// only name there, every citation in prose is passed over, and the check
|
|
648
|
+
// that fails without the two reported a consistent graph. Two families,
|
|
649
|
+
// one asked for and another ignored, are both read, and are taken.
|
|
650
|
+
for (const asked of families) {
|
|
651
|
+
const ignored = ignoreFamilies.find((name) => name.trim().toUpperCase() === asked.trim().toUpperCase());
|
|
652
|
+
if (ignored !== undefined) {
|
|
653
|
+
throw new UsageError(`--family ${asked} is given beside --ignore-family ${ignored}, and a family's citations are read or they are passed over: give one of the two`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
566
656
|
}
|
|
567
657
|
// The second word given to `rules` was dropped, so `rules a b` listed `a`
|
|
568
658
|
// and said nothing of `b`.
|