@descent-vtt/spec-guard 0.11.0 → 0.12.1
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 +443 -31
- package/README.md +403 -41
- package/dist/cites.d.ts +192 -0
- package/dist/cites.d.ts.map +1 -0
- package/dist/cites.js +599 -0
- package/dist/cites.js.map +1 -0
- package/dist/cli.d.ts +14 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +315 -22
- package/dist/cli.js.map +1 -1
- package/dist/comments.d.ts +1 -1
- package/dist/comments.js +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -5
- package/dist/config.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +38 -14
- package/dist/engine.js.map +1 -1
- package/dist/glob.d.ts +128 -24
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +304 -61
- package/dist/glob.js.map +1 -1
- package/dist/impact.d.ts +179 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +360 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +15 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +22 -75
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +206 -370
- package/dist/mcp.js.map +1 -1
- package/dist/overlay.d.ts +39 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +133 -0
- package/dist/overlay.js.map +1 -0
- package/dist/parser.d.ts +41 -24
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +217 -160
- package/dist/parser.js.map +1 -1
- package/dist/prove.d.ts +46 -0
- package/dist/prove.d.ts.map +1 -0
- package/dist/prove.js +549 -0
- package/dist/prove.js.map +1 -0
- package/dist/query.d.ts +18 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +9 -3
- package/dist/query.js.map +1 -1
- package/dist/reporter.d.ts +139 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +593 -21
- package/dist/reporter.js.map +1 -1
- package/dist/rules.js +3 -3
- package/dist/rules.js.map +1 -1
- package/dist/runner.d.ts +31 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +85 -21
- package/dist/runner.js.map +1 -1
- package/dist/specs.d.ts +27 -1
- package/dist/specs.d.ts.map +1 -1
- package/dist/specs.js +45 -10
- package/dist/specs.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +7 -6
- package/dist/structure.js.map +1 -1
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/spec-core/jsonrpc/index.d.ts +3 -0
- package/dist/vendor/spec-core/jsonrpc/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +158 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +104 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +46 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +6 -0
- package/dist/vendor/spec-core/markdown/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +40 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +26 -0
- package/dist/vendor/spec-core/markdown/links.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/links.js +394 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +23 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +37 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.js +463 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +76 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +19 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +291 -0
- package/dist/vendor/spec-core/markdown/types.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +2 -0
- package/dist/vendor/spec-core/path/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +84 -0
- package/dist/vendor/spec-core/path/posix.d.ts.map +1 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +130 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +59 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +122 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +7 -0
- package/dist/vendor/spec-core/pattern/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +68 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +3 -0
- package/dist/vendor/spec-core/text/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +73 -0
- package/dist/vendor/spec-core/text/lines.d.ts.map +1 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/package.json +9 -2
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
package/README.md
CHANGED
|
@@ -158,8 +158,21 @@ can never exclude anything:
|
|
|
158
158
|
- **a drive path** such as `C:/repo/dist`.
|
|
159
159
|
- **`.` or `/`**, the root itself.
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
So is any pattern the glob engine cannot read - an unclosed `[` or `{`, an
|
|
162
|
+
extended glob such as `+(a|b)`, a range that runs backwards, `**` inside a
|
|
163
|
+
name - rather than being read as a literal that excludes nothing. The same goes
|
|
164
|
+
for `glob`, `module`, `order` and the structure rules' patterns, and for a spec
|
|
165
|
+
pattern.
|
|
166
|
+
|
|
167
|
+
`**` means any number of directories only as a whole segment. `docs/**.md` is
|
|
168
|
+
refused, with the two ways to say what was meant: `docs/**/*.md` for any depth,
|
|
169
|
+
`*.md` for one level. The tools it could have come from read it three ways,
|
|
170
|
+
and a spec pattern read the narrow way drops every nested document without a
|
|
171
|
+
word.
|
|
172
|
+
|
|
173
|
+
In a directive a refused pattern is an invalid directive, whether or not its
|
|
174
|
+
document is in force; in the configuration's `specs` or `exclude`, a spec
|
|
175
|
+
pattern on the command line or `--spec`, or `--exclude`, it is exit 2.
|
|
163
176
|
|
|
164
177
|
List attributes accept commas or whitespace, so both of these work:
|
|
165
178
|
|
|
@@ -169,7 +182,10 @@ List attributes accept commas or whitespace, so both of these work:
|
|
|
169
182
|
```
|
|
170
183
|
|
|
171
184
|
(A path containing a space therefore cannot be written; there is no quoting
|
|
172
|
-
inside an attribute value.
|
|
185
|
+
inside an attribute value. Nor can a brace group with a comma in it:
|
|
186
|
+
`glob="*.{ts,tsx}"` is the two patterns `*.{ts` and `tsx}`, and is refused with a
|
|
187
|
+
message that says so. Write `glob="*.ts, *.tsx"`. A configuration's `exclude` is
|
|
188
|
+
a JSON array, where a brace group keeps its commas.)
|
|
173
189
|
|
|
174
190
|
### `comments` - the note about a deletion is not the deletion
|
|
175
191
|
|
|
@@ -315,9 +331,9 @@ These read the dependency rather than the text, in **five languages**:
|
|
|
315
331
|
No parser and no new dependency. JavaScript gets a full tokenizer because a
|
|
316
332
|
module reference can appear anywhere in an expression; the other four are read
|
|
317
333
|
by masking comments and string literals with the classifier from
|
|
318
|
-
[ADR-0006](docs/adr/0006-comment-classification.md) and then reading statements
|
|
334
|
+
[ADR-0006](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0006-comment-classification.md) and then reading statements
|
|
319
335
|
off what is left. Tree-sitter would have cost 94 MB unpacked against this
|
|
320
|
-
package's 0.33 MB - [ADR-0008](docs/adr/0008-polyglot-imports.md) has the
|
|
336
|
+
package's 0.33 MB - [ADR-0008](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0008-polyglot-imports.md) has the
|
|
321
337
|
measurements, and the list of things a real parser would genuinely see that
|
|
322
338
|
this does not.
|
|
323
339
|
|
|
@@ -368,7 +384,7 @@ and reported too, so a rule pointed at the wrong tree says "analysed 2 of 3
|
|
|
368
384
|
files; 1 is in a language whose imports spec-guard cannot read (.razor)" rather than quietly passing - and if *none* of them can be read, the
|
|
369
385
|
assertion fails rather than passing on an empty analysis.
|
|
370
386
|
|
|
371
|
-
[ADR-0005](docs/adr/0005-import-assertions.md) has the measurements and the
|
|
387
|
+
[ADR-0005](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0005-import-assertions.md) has the measurements and the
|
|
372
388
|
reasoning behind each boundary.
|
|
373
389
|
|
|
374
390
|
### `@assert-layers` - dependencies point one way
|
|
@@ -530,7 +546,7 @@ did not - a relative path matching no file, or an alias like `@/db` or
|
|
|
530
546
|
`--strict`. An alias spelled like a real package, `@app/db` through `tsconfig`
|
|
531
547
|
paths, is the one thing it cannot see and cannot report: that would mean
|
|
532
548
|
reading `tsconfig.json`, which is the resolver
|
|
533
|
-
[ADR-0011](docs/adr/0011-layers-and-cycles.md) declines to become, for the
|
|
549
|
+
[ADR-0011](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0011-layers-and-cycles.md) declines to become, for the
|
|
534
550
|
reasons it gives.
|
|
535
551
|
|
|
536
552
|
### `@assert-structure` - what files are called, and what comes with them
|
|
@@ -623,7 +639,7 @@ src/handlers/invoice.ts (does not exist yet)
|
|
|
623
639
|
partner: src/handlers/invoice.test.ts
|
|
624
640
|
```
|
|
625
641
|
|
|
626
|
-
[ADR-0013](docs/adr/0013-structure-assertions.md) has the design and what it
|
|
642
|
+
[ADR-0013](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0013-structure-assertions.md) has the design and what it
|
|
627
643
|
costs.
|
|
628
644
|
|
|
629
645
|
### `@assert-present` - this file exists
|
|
@@ -725,7 +741,7 @@ spec-guard docs/adr.md --print-baseline
|
|
|
725
741
|
```
|
|
726
742
|
|
|
727
743
|
prints the attribute that would exempt exactly today's violations, for you to
|
|
728
|
-
paste in. It prints; it does not edit. [ADR-0009](docs/adr/0009-debt-baselines.md)
|
|
744
|
+
paste in. It prints; it does not edit. [ADR-0009](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0009-debt-baselines.md)
|
|
729
745
|
explains why that distinction is the whole design, and why there is no `--fix`
|
|
730
746
|
for architecture rules.
|
|
731
747
|
|
|
@@ -736,8 +752,11 @@ long before anyone deletes it - because deleting it deletes the reason a
|
|
|
736
752
|
decision was made.
|
|
737
753
|
|
|
738
754
|
spec-guard reads that status from the document and honours it. A document whose
|
|
739
|
-
status is `draft`, `proposed`, `rejected`, `deprecated
|
|
740
|
-
parsed, validated, reported - and not executed.
|
|
755
|
+
status is `draft`, `proposed`, `rejected`, `deprecated`, `superseded` or
|
|
756
|
+
`archived` is parsed, validated, reported - and not executed. `archived` is what
|
|
757
|
+
`spec-brief` writes when it closes a task brief's round: the brief is kept as
|
|
758
|
+
the record, and its directives, which stated the round's premises and goals,
|
|
759
|
+
are history.
|
|
741
760
|
|
|
742
761
|
Three spellings are recognised, because three are in use:
|
|
743
762
|
|
|
@@ -760,7 +779,13 @@ Accepted (0.3.0).
|
|
|
760
779
|
```
|
|
761
780
|
|
|
762
781
|
Front-matter wins, then the `## Status` section, then the bold label (which is
|
|
763
|
-
only read above the first
|
|
782
|
+
only read above the first section heading). A front-matter `status` wins even
|
|
783
|
+
when its value cannot be read - `status: "accepted" (2024-05-01)`, text after
|
|
784
|
+
a closing quote - and then the document stays in force, the section and the
|
|
785
|
+
label are not read in its place, and the report warns on the key's line with
|
|
786
|
+
the reason. Headings are read as CommonMark reads them, so an underlined
|
|
787
|
+
`Status` is the section and a `## Status` kept in a comment is not. Anything
|
|
788
|
+
else - `Provisional`, `In review`, a
|
|
764
789
|
misspelled `Supersedded`, or no status at all - keeps enforcing. That asymmetry
|
|
765
790
|
is deliberate: an unanticipated word that keeps enforcing is a visible failure
|
|
766
791
|
with an obvious fix, while one that stops enforcing is a green build over a rule
|
|
@@ -781,7 +806,7 @@ found on the day it is written rather than on the day it is accepted.
|
|
|
781
806
|
`--ignore-status` runs everything, which is how you ask whether a draft would
|
|
782
807
|
pass if you accepted it today.
|
|
783
808
|
|
|
784
|
-
[ADR-0010](docs/adr/0010-spec-status.md) has the full reasoning, including why
|
|
809
|
+
[ADR-0010](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0010-spec-status.md) has the full reasoning, including why
|
|
785
810
|
there is no per-directive `if-status` attribute.
|
|
786
811
|
|
|
787
812
|
## Asking before writing - `query` and the MCP server
|
|
@@ -838,7 +863,7 @@ project's does not.
|
|
|
838
863
|
|
|
839
864
|
The arithmetic that decides whether a rule governs a path is tested against the
|
|
840
865
|
walk a real run makes, file for file, under both engines and on randomly
|
|
841
|
-
generated trees. [ADR-0012](docs/adr/0012-query-and-mcp.md) lists the three
|
|
866
|
+
generated trees. [ADR-0012](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0012-query-and-mcp.md) lists the three
|
|
842
867
|
things it cannot see: file content (binary, oversized, unreadable), symbolic
|
|
843
868
|
links, and letter case on case-insensitive filesystems.
|
|
844
869
|
|
|
@@ -872,6 +897,12 @@ to its checks.
|
|
|
872
897
|
| --- | --- |
|
|
873
898
|
| `get_architectural_rules(path, include_inactive?)` | The query above, as text and as structured content |
|
|
874
899
|
| `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 |
|
|
900
|
+
| `get_dependents(paths, depth?, include_inactive?)` | [`impact`](#who-depends-on-this---impact): the files that import each path, however far away, with the import that leads there, the imports that could not be followed, and the rules in play. The structured content is the document `impact --json` writes |
|
|
901
|
+
|
|
902
|
+
All three are marked read-only, idempotent and closed-world. The server's
|
|
903
|
+
instructions tell an agent when to call each: the rules before creating or
|
|
904
|
+
changing a file, the dependents before changing a file others import, and the
|
|
905
|
+
check after changing files.
|
|
875
906
|
|
|
876
907
|
| Resource | What it holds |
|
|
877
908
|
| --- | --- |
|
|
@@ -884,6 +915,174 @@ version on every request and probe with `server/discover`. Nothing is cached, so
|
|
|
884
915
|
an ADR edited mid-session is read as edited. ADR-0012 has the shapes, the
|
|
885
916
|
sources they follow, and what is deliberately not implemented.
|
|
886
917
|
|
|
918
|
+
## Can each rule fail? - `prove`
|
|
919
|
+
|
|
920
|
+
A passing rule says one of two things, and the report cannot tell which: the
|
|
921
|
+
code holds, or the rule cannot see the code. A `glob="*.js"` over a codebase
|
|
922
|
+
written in TypeScript passes, and would pass whatever the code did.
|
|
923
|
+
|
|
924
|
+
```bash
|
|
925
|
+
spec-guard prove
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
`prove` shows each rule in force a violation of itself and runs the rule over
|
|
929
|
+
it: a file holding the forbidden text beside the files the rule reads, a file
|
|
930
|
+
importing the forbidden module, two files importing each other, a file in a
|
|
931
|
+
lower layer importing a higher one, a misnamed file, a required entry or a
|
|
932
|
+
partner taken away, a file the rule requires removed. The violation is made **in
|
|
933
|
+
memory**, through the same door every read goes through. Nothing is written to
|
|
934
|
+
disk, and nothing touches git.
|
|
935
|
+
|
|
936
|
+
```text
|
|
937
|
+
spec-guard prove 3 specs · 12 rules
|
|
938
|
+
|
|
939
|
+
✖ docs/adr/0004-legacy.md:12 @assert-absence passed with a violation in place
|
|
940
|
+
"LegacyClient" must not appear in src
|
|
941
|
+
maximum: added src/spec-guard-prove.ts holding "LegacyClient", beside the code under src, none of which the rule reads, and it still passed: expected no matches, found 0
|
|
942
|
+
|
|
943
|
+
○ docs/adr/0009-queue.md:8 @assert-absence no violation could be made
|
|
944
|
+
"enqueueRaw" must not appear in src/queue
|
|
945
|
+
it fails on the tree as it stands (expected no matches, found 1), so no change can be shown to be what fails it
|
|
946
|
+
|
|
947
|
+
10 seen to fail · 1 survived · 1 unprovable · 1.10s
|
|
948
|
+
✖ 1 rule passed with a violation of itself in place
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
Each rule is **killed** (it failed on the violation), **survived** (it still
|
|
952
|
+
passed, and the violation is shown), or **unprovable** (no violation could be
|
|
953
|
+
made, and the reason is given). Both bounds of a count are probed. `prove`
|
|
954
|
+
exits 1 when a rule survived, and under `--strict` when one is unprovable. It
|
|
955
|
+
takes the run's `--root`, `--spec`, `--exclude`, `--ignore-status`, `--json`
|
|
956
|
+
and `--format sarif|github|gitlab`, and the configuration's specs, exclusions
|
|
957
|
+
and strictness.
|
|
958
|
+
|
|
959
|
+
A kill means the rule can fail, not that it catches the way the code is
|
|
960
|
+
written: `module="src/db.ts"` is killed by `import 'src/db.ts'` though every
|
|
961
|
+
file imports `./db.js`. [ADR-0016](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0016-rules-seen-to-fail.md) has the
|
|
962
|
+
violations for each kind of rule, and what `unprovable` means.
|
|
963
|
+
|
|
964
|
+
## Do the comments cite decisions in force? - `cites`
|
|
965
|
+
|
|
966
|
+
Code comments cite decisions - `// ADR-0011: the domain imports no
|
|
967
|
+
infrastructure`, `# see ADR-7`, `/* Q-172 */` - and the claim goes stale the
|
|
968
|
+
way code does: the document was never written, or it has since been superseded.
|
|
969
|
+
`spec-guard cites` reads the comment text of every source file for the ids a
|
|
970
|
+
project names, and reports each citation of a document that does not exist or
|
|
971
|
+
is no longer in force:
|
|
972
|
+
|
|
973
|
+
```bash
|
|
974
|
+
spec-guard cites
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
```text
|
|
978
|
+
spec-guard cites ADR-{n} (10 documents matching docs/adr/{n}-*.md); RFC-{n} (2 documents matching docs/rfcs/rfc-{n}.md)
|
|
979
|
+
|
|
980
|
+
⚠ crates/ledger/src/journal.rs:1 cites ADR-0007, which is superseded - cite ADR-0009 instead stale-citation
|
|
981
|
+
hint: docs/adr/0007-async-journal.md says "superseded by ADR-0009"
|
|
982
|
+
✖ crates/ledger/src/lib.rs:28 cites ADR-0011, which no document defines ghost-citation
|
|
983
|
+
hint: no document matching docs/adr/{n}-*.md has the number 11; the nearest is ADR-0010
|
|
984
|
+
|
|
985
|
+
3 citations in 2 files · 1 ghost · 1 stale · 22ms
|
|
986
|
+
✖ 1 citation names a document that does not exist
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
The families of documents are named in the configuration, one entry each - an
|
|
990
|
+
id with `{n}` for the number, and a path glob naming the documents with `{n}`
|
|
991
|
+
in the file name:
|
|
992
|
+
|
|
993
|
+
```json
|
|
994
|
+
{
|
|
995
|
+
"cites": [
|
|
996
|
+
{ "id": "ADR-{n}", "files": "docs/adr/{n}-*.md" },
|
|
997
|
+
{ "id": "RFC-{n}", "files": "docs/rfcs/rfc-{n}.md" }
|
|
998
|
+
]
|
|
999
|
+
}
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
- **The number is a number**: `ADR-7`, `ADR-007` and `docs/adr/0007-x.md` are
|
|
1003
|
+
one document. An id has a word boundary on each side, so `XADR-1` and
|
|
1004
|
+
`ADR-12a` are not citations, and one id on one line is one finding however
|
|
1005
|
+
often it is written there.
|
|
1006
|
+
- **Without `cites`**, a directory of specs whose names begin with a number, and
|
|
1007
|
+
whose titles all begin with one id and that number (`# ADR-0007: ...` in
|
|
1008
|
+
`0007-x.md`), is read as a family. Anything less is not guessed at: the
|
|
1009
|
+
command exits 0 with a note saying to name the documents.
|
|
1010
|
+
- **Comment text only**, as the comment classifier reads it: an id in a string,
|
|
1011
|
+
a raw string or a YAML value is not a citation. Markdown is spec-graph's and
|
|
1012
|
+
is not read, nor are the spec files, the project's `exclude`, and the default
|
|
1013
|
+
skips. Files in a language whose comments spec-guard does not know are
|
|
1014
|
+
counted by extension, and a file read in part - binary, unreadable, or a scan
|
|
1015
|
+
that lost its place - is named.
|
|
1016
|
+
- **Another project's ids are not checked**: `spec-core's ADR-0005`,
|
|
1017
|
+
`spec-graph ADR-0017` or `its ADR-12` name a document in another repository,
|
|
1018
|
+
and are counted instead.
|
|
1019
|
+
- **Stale** is `superseded`, `deprecated`, `rejected` or `archived`; a draft or
|
|
1020
|
+
proposal cited from the code implementing it is not. The successor is read
|
|
1021
|
+
from the document's status line - `Superseded by ADR-0014` - and followed to
|
|
1022
|
+
the one in force.
|
|
1023
|
+
|
|
1024
|
+
A ghost exits 1. A stale citation is a warning, and exits 1 under `--strict`,
|
|
1025
|
+
as does a file read in part or a check that looked for nothing. A family whose
|
|
1026
|
+
files match no document exits 2. `--json` (versioned by `formatVersion`),
|
|
1027
|
+
`--format sarif`, `--format github` and `--format gitlab` carry the same
|
|
1028
|
+
findings; `spec-guard cites src lib/a.ts` reads only those paths.
|
|
1029
|
+
[ADR-0017](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0017-citations-in-comments.md) has the design, and what is
|
|
1030
|
+
out of scope: links to symbols, which need a parser the family does not ship.
|
|
1031
|
+
|
|
1032
|
+
## Who depends on this? - `impact`
|
|
1033
|
+
|
|
1034
|
+
Before changing a file, the question after "which rules govern it" is "what
|
|
1035
|
+
else does it reach". `spec-guard impact` reads the import graph backwards: for
|
|
1036
|
+
each path, the files that import it, and the files that import those, with how
|
|
1037
|
+
far each is and the import that takes it one step closer; then the rules in
|
|
1038
|
+
force that govern the path or any of them:
|
|
1039
|
+
|
|
1040
|
+
```bash
|
|
1041
|
+
spec-guard impact src/db/client.ts --depth 3
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
```text
|
|
1045
|
+
src/db/client.ts
|
|
1046
|
+
4 files depend on it, 2 directly, up to 3 imports away
|
|
1047
|
+
1 src/app/service.ts imports src/db/client.ts (line 2)
|
|
1048
|
+
1 src/db/index.ts imports src/db/client.ts (line 1)
|
|
1049
|
+
2 src/app/cache.ts imports src/app/service.ts (line 1)
|
|
1050
|
+
3 src/ui/view.tsx imports src/app/cache.ts (line 1)
|
|
1051
|
+
|
|
1052
|
+
3 rules govern these files, from 1 document
|
|
1053
|
+
|
|
1054
|
+
ADR-0001: Layers (docs/adr/0001-layers.md)
|
|
1055
|
+
:3 @assert-layers src must keep its layers in order, src/db < src/app < src/ui
|
|
1056
|
+
governs: src/db/client.ts, src/app/service.ts, src/db/index.ts, src/app/cache.ts, src/ui/view.tsx
|
|
1057
|
+
...
|
|
1058
|
+
|
|
1059
|
+
3 imports could not be resolved, and may depend on these paths:
|
|
1060
|
+
src/ui/view.tsx:3 ./gone.js (names no file)
|
|
1061
|
+
src/ui/view.tsx:4 @/db (names no file)
|
|
1062
|
+
src/ui/view.tsx:7 import(name)
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
- **Followed**: relative JavaScript and TypeScript imports, by the table the
|
|
1066
|
+
cycle rule uses ([ADR-0011](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0011-layers-and-cycles.md)), and relative
|
|
1067
|
+
Python imports, which name a module beside the importing file.
|
|
1068
|
+
- **Counted, not followed**: Go imports, Rust `use`s, C# `using`s and absolute
|
|
1069
|
+
Python imports name modules, and which file a module is depends on `go.mod`,
|
|
1070
|
+
the crate's module tree, the compiler or `sys.path`. A path in one of those
|
|
1071
|
+
languages says its dependents are not computed, rather than showing an empty
|
|
1072
|
+
list.
|
|
1073
|
+
- **Listed, never guessed**: an import naming no file, an alias such as `@/db`,
|
|
1074
|
+
a dynamic `import(name)`. Any of them may depend on the path.
|
|
1075
|
+
|
|
1076
|
+
A directory asks about every file under it. `--depth <n>` stops `n` imports
|
|
1077
|
+
away; cycles are safe. `--ignore-status` lists rules from documents not in
|
|
1078
|
+
force, which are otherwise counted, as `query` does. `--json` is versioned by
|
|
1079
|
+
`formatVersion`. A path that does not exist exits 2, and so does a run of
|
|
1080
|
+
`impact` in which no spec matched, as `query` does: the dependents are still
|
|
1081
|
+
written, but "no rules" over no specs is nothing measured. `--allow-empty`
|
|
1082
|
+
asks for the dependents alone, and exits 0. The MCP server answers the same
|
|
1083
|
+
question as `get_dependents`, with the same document.
|
|
1084
|
+
[ADR-0018](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0018-impact.md) has the table and what it leaves out.
|
|
1085
|
+
|
|
887
1086
|
## CLI
|
|
888
1087
|
|
|
889
1088
|
```bash
|
|
@@ -891,6 +1090,9 @@ spec-guard [patterns...] [options] # execute the directives
|
|
|
891
1090
|
spec-guard --watch [patterns...] # execute them again whenever the tree changes
|
|
892
1091
|
spec-guard query <paths...> [options] # the rules in force for files or directories
|
|
893
1092
|
spec-guard mcp [options] # serve the rules over MCP on stdio
|
|
1093
|
+
spec-guard prove [patterns...] # show each rule a violation of itself, in memory
|
|
1094
|
+
spec-guard cites [paths...] # check each spec a code comment cites exists and is in force
|
|
1095
|
+
spec-guard impact <paths...> # the files that depend on each path, and the rules in play there
|
|
894
1096
|
```
|
|
895
1097
|
|
|
896
1098
|
| Option | Description |
|
|
@@ -900,8 +1102,8 @@ spec-guard mcp [options] # serve the rules over MCP on stdio
|
|
|
900
1102
|
| `-v, --verbose` | Print passing assertions too |
|
|
901
1103
|
| `--watch` | Report, then report again as the tree changes, until Ctrl+C |
|
|
902
1104
|
| `--fail-fast` | Stop at the first failing assertion |
|
|
903
|
-
| `--json` | Machine-readable report on stdout (same as `--format json`) |
|
|
904
|
-
| `--format <human\|json\|sarif>` | Output format. `sarif` uploads to GitHub code scanning |
|
|
1105
|
+
| `--json` | Machine-readable report on stdout (same as `--format json`), versioned by `formatVersion` |
|
|
1106
|
+
| `--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 |
|
|
905
1107
|
| `--engine <auto\|rg\|js>` | Search engine (default `auto`: scanner for small trees, ripgrep for big ones) |
|
|
906
1108
|
| `--strict` / `--no-strict` | Treat analysis that could not be completed as a failure |
|
|
907
1109
|
| `--allow-missing-targets` / `--no-allow-missing-targets` | Warn instead of failing when a `target` path does not exist |
|
|
@@ -909,11 +1111,12 @@ spec-guard mcp [options] # serve the rules over MCP on stdio
|
|
|
909
1111
|
| `--print-baseline` | Print the `baseline="..."` that would exempt today's violations, and exit |
|
|
910
1112
|
| `--no-default-skips` / `--default-skips` | Search `.git`, `.hg`, `.svn` and `node_modules` too |
|
|
911
1113
|
| `--exclude <globs>` | Paths no assertion looks at, beside each directive's own `exclude`. Repeatable; replaces the configuration's list, and `--exclude=` clears it |
|
|
912
|
-
| `--ignore-status` / `--no-ignore-status` | Execute directives in draft, proposed
|
|
1114
|
+
| `--ignore-status` / `--no-ignore-status` | Execute directives in documents not in force too: draft, proposed, rejected, deprecated, superseded or archived |
|
|
913
1115
|
| `--include-specs` / `--no-include-specs` | Also count matches inside the spec files themselves |
|
|
914
1116
|
| `--max-snippets <n>` | Failure snippets per assertion (default 5) |
|
|
915
1117
|
| `--concurrency <n>` | Search passes in flight at once (default 8) |
|
|
916
|
-
| `--
|
|
1118
|
+
| `--depth <n>` | `impact` only: follow dependents at most `n` imports away (default: all) |
|
|
1119
|
+
| `--allow-empty` | Exit 0 when no spec file matched the patterns, for a run, `prove` or `impact` (about the run, not an assertion) |
|
|
917
1120
|
| `--color` / `--no-color` | Force colour on or off (`NO_COLOR` honoured) |
|
|
918
1121
|
|
|
919
1122
|
Patterns are expanded by spec-guard itself, so quoted globs behave identically
|
|
@@ -958,6 +1161,7 @@ same options, at the top level, in `.spec-guard.json`:
|
|
|
958
1161
|
| `strict`, `allowMissingTargets`, `allowEmptyScope`, `ignoreStatus`, `includeSpecs`, `defaultSkips` | the flag of that name | `true` or `false` |
|
|
959
1162
|
| `maxSnippets` | `--max-snippets` | an integer, 0 or more |
|
|
960
1163
|
| `concurrency` | `--concurrency` | an integer, 1 or more |
|
|
1164
|
+
| `cites` | none; `spec-guard cites` reads it | a non-empty array of `{ "id": "ADR-{n}", "files": "docs/adr/{n}-*.md" }`, and no other key |
|
|
961
1165
|
|
|
962
1166
|
- **Only the root's files** are read: the `--root` directory, or the working
|
|
963
1167
|
directory. Nothing is inherited from a parent directory.
|
|
@@ -1016,7 +1220,7 @@ pure function of a file's bytes - tokenizing, comment masking, scanning - is
|
|
|
1016
1220
|
remembered by the bytes' hash. Each of those is a way to report a tree that no
|
|
1017
1221
|
longer exists, so a test changes trees at random and requires every report a
|
|
1018
1222
|
session gives to equal a fresh run's, with six deliberately broken sessions
|
|
1019
|
-
that it has to catch. [ADR-0014](docs/adr/0014-configuration-and-watch.md) has
|
|
1223
|
+
that it has to catch. [ADR-0014](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0014-configuration-and-watch.md) has
|
|
1020
1224
|
the design, the measurements and the limits.
|
|
1021
1225
|
|
|
1022
1226
|
On this repository, from a change to the report, in the quietest of four runs
|
|
@@ -1045,8 +1249,8 @@ symbolic link, and anything outside the root. CI stays the authority.
|
|
|
1045
1249
|
| Code | Meaning |
|
|
1046
1250
|
| --- | --- |
|
|
1047
1251
|
| `0` | Every assertion held |
|
|
1048
|
-
| `1` | An assertion failed, or a directive was malformed |
|
|
1049
|
-
| `2` | spec-guard could not run: bad usage, a malformed configuration, no spec files matched, `--engine rg` with no ripgrep, a watch that could not start |
|
|
1252
|
+
| `1` | An assertion failed, or a directive was malformed; for `prove`, a rule survived; for `cites`, a comment cites a document that does not exist |
|
|
1253
|
+
| `2` | spec-guard could not run: bad usage, a malformed configuration, no spec files matched, `--engine rg` with no ripgrep, a watch that could not start, a `cites` family whose files match no document |
|
|
1050
1254
|
| `130` | A `--watch` session was stopped |
|
|
1051
1255
|
|
|
1052
1256
|
## CI integration
|
|
@@ -1104,6 +1308,43 @@ where the fix goes. Alerts carry a fingerprint derived from what the assertion
|
|
|
1104
1308
|
is about rather than where its matches landed, so inserting a line above a
|
|
1105
1309
|
violation does not close the alert and open a new one.
|
|
1106
1310
|
|
|
1311
|
+
Two formats need no upload step. `--format github` writes one GitHub Actions
|
|
1312
|
+
workflow command per finding, which the job's log turns into an annotation on
|
|
1313
|
+
the diff:
|
|
1314
|
+
|
|
1315
|
+
```text
|
|
1316
|
+
::error file=src/a.ts,line=2,title=assert-absence::"Legacy" must not appear in src: expected no matches, found 2 (docs/adr/0004-legacy.md:12)
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
`--format gitlab` writes a
|
|
1320
|
+
[GitLab Code Quality](https://docs.gitlab.com/ci/testing/code_quality/) report,
|
|
1321
|
+
which a merge request shows beside its changes:
|
|
1322
|
+
|
|
1323
|
+
```yaml
|
|
1324
|
+
spec-guard:
|
|
1325
|
+
script:
|
|
1326
|
+
- npx @descent-vtt/spec-guard --format gitlab > gl-code-quality-report.json || true
|
|
1327
|
+
artifacts:
|
|
1328
|
+
reports:
|
|
1329
|
+
codequality: gl-code-quality-report.json
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
Every format places the same findings, and `prove` and `cites` write all three:
|
|
1333
|
+
|
|
1334
|
+
| Finding | GitHub | GitLab severity |
|
|
1335
|
+
| --- | --- | --- |
|
|
1336
|
+
| a failing assertion, on its first offending line or its directive | `error` | `critical` |
|
|
1337
|
+
| a rule that survived `prove`, on its directive | `error` | `critical` |
|
|
1338
|
+
| a ghost citation (`cites`), on its comment | `error` | `critical` |
|
|
1339
|
+
| a directive that could not be read | `error` | `major` |
|
|
1340
|
+
| a stale citation (`cites`) | `warning`; `error` under `--strict` | `minor`; `critical` under `--strict` |
|
|
1341
|
+
| a rule `prove` could make no violation for | `notice` | `minor` |
|
|
1342
|
+
| a file `cites` could read only in part | `notice` | `info` |
|
|
1343
|
+
| a document not in force, on its first line | `notice` | `info` |
|
|
1344
|
+
|
|
1345
|
+
A GitLab issue's `fingerprint` is the SHA-256 of its rule, file and message,
|
|
1346
|
+
which GitLab compares across pipelines to tell a new finding from an old one.
|
|
1347
|
+
|
|
1107
1348
|
**There is no language server, and that is a decision rather than a gap.**
|
|
1108
1349
|
spec-guard's claims are about a whole repository - "this symbol appears nowhere
|
|
1109
1350
|
in `src`" - and a language server is handed one buffer at a time. Answering a
|
|
@@ -1121,9 +1362,11 @@ parser ──▶ Directive[] ──▶ runner ──▶ Assertion[] ──▶ e
|
|
|
1121
1362
|
(pure) (I/O) (rg | js) (pure)
|
|
1122
1363
|
```
|
|
1123
1364
|
|
|
1124
|
-
1. **Parse.** Markdown is scanned for `<!-- @assert-* -->` comments.
|
|
1125
|
-
|
|
1126
|
-
|
|
1365
|
+
1. **Parse.** Markdown is scanned for `<!-- @assert-* -->` comments. Code -
|
|
1366
|
+
fenced and indented blocks, code spans, and the HTML elements whose content
|
|
1367
|
+
is not Markdown - and front matter are masked first, by the Markdown scanner
|
|
1368
|
+
every spec-* tool shares, so documentation that shows the syntax (like this
|
|
1369
|
+
README) never executes it.
|
|
1127
1370
|
2. **Resolve.** Each directive becomes a typed assertion. Bad numbers, unknown
|
|
1128
1371
|
booleans, unknown attributes and paths escaping `--root` are rejected here,
|
|
1129
1372
|
before any I/O.
|
|
@@ -1160,7 +1403,7 @@ exceeds a budget, in which case ripgrep takes over. The crossover above sits
|
|
|
1160
1403
|
between 500 and 1,000 files, which is what the Windows budget of 512 encodes;
|
|
1161
1404
|
on Linux it is far lower, because what is really being measured is process
|
|
1162
1405
|
spawn cost. `scripts/bench-engines.mjs` reproduces this, and
|
|
1163
|
-
[ADR-0004](docs/adr/0004-adaptive-engine.md) has the full tables.
|
|
1406
|
+
[ADR-0004](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0004-adaptive-engine.md) has the full tables.
|
|
1164
1407
|
|
|
1165
1408
|
Treat single measurements from one machine with suspicion. On the development
|
|
1166
1409
|
machine used here, an absence assertion over 2,000 files measures 303 ms with
|
|
@@ -1238,6 +1481,46 @@ observed count, match locations, warnings and timings. The parser
|
|
|
1238
1481
|
(`parseDirectives`) and reporter (`formatReport`, `formatJson`) are pure
|
|
1239
1482
|
functions you can use on their own.
|
|
1240
1483
|
|
|
1484
|
+
A run reads the tree through one interface, `Io` - list a directory, `stat` a
|
|
1485
|
+
path, read a file, resolve a real path - and `nodeIo` is the filesystem. Pass
|
|
1486
|
+
your own as `io` and every read the run makes goes through it: finding and
|
|
1487
|
+
reading the specs, checking targets exist, walking them, scanning files, reading
|
|
1488
|
+
imports and listing directories. That is how to run the rules over a tree that
|
|
1489
|
+
is not on disk, or not as the disk has it:
|
|
1490
|
+
|
|
1491
|
+
```ts
|
|
1492
|
+
import { nodeIo, runSpecGuard, type Io } from '@descent-vtt/spec-guard';
|
|
1493
|
+
|
|
1494
|
+
// The disk, except for one file edited in memory.
|
|
1495
|
+
const edited: Io = {
|
|
1496
|
+
...nodeIo,
|
|
1497
|
+
readFile: async (file) =>
|
|
1498
|
+
file === '/repo/src/pay.ts' ? Buffer.from('new LegacyGateway();\n') : nodeIo.readFile(file),
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
const report = await runSpecGuard({ patterns: ['docs/**/*.md'], root: '/repo', io: edited });
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
ripgrep reads the disk itself, in a process of its own, so no `io` can reach
|
|
1505
|
+
it: `engine: 'ripgrep'` with an `io` is an error, and `auto` with one searches
|
|
1506
|
+
with the built-in scanner.
|
|
1507
|
+
|
|
1508
|
+
`proveSpecGuard` is `spec-guard prove` for a caller: it takes the options that
|
|
1509
|
+
decide whether a rule passes, and an `io` too, and returns each rule's outcome
|
|
1510
|
+
with the violations it was shown. `overlayIo` is the door it makes them behind,
|
|
1511
|
+
and is there for a caller who wants to try a change of its own.
|
|
1512
|
+
|
|
1513
|
+
`findCitations` and `impactOf` are `spec-guard cites` and `spec-guard impact`,
|
|
1514
|
+
and take an `io` as well; so do `loadRuleSet` and `queryRules`. Each command's
|
|
1515
|
+
JSON is the report its function returns, with a `formatVersion` in front.
|
|
1516
|
+
|
|
1517
|
+
A process that reads the specs again for every request, as the MCP server
|
|
1518
|
+
does, can keep what it parsed: pass one `createDocumentMemo()` as `documents`
|
|
1519
|
+
to `runSpecGuard`, `loadRuleSet`, `queryRules` or `impactOf`, every time. Every
|
|
1520
|
+
spec is still found and read, so an edit is seen by the next call; a document
|
|
1521
|
+
whose bytes have not changed is not parsed again, and the memo holds only the
|
|
1522
|
+
documents of the spec set read last. A single run gains nothing from one.
|
|
1523
|
+
|
|
1241
1524
|
## Design decisions
|
|
1242
1525
|
|
|
1243
1526
|
This tool was specified loosely and built opinionatedly. Where the
|
|
@@ -1252,11 +1535,27 @@ themselves stay uncounted, because a directive is an HTML comment and a rule
|
|
|
1252
1535
|
whose own text trips it can never be satisfied. `comments="include"` counts even
|
|
1253
1536
|
those.
|
|
1254
1537
|
|
|
1255
|
-
**
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1538
|
+
**Code is masked before parsing.** A README documenting the syntax must not
|
|
1539
|
+
execute it. What is code is what CommonMark says it is, decided by
|
|
1540
|
+
[spec-core](https://github.com/DescentVTT/spec-core)'s Markdown scanner, which
|
|
1541
|
+
every spec-* tool reads documents with and which is copied into
|
|
1542
|
+
`src/vendor/spec-core` and verified by hash: fenced and indented code, code
|
|
1543
|
+
spans, `<script>`, `<pre>`, `<style>` and `<textarea>` blocks, and front matter.
|
|
1544
|
+
Comments and code spans are resolved left to right in one pass, so a backtick
|
|
1545
|
+
inside a comment is a character and `<!--` inside a code span is code, and a
|
|
1546
|
+
code span ends with its paragraph, so a stray backtick cannot hide the
|
|
1547
|
+
directives after it. A fence may be indented with the list item it sits in.
|
|
1548
|
+
Masking preserves every offset and line terminator, so reported line numbers
|
|
1549
|
+
stay exact. [ADR-0002](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0002-directive-format.md)'s amendment lists
|
|
1550
|
+
where this differs from 0.11.0.
|
|
1551
|
+
|
|
1552
|
+
What is masked is still counted. A comment shaped like a directive -
|
|
1553
|
+
`<!-- @assert-...` - in code, raw HTML or front matter is not run, and the
|
|
1554
|
+
report says how many there were and where, on one line (every one under
|
|
1555
|
+
`--verbose`, and in `maskedDirectives` in JSON), so text read as code by
|
|
1556
|
+
accident is not a rule gone quiet without a word. A code fence or raw-text
|
|
1557
|
+
block that is never closed, which makes the rest of its document code, is a
|
|
1558
|
+
warning on its opening line.
|
|
1260
1559
|
|
|
1261
1560
|
**A document's lifecycle status is read; a directive's is not.** Status is
|
|
1262
1561
|
document-level and visible in every rendered Markdown view. A per-directive
|
|
@@ -1290,6 +1589,13 @@ anything - it answers only *which files contain this text*, and the scanner
|
|
|
1290
1589
|
counts each pattern separately over the shared file contents. The batching
|
|
1291
1590
|
checks were deleted along with the risk they guarded.
|
|
1292
1591
|
|
|
1592
|
+
**Globs are matched by an automaton, not a regular expression.** Every
|
|
1593
|
+
pattern - `glob`, `exclude`, `module`, a layer, a spec pattern - is read by the
|
|
1594
|
+
glob engine the spec-* tools share, copied from spec-core and checked by hash.
|
|
1595
|
+
A `RegExp` backtracks: `*-*-*-*-*-*x` took 55 seconds to fail one long file
|
|
1596
|
+
name. The automaton cannot, and ripgrep is handed each pattern spelled as the
|
|
1597
|
+
automaton reads it ([ADR-0015](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0015-globs-from-spec-core.md)).
|
|
1598
|
+
|
|
1293
1599
|
**Exit code 2 exists.** "Your specs failed" and "spec-guard could not run" are
|
|
1294
1600
|
different facts, and CI should be able to tell them apart.
|
|
1295
1601
|
|
|
@@ -1305,11 +1611,11 @@ forbidden symbol sat in `.github/workflows/ci.yml`; the scanner and ripgrep also
|
|
|
1305
1611
|
disagreed with each other, finding two matches and four on the same tree. Scope
|
|
1306
1612
|
is now one policy that both engines are driven by, the skip list is four names
|
|
1307
1613
|
long, and anything spec-guard could not inspect is reported rather than assumed
|
|
1308
|
-
clean ([ADR-0007](docs/adr/0007-search-scope.md)).
|
|
1614
|
+
clean ([ADR-0007](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0007-search-scope.md)).
|
|
1309
1615
|
|
|
1310
1616
|
**Comments are excluded by default, and the exclusion is reported.** Counting
|
|
1311
1617
|
the note that records a deletion as an occurrence of the thing deleted punishes
|
|
1312
|
-
the documentation this tool exists to keep honest ([ADR-0006](docs/adr/0006-comment-classification.md)).
|
|
1618
|
+
the documentation this tool exists to keep honest ([ADR-0006](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0006-comment-classification.md)).
|
|
1313
1619
|
The reverse risk — a rule that quietly stops checking anything because every
|
|
1314
1620
|
match now sits in a comment — is why every run says how many matches it dropped.
|
|
1315
1621
|
|
|
@@ -1326,23 +1632,25 @@ comment and string lexer that already existed, masking the source with it, and
|
|
|
1326
1632
|
reading statements off what is left. Tree-sitter would have been the modern
|
|
1327
1633
|
answer and costs 94 MB unpacked against this package's 0.33 MB; the four things
|
|
1328
1634
|
a real parser would genuinely see that this cannot are listed in
|
|
1329
|
-
[ADR-0008](docs/adr/0008-polyglot-imports.md) rather than glossed over.
|
|
1635
|
+
[ADR-0008](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0008-polyglot-imports.md) rather than glossed over.
|
|
1330
1636
|
|
|
1331
1637
|
**There is no `--fix`.** Every edit a machine can make to a failing boundary
|
|
1332
1638
|
assertion is an edit that records the rule no longer holding: widen the bound,
|
|
1333
1639
|
add an exclusion, append to the baseline, insert an ignore comment. A one-flag
|
|
1334
1640
|
path from red to green is a bad button for a person and a much worse one for an
|
|
1335
1641
|
agent whose loop terminates on a green build. `--print-baseline` prints what you
|
|
1336
|
-
could paste; it does not paste it ([ADR-0009](docs/adr/0009-debt-baselines.md)).
|
|
1642
|
+
could paste; it does not paste it ([ADR-0009](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0009-debt-baselines.md)).
|
|
1337
1643
|
|
|
1338
1644
|
## spec-guard checks itself
|
|
1339
1645
|
|
|
1340
|
-
The invariants in [`docs/adr/0001-invariants.md`](docs/adr/0001-invariants.md)
|
|
1341
|
-
and [`docs/adr/0002-directive-format.md`](docs/adr/0002-directive-format.md) are
|
|
1646
|
+
The invariants in [`docs/adr/0001-invariants.md`](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0001-invariants.md)
|
|
1647
|
+
and [`docs/adr/0002-directive-format.md`](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0002-directive-format.md) are
|
|
1342
1648
|
executed against this repository on every CI run. The CLI never calls
|
|
1343
1649
|
`console.log`, nothing outside the engine spawns a process, and the parser and
|
|
1344
1650
|
reporter never touch the filesystem - because those documents say so, and the
|
|
1345
|
-
build fails if they stop being true.
|
|
1651
|
+
build fails if they stop being true. CI proves every one of those rules can
|
|
1652
|
+
fail, and `spec-guard cites` holds every ADR a comment in the code cites to
|
|
1653
|
+
existing and being in force.
|
|
1346
1654
|
|
|
1347
1655
|
This README is executable too:
|
|
1348
1656
|
|
|
@@ -1370,13 +1678,13 @@ prebuilt binary and is never a runtime dependency.
|
|
|
1370
1678
|
|
|
1371
1679
|
Coverage says a line ran. It does not say an assertion would notice if the line
|
|
1372
1680
|
behaved differently. This repository measures the difference: line coverage is
|
|
1373
|
-
**100%**, and **99.
|
|
1681
|
+
**100%**, and **99.20%** of 11,549 mutants are killed in CI, with 89 survivors and
|
|
1374
1682
|
no file below 95%. The second number is the one worth reading, and what was done
|
|
1375
1683
|
about the survivors matters more than the score. At 0.5.1 every surviving mutant
|
|
1376
1684
|
was checked individually, and the 101 left then produced byte-identical output.
|
|
1377
1685
|
Each release since has replayed the survivors its own code added before it
|
|
1378
1686
|
shipped: they were killed, deleted as dead code, or shown equivalent and
|
|
1379
|
-
recorded in [ADR-0003](docs/adr/0003-mutation-testing.md) with the evidence.
|
|
1687
|
+
recorded in [ADR-0003](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0003-mutation-testing.md) with the evidence.
|
|
1380
1688
|
|
|
1381
1689
|
That gap is the point. The first run scored 77.23%, and the weakest file was the
|
|
1382
1690
|
reporter at 65.48% - not because it lacked tests, but because its tests were
|
|
@@ -1405,7 +1713,7 @@ applied one at a time and run through a fingerprint of 4,176 observations, and
|
|
|
1405
1713
|
that only matters when a second statement follows, a type-only test that marks
|
|
1406
1714
|
`import A, { B } from 'x'` type-only when loosened, a comment check that opens a
|
|
1407
1715
|
block comment on `2*3`. All 29 are now tested and all 29 die. All of it is in
|
|
1408
|
-
[ADR-0003](docs/adr/0003-mutation-testing.md), including a Stryker limitation
|
|
1716
|
+
[ADR-0003](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0003-mutation-testing.md), including a Stryker limitation
|
|
1409
1717
|
found on the way: a mutant that stops a test file *loading* is reported as
|
|
1410
1718
|
survived even though the suite is in fact killing it.
|
|
1411
1719
|
|
|
@@ -1433,7 +1741,7 @@ suite takes hours against 43 minutes of hosted time, and it scores *higher*,
|
|
|
1433
1741
|
because far more mutants hang there and Stryker counts a hang as a kill. Linux
|
|
1434
1742
|
CI is the measurement.
|
|
1435
1743
|
|
|
1436
|
-
Eight cautionary tales are in [ADR-0003](docs/adr/0003-mutation-testing.md): a
|
|
1744
|
+
Eight cautionary tales are in [ADR-0003](https://github.com/DescentVTT/spec-guard/blob/main/docs/adr/0003-mutation-testing.md): a
|
|
1437
1745
|
run whose score was pure fiction because the mutants were never activated, a
|
|
1438
1746
|
tuning knob that lifted the score six points without adding a test, the platform
|
|
1439
1747
|
gap above, the baseline being re-anchored when a tokenizer arrived, and a score
|
|
@@ -1446,6 +1754,60 @@ mutants" sounded like judgement rather than an unchecked assumption - along with
|
|
|
1446
1754
|
the real bugs that chasing the gate uncovered, and a class of mutant the runner
|
|
1447
1755
|
reports as survived while the suite is in fact killing it.
|
|
1448
1756
|
|
|
1757
|
+
### Releasing
|
|
1758
|
+
|
|
1759
|
+
A release is a tag push. Nothing is published from a laptop, and there is no
|
|
1760
|
+
npm token in this repository or in its secrets.
|
|
1761
|
+
|
|
1762
|
+
```bash
|
|
1763
|
+
npm version minor # or patch / major - writes package.json and makes the tag
|
|
1764
|
+
git push origin main --follow-tags
|
|
1765
|
+
```
|
|
1766
|
+
|
|
1767
|
+
[`release.yml`](https://github.com/DescentVTT/spec-guard/blob/main/.github/workflows/release.yml) then:
|
|
1768
|
+
|
|
1769
|
+
1. refuses the release unless the tag, `package.json` and a `## <version>`
|
|
1770
|
+
heading in `CHANGELOG.md` all agree;
|
|
1771
|
+
2. type checks, builds, runs the suite and executes this repository's own ADRs.
|
|
1772
|
+
A tag push does not run CI, so the release job runs those steps itself;
|
|
1773
|
+
3. `npm pack`s the tarball, prints its file list and its SHA-256, and hands that
|
|
1774
|
+
exact file to the publishing job. Nothing is rebuilt in between, so what
|
|
1775
|
+
reaches the registry is the artifact the tests ran against;
|
|
1776
|
+
4. stages it with `npm stage publish --provenance`. The job authenticates with a
|
|
1777
|
+
short-lived OIDC token that GitHub mints for `release.yml` running in the
|
|
1778
|
+
`npm` environment, and npmjs.com accepts it only for that combination - npm's
|
|
1779
|
+
trusted publishing, so there is no credential to leak or to rotate.
|
|
1780
|
+
|
|
1781
|
+
The publishing job installs no dependencies and checks nothing out. Everything
|
|
1782
|
+
that runs third-party code - `npm ci`, a postinstall that downloads a ripgrep
|
|
1783
|
+
binary, the test suite - happens in the earlier job, which has no token.
|
|
1784
|
+
|
|
1785
|
+
Staging is not publishing. The version sits on npmjs.com visible to maintainers
|
|
1786
|
+
and installable by nobody until:
|
|
1787
|
+
|
|
1788
|
+
```bash
|
|
1789
|
+
npm stage list @descent-vtt/spec-guard
|
|
1790
|
+
npm stage view <stage-id>
|
|
1791
|
+
npm stage approve <stage-id> # asks for a second factor
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
`npm stage reject <stage-id>` discards it instead, and the tag can be deleted
|
|
1795
|
+
and remade. Approving needs 2FA and therefore a person: CI can build a release,
|
|
1796
|
+
but it cannot decide to publish one.
|
|
1797
|
+
|
|
1798
|
+
Two repository settings hold the other end of this. The `npm` environment
|
|
1799
|
+
accepts deployments only from `main` and from `v*` tags, so no branch can reach
|
|
1800
|
+
the publishing job. A `Release tags` ruleset lets only repository admins create,
|
|
1801
|
+
move or delete a `v*` tag - under this workflow, pushing one is the release, and
|
|
1802
|
+
an accidental push should not be able to start it.
|
|
1803
|
+
|
|
1804
|
+
`Actions -> Release -> Run workflow` with `dry_run` left on verifies, packs and
|
|
1805
|
+
hands the tarball to npm without spending a version number. It stops at the
|
|
1806
|
+
registry's version check - a dispatch runs from `main`, whose version is already
|
|
1807
|
+
published - and it does not exercise authentication, because the OIDC exchange
|
|
1808
|
+
happens only on a publish that intends to write. The first real tag is what
|
|
1809
|
+
proves that end.
|
|
1810
|
+
|
|
1449
1811
|
## Requirements
|
|
1450
1812
|
|
|
1451
1813
|
- Node.js 22 or newer (native ESM)
|