@descent-vtt/spec-graph 0.15.0 → 0.16.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 CHANGED
@@ -11,6 +11,30 @@ 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.16.0
15
+
16
+ `--color` beside `--no-color` is refused, exit 2, where the later of the
17
+ two decided and the earlier went unread, as `--ascii` beside `--no-ascii`
18
+ already was.
19
+
20
+ ### Changed
21
+
22
+ - **`--color` beside `--no-color` is refused.** The later of the two
23
+ decided and the earlier went unread, with no word of it: `check --color
24
+ --no-color` printed a report without colour and `check --no-color --color`
25
+ one with it. Given together, in either order and in every format of a
26
+ check, they are exit `2` now, `--color is given beside --no-color, and a
27
+ report is coloured or it is not: give one of the two`, as `--ascii` beside
28
+ `--no-ascii` is since 0.15.0. Those are the two pairs the command line
29
+ has, so every option given beside its own opposite is refused. Either
30
+ alone wins over `NO_COLOR`, `FORCE_COLOR` and the terminal as before, the
31
+ same option twice is that option once, and `--help` and `--version` are
32
+ answered first
33
+ ([ADR-0010](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0010-configuration-belongs-to-the-repository.md)).
34
+ Upgrading: where a script adds `--no-color` or `--color` to a command line
35
+ that may already hold the other, keep the one the run should have and drop
36
+ the other.
37
+
14
38
  ## 0.15.0
15
39
 
16
40
  A list of families that names one no document belongs to, and leaves out
package/README.md CHANGED
@@ -1094,7 +1094,7 @@ spec-graph diff <before> <after> Compare two JSON graph exports.
1094
1094
  --strict Raise every warning to an error
1095
1095
  --max <n> Show at most n findings
1096
1096
  --max-warnings <n> Fail when warnings exceed n
1097
- --color / --no-color Force colour
1097
+ --color / --no-color Force colour on or off; one of the two, not both
1098
1098
  --ascii / --no-ascii ASCII glyphs or Unicode ones; SPEC_GRAPH_ASCII=1 asks for ASCII
1099
1099
  --verbose Include parse problems
