@descent-vtt/spec-guard 0.6.0 → 0.8.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 (48) hide show
  1. package/CHANGELOG.md +132 -8
  2. package/README.md +191 -9
  3. package/dist/cli.d.ts +17 -2
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +127 -7
  6. package/dist/cli.js.map +1 -1
  7. package/dist/glob.d.ts +25 -2
  8. package/dist/glob.d.ts.map +1 -1
  9. package/dist/glob.js +26 -9
  10. package/dist/glob.js.map +1 -1
  11. package/dist/index.d.ts +17 -7
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +8 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp.d.ts +117 -0
  16. package/dist/mcp.d.ts.map +1 -0
  17. package/dist/mcp.js +545 -0
  18. package/dist/mcp.js.map +1 -0
  19. package/dist/parser.d.ts +25 -0
  20. package/dist/parser.d.ts.map +1 -1
  21. package/dist/parser.js +71 -15
  22. package/dist/parser.js.map +1 -1
  23. package/dist/query.d.ts +108 -0
  24. package/dist/query.d.ts.map +1 -0
  25. package/dist/query.js +211 -0
  26. package/dist/query.js.map +1 -0
  27. package/dist/reporter.d.ts.map +1 -1
  28. package/dist/reporter.js +13 -4
  29. package/dist/reporter.js.map +1 -1
  30. package/dist/rules.d.ts +119 -0
  31. package/dist/rules.d.ts.map +1 -0
  32. package/dist/rules.js +197 -0
  33. package/dist/rules.js.map +1 -0
  34. package/dist/runner.d.ts +19 -0
  35. package/dist/runner.d.ts.map +1 -1
  36. package/dist/runner.js +289 -97
  37. package/dist/runner.js.map +1 -1
  38. package/dist/specs.d.ts +44 -0
  39. package/dist/specs.d.ts.map +1 -0
  40. package/dist/specs.js +62 -0
  41. package/dist/specs.js.map +1 -0
  42. package/dist/structure.d.ts +104 -0
  43. package/dist/structure.d.ts.map +1 -0
  44. package/dist/structure.js +253 -0
  45. package/dist/structure.js.map +1 -0
  46. package/dist/types.d.ts +33 -3
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,7 +5,71 @@ All notable changes to this project are documented here. Versions follow
5
5
  may change in a minor release — each such change is listed under **Changed**
6
6
  with the flag that restores the previous behaviour.
7
7
 
8
- ## Unreleased
8
+ ## 0.8.0
9
+
10
+ An architecture document can now state conventions about names and layout: what
11
+ the files in a directory are called, what every package holds, and which files
12
+ come in pairs. [ADR-0013](docs/adr/0013-structure-assertions.md).
13
+
14
+ ### Added
15
+
16
+ - **`@assert-structure`**, with exactly one of three claims per directive:
17
+ - `pattern="*.entity.ts, index.ts"` - every file in scope is named by one of
18
+ the patterns;
19
+ - `required="package.json, README.md"` - every directory holds each entry.
20
+ The directories are the targets, or with `dirs="*"` their children and with
21
+ `dirs="**"` every directory below them, empty ones included. An entry may be
22
+ a path, may end in a glob, and must be a directory when it ends in `/`;
23
+ - `partner="[name].test.[ext], tests/[dir]/test_[name].py"` - every file has
24
+ one of its partners. Three placeholders and no other grammar: `[name]` and
25
+ `[ext]` split the file name at its last dot, and `[dir]` is the file's
26
+ directory below its target.
27
+
28
+ Names are compared exactly from directory listings, never by looking a path
29
+ up, so `Readme.md` does not satisfy `README.md` on Windows or macOS any more
30
+ than it does on Linux. A scope with no files or no directories fails, as does
31
+ a partner template naming the file itself and a `required` target that is a
32
+ file. When a file has no partner because it *is* one, the report says so and
33
+ suggests the `exclude`. Takes `glob`, `exclude`, `max`, `allow-empty`,
34
+ `baseline` and `ratchet`. Spec files are in scope, and symbolic links are not
35
+ followed. All structure rules in a run share one walk per target and read
36
+ each directory once.
37
+ - `spec-guard query` and the MCP server show what a structure rule asks of a
38
+ path: whether a file's name is allowed, the partners it needs, and - for a
39
+ file in a directory a `required` rule holds - that rule.
40
+ - The JSON report carries `claim` on structure results. A match with `line: 0`
41
+ is a path rather than a place in one, and SARIF annotates a misnamed or
42
+ partnerless file at its first line, and a directory missing an entry at the
43
+ directive, with the directory in the message.
44
+ - `checkStructure`, `createTreeIndex`, `expandPartner`, `partnerTemplateIssue`
45
+ and `requiredEntryIssue` are exported, with their types. `walkPaths` takes an
46
+ `onDirectory` callback.
47
+
48
+ ### Changed
49
+
50
+ - **`executeAssertion` needs a `tree`** in its options: `createTreeIndex(root)`,
51
+ the run's directory listings, beside the `imports` and `hasFiles` caches it
52
+ already takes. Only code calling `executeAssertion` directly is affected;
53
+ `runSpecGuard` and the command line create it themselves.
54
+ - The MCP server's instructions and the `get_architectural_rules` description
55
+ tell an agent that the rules for a path now include what it must be named and
56
+ the partner files it needs.
57
+ - **Mutation score 98.63%** over 6,939 mutants, up from 0.7.0's 98.47%, with 92
58
+ survivors - one fewer than before the 676 mutants this release adds.
59
+ `structure.ts` is at 100%, and every other module has exactly the survivors it
60
+ had, less one in `glob.ts` that the new tests turned into a genuine hang. A
61
+ local sweep had reported four survivors in the new listing cache as killed; CI
62
+ found them, and they are tested. See
63
+ [ADR-0003](docs/adr/0003-mutation-testing.md).
64
+
65
+ ## 0.7.0
66
+
67
+ Two things an architecture document could not do before. It can state the shape
68
+ of a codebase: which layers depend on which, and that nothing imports itself
69
+ round a loop. And it can be asked, before any code is written, which of its
70
+ rules govern the file about to change - from the command line, or by an AI agent
71
+ over the Model Context Protocol. Building the second found a false green older
72
+ than every release, and it is fixed below.
9
73
 
