@descent-vtt/spec-guard 0.11.0 → 0.12.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 +354 -3
- package/README.md +377 -22
- 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 +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +16 -75
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +198 -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 +11 -0
- 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 +24 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +84 -21
- package/dist/runner.js.map +1 -1
- package/dist/specs.d.ts +5 -1
- package/dist/specs.d.ts.map +1 -1
- package/dist/specs.js +5 -1
- 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 +23 -0
- package/dist/vendor/spec-core/markdown/links.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -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 +439 -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 +288 -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 +8 -1
- 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
|
|
|
@@ -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
|
|
@@ -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](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](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](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](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.
|
|
@@ -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.
|
|
@@ -1238,6 +1481,39 @@ 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
|
+
|
|
1241
1517
|
## Design decisions
|
|
1242
1518
|
|
|
1243
1519
|
This tool was specified loosely and built opinionatedly. Where the
|
|
@@ -1252,11 +1528,27 @@ themselves stay uncounted, because a directive is an HTML comment and a rule
|
|
|
1252
1528
|
whose own text trips it can never be satisfied. `comments="include"` counts even
|
|
1253
1529
|
those.
|
|
1254
1530
|
|
|
1255
|
-
**
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1531
|
+
**Code is masked before parsing.** A README documenting the syntax must not
|
|
1532
|
+
execute it. What is code is what CommonMark says it is, decided by
|
|
1533
|
+
[spec-core](https://github.com/DescentVTT/spec-core)'s Markdown scanner, which
|
|
1534
|
+
every spec-* tool reads documents with and which is copied into
|
|
1535
|
+
`src/vendor/spec-core` and verified by hash: fenced and indented code, code
|
|
1536
|
+
spans, `<script>`, `<pre>`, `<style>` and `<textarea>` blocks, and front matter.
|
|
1537
|
+
Comments and code spans are resolved left to right in one pass, so a backtick
|
|
1538
|
+
inside a comment is a character and `<!--` inside a code span is code, and a
|
|
1539
|
+
code span ends with its paragraph, so a stray backtick cannot hide the
|
|
1540
|
+
directives after it. A fence may be indented with the list item it sits in.
|
|
1541
|
+
Masking preserves every offset and line terminator, so reported line numbers
|
|
1542
|
+
stay exact. [ADR-0002](docs/adr/0002-directive-format.md)'s amendment lists
|
|
1543
|
+
where this differs from 0.11.0.
|
|
1544
|
+
|
|
1545
|
+
What is masked is still counted. A comment shaped like a directive -
|
|
1546
|
+
`<!-- @assert-...` - in code, raw HTML or front matter is not run, and the
|
|
1547
|
+
report says how many there were and where, on one line (every one under
|
|
1548
|
+
`--verbose`, and in `maskedDirectives` in JSON), so text read as code by
|
|
1549
|
+
accident is not a rule gone quiet without a word. A code fence or raw-text
|
|
1550
|
+
block that is never closed, which makes the rest of its document code, is a
|
|
1551
|
+
warning on its opening line.
|
|
1260
1552
|
|
|
1261
1553
|
**A document's lifecycle status is read; a directive's is not.** Status is
|
|
1262
1554
|
document-level and visible in every rendered Markdown view. A per-directive
|
|
@@ -1290,6 +1582,13 @@ anything - it answers only *which files contain this text*, and the scanner
|
|
|
1290
1582
|
counts each pattern separately over the shared file contents. The batching
|
|
1291
1583
|
checks were deleted along with the risk they guarded.
|
|
1292
1584
|
|
|
1585
|
+
**Globs are matched by an automaton, not a regular expression.** Every
|
|
1586
|
+
pattern - `glob`, `exclude`, `module`, a layer, a spec pattern - is read by the
|
|
1587
|
+
glob engine the spec-* tools share, copied from spec-core and checked by hash.
|
|
1588
|
+
A `RegExp` backtracks: `*-*-*-*-*-*x` took 55 seconds to fail one long file
|
|
1589
|
+
name. The automaton cannot, and ripgrep is handed each pattern spelled as the
|
|
1590
|
+
automaton reads it ([ADR-0015](docs/adr/0015-globs-from-spec-core.md)).
|
|
1591
|
+
|
|
1293
1592
|
**Exit code 2 exists.** "Your specs failed" and "spec-guard could not run" are
|
|
1294
1593
|
different facts, and CI should be able to tell them apart.
|
|
1295
1594
|
|
|
@@ -1342,7 +1641,9 @@ and [`docs/adr/0002-directive-format.md`](docs/adr/0002-directive-format.md) are
|
|
|
1342
1641
|
executed against this repository on every CI run. The CLI never calls
|
|
1343
1642
|
`console.log`, nothing outside the engine spawns a process, and the parser and
|
|
1344
1643
|
reporter never touch the filesystem - because those documents say so, and the
|
|
1345
|
-
build fails if they stop being true.
|
|
1644
|
+
build fails if they stop being true. CI proves every one of those rules can
|
|
1645
|
+
fail, and `spec-guard cites` holds every ADR a comment in the code cites to
|
|
1646
|
+
existing and being in force.
|
|
1346
1647
|
|
|
1347
1648
|
This README is executable too:
|
|
1348
1649
|
|
|
@@ -1370,7 +1671,7 @@ prebuilt binary and is never a runtime dependency.
|
|
|
1370
1671
|
|
|
1371
1672
|
Coverage says a line ran. It does not say an assertion would notice if the line
|
|
1372
1673
|
behaved differently. This repository measures the difference: line coverage is
|
|
1373
|
-
**100%**, and **99.
|
|
1674
|
+
**100%**, and **99.20%** of 11,537 mutants are killed in CI, with 89 survivors and
|
|
1374
1675
|
no file below 95%. The second number is the one worth reading, and what was done
|
|
1375
1676
|
about the survivors matters more than the score. At 0.5.1 every surviving mutant
|
|
1376
1677
|
was checked individually, and the 101 left then produced byte-identical output.
|
|
@@ -1446,6 +1747,60 @@ mutants" sounded like judgement rather than an unchecked assumption - along with
|
|
|
1446
1747
|
the real bugs that chasing the gate uncovered, and a class of mutant the runner
|
|
1447
1748
|
reports as survived while the suite is in fact killing it.
|
|
1448
1749
|
|
|
1750
|
+
### Releasing
|
|
1751
|
+
|
|
1752
|
+
A release is a tag push. Nothing is published from a laptop, and there is no
|
|
1753
|
+
npm token in this repository or in its secrets.
|
|
1754
|
+
|
|
1755
|
+
```bash
|
|
1756
|
+
npm version minor # or patch / major - writes package.json and makes the tag
|
|
1757
|
+
git push origin main --follow-tags
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
[`release.yml`](.github/workflows/release.yml) then:
|
|
1761
|
+
|
|
1762
|
+
1. refuses the release unless the tag, `package.json` and a `## <version>`
|
|
1763
|
+
heading in `CHANGELOG.md` all agree;
|
|
1764
|
+
2. type checks, builds, runs the suite and executes this repository's own ADRs.
|
|
1765
|
+
A tag push does not run CI, so the release job runs those steps itself;
|
|
1766
|
+
3. `npm pack`s the tarball, prints its file list and its SHA-256, and hands that
|
|
1767
|
+
exact file to the publishing job. Nothing is rebuilt in between, so what
|
|
1768
|
+
reaches the registry is the artifact the tests ran against;
|
|
1769
|
+
4. stages it with `npm stage publish --provenance`. The job authenticates with a
|
|
1770
|
+
short-lived OIDC token that GitHub mints for `release.yml` running in the
|
|
1771
|
+
`npm` environment, and npmjs.com accepts it only for that combination - npm's
|
|
1772
|
+
trusted publishing, so there is no credential to leak or to rotate.
|
|
1773
|
+
|
|
1774
|
+
The publishing job installs no dependencies and checks nothing out. Everything
|
|
1775
|
+
that runs third-party code - `npm ci`, a postinstall that downloads a ripgrep
|
|
1776
|
+
binary, the test suite - happens in the earlier job, which has no token.
|
|
1777
|
+
|
|
1778
|
+
Staging is not publishing. The version sits on npmjs.com visible to maintainers
|
|
1779
|
+
and installable by nobody until:
|
|
1780
|
+
|
|
1781
|
+
```bash
|
|
1782
|
+
npm stage list @descent-vtt/spec-guard
|
|
1783
|
+
npm stage view <stage-id>
|
|
1784
|
+
npm stage approve <stage-id> # asks for a second factor
|
|
1785
|
+
```
|
|
1786
|
+
|
|
1787
|
+
`npm stage reject <stage-id>` discards it instead, and the tag can be deleted
|
|
1788
|
+
and remade. Approving needs 2FA and therefore a person: CI can build a release,
|
|
1789
|
+
but it cannot decide to publish one.
|
|
1790
|
+
|
|
1791
|
+
Two repository settings hold the other end of this. The `npm` environment
|
|
1792
|
+
accepts deployments only from `main` and from `v*` tags, so no branch can reach
|
|
1793
|
+
the publishing job. A `Release tags` ruleset lets only repository admins create,
|
|
1794
|
+
move or delete a `v*` tag - under this workflow, pushing one is the release, and
|
|
1795
|
+
an accidental push should not be able to start it.
|
|
1796
|
+
|
|
1797
|
+
`Actions -> Release -> Run workflow` with `dry_run` left on verifies, packs and
|
|
1798
|
+
hands the tarball to npm without spending a version number. It stops at the
|
|
1799
|
+
registry's version check - a dispatch runs from `main`, whose version is already
|
|
1800
|
+
published - and it does not exercise authentication, because the OIDC exchange
|
|
1801
|
+
happens only on a publish that intends to write. The first real tag is what
|
|
1802
|
+
proves that end.
|
|
1803
|
+
|
|
1449
1804
|
## Requirements
|
|
1450
1805
|
|
|
1451
1806
|
- Node.js 22 or newer (native ESM)
|