@descent-vtt/spec-guard 0.24.0 → 0.26.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +59 -22
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +82 -17
  6. package/dist/cli.js.map +1 -1
  7. package/dist/comments.d.ts.map +1 -1
  8. package/dist/comments.js +12 -0
  9. package/dist/comments.js.map +1 -1
  10. package/dist/engine.d.ts.map +1 -1
  11. package/dist/engine.js +2 -4
  12. package/dist/engine.js.map +1 -1
  13. package/dist/glob.d.ts.map +1 -1
  14. package/dist/glob.js +11 -0
  15. package/dist/glob.js.map +1 -1
  16. package/dist/polyglot.d.ts +12 -0
  17. package/dist/polyglot.d.ts.map +1 -1
  18. package/dist/polyglot.js +150 -36
  19. package/dist/polyglot.js.map +1 -1
  20. package/dist/reporter.d.ts +6 -1
  21. package/dist/reporter.d.ts.map +1 -1
  22. package/dist/reporter.js +30 -8
  23. package/dist/reporter.js.map +1 -1
  24. package/dist/runner.d.ts.map +1 -1
  25. package/dist/runner.js.map +1 -1
  26. package/dist/text.d.ts +19 -0
  27. package/dist/text.d.ts.map +1 -1
  28. package/dist/text.js +21 -0
  29. package/dist/text.js.map +1 -1
  30. package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -1
  31. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  32. package/dist/vendor/spec-core/markdown/headings.js.map +1 -1
  33. package/dist/vendor/spec-core/markdown/links.js.map +1 -1
  34. package/dist/vendor/spec-core/markdown/scan.js.map +1 -1
  35. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -1
  36. package/dist/vendor/spec-core/pattern/charset.js.map +1 -1
  37. package/dist/vendor/spec-core/pattern/regex.js.map +1 -1
  38. package/dist/vendor/spec-core/text/lines.js.map +1 -1
  39. package/dist/vendor/spec-core/text/width.js.map +1 -1
  40. package/dist/watch.d.ts.map +1 -1
  41. package/dist/watch.js +6 -3
  42. package/dist/watch.js.map +1 -1
  43. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,108 @@ what to do when upgrading; the family's policy, including how a flag or field
10
10
  is deprecated before it goes, is spec-core's
11
11
  [ADR-0009](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md).
12
12
 