10
74
  ### Added
11
75
 
@@ -30,16 +94,76 @@ with the flag that restores the previous behaviour.
30
94
  and for why the other four languages are not in the graph.
31
95
  - `buildGraph`, `resolveReference`, `stronglyConnected`, `cyclicComponents`,
32
96
  `witness`, `checkLayers` and `referenceForms` are exported, with their types.
97
+ - **`spec-guard query <paths...>`** - the rules in force for a file or directory,
98
+ grouped by the document that states them, with each rule's reason, a path's
99
+ position in every layer order (`may import`, `must not import`), and any
100
+ baseline entry that exempts it. Answered from the specs alone, without reading
101
+ the codebase, so it works for a file that does not exist yet; `--json` for
102
+ scripts. Rules in documents that are not in force are counted and named, and
103
+ listed with `--ignore-status`. The arithmetic behind it is tested against the
104
+ files a real run searches, under both engines and on random trees; see
105
+ [ADR-0012](docs/adr/0012-query-and-mcp.md) for the three things it cannot see.
106
+ - **`spec-guard mcp`** - a Model Context Protocol server on stdio, with no
107
+ dependency on the MCP SDK. Tools `get_architectural_rules` (the query) and
108
+ `check_architecture` (a run, narrowed to the rules governing given paths, each
109
+ judged over its whole scope, with violations marked as in those paths or not);
110
+ resources `spec://rules` and `spec://doc/{+path}`. Serves clients that open
111
+ with `initialize` (2024-10-07 to 2025-11-25) and clients on 2026-07-28 that
112
+ version every request and probe with `server/discover`, classifying each
113
+ request the way the TypeScript SDK's own server does. Run end to end against
114
+ the official clients: `@modelcontextprotocol/sdk` 1.30.0, and
115
+ `@modelcontextprotocol/client` 2.0.0 pinned to 2026-07-28, probing, and on
116
+ the legacy handshake.
117
+ - **`--spec <pattern>`**, repeatable: where `query` and `mcp` take their specs,
118
+ and an alternative to positional patterns for a run.
119
+ - `select` on `runSpecGuard`, to execute only some of the resolved assertions.
120
+ `readSpecs`, `parseDocument`, `parseTitle`, `walkPaths`, `governs`,
121
+ `queryRules`, `createMcpHandler` and `serveStdio` are exported, with their types.
122
+
123
+ ### Fixed
124
+
125
+ - **A rule whose every `target` was missing searched the whole repository**
126
+ under `--allow-missing-targets`, because an empty target list means the root
127
+ to the engine. So `@assert-count target="src/auth" symbol="verifyToken"
128
+ min="1"` kept passing after `src/auth` was deleted, as long as a test still
129
+ named `verifyToken`; import, layer and cycle rules did the same. This has been
130
+ true since 0.1.0 - the 0.3.0 entry below that describes the old behaviour as
131
+ "search nothing, find nothing" was wrong about what the code did. Such a rule
132
+ now has an empty scope: it fails as one, and with `--allow-empty-scope` or
133
+ `allow-empty="true"` it finds nothing. **If you rely on
134
+ `--allow-missing-targets` for a rule whose only target is gone, add
135
+ `--allow-empty-scope`.**
136
+ - An import rule whose only target is missing, without `--allow-missing-targets`,
137
+ now says the target does not exist instead of that its scope is empty.
33
138
 