1100
1100
  ```
@@ -1127,7 +1127,9 @@ variable in `--family "$FAMILIES"` is the usual way to write one:
1127
1127
  - `--format json` asked of `graph`, whose JSON is `--graph-format json`, or
1128
1128
  of `rules`, which has one format;
1129
1129
  - an option given to a command that does not read it, in the table below;
1130
- - `--ascii` beside `--no-ascii`, of which one would go unread;
1130
+ - an option beside its own opposite, `--color` beside `--no-color` or
1131
+ `--ascii` beside `--no-ascii`, in either order: the later would decide and
1132
+ the earlier go unread ([Colour and glyphs](#colour-and-glyphs));
1131
1133
  - a value that begins with `--`, which reads as the option after one whose
1132
1134
  value was forgotten: `--ignore --strict` would ignore a directory called
1133
1135
  `--strict`, and `--record-baseline --verbose` write a file of that name.
@@ -1180,10 +1182,25 @@ the report is coloured when stdout is a terminal. `CI` is not read.
1180
1182
 
1181
1183
  `--ascii` draws the report in ASCII glyphs and `--no-ascii` in the Unicode
1182
1184
  ones, `✖`, `⚠`, `↳` and the like, on any platform and whatever the
1183
- environment says. Given together they are refused, exit `2`: one of the two
1184
- would go unread. `--no-ascii` belongs to `check`, as `--color` does; a format
1185
+ environment says. `--no-ascii` belongs to `check`, as `--color` does; a format
1185
1186
  of a check that draws no glyph takes it all the same.
1186
1187
 
1188
+ An option given beside its own opposite is refused, exit `2`, whichever of
1189
+ the two comes first and in every format of a check:
1190
+
1191
+ ```text
1192
+ spec-graph: --color is given beside --no-color, and a report is coloured or it is not: give one of the two
1193
+ spec-graph: --ascii is given beside --no-ascii, and the glyphs are drawn one way: give one of the two
1194
+ ```
1195
+
1196
+ The later of the two would decide and the earlier go unread, and a script
1197
+ that adds `--no-color` to a line already holding `--color` would be told
1198
+ nothing of which it got. Those are the two pairs the command line has; no
1199
+ other option has an opposite to be given beside. Either of a pair alone wins
1200
+ over the environment, which is what it is for, and the same option given
1201
+ twice is that option once. `--help` and `--version` are answered whatever
1202
+ else is on the line.
1203
+
1187
1204
  Without either, `SPEC_GRAPH_ASCII` set to anything but `0`, `false` or nothing
1188
1205
  asks for ASCII; and without that, a Windows console that sets neither
1189
1206
  `WT_SESSION` nor `TERM_PROGRAM`, or sets them to nothing, gets ASCII and every
@@ -1450,9 +1467,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
1450
1467
  than installed. So the whole package is auditable in an afternoon — and nothing
1451
1468
  in the pipeline can take longer than its input.
1452
1469
 
1453
- **Verified, not just covered.** 2,231 tests; 99.4% statement and 99.8% line
1470
+ **Verified, not just covered.** 2,264 tests; 99.4% statement and 99.8% line
1454
1471
  coverage. Coverage says a line ran, so the suite is also held to a mutation
1455
- score - 96.53% over 8,895 mutants, measured by CI's full sweep, and a build
1472
+ score - 96.56% over 8,901 mutants, measured by CI's full sweep, and a build
1456
1473
  fails under 93 - because a vocabulary entry or a boundary
1457
1474
  condition can be weakened by an ordinary-looking refactor without a single test
1458
1475
  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 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 is\n an option beside its own opposite, of which one would go unread.\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,11 @@ 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
+
141
147
  A value that begins with -- reads as an option, so the option before it is
142
148
  refused as missing its value. A name that does begin so is written ./--name
143
149
  for a file, [-]-name in a pattern and \\--name for a reference target.
@@ -170,7 +176,8 @@ EXIT CODES
170
176
  An input that is set and names nothing is a 2, named on stderr, and never
171
177
  read as if it had not been given: an empty value, a family name no family
172
178
  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.
179
+ directory, a second word after rules, an option of another command. So is
180
+ an option beside its own opposite, of which one would go unread.
174
181
 
175
182
  CONFIGURATION
176
183
  Anything repeated on every run belongs in the repository rather than in the
@@ -310,6 +317,28 @@ function readersOf(option) {
310
317
  }
311
318
  return undefined;
312
319
  }
320
+ /**
321
+ * The option that undoes this one, or nothing for an option that has none.
322
+ *
323
+ * Given both, the later decided and the earlier went unread: a script that
324
+ * adds `--no-color` to a line already holding `--color` was told nothing of
325
+ * which it got, and two things of which one would go unread are refused
326
+ * together (spec-core's ADR-0005, 2026-10-11). Either alone still wins over
327
+ * the environment, which is what it is for.
328
+ *
329
+ * Asked of the affirmative alone, so each pair is found once, by the option
330
+ * that names it, and each name here decides a refusal. A function and not a
331
+ * table, for the reason `readersOf` is one.
332
+ */
333
+ function oppositeOf(option) {
334
+ switch (option) {
335
+ case '--color':
336
+ return { option: '--no-color', decided: 'a report is coloured or it is not' };
337
+ case '--ascii':
338
+ return { option: '--no-ascii', decided: 'the glyphs are drawn one way' };
339
+ }
340
+ return undefined;
341
+ }
313
342
  /** `a`, `a or b`, `a, b or c`: one name or more in a sentence, with the word that joins the last. */
314
343
  function listed(names, joint) {
315
344
  return names.length < 2 ? names[0] : `${names.slice(0, -1).join(', ')} ${joint} ${names[names.length - 1]}`;
@@ -557,12 +586,15 @@ export function parseArgs(argv, cwd) {
557
586
  throw new UsageError(`${option} belongs to ${listed(readers, 'or')}, not to ${command}`);
558
587
  }
559
588
  }
560
- }
561
- // The later of the two would decide and the earlier go unread: a script
562
- // that adds `--no-ascii` to a line already holding `--ascii` would be told
563
- // nothing of which it got.
564
- if (given.has('--ascii') && given.has('--no-ascii') && !help && !version) {
565
- throw new UsageError('--ascii is given beside --no-ascii, and the glyphs are drawn one way: give one of the two');
589
+ // After the loop above, so that an option of another command is named
590
+ // before the pair it is half of: `query --color --no-color` is told
591
+ // that `--color` is a check's, which is the line to act on first.
592
+ for (const option of given) {
593
+ const opposite = oppositeOf(option);
594
+ if (opposite !== undefined && given.has(opposite.option)) {
595
+ throw new UsageError(`${option} is given beside ${opposite.option}, and ${opposite.decided}: give one of the two`);
596
+ }
597
+ }
566
598
  }
567
599
  // The second word given to `rules` was dropped, so `rules a b` listed `a`
568
600
  // and said nothing of `b`.