13
+ ## 0.26.0
14
+
15
+ An option that takes one value is refused given two, exit 2, and so is
16
+ `--json` beside a `--format` that names another format, where the later
17
+ was read with no word of the earlier. And a Python import statement is
18
+ read as Python reads it: `from a import(b)` was read as no import, and an
19
+ import rule passed over it.
20
+
21
+ ### Changed
22
+
23
+ - **An option that takes one value is refused where it is given two, and
24
+ `--json` beside a `--format` that names another format.** `--json
25
+ --format sarif` wrote SARIF and `--format sarif --json` wrote JSON, with
26
+ no word of the one that lost; and so it was for `--format`, `--root`,
27
+ `--engine`, `--max-snippets`, `--concurrency` and `--depth` given twice
28
+ with two values, on every command that takes them. Each is now exit 2 and
29
+ one line that names both as they were written. The same value given twice
30
+ is read as once, however it is spelled, and so is `--json` beside
31
+ `--format json`; and `--spec` and `--exclude` are lists, as they were.
32
+ Such a line is refused with `--help` or `--version` on it too, as a line
33
+ with a pair has been since 0.25.0: `--json --format sarif --help` printed
34
+ the help and is exit 2
35
+ ([ADR-0014](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0014-configuration-and-watch.md)).
36
+ Upgrading: give the option once. A script that adds `--format` or `--root`
37
+ to a line it was handed, to override the one already there, takes that
38
+ one off the line instead.
39
+ - **A Python import statement is read as Python reads it.** `from a
40
+ import(b)`, with no blank before the parenthesis, was read as no import,
41
+ and so were `from a import*`, `from.a import b`, `from . a import b`, a
42
+ dotted name with blanks about its dots, a form feed between two words,
43
+ and a `from` statement whose module has a letter beyond ASCII: an import
44
+ rule passed over each. A statement is now read by Python's own grammar,
45
+ token by token, and reported on the line it stands on, which was another
46
+ for a statement after a `;` that followed one of several lines. Python
47
+ 3.15's `lazy import` and `lazy from ... import` are read as the imports
48
+ they are. And a statement Python refuses is read as nothing, where
49
+ `from a import b,`, `import (a)`, `import a b` and `import os, , sys`
50
+ were each read as an import. Of 322 spellings given to Python 3.11.9 and
51
+ to spec-guard, 0.25.0 read 59 otherwise than Python does and found an
52
+ import in 78 that Python refuses; over the 4,341 files of that Python's
53
+ own library both read all 32,777 imports alike
54
+ ([ADR-0008](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0008-polyglot-imports.md)).
55
+ Upgrading: an `@assert-import-absence`, `@assert-import-count`,
56
+ `@assert-layers` or `@assert-import-cycle` rule over Python that passed
57
+ may now fail for an import written one of those ways; mend the import it
58
+ names, or give the rule a `baseline`. A rule that failed only for a
59
+ statement Python refuses now passes over it. `impact` lists the
60
+ dependents those imports make, and `logicalLines`, from the package, also
61
+ says where each physical line of a statement begins, as `starts`.
62
+
63
+ ## 0.25.0
64
+
65
+ A Python file with CRLF line endings is read as it is read with LF: an
66
+ import rule over one passed in a Windows checkout where the same file
67
+ fails it anywhere else. An option given beside its own opposite is
68
+ refused, exit 2, where the last of the two won with no word of the other.
69
+ And a report asked for in ASCII is ASCII.
70
+
71
+ ### Changed
72
+
73
+ - **An option given beside its own opposite is refused, where the last of
74
+ the two won.** `--strict --no-strict` ran as `--no-strict` alone, and
75
+ `--no-color --color` in colour, with no word of the option that lost: so
76
+ it was for the colour pair and for each on/off option a configuration can
77
+ set, `--strict`, `--allow-missing-targets`, `--allow-empty-scope`,
78
+ `--default-skips`, `--ignore-status` and `--include-specs`, on every
79
+ command that takes both. Each pair is now exit 2 and one line that names
80
+ both, in either order, as `--ascii` beside `--no-ascii` has been since
81
+ 0.24.0; that pair's line now reads as the others do. The same option
82
+ given twice is read as once, an opposite alone still wins over a
83
+ configuration, and a command that takes only one of a pair refuses the
84
+ other as it did
85
+ ([ADR-0014](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0014-configuration-and-watch.md)).
86
+ Upgrading: give one of the two. A script that adds an opposite to a line
87
+ it was handed, to turn an option off, takes the option off the line
88
+ instead; a configuration is overridden by the opposite alone, as before.
89
+ - **A Python file with CRLF line endings is read as it is read with LF.**
90
+ A line ended at LF alone, so in a CRLF file every line kept its carriage
91
+ return: no `from x import y` was read from the file, and a backslash at
92
+ the end of a line continued nothing, so `import a, \` named a module `\`.
93
+ An import rule over such a file passed where the same file with LF fails
94
+ it, as in a Windows checkout with `core.autocrlf` on. A line now ends
95
+ where Python ends one, at LF, at CRLF and at a lone carriage return, and
96
+ each import is reported on the line it was on. Go, Rust, C#, JavaScript
97
+ and TypeScript were read right already
98
+ ([ADR-0008](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0008-polyglot-imports.md)).
99
+ Upgrading: an `@assert-import-absence`, `@assert-import-count`,
100
+ `@assert-layers` or `@assert-import-cycle` rule over Python that passed
101
+ in a CRLF checkout may now fail there, as it does in an LF one; mend the
102
+ import it names, or give the rule a `baseline`. `impact` lists the
103
+ dependents those imports make.
104
+
105
+ ### Fixed
106
+
107
+ - A report asked for in ASCII is ASCII. Under `--ascii`, `SPEC_GUARD_ASCII`
108
+ or a Windows console that names no terminal, the lines of a run, `prove`,
109
+ `cites` and a watch session still held `·` between their counts, and a
110
+ snippet whose line was cut still ended in `…`. They are ` - ` and `...`
111
+ now, and so in the text of the server's `check_architecture`, which asks
112
+ for its report in ASCII. Where the marks are drawn nothing changes, and
113
+ `--json` and `--format sarif` carry a cut snippet as they did.
114
+
13
115
  ## 0.24.0
14
116
 
15
117
  A `cites --strict` that fails for a file read in part, or for a family that
package/README.md CHANGED
@@ -231,7 +231,7 @@ These read the dependency rather than the text, in **five languages**:
231
231
  | Language | Extensions | Reads |
232
232
  | --- | --- | --- |
233
233
  | JavaScript / TypeScript | `.js .mjs .cjs .jsx .ts .mts .cts .tsx` | `import`, `export ... from`, `require()`, `import()` |
234
- | Python | `.py .pyi` | `import a.b`, `from .rel import x`, `importlib.import_module("x")` |
234
+ | Python | `.py .pyi` | `import a.b` and `from .rel import x` in every spelling Python's grammar takes, 3.15's `lazy import` among them, and `importlib.import_module("x")` |
235
235
  | Go | `.go` | `import "x"` and grouped `import ( ... )`, including aliases and `_` |
236
236
  | Rust | `.rs` | `use a::{b, c}` with nested groups, `pub use`, `extern crate` |
237
237
  | C# | `.cs .csx` | `using`, `using static`, `global using`, `using X = A.B`, `global::` and extern-alias qualifiers, and a using relative to its namespace |
@@ -1301,8 +1301,8 @@ spec-guard impact <paths...> # the files that depend on each path, and
1301
1301
  | `-v, --verbose` | Print passing assertions too |
1302
1302
  | `--watch` | Report, then report again as the tree changes, until Ctrl+C |
1303
1303
  | `--fail-fast` | Stop at the first failing assertion |
1304
- | `--json` | Machine-readable report on stdout (same as `--format json`), versioned by `formatVersion` |
1305
- | `--format <human\|json\|sarif\|github\|gitlab>` | Output format. `sarif` uploads to GitHub code scanning, `github` annotates a pull request from the job's log, `gitlab` is a GitLab Code Quality report |
1304
+ | `--json` | Machine-readable report on stdout (same as `--format json`), versioned by `formatVersion`. Beside a `--format` that names another format it is refused (below) |
1305
+ | `--format <human\|json\|sarif\|github\|gitlab>` | Output format. `sarif` uploads to GitHub code scanning, `github` annotates a pull request from the job's log, `gitlab` is a GitLab Code Quality report. Given twice with two formats it is refused, as every option that takes one value is (below) |
1306
1306
  | `--engine <auto\|rg\|js>` | Search engine (default `auto`: scanner for small trees, ripgrep for big ones). ripgrep is `rg` on `PATH`, or the program `SPEC_GUARD_RG` names; `rg` with no ripgrep that runs is exit 2 ([below](#ci-integration)) |
1307
1307
  | `--strict` / `--no-strict` | Treat analysis that could not be completed as a failure, and a run that executed no assertion: its specs matched and state no rule in force |
1308
1308
  | `--allow-missing-targets` / `--no-allow-missing-targets` | Warn instead of failing when a `target` path does not exist |
@@ -1317,28 +1317,31 @@ spec-guard impact <paths...> # the files that depend on each path, and
1317
1317
  | `--depth <n>` | `impact` only: follow dependents at most `n` imports away (default: all) |
1318
1318
  | `--allow-empty` | Let a run, `prove` or `impact` through when no spec file matched the patterns, or when one pattern matched none beside others that did, each of which is exit 2 without it; such a pattern is still named on stderr (about the run, not an assertion) |
1319
1319
  | `--color` / `--no-color` | Force colour on or off. Without either flag, `NO_COLOR` set to anything turns it off; otherwise `FORCE_COLOR` turns it on, and `FORCE_COLOR=0` or `FORCE_COLOR=false` off; otherwise `TERM=dumb` turns it off; otherwise a terminal has colour. `NO_COLOR` or `FORCE_COLOR` set to nothing is as unset |
1320
- | `--ascii` / `--no-ascii` | Draw a report's marks in ASCII, or as the marks, on any platform. Either wins over the environment, and the two together are refused, exit 2. Without either, `SPEC_GUARD_ASCII` set to anything but nothing, `0` or `false` asks for ASCII; otherwise Windows decides as below |
1320
+ | `--ascii` / `--no-ascii` | Write a report in ASCII, or with the marks, on any platform. Either wins over the environment. Without either, `SPEC_GUARD_ASCII` set to anything but nothing, `0` or `false` asks for ASCII; otherwise Windows decides as below |
1321
1321
 
1322
1322
  Patterns are expanded by spec-guard itself, so quoted globs behave identically
1323
1323
  on Windows, macOS and Linux. A directory expands to the Markdown files in it.
1324
- The second form of each on/off option exists to override a configuration.
1325
-
1326
- On Windows a report's marks are drawn in ASCII, `+`, `x`, `!`, `o` and `...`
1327
- for `✔`, `✖`, `⚠`, `○` and `…`, unless `WT_SESSION` or `TERM_PROGRAM` is set
1328
- to something: Windows Terminal sets the first, and VS Code's terminal and
1329
- mintty the second. `TERM` is not read for it, since Git for Windows and the
1330
- Cygwin and MSYS2 runtimes set it for whatever they start, in a console whose
1331
- fonts hold none of the first three marks. Anywhere else the marks are always
1332
- drawn.
1333
-
1334
- Either can be asked for outright. `--ascii` draws the ASCII marks and
1335
- `--no-ascii` the others, on any platform and whatever the environment says:
1336
- an ssh session into Windows names no terminal, and `--no-ascii` is how it
1337
- gets the marks its own terminal draws. `SPEC_GUARD_ASCII=1` asks for ASCII
1324
+ The second form of each on/off option exists to override a configuration;
1325
+ beside the first, on one command line, it is refused (below).
1326
+
1327
+ On Windows a report is written in ASCII unless `WT_SESSION` or `TERM_PROGRAM`
1328
+ is set to something: Windows Terminal sets the first, and VS Code's terminal
1329
+ and mintty the second. `TERM` is not read for it, since Git for Windows and
1330
+ the Cygwin and MSYS2 runtimes set it for whatever they start, in a console
1331
+ whose fonts hold none of the first three marks. Anywhere else the marks are
1332
+ always drawn. In ASCII a report holds no character of its own beyond it:
1333
+ `+`, `x`, `!` and `o` for `✔`, `✖`, `⚠` and `○`, `...` for the `…` of what is
1334
+ left out, a cut line among it, and `-` for the `·` between the counts of a
1335
+ line. A path, a line of source or a reason is written as the project wrote
1336
+ it.
1337
+
1338
+ Either can be asked for outright. `--ascii` writes a report in ASCII and
1339
+ `--no-ascii` with the marks, on any platform and whatever the environment
1340
+ says: an ssh session into Windows names no terminal, and `--no-ascii` is how
1341
+ it gets the marks its own terminal draws. `SPEC_GUARD_ASCII=1` asks for ASCII
1338
1342
  for every run that inherits it; set to nothing, `0` or `false`, in any case,
1339
1343
  it asks for nothing, and it never asks for the marks. An option wins over
1340
- the variable, and `--ascii` beside `--no-ascii` is exit 2, since one of the
1341
- two would go unread.
1344
+ the variable.
1342
1345
 
1343
1346
  An option that means nothing to a command is refused rather than ignored, so
1344
1347
  `spec-guard query --strict` does not look like a strict query. `query` does take
@@ -1356,6 +1359,40 @@ strict and `--allow-empty=false` let an empty run through. An option a
1356
1359
  configuration can set has an opposite to turn it off, `--no-strict`; any
1357
1360
  other is left out.
1358
1361
 
1362
+ So is an option given beside its own opposite, in either order: `--strict`
1363
+ beside `--no-strict`, `--color` beside `--no-color`, and every other pair of
1364
+ the table. The last of the two used to win, and nothing said the other had
1365
+ been given; a line that asks for both was not meant, and one of the two would
1366
+ go unread:
1367
+
1368
+ ```text
1369
+ spec-guard: Option --strict is given beside --no-strict, and one of the two would go unread. Give one of the two.
1370
+ ```
1371
+
1372
+ The same option given twice is read as once. An opposite alone still wins
1373
+ over a configuration, which is what it is for. And where a command takes
1374
+ only one of a pair, as `query` takes `--no-color` and not `--color`, the
1375
+ other is refused for that, as it was.
1376
+
1377
+ So is an option that takes one value and is given two: `--root`, `--format`,
1378
+ `--engine`, `--max-snippets`, `--concurrency` and `--depth`. The later of the
1379
+ two used to be read, and nothing said the earlier had been given. And so is
1380
+ `--json` beside a `--format` that names another format, since `--json` is
1381
+ `--format json`: `--json --format sarif` wrote SARIF, and
1382
+ `--format sarif --json` wrote JSON. Each is named as it was written, in the
1383
+ order the two were given:
1384
+
1385
+ ```text
1386
+ spec-guard: Option --json is given beside --format "sarif", and one of the two would go unread. Give one of the two.
1387
+ ```
1388
+
1389
+ The same value given twice is read as once, however it is spelled: `--root .`
1390
+ beside `--root ./`, `--format JSON` beside `--format json`, and `--json`
1391
+ beside `--format json`. `--spec` and `--exclude` are lists, and each use adds
1392
+ to its list; `--exclude=` adds nothing to it, and the configuration's list is
1393
+ replaced either way. Such a line is refused with `--help` or `--version` on
1394
+ it too, as a line with a pair is.
1395
+
1359
1396
  And so is a spec pattern that matches no file beside others that do. After
1360
1397
  `old-docs` was renamed, `spec-guard docs old-docs` ran the specs under `docs`
1361
1398
  and said nothing of the rest: every rule in the renamed directory had stopped
@@ -1941,8 +1978,8 @@ build fails if they stop being true. CI proves every one of those rules can
1941
1978
  fail, and `spec-guard cites` holds every ADR a comment in the code cites to
1942
1979
  existing and being in force.
1943
1980
 
1944
- Line coverage is 100%, CI's full mutation sweep kills 99.19% of 12,826 mutants
1945
- (0.24.0), and every survivor a release adds is replayed before it ships
1981
+ Line coverage is 100%, CI's full mutation sweep kills 99.26% of 12,993 mutants
1982
+ (0.26.0), and every survivor a release adds is replayed before it ships
1946
1983
  ([ADR-0003](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0003-mutation-testing.md));
1947
1984
  [CONTRIBUTING.md](https://github.com/DescentVTT/spec-guard/blob/main/CONTRIBUTING.md)
1948
1985
  has how to build, test and release, and
package/dist/cli.d.ts CHANGED
@@ -101,7 +101,7 @@ export declare class UsageError extends Error {
101
101
  * fallback nobody knows still works.
102
102
  */
103
103
  export declare function version(manifest?: string): string;
104
- export declare const HELP = "spec-guard - Executable architecture assertions for Markdown specs & ADRs\n\nUsage\n spec-guard [patterns...] [options] execute the directives in the specs\n spec-guard query <paths...> [options] list the rules in force for files or directories\n spec-guard mcp [options] serve the rules to an AI agent over MCP on stdio\n spec-guard prove [patterns...] [options]\n show each rule a violation of itself, in memory, and report any that pass\n spec-guard cites [paths...] [options] check that each spec a code comment cites exists and is in force\n spec-guard impact <paths...> [options] the files that depend on each path, and the rules in play there\n\nPatterns\n Globs or paths to the Markdown specs to execute. A directory expands to the\n Markdown files inside it. Defaults to \"docs/**/*.md\" when omitted. query,\n mcp, cites and impact take theirs from --spec.\n\nOptions\n -r, --root <path> Codebase root that assertions are resolved against, a directory (default: cwd)\n --spec <pattern> A spec glob or path; repeatable (default: \"docs/**/*.md\")\n -v, --verbose Print passing assertions too\n --watch Run again as the tree changes, until Ctrl+C (human output only)\n --fail-fast Stop at the first failing assertion\n --json Emit a machine-readable JSON report (same as --format json)\n --format <name> human | json | sarif | github | gitlab (sarif uploads to code scanning,\n github annotates a pull request from the log, gitlab is Code Quality)\n --engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)\n ripgrep is rg on PATH, or the program SPEC_GUARD_RG names; where that\n is no ripgrep that runs, rg is exit 2 and auto leaves it to the scanner\n --strict Treat analysis that could not be completed, and a run that\n executed no assertion, as a failure\n --allow-missing-targets\n Tolerate target paths that do not exist (they fail by default)\n --allow-empty-scope Tolerate assertions whose scope holds no files (they fail by default)\n --print-baseline Print the baseline=\"...\" that would exempt today's violations, and exit\n --no-default-skips Search .git, .hg, .svn and node_modules too\n --exclude <globs> Paths no assertion looks at, beside each directive's exclude; repeatable\n --ignore-status Execute directives in documents not in force too (see Directives)\n --include-specs Also count matches inside the spec files themselves\n --max-snippets <n> Failure snippets per assertion (default: 5)\n --concurrency <n> Assertions executed in parallel, 1 or more (default: 8)\n --depth <n> impact: follow dependents at most n imports away (default: all)\n --allow-empty Let a run, prove or impact through when no spec file matched, or when\n one pattern matched none beside others that did (not an assertion)\n --color/--no-color Force colour on or off (NO_COLOR, FORCE_COLOR and TERM=dumb are honoured)\n --ascii/--no-ascii Draw a report's marks as + x ! o, or as their Unicode forms, on any\n platform; the two together are refused. Without either,\n SPEC_GUARD_ASCII asks for ASCII unless it is empty, 0 or false\n -h, --help Show this help\n --version Print the version\n\n An option that is set and names nothing is refused, exit 2, never read as if\n it were not there: an empty --root or spec pattern, --exclude \",\" (--exclude=\n with nothing clears the list), --concurrency 0, a root that is no directory,\n and a value given to an option that takes none, as in --strict=false.\n\n A spec pattern that matches no file, beside others that do, is refused the\n same way and by name, since none of its rules was read: a run, prove, impact\n and query exit 2. --allow-empty lets the first three through and still names\n it; --watch and mcp carry on, and say which pattern it is; cites names it\n and goes on, and exits 1 for it under --strict (see cites, below).\n\nConfiguration\n package.json in the root can hold specs, exclude, engine, strict,\n allowMissingTargets, allowEmptyScope, ignoreStatus, includeSpecs, defaultSkips,\n maxSnippets and concurrency under \"specGuard\"; a root with no package.json can\n keep them in .spec-guard.json instead, but not in both. A flag wins over the\n file, every on/off option there also takes its opposite (--no-strict,\n --default-skips, ...), and --exclude= with nothing clears exclude.\n \"cites\": [{ \"id\": \"ADR-{n}\", \"files\": \"docs/adr/{n}-*.md\" }] names the\n documents code comments cite; without it, cites reads them off the titles of\n numbered specs.\n\nDirectives\n <!-- @assert-absence target=\"src/\" symbol=\"LegacyGateway\" exclude=\"src/legacy/**\" -->\n <!-- @assert-count target=\"src/\" symbol=\"SessionManager\" expected=\"1\" -->\n <!-- @assert-present file=\"SECURITY.md\" -->\n <!-- @assert-import-cycle target=\"src/\" types=\"ignore\" -->\n <!-- @assert-layers target=\"src/\" order=\"domain, application, infrastructure\" -->\n <!-- @assert-structure target=\"src/\" exclude=\"*.test.ts\" partner=\"[name].test.ts\" -->\n\n Matches inside comments do not count; add comments=\"include\" to count them.\n An assertion whose scope holds no files fails; add allow-empty=\"true\" to allow it.\n A document whose status is draft, proposed, rejected, deprecated, superseded\n or archived is reported and not executed; --ignore-status runs it anyway.\n The status is read in English, whatever language the document is written in:\n status: superseded.\n\nprove adds, changes or removes files in memory only - never on disk - to make\nthe violation each rule forbids, and runs the rule over that tree. A rule that\nstill passes survived, and exits 1; one no violation could be made for is\nunprovable, and exits 1 under --strict.\n\ncites reads comment text only - not strings, not Markdown - for ids such as\nADR-0007, ADR-7 and ADR-007, which are one document. A ghost citation names no\ndocument, and exits 1; a stale one names a document superseded, deprecated,\nrejected or archived, and exits 1 under --strict. A family whose files match no\ndocument exits 2. A spec pattern that matches no file beside others that do is\nnamed on stderr. Without \"cites\" in the configuration the families are read off\nthe specs, so one kept under that pattern was not read and no citation of it\nwas looked for: exit 1 under --strict. Families \"cites\" names lose nothing.\n\nimpact follows the import graph backwards: relative JavaScript, TypeScript and\nPython imports that name a file. Go, Rust, C# and absolute Python imports name\nmodules, and are counted rather than followed; an import that names no file is\nlisted, never guessed at.\n\nquery answers from the specs alone, without reading the codebase, so it works\nfor a file that does not exist yet. mcp offers the same answer, a check and\nimpact's, as the tools get_architectural_rules, check_architecture and\nget_dependents.\n\nExit codes\n 0 all assertions passed 1 an assertion failed, or none ran under --strict\n 2 spec-guard could not run 130 a --watch session was stopped";
104
+ export declare const HELP = "spec-guard - Executable architecture assertions for Markdown specs & ADRs\n\nUsage\n spec-guard [patterns...] [options] execute the directives in the specs\n spec-guard query <paths...> [options] list the rules in force for files or directories\n spec-guard mcp [options] serve the rules to an AI agent over MCP on stdio\n spec-guard prove [patterns...] [options]\n show each rule a violation of itself, in memory, and report any that pass\n spec-guard cites [paths...] [options] check that each spec a code comment cites exists and is in force\n spec-guard impact <paths...> [options] the files that depend on each path, and the rules in play there\n\nPatterns\n Globs or paths to the Markdown specs to execute. A directory expands to the\n Markdown files inside it. Defaults to \"docs/**/*.md\" when omitted. query,\n mcp, cites and impact take theirs from --spec.\n\nOptions\n -r, --root <path> Codebase root that assertions are resolved against, a directory (default: cwd)\n --spec <pattern> A spec glob or path; repeatable (default: \"docs/**/*.md\")\n -v, --verbose Print passing assertions too\n --watch Run again as the tree changes, until Ctrl+C (human output only)\n --fail-fast Stop at the first failing assertion\n --json Emit a machine-readable JSON report (same as --format json)\n --format <name> human | json | sarif | github | gitlab (sarif uploads to code scanning,\n github annotates a pull request from the log, gitlab is Code Quality)\n --engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)\n ripgrep is rg on PATH, or the program SPEC_GUARD_RG names; where that\n is no ripgrep that runs, rg is exit 2 and auto leaves it to the scanner\n --strict Treat analysis that could not be completed, and a run that\n executed no assertion, as a failure\n --allow-missing-targets\n Tolerate target paths that do not exist (they fail by default)\n --allow-empty-scope Tolerate assertions whose scope holds no files (they fail by default)\n --print-baseline Print the baseline=\"...\" that would exempt today's violations, and exit\n --no-default-skips Search .git, .hg, .svn and node_modules too\n --exclude <globs> Paths no assertion looks at, beside each directive's exclude; repeatable\n --ignore-status Execute directives in documents not in force too (see Directives)\n --include-specs Also count matches inside the spec files themselves\n --max-snippets <n> Failure snippets per assertion (default: 5)\n --concurrency <n> Assertions executed in parallel, 1 or more (default: 8)\n --depth <n> impact: follow dependents at most n imports away (default: all)\n --allow-empty Let a run, prove or impact through when no spec file matched, or when\n one pattern matched none beside others that did (not an assertion)\n --color/--no-color Force colour on or off (NO_COLOR, FORCE_COLOR and TERM=dumb are honoured)\n --ascii/--no-ascii Write a report in ASCII, its marks as + x ! o, or with their Unicode\n forms, on any platform. Without either, SPEC_GUARD_ASCII asks for\n ASCII unless it is empty, 0 or false\n -h, --help Show this help\n --version Print the version\n\n An option that is set and names nothing is refused, exit 2, never read as if\n it were not there: an empty --root or spec pattern, --exclude \",\" (--exclude=\n with nothing clears the list), --concurrency 0, a root that is no directory,\n and a value given to an option that takes none, as in --strict=false.\n\n An option given beside its own opposite is refused the same way, in either\n order, since one of the two would go unread: --strict beside --no-strict,\n --color beside --no-color, and each other pair. The same option given twice\n is read as once.\n\n So is an option that takes one value and is given two, since the first of\n them would go unread: --root a --root b, --format json --format sarif, and\n --json beside a --format that names another format. The same value given\n twice is read as once, as --json beside --format json is. --spec and\n --exclude are lists, and each use adds to its list. Such a line is refused\n with --help or --version on it too, as a line with a pair is.\n\n A spec pattern that matches no file, beside others that do, is refused the\n same way and by name, since none of its rules was read: a run, prove, impact\n and query exit 2. --allow-empty lets the first three through and still names\n it; --watch and mcp carry on, and say which pattern it is; cites names it\n and goes on, and exits 1 for it under --strict (see cites, below).\n\nConfiguration\n package.json in the root can hold specs, exclude, engine, strict,\n allowMissingTargets, allowEmptyScope, ignoreStatus, includeSpecs, defaultSkips,\n maxSnippets and concurrency under \"specGuard\"; a root with no package.json can\n keep them in .spec-guard.json instead, but not in both. A flag wins over the\n file, every on/off option there also takes its opposite (--no-strict,\n --default-skips, ...), and --exclude= with nothing clears exclude.\n \"cites\": [{ \"id\": \"ADR-{n}\", \"files\": \"docs/adr/{n}-*.md\" }] names the\n documents code comments cite; without it, cites reads them off the titles of\n numbered specs.\n\nDirectives\n <!-- @assert-absence target=\"src/\" symbol=\"LegacyGateway\" exclude=\"src/legacy/**\" -->\n <!-- @assert-count target=\"src/\" symbol=\"SessionManager\" expected=\"1\" -->\n <!-- @assert-present file=\"SECURITY.md\" -->\n <!-- @assert-import-cycle target=\"src/\" types=\"ignore\" -->\n <!-- @assert-layers target=\"src/\" order=\"domain, application, infrastructure\" -->\n <!-- @assert-structure target=\"src/\" exclude=\"*.test.ts\" partner=\"[name].test.ts\" -->\n\n Matches inside comments do not count; add comments=\"include\" to count them.\n An assertion whose scope holds no files fails; add allow-empty=\"true\" to allow it.\n A document whose status is draft, proposed, rejected, deprecated, superseded\n or archived is reported and not executed; --ignore-status runs it anyway.\n The status is read in English, whatever language the document is written in:\n status: superseded.\n\nprove adds, changes or removes files in memory only - never on disk - to make\nthe violation each rule forbids, and runs the rule over that tree. A rule that\nstill passes survived, and exits 1; one no violation could be made for is\nunprovable, and exits 1 under --strict.\n\ncites reads comment text only - not strings, not Markdown - for ids such as\nADR-0007, ADR-7 and ADR-007, which are one document. A ghost citation names no\ndocument, and exits 1; a stale one names a document superseded, deprecated,\nrejected or archived, and exits 1 under --strict. A family whose files match no\ndocument exits 2. A spec pattern that matches no file beside others that do is\nnamed on stderr. Without \"cites\" in the configuration the families are read off\nthe specs, so one kept under that pattern was not read and no citation of it\nwas looked for: exit 1 under --strict. Families \"cites\" names lose nothing.\n\nimpact follows the import graph backwards: relative JavaScript, TypeScript and\nPython imports that name a file. Go, Rust, C# and absolute Python imports name\nmodules, and are counted rather than followed; an import that names no file is\nlisted, never guessed at.\n\nquery answers from the specs alone, without reading the codebase, so it works\nfor a file that does not exist yet. mcp offers the same answer, a check and\nimpact's, as the tools get_architectural_rules, check_architecture and\nget_dependents.\n\nExit codes\n 0 all assertions passed 1 an assertion failed, or none ran under --strict\n 2 spec-guard could not run 130 a --watch session was stopped";
105
105
  /** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
106
106
  export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
107
107
  /**
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -109,9 +109,9 @@ Options
109
109
  --allow-empty Let a run, prove or impact through when no spec file matched, or when
110
110
  one pattern matched none beside others that did (not an assertion)
111
111
  --color/--no-color Force colour on or off (NO_COLOR, FORCE_COLOR and TERM=dumb are honoured)
112
- --ascii/--no-ascii Draw a report's marks as + x ! o, or as their Unicode forms, on any
113
- platform; the two together are refused. Without either,
114
- SPEC_GUARD_ASCII asks for ASCII unless it is empty, 0 or false
112
+ --ascii/--no-ascii Write a report in ASCII, its marks as + x ! o, or with their Unicode
113
+ forms, on any platform. Without either, SPEC_GUARD_ASCII asks for
114
+ ASCII unless it is empty, 0 or false
115
115
  -h, --help Show this help
116
116
  --version Print the version
117
117
 
@@ -120,6 +120,18 @@ Options
120
120
  with nothing clears the list), --concurrency 0, a root that is no directory,
121
121
  and a value given to an option that takes none, as in --strict=false.
122
122
 
123
+ An option given beside its own opposite is refused the same way, in either
124
+ order, since one of the two would go unread: --strict beside --no-strict,
125
+ --color beside --no-color, and each other pair. The same option given twice
126
+ is read as once.
127
+
128
+ So is an option that takes one value and is given two, since the first of
129
+ them would go unread: --root a --root b, --format json --format sarif, and
130
+ --json beside a --format that names another format. The same value given
131
+ twice is read as once, as --json beside --format json is. --spec and
132
+ --exclude are lists, and each use adds to its list. Such a line is refused
133
+ with --help or --version on it too, as a line with a pair is.
134
+
123
135
  A spec pattern that matches no file, beside others that do, is refused the
124
136
  same way and by name, since none of its rules was read: a run, prove, impact
125
137
  and query exit 2. --allow-empty lets the first three through and still names
@@ -294,7 +306,7 @@ const NOT_WITH_WATCH = [
294
306
  ['--engine', 'a session always scans in-process, where it can see what each rule reads'],
295
307
  ];
296
308
  /** The long name of an option, whichever way it was spelled. */
297
- const LONG_NAMES = { '-v': '--verbose' };
309
+ const LONG_NAMES = { '-v': '--verbose', '-r': '--root' };
298
310
  /** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
299
311
  export function parseArgs(argv, cwd) {
300
312
  const command = COMMANDS.has(argv[0]) ? argv[0] : 'check';
@@ -328,6 +340,14 @@ export function parseArgs(argv, cwd) {
328
340
  const set = options.fromCommandLine;
329
341
  /** Every option given, by its long name, for the refusals that depend on another option. */
330
342
  const given = new Set();
343
+ /**
344
+ * What each thing that is one value was first asked to be: as the option
345
+ * stood on the command line, for the refusal, and as it was read, since
346
+ * `--root .` and `--root ./` name one directory and ask for one thing.
347
+ */
348
+ const asked = new Map();
349
+ /** The first two options that ask for one thing as two values, as each was written. */
350
+ let unread;
331
351
  let onlyPositional = false;
332
352
  for (let index = command === 'check' ? 0 : 1; index < argv.length; index++) {
333
353
  const argument = argv[index];
@@ -352,10 +372,23 @@ export function parseArgs(argv, cwd) {
352
372
  index += 1;
353
373
  return requireValue(name, argv[index]);
354
374
  };
355
- if (command !== 'check' && NOT_FOR[command].has(LONG_NAMES[name] ?? name)) {
375
+ const long = LONG_NAMES[name] ?? name;
376
+ if (command !== 'check' && NOT_FOR[command].has(long)) {
356
377
  throw new UsageError(`Option ${name} does not apply to spec-guard ${command}.`);
357
378
  }
358
- given.add(LONG_NAMES[name] ?? name);
379
+ given.add(long);
380
+ // Called by an option that takes one value, with what it read: the same
381
+ // thing asked twice is asked once, and a second value is kept for the
382
+ // refusal below. An option is the thing it asks for, by its long name;
383
+ // `--json` asks for a format.
384
+ const asks = (read, value, thing = long) => {
385
+ const written = value === undefined ? name : `${name} ${JSON.stringify(value)}`;
386
+ const first = asked.get(thing);
387
+ if (first === undefined)
388
+ asked.set(thing, { written, read });
389
+ else if (first.read !== read)
390
+ unread ??= [first.written, written];
391
+ };
359
392
  switch (name) {
360
393
  case '-h':
361
394
  case '--help':
@@ -377,9 +410,11 @@ export function parseArgs(argv, cwd) {
377
410
  case '--json':
378
411
  options.json = true;
379
412
  options.format = 'json';
413
+ asks('json', undefined, '--format');
380
414
  break;
381
415
  case '--format': {
382
- const written = nextValue().toLowerCase();
416
+ const raw = nextValue();
417
+ const written = raw.toLowerCase();
383
418
  // Whether it is a format at all is decided just below, by the lists.
384
419
  const value = written;
385
420
  // mcp refused --format above, so every command that gets here writes one.
@@ -393,6 +428,7 @@ export function parseArgs(argv, cwd) {
393
428
  }
394
429
  options.format = value;
395
430
  options.json = value === 'json';
431
+ asks(value, raw);
396
432
  break;
397
433
  }
398
434
  case '--strict':
@@ -471,13 +507,15 @@ export function parseArgs(argv, cwd) {
471
507
  throw new UsageError(`Option ${name} expects a directory, got ${JSON.stringify(value)}. Name one, or leave the option out to run in the working directory.`);
472
508
  }
473
509
  options.root = path.resolve(cwd, value);
510
+ asks(options.root, value);
474
511
  break;
475
512
  }
476
513
  case '--spec':
477
514
  specs.push(nextValue());
478
515
  break;
479
516
  case '--engine': {
480
- const value = nextValue().toLowerCase();
517
+ const raw = nextValue();
518
+ const value = raw.toLowerCase();
481
519
  try {
482
520
  options.engine = engineNamed(value);
483
521
  }
@@ -485,29 +523,37 @@ export function parseArgs(argv, cwd) {
485
523
  throw new UsageError(error.message);
486
524
  }
487
525
  set.add('engine');
526
+ asks(options.engine, raw);
488
527
  break;
489
528
  }
490
- case '--max-snippets':
491
- options.maxSnippets = positiveInteger(name, nextValue());
529
+ case '--max-snippets': {
530
+ const value = nextValue();
531
+ options.maxSnippets = positiveInteger(name, value);
492
532
  set.add('maxSnippets');
533
+ asks(options.maxSnippets, value);
493
534
  break;
535
+ }
494
536
  case '--concurrency': {
495
537
  // Refused as the configuration's key refuses it. Made 1, it was a run
496
538
  // somebody had asked something else of.
497
- const concurrency = positiveInteger(name, nextValue());
539
+ const value = nextValue();
540
+ const concurrency = positiveInteger(name, value);
498
541
  if (concurrency === 0)
499
542
  throw new UsageError('Option --concurrency expects 1 or more: a concurrency of 0 would run no search.');
500
543
  options.concurrency = concurrency;
501
544
  set.add('concurrency');
545
+ asks(concurrency, value);
502
546
  break;
503
547
  }
504
548
  case '--depth': {
505
549
  if (command !== 'impact')
506
550
  throw new UsageError('Option --depth applies only to spec-guard impact.');
507
- const depth = positiveInteger(name, nextValue());
551
+ const value = nextValue();
552
+ const depth = positiveInteger(name, value);
508
553
  if (depth === 0)
509
554
  throw new UsageError('Option --depth expects 1 or more: a depth of 0 would follow no import.');
510
555
  options.depth = depth;
556
+ asks(depth, value);
511
557
  break;
512
558
  }
513
559
  default:
@@ -520,11 +566,30 @@ export function parseArgs(argv, cwd) {
520
566
  throw new UsageError(`Option ${name} takes no value, got "${inlineValue}". Give it alone or leave it out; an option a configuration can set has an opposite that turns it off, as --no-strict is to --strict.`);
521
567
  }
522
568
  }
523
- // The last of `--color` and `--no-color` wins, and says nothing of the
524
- // other. This pair is refused together: one of the two would go unread, and
525
- // a line that asks for both ways of drawing the marks was not meant.
526
- if (given.has('--ascii') && given.has('--no-ascii')) {
527
- throw new UsageError('Option --ascii is given beside --no-ascii, and the marks are drawn one way or the other. Give one of the two.');
569
+ // An option beside its own opposite, in either order: one of the two would
570
+ // go unread, and a line that asks for both was not meant. The last of them
571
+ // won, with no word of the other, for every pair but the one that draws the
572
+ // marks (ADR-0014, amended 2026-10-12). Named by the option that turns
573
+ // something on, whichever came first. The same option twice asks for one
574
+ // thing and is read as once, and an opposite alone is what overrides a
575
+ // configuration.
576
+ for (const off of given) {
577
+ // `--no-strict` is `--strict` turned off. An option that begins any other
578
+ // way reads back as itself, and is no pair with itself.
579
+ const on = off.replace('--no-', '--');
580
+ if (on !== off && given.has(on)) {
581
+ throw new UsageError(`Option ${on} is given beside ${off}, and one of the two would go unread. Give one of the two.`);
582
+ }
583
+ }
584
+ // Two values for a thing that is one: the later was read and the earlier
585
+ // went unread, with no word of it, for each option that takes one value and
586
+ // for `--json` beside a `--format` that names another format (ADR-0014,
587
+ // amended 2026-10-14). Named in the order they were given, each as it was
588
+ // written. Refused with `--help` or `--version` on the line too, as the
589
+ // pair above is: both are a wrong command line, and one tool does not
590
+ // answer the two first for some wrong lines and after others.
591
+ if (unread !== undefined) {
592
+ throw new UsageError(`Option ${unread[0]} is given beside ${unread[1]}, and one of the two would go unread. Give one of the two.`);
528
593
  }
529
594
  if (options.watch) {
530
595
  for (const [option, reason] of NOT_WITH_WATCH) {