34
139
  ### Changed
35
140
 
36
- - **Mutation score 97.97%** over 5,078 mutants, up from 0.6.0's 97.77%. The two
37
- new modules are at 99.51% (`graph.ts`) and 96.43% (`layers.ts`), and each of
38
- their three survivors was shown equivalent by running the mutated build
39
- against the real one on thousands of random inputs. The first sweep of this
40
- work found 34 survivors that 37 hand-written negative controls had not; see
41
- [ADR-0003](docs/adr/0003-mutation-testing.md) for how they were replayed from
42
- the report and killed.
141
+ - **`query` and `mcp` are commands when they are the first argument.** A spec
142
+ file literally named `query` or `mcp`, passed as the first pattern, now starts
143
+ a command instead; write `./query`, or put it after `--`.
144
+ - **A rule whose every `target` is missing has an empty scope under
145
+ `--allow-missing-targets`**, where it used to search the whole repository -
146
+ see Fixed. Add `--allow-empty-scope`, or `allow-empty="true"` on the rule, for
147
+ it to pass. Searching the root in its place is not restorable: it was the bug.
148
+ - **Mutation score 98.47%** over 6,263 mutants, up from 0.6.0's 97.77%, with 93
149
+ survivors - four fewer than before the 1,185 mutants this release adds. The
150
+ layering and cycle modules are at 99.51% (`graph.ts`) and 96.43%
151
+ (`layers.ts`); their three survivors were shown equivalent by running the
152
+ mutated build against the real one on thousands of random inputs. The query
153
+ and the server (`query.ts`, `rules.ts`, `mcp.ts`, `specs.ts`) are all at 100%;
154
+ most of the survivors found on the way were code nothing could observe, and it
155
+ was deleted. See [ADR-0003](docs/adr/0003-mutation-testing.md) for both
156
+ accounts, and for why one sweep's count is good to about three.
157
+ - `version(manifest?)` takes the manifest to read, so its fallback for a broken
158
+ install - `0.0.0` rather than a crash - is tested.
159
+ - **Reading specs is several times faster**, which a query's budget made worth
160
+ measuring. `maskCode` builds ranges instead of blanking a character array
161
+ (7.8 ms to 1.1 ms over this repository's specs, identical output on 72 real
162
+ documents and 100,000 random inputs); spec globs no longer stat every file
163
+ they list (102 ms to 18 ms over 1,200 specs on Windows); and specs are read
164
+ 16 at a time. A query over this repository went from 24.6 ms to 5.1 ms. A run
165
+ spends most of its time searching code, so this repository's selfcheck gained
166
+ about 8%.
43
167
  - The SARIF fingerprint of an assertion that names neither a symbol nor a file
44
168
  now includes its description. Only the two new kinds are affected; every
45
169
  existing alert keeps its identity. Without it, two cycle rules on one target
package/README.md CHANGED
@@ -422,6 +422,99 @@ reading `tsconfig.json`, which is the resolver
422
422
  [ADR-0011](docs/adr/0011-layers-and-cycles.md) declines to become, for the
423
423
  reasons it gives.
424
424
 
