@descent-vtt/spec-guard 0.5.1 → 0.6.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 CHANGED
@@ -1,283 +1,377 @@
1
- # Changelog
2
-
3
- All notable changes to this project are documented here. Versions follow
4
- [semantic versioning](https://semver.org), with the 0.x caveat that behaviour
5
- may change in a minor release — each such change is listed under **Changed**
6
- with the flag that restores the previous behaviour.
7
-
8
- ## 0.5.1
9
-
10
- Writing down what the code already claimed. Every module was driven to its
11
- honest mutation-testing ceiling, and the exercise turned up six wrong answers
12
- rather than six missing tests — which is the argument for doing it at all. No
13
- new features, and nothing a user has to change.
14
-
15
- ### Fixed
16
-
17
- - **Two assertions on the same symbol answered each other's comment handling.**
18
- The result cache keyed on the symbol, the targets and the search flags, but
19
- not on `comments`. A spec with `<!-- @assert-count symbol="X" ... -->` above
20
- `<!-- @assert-count symbol="X" ... comments="include" -->` returned whichever
21
- count ran first for both. The grouping test had the same omission, so the two
22
- were also merged into a single pass and the second was scanned with the
23
- first's mask. There is now one definition of "the same question", in three
24
- nested scopes, and the scope policy is part of it too.
25
- - **An unreadable directory went unreported on a small repository.** The
26
- adaptive engine had the walk's skip ledger in hand and returned without it on
27
- any tree small enough to scan in process, so the same directory was a reported
28
- gap on a large repository and silence on a small one. Same tree, two answers,
29
- decided by its size.
30
- - **Snippets from CRLF files carried a carriage return into the report**, which
31
- returns the terminal cursor to column 0 and overwrites the line just printed.
32
- The trim looked for `\r?\n` at the end of a line the caller had already cut at
33
- the newline, so it could never match.
34
- - **A bad `regex="true"` pattern was reported as `Invalid regular expression:
35
- Invalid regular expression: /(/: ...`** — V8's message already says it once.
36
-
37
- ### Changed
38
-
39
- - **Two requests differing only in `scope` are no longer merged into one
40
- search pass.** Reachable today only through the programmatic API, where a
41
- caller may build more than one `ScopePolicy` per run.
42
- - `enumerateCandidates` takes a `WalkRequest` — a `SearchRequest` without the
43
- `symbol`, because a walk does not depend on one. Existing callers are
44
- unaffected; the symbol is now optional rather than required.
45
- - `CachedEngine` declares `searchBatch` as present rather than optional, which
46
- `createCachedEngine` has always guaranteed.
47
- - `KINDS` and `ALLOWED_ATTRIBUTES` are exported from the parser, so the
48
- directive grammar can be asserted rather than restated.
49
- - **The mutation gate moves from 85 to 97**, against a CI measurement of 97.65%
50
- over 4,299 mutants. Every module is above 95% and three are at 100%, where
51
- 0.5.0 ran from 73% to 96%. The 101 mutants still alive have each been applied
52
- individually and produce byte-identical output, so that figure is the ceiling
53
- rather than a way-point — and 29 that were assumed equivalent turned out not
54
- to be, and are now tested. See
55
- [ADR-0003](docs/adr/0003-mutation-testing.md), which also records a Stryker
56
- limitation found on the way: a mutant that stops a test file *loading* is
57
- reported as survived even though the suite is killing it.
58
- - **`src/imports.ts` went from 73.74% to 95.89%.** The tokenizer was tested as a
59
- step towards a list of module references rather than as a thing with an
60
- output, so a template resumption that lost its place, a regex escape that
61
- skipped a character, and three lookup tables were all unpinned. One
62
- behaviour-preserving change came with it: the word branch starts its scan one
63
- character in, so progress is unconditional by construction rather than a
64
- consequence of a relation nothing enforced. Verified identical on 53.6 MB of
65
- real JavaScript.
66
-
67
- ### Internal
68
-
69
- Not user-visible, but this is where the release's weight is: nine new test
70
- files, 1,770 tests against 1,206 at 0.5.0, 100% line coverage, and the analyser
71
- verified byte-identical to the published 0.5.0 across 6,976 files of real code.
72
- Thirty-five negative controls confirm the suite goes red for each defect above,
73
- and `scripts/mutation-equivalence.mjs` is in the repository so the equivalence
74
- measurement stays reproducible.
75
-
76
- ## 0.5.0
77
-
78
- Import assertions covered one language, so a dependency rule pointed at a
79
- directory of Go files did not fail - it passed, having analysed nothing. That is
80
- the same defect 0.4.0 was written to remove, and this release closes the two
81
- remaining shapes of it: a rule that cannot read the language, and a rule whose
82
- scope holds no files at all.
83
-
84
- ### Added
85
-
86
- - **Import assertions read Python, Go, Rust and C#**, alongside JavaScript and
87
- TypeScript. `import a.b` / `from .rel import x`, `import ( ... )` groups,
88
- `use a::{b, c}` with nested expansion, `using static` and `global using`, and
89
- the dynamic forms (`importlib.import_module`) that can only be reported.
90
- Not four new tokenizers: the comment and string lexer from ADR-0006 masks the
91
- source and the readers work on what is left. Tree-sitter measured 94 MB
92
- unpacked against this package's 0.33 MB. See
93
- [ADR-0008](docs/adr/0008-polyglot-imports.md).
94
- - **`baseline` and `ratchet`** on the absence assertions, for adopting a strict
95
- rule on a codebase that already breaks it. `baseline="src/legacy/a.ts:2"`
96
- names the debt by file and count. The ratchet is two-sided: new violations
97
- fail, and so does an entry the code no longer supports, because a baseline
98
- that only grows is an `exclude` with extra steps. `ratchet="one-way"` relaxes
99
- the second half. See [ADR-0009](docs/adr/0009-debt-baselines.md).
100
- - **`--print-baseline`** prints the attribute that would exempt today's
101
- violations, for a human to paste. It prints; it does not edit. That is the
102
- whole answer to `--fix`, and ADR-0009 argues it.
103
- - **`--format sarif`** writes SARIF 2.1.0, which GitHub turns into an annotation
104
- on the offending line. One alert per broken rule, anchored on the code, with
105
- the directive as a related location and a fingerprint that survives the code
106
- moving. `--json` is unchanged and is now also `--format json`.
107
- - **`allow-empty` and `--allow-empty-scope`**, for the rules where covering
108
- nothing is the honest state of the world.
109
- - **`baselinedMatches`, `staleBaseline` and `fileMatches`** on each result in
110
- `--json`.
111
-
112
- ### Changed
113
-
114
- - **An assertion that inspected no files now fails.** A rule whose scope holds
115
- nothing passes every time and reads in the report exactly like a rule that
116
- inspected a thousand files and found nothing. The usual causes are a `glob`
117
- matching no extension in the tree, an `exclude` that swallowed the target, or
118
- an emptied directory. **If a run starts failing this way, the rule was
119
- covering nothing before it started failing;** `allow-empty="true"` on the
120
- directive, or `--allow-empty-scope` for the run, restores the old behaviour.
121
- - **An import assertion fails when nothing in scope is in a language it can
122
- read**, rather than reporting "analysed 0 of 12 files" in a warning and
123
- passing.
124
- - **`.py`, `.pyi`, `.go`, `.rs`, `.cs` and `.csx` files are now analysed** by import
125
- assertions rather than counted as skipped, so a rule over a polyglot tree
126
- starts finding dependencies it previously reported as unanalysable.
127
- - **Mutation testing in CI runs in two tiers**: incremental on branches,
128
- a full authoritative sweep on `main` and nightly. The gate stays at 85 in
129
- both. See [ADR-0003](docs/adr/0003-mutation-testing.md).
130
-
131
- ### Fixed
132
-
133
- - `excludeFiles` was applied after enumeration rather than during it, so an
134
- excluded spec file counted towards a budgeted walk. Only reachable through the
135
- adaptive engine's probe, where it could make a tree look larger than the file
136
- set actually being searched.
137
- - The README described the safe-batching apparatus - containment and
138
- dovetailing checks before merging literals into a ripgrep alternation - as
139
- current behaviour. It was deleted in 0.4.0 when ripgrep became a pre-filter.
140
-
141
- ## 0.4.0
142
-
143
- An audit found spec-guard reporting a clean pass on a repository that contained
144
- the forbidden symbol. On a tree with eight copies of one token, the scanner
145
- found two and ripgrep found four, and neither said anything about the rest.
146
-
147
- ### Changed
148
-
149
- - **Hidden directories are searched.** `.github`, `.husky`, `.claude-rules` and
150
- `.agents` hold CI, hooks and agent rules, and both engines skipped them
151
- entirely - so an absence assertion passed while the forbidden thing sat in a
152
- workflow file.
153
- - **The skip list is now four names**: `.git`, `.hg`, `.svn`, `node_modules`.
154
- `dist`, `build`, `out`, `coverage`, `.next` and eight others are searched,
155
- because spec-guard cannot tell build output from a directory of build scripts,
156
- and guessing wrong means a rule silently covers nothing. **If a run starts
157
- failing on your build output, that is this change, and the fix is to say so:**
158
-
159
- ```md
160
- <!-- @assert-absence target="." symbol="TODO" exclude="dist coverage" -->
161
- ```
162
-
163
- - **`.gitignore` is no longer consulted.** It describes what git should carry,
164
- not what a rule covers - and ripgrep applies it only inside a git repository,
165
- so the same tree gave different answers depending on whether a `.git`
166
- directory existed above it.
167
- - **Binary files are searched, and a match in one is reported** rather than
168
- dropped. It is not counted as a violation, but it is no longer invisible.
169
- "Binary" now means "contains a NUL byte" for both engines; the scanner used to
170
- look only at the first 8KB, which disagreed with ripgrep on files whose first
171
- NUL came later.
172
- - **`--strict` fails when a file could not be inspected**, which now has a
173
- precise meaning: unreadable, or binary and containing the symbol.
174
- - **The engines are one implementation.** ripgrep now answers only *which files
175
- contain this text*; the scanner does all counting, comment classification,
176
- binary handling and reporting for both. `--engine` changes how long a run
177
- takes, not what it concludes. See [ADR-0007](docs/adr/0007-search-scope.md).
178
-
179
- ### Added
180
-
181
- - **`--no-default-skips`** to search even those four directories.
182
- - **`skipped`** on each result in `--json`: what was not inspected, and why.
183
-
184
- ### Removed
185
-
186
- - `DEFAULT_IGNORED_DIRECTORIES`, `canBatchLiterals`, `shouldBatchPatterns`,
187
- `createRipgrepSink` and `byteColumnToCharacter` from the public API. All of
188
- them existed to make ripgrep's own counting trustworthy; as a pre-filter it
189
- does not count, so pattern batching no longer has to be proved safe and
190
- ripgrep's byte columns no longer need converting.
191
-
192
- ### Fixed
193
-
194
- - ripgrep's per-file errors are no longer discarded. `--no-messages` meant a
195
- file that could not be opened produced no match, no error, and no way to tell
196
- it apart from one that was read and found clean.
197
- - `comments="include"` no longer changes whether binary files are searched. An
198
- attribute about comments decided that, because it selected a different code
199
- path through the engine.
200
-
201
- ## 0.3.0
202
-
203
- ### Added
204
-
205
- - **`exclude` attribute.** Most real rules are "nowhere except one place", not
206
- "not here": `exclude="src/config/**"` expresses that in one assertion instead
207
- of a hand-maintained list of every directory that is not `src/config`. Follows
208
- gitignore semantics — deliberately different from `glob`'s basename matching —
209
- and a parity matrix asserts both engines agree.
210
- - **Import assertions:** `@assert-import-absence` and `@assert-import-count`.
211
- "The UI layer must not depend on the database layer" is a dependency claim,
212
- not a text claim, and approximating it with a string search fails in both
213
- directions. A zero-dependency tokenizer resolves `import`, `export … from`,
214
- `import type`, dynamic `import()` with a literal, and `require()`. Module
215
- references it cannot resolve statically are reported, never counted as clean.
216
- See [ADR-0005](docs/adr/0005-import-assertions.md).
217
- - **`comments` attribute.** See Changed, below.
218
- - **`--allow-missing-targets`** to restore the previous handling of a `target`
219
- path that does not exist.
220
- - **`commentMatches` and `unclassifiedFiles`** on every result in `--json`.
221
-
222
- ### Changed
223
-
224
- - **Matches inside comments no longer count**, so the note recording that a
225
- symbol was deleted is no longer read as an occurrence of that symbol. This is
226
- the defect where documenting a decision defeated the assertion enforcing it.
227
- Comment syntax is known for 59 extensions across 9 families; strings are
228
- tracked too, since `//` inside a URL is not a comment. Where classification is
229
- uncertain the text counts as code, and every run reports how many matches it
230
- excluded — a pass caused by comment exclusion is never silent.
231
- `comments="include"` restores counting per assertion.
232
- See [ADR-0006](docs/adr/0006-comment-classification.md).
233
- - **A `target` path that does not exist now fails the run** instead of warning
234
- and searching what remained. An assertion pointed at a renamed directory used
235
- to search nothing, find nothing, and report success. `--allow-missing-targets`
236
- restores the old behaviour.
237
- - **`--strict`** now means "treat analysis that could not be completed as a
238
- failure" — it no longer carries the missing-target meaning, which is now the
239
- default.
240
-
241
- ### Fixed
242
-
243
- - `search()` on the ripgrep engine was not comment-aware while `searchBatch()`
244
- was, so a spec containing exactly one assertion kept its comment matches.
245
- - The two engines reported different columns for the same match on a line
246
- containing non-ASCII text: ripgrep counts bytes, the scanner counts
247
- characters, and editors count characters. ripgrep's column is now converted,
248
- so a reported location points at the match in both engines.
249
- - Leftover `*.test.ts` files in scratch fixture directories were collected by
250
- Vitest, breaking a run with "no test suite found" because of debris from a
251
- previous one.
252
-
253
- ### Notes
254
-
255
- The version in `package.json` was bumped to 0.3.0 during development but never
256
- published; npm went 0.1.0 -> 0.2.0. Everything above ships together as the
257
- first published 0.3.0.
258
-
259
- ## 0.2.0
260
-
261
- ### Added
262
-
263
- - **Adaptive engine selection.** `--engine auto` now measures rather than
264
- guesses: a budgeted, stat-only enumeration decides whether a tree is small
265
- enough for the JavaScript scanner to beat a ripgrep process spawn. The probe
266
- is the work — the enumeration it performs is reused by the scanner, so
267
- choosing costs nothing when the scanner wins.
268
- See [ADR-0004](docs/adr/0004-adaptive-engine.md).
269
- - **Mutation testing** with Stryker, wired into CI on every push, plus the tests
270
- that closed the gaps it exposed. See [ADR-0003](docs/adr/0003-mutation-testing.md).
271
-
272
- ### Changed
273
-
274
- - Published as `@descent-vtt/spec-guard`. The binary is still plain
275
- `spec-guard`.
276
-
277
- ## 0.1.0
278
-
279
- Initial release: `@assert-absence`, `@assert-count` and `@assert-present`,
280
- written as HTML comments that are invisible in every Markdown renderer. ripgrep
281
- primary with a pure-JavaScript fallback of identical semantics, safe assertion
282
- batching, `--json`, and exit code 2 for "spec-guard could not run" as distinct
283
- from "your specs failed".
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Versions follow
4
+ [semantic versioning](https://semver.org), with the 0.x caveat that behaviour
5
+ may change in a minor release — each such change is listed under **Changed**
6
+ with the flag that restores the previous behaviour.
7
+
8
+ ## Unreleased
9
+
10
+ ### Added
11
+
12
+ - **`@assert-layers`** - one directive for a layered architecture.
13
+ `order="src/domain, src/application, src/infrastructure"` runs from the layer
14
+ everything may depend on to the layer that may depend on everything; a file
15
+ importing from a layer listed after its own is a violation, reported with both
16
+ layers and the import. Layers are patterns in the `module=` language, so a bare
17
+ `domain` holds Python, Go, Rust and C# code to the same order as TypeScript.
18
+ A layer that matches no file, a file two layers both claim, and a missing
19
+ target all fail rather than pass; files no layer claims are counted in a
20
+ warning. Takes `max`, `types`, `exclude`, `allow-empty` and a debt `baseline`.
21
+ - **`@assert-import-cycle`** - import cycles, counted as strongly connected
22
+ components rather than simple cycles, so a second route around an existing
23
+ knot does not move the count. Each is shown as its shortest loop with the line
24
+ of every import on it. Found with an iterative Tarjan's algorithm: the
25
+ recursive form overflows Node's default stack at 10,000 files.
26
+ `types="ignore"` asks the runtime question. JavaScript and TypeScript only,
27
+ because a cycle needs to know which file an import *is*; imports that should
28
+ have become an edge and did not are reported, and fail under `--strict`.
29
+ See [ADR-0011](docs/adr/0011-layers-and-cycles.md) for the resolution table,
30
+ and for why the other four languages are not in the graph.
31
+ - `buildGraph`, `resolveReference`, `stronglyConnected`, `cyclicComponents`,
32
+ `witness`, `checkLayers` and `referenceForms` are exported, with their types.
33
+
34
+ ### Changed
35
+
36
+ - **Mutation score 97.97%** over 5,078 mutants, up from 0.6.0's 97.77%. The two
37
+ new modules are at 99.51% (`graph.ts`) and 96.43% (`layers.ts`), and each of
38
+ their three survivors was shown equivalent by running the mutated build
39
+ against the real one on thousands of random inputs. The first sweep of this
40
+ work found 34 survivors that 37 hand-written negative controls had not; see
41
+ [ADR-0003](docs/adr/0003-mutation-testing.md) for how they were replayed from
42
+ the report and killed.
43
+ - The SARIF fingerprint of an assertion that names neither a symbol nor a file
44
+ now includes its description. Only the two new kinds are affected; every
45
+ existing alert keeps its identity. Without it, two cycle rules on one target
46
+ would have been merged into one alert.
47
+
48
+ ## 0.6.0
49
+
50
+ ADRs have a life, and spec-guard now reads it. A proposed ADR can carry live
51
+ assertions without breaking the build, and a superseded one can stay on disk
52
+ without enforcing the rule it records being replaced. Most of the work is in
53
+ making sure a rule that stops running can never look like a rule that passed.
54
+
55
+ ### Added
56
+
57
+ - **Document lifecycle status.** A Markdown document whose status is `draft`,
58
+ `proposed`, `rejected`, `deprecated` or `superseded` is parsed, validated,
59
+ reported by name - and not executed. A proposed ADR can now be written with
60
+ its assertions live and the build green, and a superseded ADR can stay on
61
+ disk, intact, enforcing nothing. Three spellings are read, because three are
62
+ in use: MADR front-matter (quoted, as MADR's template writes it, or not), a
63
+ Nygard `## Status` section, and a bold `**Status:**` label in the preamble.
64
+ CRLF documents read the same as LF ones. Anything else - an unrecognised
65
+ word, a misspelling, no status at all - keeps enforcing, which is the
66
+ direction that cannot turn a typo into a silently disabled rule.
67
+
68
+ Withholding is never quiet. Every withheld document is named in the human
69
+ report (`○ docs/adr/0011.md is Proposed. - 2 assertions not executed`), in
70
+ `--format json` as `inactiveSpecs`, and in `--format sarif` as a note-level
71
+ execution notification - because a rule that has gone quiet is otherwise
72
+ indistinguishable from a rule that passed. Directives in a withheld document
73
+ are still checked for typos, bad attributes and unresolvable values, so a
74
+ draft's mistake is found on the day it is written rather than on the day
75
+ everyone agrees the rule is right and stops looking at it.
76
+ See [ADR-0010](docs/adr/0010-spec-status.md), which also records why there is
77
+ no per-directive `if-status` attribute.
78
+ - `--ignore-status` executes every directive whatever its document declares -
79
+ how to ask whether a draft would pass if you accepted it today.
80
+ - `parseStatus`, `INACTIVE_STATUSES`, and the `SpecStatus` / `InactiveSpec`
81
+ types are exported from the programmatic API.
82
+
83
+ ### Changed
84
+
85
+ - **A run that executed no assertions no longer reports that every assertion
86
+ holds.** It says `no assertion was executed, so nothing was verified`. True
87
+ and useless was the old sentence's problem: it is the exact line someone
88
+ reads as proof their specification is being enforced. Reachable before this
89
+ release through a spec file with no directives in it; withholding made it
90
+ easy to reach.
91
+ - `RunSummary` gains `inactive` and `RunReport` gains `inactiveSpecs`. Both are
92
+ always present; consumers deep-comparing a summary object will see the new
93
+ field.
94
+ - **Mutation score 97.77%** over 4,482 mutants, against the gate of 97 - up
95
+ from the 97.65% 0.5.1 shipped with, and no survivor in the new code. Getting there turned up a way a local sweep overstates a score
96
+ that the timeout warning could not see: a regex constant at module scope runs
97
+ the whole suite per mutant, so on a loaded machine an unrelated test timing
98
+ out is scored as a kill. Those patterns are now verified by applying every
99
+ mutant by hand, `scripts/mutation-regex.mjs`, and the account is in
100
+ [ADR-0003](docs/adr/0003-mutation-testing.md).
101
+
102
+ ## 0.5.1
103
+
104
+ Writing down what the code already claimed. Every module was driven to its
105
+ honest mutation-testing ceiling, and the exercise turned up six wrong answers
106
+ rather than six missing tests — which is the argument for doing it at all. No
107
+ new features, and nothing a user has to change.
108
+
109
+ ### Fixed
110
+
111
+ - **Two assertions on the same symbol answered each other's comment handling.**
112
+ The result cache keyed on the symbol, the targets and the search flags, but
113
+ not on `comments`. A spec with `<!-- @assert-count symbol="X" ... -->` above
114
+ `<!-- @assert-count symbol="X" ... comments="include" -->` returned whichever
115
+ count ran first for both. The grouping test had the same omission, so the two
116
+ were also merged into a single pass and the second was scanned with the
117
+ first's mask. There is now one definition of "the same question", in three
118
+ nested scopes, and the scope policy is part of it too.
119
+ - **An unreadable directory went unreported on a small repository.** The
120
+ adaptive engine had the walk's skip ledger in hand and returned without it on
121
+ any tree small enough to scan in process, so the same directory was a reported
122
+ gap on a large repository and silence on a small one. Same tree, two answers,
123
+ decided by its size.
124
+ - **Snippets from CRLF files carried a carriage return into the report**, which
125
+ returns the terminal cursor to column 0 and overwrites the line just printed.
126
+ The trim looked for `\r?\n` at the end of a line the caller had already cut at
127
+ the newline, so it could never match.
128
+ - **A bad `regex="true"` pattern was reported as `Invalid regular expression:
129
+ Invalid regular expression: /(/: ...`** — V8's message already says it once.
130
+
131
+ ### Changed
132
+
133
+ - **Two requests differing only in `scope` are no longer merged into one
134
+ search pass.** Reachable today only through the programmatic API, where a
135
+ caller may build more than one `ScopePolicy` per run.
136
+ - `enumerateCandidates` takes a `WalkRequest` — a `SearchRequest` without the
137
+ `symbol`, because a walk does not depend on one. Existing callers are
138
+ unaffected; the symbol is now optional rather than required.
139
+ - `CachedEngine` declares `searchBatch` as present rather than optional, which
140
+ `createCachedEngine` has always guaranteed.
141
+ - `KINDS` and `ALLOWED_ATTRIBUTES` are exported from the parser, so the
142
+ directive grammar can be asserted rather than restated.
143
+ - **The mutation gate moves from 85 to 97**, against a CI measurement of 97.65%
144
+ over 4,299 mutants. Every module is above 95% and three are at 100%, where
145
+ 0.5.0 ran from 73% to 96%. The 101 mutants still alive have each been applied
146
+ individually and produce byte-identical output, so that figure is the ceiling
147
+ rather than a way-point — and 29 that were assumed equivalent turned out not
148
+ to be, and are now tested. See
149
+ [ADR-0003](docs/adr/0003-mutation-testing.md), which also records a Stryker
150
+ limitation found on the way: a mutant that stops a test file *loading* is
151
+ reported as survived even though the suite is killing it.
152
+ - **`src/imports.ts` went from 73.74% to 95.89%.** The tokenizer was tested as a
153
+ step towards a list of module references rather than as a thing with an
154
+ output, so a template resumption that lost its place, a regex escape that
155
+ skipped a character, and three lookup tables were all unpinned. One
156
+ behaviour-preserving change came with it: the word branch starts its scan one
157
+ character in, so progress is unconditional by construction rather than a
158
+ consequence of a relation nothing enforced. Verified identical on 53.6 MB of
159
+ real JavaScript.
160
+
161
+ ### Internal
162
+
163
+ Not user-visible, but this is where the release's weight is: nine new test
164
+ files, 1,770 tests against 1,206 at 0.5.0, 100% line coverage, and the analyser
165
+ verified byte-identical to the published 0.5.0 across 6,976 files of real code.
166
+ Thirty-five negative controls confirm the suite goes red for each defect above,
167
+ and `scripts/mutation-equivalence.mjs` is in the repository so the equivalence
168
+ measurement stays reproducible.
169
+
170
+ ## 0.5.0
171
+
172
+ Import assertions covered one language, so a dependency rule pointed at a
173
+ directory of Go files did not fail - it passed, having analysed nothing. That is
174
+ the same defect 0.4.0 was written to remove, and this release closes the two
175
+ remaining shapes of it: a rule that cannot read the language, and a rule whose
176
+ scope holds no files at all.
177
+
178
+ ### Added
179
+
180
+ - **Import assertions read Python, Go, Rust and C#**, alongside JavaScript and
181
+ TypeScript. `import a.b` / `from .rel import x`, `import ( ... )` groups,
182
+ `use a::{b, c}` with nested expansion, `using static` and `global using`, and
183
+ the dynamic forms (`importlib.import_module`) that can only be reported.
184
+ Not four new tokenizers: the comment and string lexer from ADR-0006 masks the
185
+ source and the readers work on what is left. Tree-sitter measured 94 MB
186
+ unpacked against this package's 0.33 MB. See
187
+ [ADR-0008](docs/adr/0008-polyglot-imports.md).
188
+ - **`baseline` and `ratchet`** on the absence assertions, for adopting a strict
189
+ rule on a codebase that already breaks it. `baseline="src/legacy/a.ts:2"`
190
+ names the debt by file and count. The ratchet is two-sided: new violations
191
+ fail, and so does an entry the code no longer supports, because a baseline
192
+ that only grows is an `exclude` with extra steps. `ratchet="one-way"` relaxes
193
+ the second half. See [ADR-0009](docs/adr/0009-debt-baselines.md).
194
+ - **`--print-baseline`** prints the attribute that would exempt today's
195
+ violations, for a human to paste. It prints; it does not edit. That is the
196
+ whole answer to `--fix`, and ADR-0009 argues it.
197
+ - **`--format sarif`** writes SARIF 2.1.0, which GitHub turns into an annotation
198
+ on the offending line. One alert per broken rule, anchored on the code, with
199
+ the directive as a related location and a fingerprint that survives the code
200
+ moving. `--json` is unchanged and is now also `--format json`.
201
+ - **`allow-empty` and `--allow-empty-scope`**, for the rules where covering
202
+ nothing is the honest state of the world.
203
+ - **`baselinedMatches`, `staleBaseline` and `fileMatches`** on each result in
204
+ `--json`.
205
+
206
+ ### Changed
207
+
208
+ - **An assertion that inspected no files now fails.** A rule whose scope holds
209
+ nothing passes every time and reads in the report exactly like a rule that
210
+ inspected a thousand files and found nothing. The usual causes are a `glob`
211
+ matching no extension in the tree, an `exclude` that swallowed the target, or
212
+ an emptied directory. **If a run starts failing this way, the rule was
213
+ covering nothing before it started failing;** `allow-empty="true"` on the
214
+ directive, or `--allow-empty-scope` for the run, restores the old behaviour.
215
+ - **An import assertion fails when nothing in scope is in a language it can
216
+ read**, rather than reporting "analysed 0 of 12 files" in a warning and
217
+ passing.
218
+ - **`.py`, `.pyi`, `.go`, `.rs`, `.cs` and `.csx` files are now analysed** by import
219
+ assertions rather than counted as skipped, so a rule over a polyglot tree
220
+ starts finding dependencies it previously reported as unanalysable.
221
+ - **Mutation testing in CI runs in two tiers**: incremental on branches,
222
+ a full authoritative sweep on `main` and nightly. The gate stays at 85 in
223
+ both. See [ADR-0003](docs/adr/0003-mutation-testing.md).
224
+
225
+ ### Fixed
226
+
227
+ - `excludeFiles` was applied after enumeration rather than during it, so an
228
+ excluded spec file counted towards a budgeted walk. Only reachable through the
229
+ adaptive engine's probe, where it could make a tree look larger than the file
230
+ set actually being searched.
231
+ - The README described the safe-batching apparatus - containment and
232
+ dovetailing checks before merging literals into a ripgrep alternation - as
233
+ current behaviour. It was deleted in 0.4.0 when ripgrep became a pre-filter.
234
+
235
+ ## 0.4.0
236
+
237
+ An audit found spec-guard reporting a clean pass on a repository that contained
238
+ the forbidden symbol. On a tree with eight copies of one token, the scanner
239
+ found two and ripgrep found four, and neither said anything about the rest.
240
+
241
+ ### Changed
242
+
243
+ - **Hidden directories are searched.** `.github`, `.husky`, `.claude-rules` and
244
+ `.agents` hold CI, hooks and agent rules, and both engines skipped them
245
+ entirely - so an absence assertion passed while the forbidden thing sat in a
246
+ workflow file.
247
+ - **The skip list is now four names**: `.git`, `.hg`, `.svn`, `node_modules`.
248
+ `dist`, `build`, `out`, `coverage`, `.next` and eight others are searched,
249
+ because spec-guard cannot tell build output from a directory of build scripts,
250
+ and guessing wrong means a rule silently covers nothing. **If a run starts
251
+ failing on your build output, that is this change, and the fix is to say so:**
252
+
253
+ ```md
254
+ <!-- @assert-absence target="." symbol="TODO" exclude="dist coverage" -->
255
+ ```
256
+
257
+ - **`.gitignore` is no longer consulted.** It describes what git should carry,
258
+ not what a rule covers - and ripgrep applies it only inside a git repository,
259
+ so the same tree gave different answers depending on whether a `.git`
260
+ directory existed above it.
261
+ - **Binary files are searched, and a match in one is reported** rather than
262
+ dropped. It is not counted as a violation, but it is no longer invisible.
263
+ "Binary" now means "contains a NUL byte" for both engines; the scanner used to
264
+ look only at the first 8KB, which disagreed with ripgrep on files whose first
265
+ NUL came later.
266
+ - **`--strict` fails when a file could not be inspected**, which now has a
267
+ precise meaning: unreadable, or binary and containing the symbol.
268
+ - **The engines are one implementation.** ripgrep now answers only *which files
269
+ contain this text*; the scanner does all counting, comment classification,
270
+ binary handling and reporting for both. `--engine` changes how long a run
271
+ takes, not what it concludes. See [ADR-0007](docs/adr/0007-search-scope.md).
272
+
273
+ ### Added
274
+
275
+ - **`--no-default-skips`** to search even those four directories.
276
+ - **`skipped`** on each result in `--json`: what was not inspected, and why.
277
+
278
+ ### Removed
279
+
280
+ - `DEFAULT_IGNORED_DIRECTORIES`, `canBatchLiterals`, `shouldBatchPatterns`,
281
+ `createRipgrepSink` and `byteColumnToCharacter` from the public API. All of
282
+ them existed to make ripgrep's own counting trustworthy; as a pre-filter it
283
+ does not count, so pattern batching no longer has to be proved safe and
284
+ ripgrep's byte columns no longer need converting.
285
+
286
+ ### Fixed
287
+
288
+ - ripgrep's per-file errors are no longer discarded. `--no-messages` meant a
289
+ file that could not be opened produced no match, no error, and no way to tell
290
+ it apart from one that was read and found clean.
291
+ - `comments="include"` no longer changes whether binary files are searched. An
292
+ attribute about comments decided that, because it selected a different code
293
+ path through the engine.
294
+
295
+ ## 0.3.0
296
+
297
+ ### Added
298
+
299
+ - **`exclude` attribute.** Most real rules are "nowhere except one place", not
300
+ "not here": `exclude="src/config/**"` expresses that in one assertion instead
301
+ of a hand-maintained list of every directory that is not `src/config`. Follows
302
+ gitignore semantics — deliberately different from `glob`'s basename matching —
303
+ and a parity matrix asserts both engines agree.
304
+ - **Import assertions:** `@assert-import-absence` and `@assert-import-count`.
305
+ "The UI layer must not depend on the database layer" is a dependency claim,
306
+ not a text claim, and approximating it with a string search fails in both
307
+ directions. A zero-dependency tokenizer resolves `import`, `export … from`,
308
+ `import type`, dynamic `import()` with a literal, and `require()`. Module
309
+ references it cannot resolve statically are reported, never counted as clean.
310
+ See [ADR-0005](docs/adr/0005-import-assertions.md).
311
+ - **`comments` attribute.** See Changed, below.
312
+ - **`--allow-missing-targets`** to restore the previous handling of a `target`
313
+ path that does not exist.
314
+ - **`commentMatches` and `unclassifiedFiles`** on every result in `--json`.
315
+
316
+ ### Changed
317
+
318
+ - **Matches inside comments no longer count**, so the note recording that a
319
+ symbol was deleted is no longer read as an occurrence of that symbol. This is
320
+ the defect where documenting a decision defeated the assertion enforcing it.
321
+ Comment syntax is known for 59 extensions across 9 families; strings are
322
+ tracked too, since `//` inside a URL is not a comment. Where classification is
323
+ uncertain the text counts as code, and every run reports how many matches it
324
+ excluded — a pass caused by comment exclusion is never silent.
325
+ `comments="include"` restores counting per assertion.
326
+ See [ADR-0006](docs/adr/0006-comment-classification.md).
327
+ - **A `target` path that does not exist now fails the run** instead of warning
328
+ and searching what remained. An assertion pointed at a renamed directory used
329
+ to search nothing, find nothing, and report success. `--allow-missing-targets`
330
+ restores the old behaviour.
331
+ - **`--strict`** now means "treat analysis that could not be completed as a
332
+ failure" — it no longer carries the missing-target meaning, which is now the
333
+ default.
334
+
335
+ ### Fixed
336
+
337
+ - `search()` on the ripgrep engine was not comment-aware while `searchBatch()`
338
+ was, so a spec containing exactly one assertion kept its comment matches.
339
+ - The two engines reported different columns for the same match on a line
340
+ containing non-ASCII text: ripgrep counts bytes, the scanner counts
341
+ characters, and editors count characters. ripgrep's column is now converted,
342
+ so a reported location points at the match in both engines.
343
+ - Leftover `*.test.ts` files in scratch fixture directories were collected by
344
+ Vitest, breaking a run with "no test suite found" because of debris from a
345
+ previous one.
346
+
347
+ ### Notes
348
+
349
+ The version in `package.json` was bumped to 0.3.0 during development but never
350
+ published; npm went 0.1.0 -> 0.2.0. Everything above ships together as the
351
+ first published 0.3.0.
352
+
353
+ ## 0.2.0
354
+
355
+ ### Added
356
+
357
+ - **Adaptive engine selection.** `--engine auto` now measures rather than
358
+ guesses: a budgeted, stat-only enumeration decides whether a tree is small
359
+ enough for the JavaScript scanner to beat a ripgrep process spawn. The probe
360
+ is the work — the enumeration it performs is reused by the scanner, so
361
+ choosing costs nothing when the scanner wins.
362
+ See [ADR-0004](docs/adr/0004-adaptive-engine.md).
363
+ - **Mutation testing** with Stryker, wired into CI on every push, plus the tests
364
+ that closed the gaps it exposed. See [ADR-0003](docs/adr/0003-mutation-testing.md).
365
+
366
+ ### Changed
367
+
368
+ - Published as `@descent-vtt/spec-guard`. The binary is still plain
369
+ `spec-guard`.
370
+
371
+ ## 0.1.0
372
+
373
+ Initial release: `@assert-absence`, `@assert-count` and `@assert-present`,
374
+ written as HTML comments that are invisible in every Markdown renderer. ripgrep
375
+ primary with a pure-JavaScript fallback of identical semantics, safe assertion
376
+ batching, `--json`, and exit code 2 for "spec-guard could not run" as distinct
377
+ from "your specs failed".