@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.
- package/CHANGELOG.md +132 -8
- package/README.md +191 -9
- package/dist/cli.d.ts +17 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +127 -7
- package/dist/cli.js.map +1 -1
- package/dist/glob.d.ts +25 -2
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +26 -9
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +117 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +545 -0
- package/dist/mcp.js.map +1 -0
- package/dist/parser.d.ts +25 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +71 -15
- package/dist/parser.js.map +1 -1
- package/dist/query.d.ts +108 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +211 -0
- package/dist/query.js.map +1 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +13 -4
- package/dist/reporter.js.map +1 -1
- package/dist/rules.d.ts +119 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +197 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +19 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +289 -97
- package/dist/runner.js.map +1 -1
- package/dist/specs.d.ts +44 -0
- package/dist/specs.d.ts.map +1 -0
- package/dist/specs.js +62 -0
- package/dist/specs.js.map +1 -0
- package/dist/structure.d.ts +104 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +253 -0
- package/dist/structure.js.map +1 -0
- package/dist/types.d.ts +33 -3
- package/dist/types.d.ts.map +1 -1
- 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
|
-
##
|
|
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
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
the
|
|
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` |
|
|
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` |
|
|
442
|
-
| `min` / `max` | count (`max`
|
|
443
|
-
| `glob` | absence, count | Include-only file filters, e.g. `*.ts,*.tsx` (ripgrep `-g` semantics) |
|
|
444
|
-
| `exclude` |
|
|
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` |
|
|
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
|
-
|
|
48
|
-
|
|
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;
|
|
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"}
|