425
+ ### `@assert-structure` - what files are called, and what comes with them
426
+
427
+ Architecture documents state conventions over sets of files rather than naming
428
+ files one by one. Each directive makes exactly one of three claims:
429
+
430
+ ```md
431
+ <!-- @assert-structure target="src/domain" exclude="*.test.ts" pattern="*.entity.ts, index.ts" -->
432
+ <!-- @assert-structure target="packages" dirs="*" required="package.json, README.md" -->
433
+ <!-- @assert-structure target="src/handlers" exclude="*.test.ts" partner="[name].test.ts" -->
434
+ ```
435
+
436
+ - **`pattern`** - every file in scope is named by one of the patterns. They are
437
+ globs, read like `glob=`: without a `/` against the file name, with one
438
+ against the path from the root.
439
+ - **`required`** - every directory holds every entry. Without `dirs` the
440
+ directories are the targets; `dirs="*"` means their immediate children and
441
+ `dirs="**"` every directory below them. An entry may be a path
442
+ (`src/index.ts`), may end in a glob (`*.csproj`), and must be a directory
443
+ when it ends in `/`.
444
+ - **`partner`** - every file has a partner, named by a template.
445
+
446
+ Each failure names the file or directory, since there is no line to point at:
447
+
448
+ ```text
449
+ ✖ docs/architecture.md:3 @assert-structure
450
+ files in src/domain must be named *.entity.ts or index.ts (excluding *.test.ts)
451
+ expected no misnamed files, found 1
452
+ src/domain/helpers.ts matches none of *.entity.ts, index.ts
453
+
454
+ ✖ docs/architecture.md:4 @assert-structure
455
+ directories matching * under packages must contain package.json, README.md
456
+ expected no directories missing an entry, found 2
457
+ packages/billing missing README.md
458
+ packages/web missing package.json, README.md
459
+
460
+ ✖ docs/architecture.md:5 @assert-structure
461
+ files in src/handlers must each have a partner [name].test.ts (excluding *.test.ts)
462
+ expected no files without a partner, found 1
463
+ src/handlers/refund.ts has no partner src/handlers/refund.test.ts
464
+ ```
465
+
466
+ A partner template has three placeholders and nothing else - no regular
467
+ expressions, no conditionals - so a reader can expand one by eye:
468
+
469
+ | Placeholder | For `src/api/user.handler.ts`, `target="src"` |
470
+ | --- | --- |
471
+ | `[name]` | `user.handler` - the file name up to its last dot |
472
+ | `[ext]` | `ts` - what follows that dot |
473
+ | `[dir]` | `api` - the file's directory below the target, empty at the top |
474
+
475
+ A template without a `/` names a file beside it. One with a `/` is a path from
476
+ the root, which is how a mirrored test tree is written:
477
+ `partner="tests/[dir]/test_[name].py"`. A comma-separated list gives
478
+ alternatives, any one of which will do.
479
+
480
+ The partners themselves are in scope unless `exclude` says otherwise. Forgetting
481
+ to exclude them is loud rather than silent, and says what probably happened:
482
+
483
+ ```text
484
+ src/handlers/order.test.ts has no partner src/handlers/order.test.test.ts (it is the partner of src/handlers/order.ts - exclude it?)
485
+ ```
486
+
487
+ Some things fail rather than pass, because each is a rule that would otherwise
488
+ check nothing:
489
+ - a scope holding no files, or selecting no directories (`allow-empty` as
490
+ everywhere else);
491
+ - a template that names the file itself (`partner="[name].[ext]"`);
492
+ - a `required` target that is a file.
493
+
494
+ **Names are compared exactly, on every platform.** Whether `README.md` exists is
495
+ decided by reading its directory, never by asking the filesystem for the path.
496
+ That lookup finds `Readme.md` on Windows and macOS and does not find it on Linux,
497
+ and the same rule would pass on a laptop and fail in CI.
498
+
499
+ A structure rule reads names, never contents. It walks with the run's usual
500
+ skips, follows no symbolic link, and counts the spec files - which every other
501
+ rule leaves out, and which a naming rule about `docs/adr` is about. `max`,
502
+ `baseline` and `ratchet` work as they do on `@assert-absence`, and a baseline
503
+ lists files or directories. `spec-guard query` shows what a rule asks of a file
504
+ that does not exist yet:
505
+
506
+ ```text
507
+ src/handlers/invoice.ts (does not exist yet)
508
+ 1 rule from 1 document
509
+
510
+ Architecture (docs/architecture.md)
511
+ :5 @assert-structure files in src/handlers must each have a partner [name].test.ts (excluding *.test.ts)
512
+ partner: src/handlers/invoice.test.ts
513
+ ```
514
+
515
+ [ADR-0013](docs/adr/0013-structure-assertions.md) has the design and what it
516
+ costs.
517
+
425
518
  ### `@assert-present` - this file exists
426
519
 
427
520
  ```md
@@ -435,23 +528,27 @@ Passes when every listed path exists relative to `--root`. Directories count.
435
528
 
436
529
  | Attribute | Applies to | Meaning |
437
530
  | --- | --- | --- |
438
- | `target` | absence, count | Comma-separated paths to search, relative to `--root`. Default `.` |
531
+ | `target` | all but present | Comma-separated paths, relative to `--root`. Default `.` |
439
532
  | `symbol` | absence, count | The literal string to search for (or a regex with `regex="true"`) |
440
533
  | `file` | present | Comma-separated paths that must exist |
441
- | `expected` | absence, count | Upper bound for absence; exact count for count |
442
- | `min` / `max` | count (`max` also on absence) | Inclusive bounds |
443
- | `glob` | absence, count | Include-only file filters, e.g. `*.ts,*.tsx` (ripgrep `-g` semantics) |
444
- | `exclude` | absence, count | Paths to leave out, gitignore-style: `src/config/**`, `tests`, `*.test.ts` |
534
+ | `expected` | all but present | Exact count for count; an upper bound everywhere else |
535
+ | `min` / `max` | count (`max` on all but present) | Inclusive bounds |
536
+ | `glob` | absence, count, structure | Include-only file filters, e.g. `*.ts,*.tsx` (ripgrep `-g` semantics) |
537
+ | `exclude` | all but present | Paths to leave out, gitignore-style: `src/config/**`, `tests`, `*.test.ts` |
445
538
  | `regex` | absence, count | Treat `symbol` as a regular expression |
446
539
  | `word` | absence, count | Require word boundaries, so `Primary` does not match `PrimaryButton` |
447
540
  | `ignore-case` | absence, count | Case-insensitive matching |
448
541
  | `comments` | absence, count | `ignore` (default) or `include` for matches inside comments |
449
542
  | `module` | import-absence, import-count | Which dependency, matched like `exclude` |
450
543
  | `order` | layers | The layers, from the one everything may depend on to the one that may depend on everything |
544
+ | `pattern` | structure | The names every file in scope must match one of |
545
+ | `required` | structure | The entries every chosen directory must hold; a trailing `/` means a directory |
546
+ | `dirs` | structure, with `required` | Which directories below each target: `*` for children, `**` for all |
547
+ | `partner` | structure | Partner templates, any one of which must exist: `[name].test.[ext]` |
451
548
  | `types` | import assertions, layers, cycles | `include` (default) or `ignore` for `import type` |
452
- | `allow-empty` | absence, count, import assertions, layers, cycles | Tolerate a scope that holds no files, or a layer that matches none. Off by default - see below |
453
- | `baseline` | absence, import-absence, layers | Known violations that do not count: `path` or `path:count` |
454
- | `ratchet` | absence, import-absence, layers | `two-sided` (default) or `one-way` - see below |
549
+ | `allow-empty` | all but present | Tolerate a scope that holds no files, a layer that matches none, or no chosen directories. Off by default - see below |
550
+ | `baseline` | absence, import-absence, layers, structure | Known violations that do not count: `path` or `path:count` |
551
+ | `ratchet` | absence, import-absence, layers, structure | `two-sided` (default) or `one-way` - see below |
455
552
  | `reason` | all | Human-readable justification, printed on failure |
456
553
 
457
554
  Unknown attributes are an error, not a shrug: `expct="1"` fails the run instead
@@ -575,15 +672,100 @@ pass if you accepted it today.
575
672
  [ADR-0010](docs/adr/0010-spec-status.md) has the full reasoning, including why
576
673
  there is no per-directive `if-status` attribute.
577
674
 
675
+ ## Asking before writing - `query` and the MCP server
676
+
677
+ A run tells you, after the fact, that code broke a rule. `spec-guard query`
678
+ tells you which rules a file is under before you touch it:
679
+
680
+ ```bash
681
+ spec-guard query src/domain/user.ts --spec "docs/**/*.md"
682
+ ```
683
+
684
+ ```text
685
+ src/domain/user.ts
686
+ 3 rules from 2 documents
687
+
688
+ ADR-0004: No legacy client (docs/adr/0004-legacy.md, accepted)
689
+ :12 @assert-absence "LegacyClient" must not appear in src
690
+
691
+ ADR-0011: Layering constraints (docs/adr/0011-layers.md, accepted)
692
+ :40 @assert-layers src must keep its layers in order, src/domain < src/application < src/infrastructure
693
+ layer: src/domain (1 of 3)
694
+ may import: src/domain
695
+ must not import: src/application, src/infrastructure
696
+ reason: the domain depends on nothing
697
+ :41 @assert-import-absence src/domain must not import "pg"
698
+
699
+ 1 more rule would govern this path if docs/adr/0014-clocks.md (proposed) were in force; --ignore-status lists it
700
+
701
+ 14 spec files read in 6.1ms
702
+ ```
703
+
704
+ It answers from the specs alone, without reading the codebase, so it works for
705
+ a file that does not exist yet. A directory lists every rule that could reach a
706
+ file under it. `--json` is the same answer for a script. Rules in documents that
707
+ are not in force are counted and named, never silently left out.
708
+
709
+ The arithmetic that decides whether a rule governs a path is tested against the
710
+ walk a real run makes, file for file, under both engines and on randomly
711
+ generated trees. [ADR-0012](docs/adr/0012-query-and-mcp.md) lists the three
712
+ things it cannot see: file content (binary, oversized, unreadable), symbolic
713
+ links, and letter case on case-insensitive filesystems.
714
+
715
+ ### The MCP server
716
+
717
+ `spec-guard mcp` serves the same answer to an AI agent over the
718
+ [Model Context Protocol](https://modelcontextprotocol.io) on stdio, with no
719
+ dependency on the MCP SDK. Register it with your client as a stdio server; the
720
+ usual `mcpServers` entry is:
721
+
722
+ ```json
723
+ {
724
+ "mcpServers": {
725
+ "spec-guard": {
726
+ "command": "npx",
727
+ "args": ["spec-guard", "mcp", "--spec", "docs/**/*.md"]
728
+ }
729
+ }
730
+ }
731
+ ```
732
+
733
+ The root defaults to the directory the client starts the server in; pass
734
+ `--root` if yours starts servers somewhere else. On Windows, `npx` is a `.cmd`
735
+ script that a client spawning without a shell cannot launch; point `command` at
736
+ `node` and the first argument at
737
+ `node_modules/@descent-vtt/spec-guard/bin/spec-guard.js` instead. Run flags such
738
+ as `--engine`, `--strict`, `--ignore-status` and `--allow-missing-targets` apply
739
+ to its checks.
740
+
741
+ | Tool | What it does |
742
+ | --- | --- |
743
+ | `get_architectural_rules(path, include_inactive?)` | The query above, as text and as structured content |
744
+ | `check_architecture(paths?)` | A run. Given paths, only the rules that govern them, each over its whole scope, with every violation marked as in those paths or not |
745
+
746
+ | Resource | What it holds |
747
+ | --- | --- |
748
+ | `spec://rules` | Every rule in force, as JSON |
749
+ | `spec://doc/{+path}` | Any spec document, in force or not |
750
+
751
+ Both protocol eras are served: clients that open with `initialize` (revisions
752
+ 2024-10-07 to 2025-11-25), and clients on 2026-07-28 that put the protocol
753
+ version on every request and probe with `server/discover`. Nothing is cached, so
754
+ an ADR edited mid-session is read as edited. ADR-0012 has the shapes, the
755
+ sources they follow, and what is deliberately not implemented.
756
+
578
757
  ## CLI
579
758
 
580
759
  ```bash
581
- spec-guard [patterns...] [options]
760
+ spec-guard [patterns...] [options] # execute the directives
761
+ spec-guard query <paths...> [options] # the rules in force for files or directories
762
+ spec-guard mcp [options] # serve the rules over MCP on stdio
582
763
  ```
583
764
 
584
765
  | Option | Description |
585
766
  | --- | --- |
586
767
  | `-r, --root <path>` | Codebase root that assertions resolve against (default: cwd) |
768
+ | `--spec <pattern>` | A spec glob or path, repeatable. `query` and `mcp` take their specs only from here |
587
769
  | `-v, --verbose` | Print passing assertions too |
588
770
  | `--fail-fast` | Stop at the first failing assertion |
589
771
  | `--json` | Machine-readable report on stdout (same as `--format json`) |
package/dist/cli.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * 1 - an assertion failed, or a directive was invalid
7
7
  * 2 - spec-guard could not run (bad usage, no spec files, missing engine)
8
8
  */
9
+ import type { Readable } from 'node:stream';
9
10
  import type { EnginePreference } from './engine.js';
10
11
  export declare const EXIT_OK = 0;
11
12
  export declare const EXIT_FAILED = 1;
@@ -16,11 +17,18 @@ export interface CliIO {
16
17
  env: NodeJS.ProcessEnv;
17
18
  cwd: string;
18
19
  isTTY: boolean;
20
+ /** Where `spec-guard mcp` reads its messages. Only that command reads input. */
21
+ stdin?: Readable;
19
22
  }
23
+ /** What the command line asked for: a run, a query, or a server. */
24
+ export type Command = 'check' | 'query' | 'mcp';
20
25
  /** How a finished run is written out. */
21
26
  export type OutputFormat = 'human' | 'json' | 'sarif';
22
27
  export interface CliOptions {
28
+ command: Command;
23
29
  patterns: string[];
30
+ /** The paths `spec-guard query` asks about. */
31
+ paths: string[];
24
32
  root: string;
25
33
  verbose: boolean;
26
34
  failFast: boolean;
@@ -44,8 +52,15 @@ export interface CliOptions {
44
52
  }
45
53
  export declare class UsageError extends Error {
46
54
  }
47
- export declare function version(): string;
48
- export declare const HELP = "spec-guard - Executable architecture assertions for Markdown specs & ADRs\n\nUsage\n spec-guard [patterns...] [options]\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.\n\nOptions\n -r, --root <path> Codebase root that assertions are resolved against (default: cwd)\n -v, --verbose Print passing assertions too\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 (sarif uploads to GitHub code scanning)\n --engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)\n --strict Treat analysis that could not be completed 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 --ignore-status Execute directives in draft, proposed and superseded documents too\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 (default: 8)\n --allow-empty Exit 0 when no spec files matched (about the run, not an assertion)\n --color/--no-color Force colour on or off (NO_COLOR is honoured)\n -h, --help Show this help\n --version Print the version\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\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 or superseded\n is reported and not executed; --ignore-status runs it anyway.\n\nExit codes\n 0 all assertions passed 1 an assertion failed 2 spec-guard could not run";
55
+ /**
56
+ * The installed version, or `0.0.0` when the manifest cannot say.
57
+ *
58
+ * The manifest is a parameter so the fallback can be tested: it is what a
59
+ * broken install reports instead of crashing, and a fallback nothing runs is a
60
+ * fallback nobody knows still works.
61
+ */
62
+ export declare function version(manifest?: string): string;
63
+ 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\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 and\n mcp take theirs from --spec.\n\nOptions\n -r, --root <path> Codebase root that assertions are resolved against (default: cwd)\n --spec <pattern> A spec glob or path; repeatable (default: \"docs/**/*.md\")\n -v, --verbose Print passing assertions too\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 (sarif uploads to GitHub code scanning)\n --engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)\n --strict Treat analysis that could not be completed 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 --ignore-status Execute directives in draft, proposed and superseded documents too\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 (default: 8)\n --allow-empty Exit 0 when no spec files matched (about the run, not an assertion)\n --color/--no-color Force colour on or off (NO_COLOR is honoured)\n -h, --help Show this help\n --version Print the version\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 or superseded\n is reported and not executed; --ignore-status runs it anyway.\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, and a check,\nas the tools get_architectural_rules and check_architecture.\n\nExit codes\n 0 all assertions passed 1 an assertion failed 2 spec-guard could not run";
49
64
  /** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
50
65
  export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
51
66
  /** Runs the CLI and resolves to the process exit code. */
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,yCAAyC;AACzC,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,4EAA4E;IAC5E,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,gBAAgB,CAAC;IACzB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,wBAAgB,OAAO,IAAI,MAAM,CAShC;AAED,eAAO,MAAM,IAAI,wlFA4C8D,CAAC;AAgChF,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAkI1E;AAYD,0DAA0D;AAC1D,wBAAsB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,EAAE,EAAE,GAAE,KAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4EpH"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAM5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,gFAAgF;IAChF,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED,oEAAoE;AACpE,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;AAEhD,yCAAyC;AACzC,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,4EAA4E;IAC5E,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,gBAAgB,CAAC;IACzB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,QAAQ,SAAoB,GAAG,MAAM,CAQ5D;AAED,eAAO,MAAM,IAAI,mvGAqD8D,CAAC;AA2DhF,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAqJ1E;AAoED,0DAA0D;AAC1D,wBAAsB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,EAAE,EAAE,GAAE,KAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8EpH"}