@descent-vtt/spec-guard 0.3.0 → 0.5.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 +125 -0
- package/README.md +292 -49
- package/dist/cli.d.ts +9 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +46 -7
- package/dist/cli.js.map +1 -1
- package/dist/comments.d.ts +23 -4
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +40 -14
- package/dist/comments.js.map +1 -1
- package/dist/engine.d.ts +52 -49
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +185 -259
- package/dist/engine.js.map +1 -1
- package/dist/glob.d.ts +19 -6
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +14 -28
- package/dist/glob.js.map +1 -1
- package/dist/imports.d.ts +28 -4
- package/dist/imports.d.ts.map +1 -1
- package/dist/imports.js +36 -4
- package/dist/imports.js.map +1 -1
- package/dist/index.d.ts +15 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +28 -23
- package/dist/parser.js.map +1 -1
- package/dist/polyglot.d.ts +129 -0
- package/dist/polyglot.d.ts.map +1 -0
- package/dist/polyglot.js +544 -0
- package/dist/polyglot.js.map +1 -0
- package/dist/reporter.d.ts +41 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +187 -4
- package/dist/reporter.js.map +1 -1
- package/dist/runner.d.ts +47 -3
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +371 -105
- package/dist/runner.js.map +1 -1
- package/dist/scope.d.ts +108 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +115 -0
- package/dist/scope.js.map +1 -0
- package/dist/text.d.ts +26 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +69 -0
- package/dist/text.js.map +1 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,131 @@ All notable changes to this project are documented here. Versions follow
|
|
|
5
5
|
may change in a minor release — each such change is listed under **Changed**
|
|
6
6
|
with the flag that restores the previous behaviour.
|
|
7
7
|
|
|
8
|
+
## 0.5.0
|
|
9
|
+
|
|
10
|
+
Import assertions covered one language, so a dependency rule pointed at a
|
|
11
|
+
directory of Go files did not fail - it passed, having analysed nothing. That is
|
|
12
|
+
the same defect 0.4.0 was written to remove, and this release closes the two
|
|
13
|
+
remaining shapes of it: a rule that cannot read the language, and a rule whose
|
|
14
|
+
scope holds no files at all.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Import assertions read Python, Go, Rust and C#**, alongside JavaScript and
|
|
19
|
+
TypeScript. `import a.b` / `from .rel import x`, `import ( ... )` groups,
|
|
20
|
+
`use a::{b, c}` with nested expansion, `using static` and `global using`, and
|
|
21
|
+
the dynamic forms (`importlib.import_module`) that can only be reported.
|
|
22
|
+
Not four new tokenizers: the comment and string lexer from ADR-0006 masks the
|
|
23
|
+
source and the readers work on what is left. Tree-sitter measured 94 MB
|
|
24
|
+
unpacked against this package's 0.33 MB. See
|
|
25
|
+
[ADR-0008](docs/adr/0008-polyglot-imports.md).
|
|
26
|
+
- **`baseline` and `ratchet`** on the absence assertions, for adopting a strict
|
|
27
|
+
rule on a codebase that already breaks it. `baseline="src/legacy/a.ts:2"`
|
|
28
|
+
names the debt by file and count. The ratchet is two-sided: new violations
|
|
29
|
+
fail, and so does an entry the code no longer supports, because a baseline
|
|
30
|
+
that only grows is an `exclude` with extra steps. `ratchet="one-way"` relaxes
|
|
31
|
+
the second half. See [ADR-0009](docs/adr/0009-debt-baselines.md).
|
|
32
|
+
- **`--print-baseline`** prints the attribute that would exempt today's
|
|
33
|
+
violations, for a human to paste. It prints; it does not edit. That is the
|
|
34
|
+
whole answer to `--fix`, and ADR-0009 argues it.
|
|
35
|
+
- **`--format sarif`** writes SARIF 2.1.0, which GitHub turns into an annotation
|
|
36
|
+
on the offending line. One alert per broken rule, anchored on the code, with
|
|
37
|
+
the directive as a related location and a fingerprint that survives the code
|
|
38
|
+
moving. `--json` is unchanged and is now also `--format json`.
|
|
39
|
+
- **`allow-empty` and `--allow-empty-scope`**, for the rules where covering
|
|
40
|
+
nothing is the honest state of the world.
|
|
41
|
+
- **`baselinedMatches`, `staleBaseline` and `fileMatches`** on each result in
|
|
42
|
+
`--json`.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **An assertion that inspected no files now fails.** A rule whose scope holds
|
|
47
|
+
nothing passes every time and reads in the report exactly like a rule that
|
|
48
|
+
inspected a thousand files and found nothing. The usual causes are a `glob`
|
|
49
|
+
matching no extension in the tree, an `exclude` that swallowed the target, or
|
|
50
|
+
an emptied directory. **If a run starts failing this way, the rule was
|
|
51
|
+
covering nothing before it started failing;** `allow-empty="true"` on the
|
|
52
|
+
directive, or `--allow-empty-scope` for the run, restores the old behaviour.
|
|
53
|
+
- **An import assertion fails when nothing in scope is in a language it can
|
|
54
|
+
read**, rather than reporting "analysed 0 of 12 files" in a warning and
|
|
55
|
+
passing.
|
|
56
|
+
- **`.py`, `.pyi`, `.go`, `.rs`, `.cs` and `.csx` files are now analysed** by import
|
|
57
|
+
assertions rather than counted as skipped, so a rule over a polyglot tree
|
|
58
|
+
starts finding dependencies it previously reported as unanalysable.
|
|
59
|
+
- **Mutation testing in CI runs in two tiers**: incremental on branches,
|
|
60
|
+
a full authoritative sweep on `main` and nightly. The gate stays at 85 in
|
|
61
|
+
both. See [ADR-0003](docs/adr/0003-mutation-testing.md).
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- `excludeFiles` was applied after enumeration rather than during it, so an
|
|
66
|
+
excluded spec file counted towards a budgeted walk. Only reachable through the
|
|
67
|
+
adaptive engine's probe, where it could make a tree look larger than the file
|
|
68
|
+
set actually being searched.
|
|
69
|
+
- The README described the safe-batching apparatus - containment and
|
|
70
|
+
dovetailing checks before merging literals into a ripgrep alternation - as
|
|
71
|
+
current behaviour. It was deleted in 0.4.0 when ripgrep became a pre-filter.
|
|
72
|
+
|
|
73
|
+
## 0.4.0
|
|
74
|
+
|
|
75
|
+
An audit found spec-guard reporting a clean pass on a repository that contained
|
|
76
|
+
the forbidden symbol. On a tree with eight copies of one token, the scanner
|
|
77
|
+
found two and ripgrep found four, and neither said anything about the rest.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- **Hidden directories are searched.** `.github`, `.husky`, `.claude-rules` and
|
|
82
|
+
`.agents` hold CI, hooks and agent rules, and both engines skipped them
|
|
83
|
+
entirely - so an absence assertion passed while the forbidden thing sat in a
|
|
84
|
+
workflow file.
|
|
85
|
+
- **The skip list is now four names**: `.git`, `.hg`, `.svn`, `node_modules`.
|
|
86
|
+
`dist`, `build`, `out`, `coverage`, `.next` and eight others are searched,
|
|
87
|
+
because spec-guard cannot tell build output from a directory of build scripts,
|
|
88
|
+
and guessing wrong means a rule silently covers nothing. **If a run starts
|
|
89
|
+
failing on your build output, that is this change, and the fix is to say so:**
|
|
90
|
+
|
|
91
|
+
```md
|
|
92
|
+
<!-- @assert-absence target="." symbol="TODO" exclude="dist coverage" -->
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **`.gitignore` is no longer consulted.** It describes what git should carry,
|
|
96
|
+
not what a rule covers - and ripgrep applies it only inside a git repository,
|
|
97
|
+
so the same tree gave different answers depending on whether a `.git`
|
|
98
|
+
directory existed above it.
|
|
99
|
+
- **Binary files are searched, and a match in one is reported** rather than
|
|
100
|
+
dropped. It is not counted as a violation, but it is no longer invisible.
|
|
101
|
+
"Binary" now means "contains a NUL byte" for both engines; the scanner used to
|
|
102
|
+
look only at the first 8KB, which disagreed with ripgrep on files whose first
|
|
103
|
+
NUL came later.
|
|
104
|
+
- **`--strict` fails when a file could not be inspected**, which now has a
|
|
105
|
+
precise meaning: unreadable, or binary and containing the symbol.
|
|
106
|
+
- **The engines are one implementation.** ripgrep now answers only *which files
|
|
107
|
+
contain this text*; the scanner does all counting, comment classification,
|
|
108
|
+
binary handling and reporting for both. `--engine` changes how long a run
|
|
109
|
+
takes, not what it concludes. See [ADR-0007](docs/adr/0007-search-scope.md).
|
|
110
|
+
|
|
111
|
+
### Added
|
|
112
|
+
|
|
113
|
+
- **`--no-default-skips`** to search even those four directories.
|
|
114
|
+
- **`skipped`** on each result in `--json`: what was not inspected, and why.
|
|
115
|
+
|
|
116
|
+
### Removed
|
|
117
|
+
|
|
118
|
+
- `DEFAULT_IGNORED_DIRECTORIES`, `canBatchLiterals`, `shouldBatchPatterns`,
|
|
119
|
+
`createRipgrepSink` and `byteColumnToCharacter` from the public API. All of
|
|
120
|
+
them existed to make ripgrep's own counting trustworthy; as a pre-filter it
|
|
121
|
+
does not count, so pattern batching no longer has to be proved safe and
|
|
122
|
+
ripgrep's byte columns no longer need converting.
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
- ripgrep's per-file errors are no longer discarded. `--no-messages` meant a
|
|
127
|
+
file that could not be opened produced no match, no error, and no way to tell
|
|
128
|
+
it apart from one that was read and found clean.
|
|
129
|
+
- `comments="include"` no longer changes whether binary files are searched. An
|
|
130
|
+
attribute about comments decided that, because it selected a different code
|
|
131
|
+
path through the engine.
|
|
132
|
+
|
|
8
133
|
## 0.3.0
|
|
9
134
|
|
|
10
135
|
### Added
|
package/README.md
CHANGED
|
@@ -199,6 +199,53 @@ an unknown extension, an unterminated literal — the text counts as code, and t
|
|
|
199
199
|
report says which files it could not classify. A match wrongly kept is a visible
|
|
200
200
|
failure you can argue with; a match wrongly dropped is a lie.
|
|
201
201
|
|
|
202
|
+
## What gets searched
|
|
203
|
+
|
|
204
|
+
An assertion is worth exactly as much as the set of files behind it, so
|
|
205
|
+
spec-guard is explicit about that set and never quietly narrows it.
|
|
206
|
+
|
|
207
|
+
**Four directory names are skipped**, and nothing else:
|
|
208
|
+
|
|
209
|
+
| Skipped | Why |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `.git`, `.hg`, `.svn` | version-control stores hold compressed copies of code you deleted on purpose |
|
|
212
|
+
| `node_modules` | code you did not write, which your architecture rules are not about |
|
|
213
|
+
|
|
214
|
+
Everything else is searched. That includes **hidden directories** - `.github`,
|
|
215
|
+
`.husky`, `.claude-rules`, `.agents` - because that is where CI, hooks and agent
|
|
216
|
+
rules live, and a rule that cannot see your workflow files is not enforcing much.
|
|
217
|
+
It also includes `dist`, `build`, `out` and `coverage`, because spec-guard
|
|
218
|
+
cannot tell build output from a directory of build scripts, and guessing wrong
|
|
219
|
+
means a rule silently stops covering anything.
|
|
220
|
+
|
|
221
|
+
`.gitignore` is not consulted. It describes what git should carry, not what a
|
|
222
|
+
rule covers - and ripgrep applies it only inside a git repository, so honouring
|
|
223
|
+
it made the same tree answer differently depending on whether a `.git` directory
|
|
224
|
+
happened to exist above it.
|
|
225
|
+
|
|
226
|
+
To narrow scope, say so in the assertion:
|
|
227
|
+
|
|
228
|
+
```md
|
|
229
|
+
<!-- @assert-absence target="src" symbol="TODO" exclude="dist coverage" -->
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`--no-default-skips` removes even those four, for a run that has to be certain.
|
|
233
|
+
|
|
234
|
+
### Nothing is skipped quietly
|
|
235
|
+
|
|
236
|
+
A file spec-guard could not read, or one whose bytes are not text but which
|
|
237
|
+
contained the symbol anyway, is a gap in the answer rather than a detail of it.
|
|
238
|
+
Those are reported, and `--strict` fails on them:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
✖ docs/adr.md:3 @assert-absence
|
|
242
|
+
"ApiKey" must not appear in .
|
|
243
|
+
expected no matches, found 0, and 1 file could not be inspected
|
|
244
|
+
⚠ 1 match in 1 binary file not counted: build/app.bin
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The counts are in `--json` too, as `skipped` on each result.
|
|
248
|
+
|
|
202
249
|
### `@assert-count` - this symbol occurs exactly / at least / at most N times
|
|
203
250
|
|
|
204
251
|
```md
|
|
@@ -234,10 +281,33 @@ matches a mention in a comment, and it misses `export * from '../db'`.
|
|
|
234
281
|
<!-- @assert-import-count target="src" module="axios" max="1" -->
|
|
235
282
|
```
|
|
236
283
|
|
|
237
|
-
These read the dependency rather than the text
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
284
|
+
These read the dependency rather than the text, in **five languages**:
|
|
285
|
+
|
|
286
|
+
| Language | Extensions | Reads |
|
|
287
|
+
| --- | --- | --- |
|
|
288
|
+
| JavaScript / TypeScript | `.js .mjs .cjs .jsx .ts .mts .cts .tsx` | `import`, `export ... from`, `require()`, `import()` |
|
|
289
|
+
| Python | `.py .pyi` | `import a.b`, `from .rel import x`, `importlib.import_module("x")` |
|
|
290
|
+
| Go | `.go` | `import "x"` and grouped `import ( ... )`, including aliases and `_` |
|
|
291
|
+
| Rust | `.rs` | `use a::{b, c}` with nested groups, `pub use`, `extern crate` |
|
|
292
|
+
| C# | `.cs .csx` | `using`, `using static`, `global using`, `using X = A.B` |
|
|
293
|
+
|
|
294
|
+
No parser and no new dependency. JavaScript gets a full tokenizer because a
|
|
295
|
+
module reference can appear anywhere in an expression; the other four are read
|
|
296
|
+
by masking comments and string literals with the classifier from
|
|
297
|
+
[ADR-0006](docs/adr/0006-comment-classification.md) and then reading statements
|
|
298
|
+
off what is left. Tree-sitter would have cost 94 MB unpacked against this
|
|
299
|
+
package's 0.33 MB - [ADR-0008](docs/adr/0008-polyglot-imports.md) has the
|
|
300
|
+
measurements, and the list of things a real parser would genuinely see that
|
|
301
|
+
this does not.
|
|
302
|
+
|
|
303
|
+
Module patterns are matched against the resolved `/`-separated path *and*
|
|
304
|
+
against the specifier as written, so `module="System.Text.Json"` and
|
|
305
|
+
`module="System/Text/Json"` both work. Matching is case-sensitive, so a rule
|
|
306
|
+
spanning C# and Go needs both conventions:
|
|
307
|
+
`module="app/db/** App/Db/**"`.
|
|
308
|
+
|
|
309
|
+
The unit counted is **files** that depend on the module. For JavaScript and
|
|
310
|
+
TypeScript it understands:
|
|
241
311
|
|
|
242
312
|
| Form | Counted |
|
|
243
313
|
| --- | --- |
|
|
@@ -249,7 +319,8 @@ no parser and no new dependency - extracts module references and counts the
|
|
|
249
319
|
| `import(name)`, `require(expr)` | **no - reported, see below** |
|
|
250
320
|
|
|
251
321
|
Relative specifiers are resolved by path arithmetic, so `module="src/db"`
|
|
252
|
-
matches `../db/client.js` seen from `src/ui
|
|
322
|
+
matches `../db/client.js` seen from `src/ui/`, and Python's `from ..core import
|
|
323
|
+
x` resolves against the importing file the same way. There is deliberately no
|
|
253
324
|
filesystem resolution: tsconfig `paths` aliases and package `exports` maps are
|
|
254
325
|
not followed, so an alias is written out as itself (`module="@app/db"`).
|
|
255
326
|
|
|
@@ -266,8 +337,9 @@ are reported rather than counted as clean:
|
|
|
266
337
|
The count is still true of everything that could be seen; the warning is what
|
|
267
338
|
stops it being mistaken for a complete answer. `--strict` turns those warnings
|
|
268
339
|
into failures. Files in scope that are not JavaScript or TypeScript are counted
|
|
269
|
-
and reported too, so a rule pointed at the wrong tree says "analysed
|
|
270
|
-
files" rather than quietly passing
|
|
340
|
+
and reported too, so a rule pointed at the wrong tree says "analysed 2 of 3
|
|
341
|
+
files" rather than quietly passing - and if *none* of them can be read, the
|
|
342
|
+
assertion fails rather than passing on an empty analysis.
|
|
271
343
|
|
|
272
344
|
[ADR-0005](docs/adr/0005-import-assertions.md) has the measurements and the
|
|
273
345
|
reasoning behind each boundary.
|
|
@@ -298,11 +370,77 @@ Passes when every listed path exists relative to `--root`. Directories count.
|
|
|
298
370
|
| `comments` | absence, count | `ignore` (default) or `include` for matches inside comments |
|
|
299
371
|
| `module` | import assertions | Which dependency, matched like `exclude` |
|
|
300
372
|
| `types` | import assertions | `include` (default) or `ignore` for `import type` |
|
|
373
|
+
| `allow-empty` | absence, count, import assertions | Tolerate a scope that holds no files. Off by default - see below |
|
|
374
|
+
| `baseline` | absence, import-absence | Known violations that do not count: `path` or `path:count` |
|
|
375
|
+
| `ratchet` | absence, import-absence | `two-sided` (default) or `one-way` - see below |
|
|
301
376
|
| `reason` | all | Human-readable justification, printed on failure |
|
|
302
377
|
|
|
303
378
|
Unknown attributes are an error, not a shrug: `expct="1"` fails the run instead
|
|
304
379
|
of silently asserting nothing.
|
|
305
380
|
|
|
381
|
+
### `allow-empty` - an assertion that covers nothing is a failure
|
|
382
|
+
|
|
383
|
+
```md
|
|
384
|
+
<!-- @assert-absence target="services" symbol="Legacy" glob="*.ts" -->
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
If `services/` holds no `.ts` file, that rule passes every time, forever,
|
|
388
|
+
without inspecting anything - and in the report it is indistinguishable from a
|
|
389
|
+
rule that inspected a thousand files and found nothing. So it fails instead:
|
|
390
|
+
|
|
391
|
+
```text
|
|
392
|
+
✖ docs/adr.md:3 @assert-absence
|
|
393
|
+
"Legacy" must not appear in services
|
|
394
|
+
no files were inspected, so this assertion verified nothing
|
|
395
|
+
(add allow-empty="true" if that is expected)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The usual causes are a `glob` matching no extension in the tree, an `exclude`
|
|
399
|
+
that swallowed the target, or a directory somebody emptied. Where covering
|
|
400
|
+
nothing yet is the honest state of the world - a rule written before the code
|
|
401
|
+
it guards - `allow-empty="true"` says so, and `--allow-empty-scope` says it for
|
|
402
|
+
a whole run.
|
|
403
|
+
|
|
404
|
+
### `baseline` - adopting a rule the codebase already breaks
|
|
405
|
+
|
|
406
|
+
A strict rule introduced into a mature codebase lands on violations that already
|
|
407
|
+
exist. `exclude` turns those into a permanent blind spot, and `expected="5"`
|
|
408
|
+
cannot tell "one fixed, one added" from "nothing happened". A baseline names
|
|
409
|
+
them:
|
|
410
|
+
|
|
411
|
+
```md
|
|
412
|
+
<!-- @assert-absence target="src" symbol="LegacyGateway"
|
|
413
|
+
baseline="src/legacy/gateway.ts:2
|
|
414
|
+
src/legacy/adapter.ts" -->
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The rule now holds when no file outside the list matches and no file inside it
|
|
418
|
+
matches more than it declares. A bare path means one match.
|
|
419
|
+
|
|
420
|
+
**It ratchets in both directions.** New debt fails, obviously. So does debt that
|
|
421
|
+
has been *paid* and left on the list, because an entry the code no longer
|
|
422
|
+
supports is a spec asserting something untrue:
|
|
423
|
+
|
|
424
|
+
```text
|
|
425
|
+
expected no matches, found 0; the baseline is out of date and must be
|
|
426
|
+
pruned: src/legacy/adapter.ts (no longer matches)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
The fix is deleting the line the message names. `ratchet="one-way"` relaxes that
|
|
430
|
+
half to a report-only note. Every run says how many matches the baseline
|
|
431
|
+
excluded, because a pass bought by an exemption is never silent.
|
|
432
|
+
|
|
433
|
+
To adopt a rule on a codebase that already violates it:
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
spec-guard docs/adr.md --print-baseline
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
prints the attribute that would exempt exactly today's violations, for you to
|
|
440
|
+
paste in. It prints; it does not edit. [ADR-0009](docs/adr/0009-debt-baselines.md)
|
|
441
|
+
explains why that distinction is the whole design, and why there is no `--fix`
|
|
442
|
+
for architecture rules.
|
|
443
|
+
|
|
306
444
|
## CLI
|
|
307
445
|
|
|
308
446
|
```bash
|
|
@@ -314,14 +452,18 @@ spec-guard [patterns...] [options]
|
|
|
314
452
|
| `-r, --root <path>` | Codebase root that assertions resolve against (default: cwd) |
|
|
315
453
|
| `-v, --verbose` | Print passing assertions too |
|
|
316
454
|
| `--fail-fast` | Stop at the first failing assertion |
|
|
317
|
-
| `--json` | Machine-readable report on stdout |
|
|
455
|
+
| `--json` | Machine-readable report on stdout (same as `--format json`) |
|
|
456
|
+
| `--format <human\|json\|sarif>` | Output format. `sarif` uploads to GitHub code scanning |
|
|
318
457
|
| `--engine <auto\|rg\|js>` | Search engine (default `auto`: scanner for small trees, ripgrep for big ones) |
|
|
319
458
|
| `--strict` | Treat analysis that could not be completed as a failure |
|
|
320
459
|
| `--allow-missing-targets` | Warn instead of failing when a `target` path does not exist |
|
|
460
|
+
| `--allow-empty-scope` | Warn instead of failing when an assertion inspects no files |
|
|
461
|
+
| `--print-baseline` | Print the `baseline="..."` that would exempt today's violations, and exit |
|
|
462
|
+
| `--no-default-skips` | Search `.git`, `.hg`, `.svn` and `node_modules` too |
|
|
321
463
|
| `--include-specs` | Also count matches inside the spec files themselves |
|
|
322
464
|
| `--max-snippets <n>` | Failure snippets per assertion (default 5) |
|
|
323
465
|
| `--concurrency <n>` | Search passes in flight at once (default 8) |
|
|
324
|
-
| `--allow-empty` | Exit 0 when no spec file matched the patterns |
|
|
466
|
+
| `--allow-empty` | Exit 0 when no spec file matched the patterns (about the run, not an assertion) |
|
|
325
467
|
| `--color` / `--no-color` | Force colour on or off (`NO_COLOR` honoured) |
|
|
326
468
|
|
|
327
469
|
Patterns are expanded by spec-guard itself, so quoted globs behave identically
|
|
@@ -370,6 +512,35 @@ As a pre-commit hook (assuming a local install, so the bare command resolves):
|
|
|
370
512
|
npx spec-guard "docs/**/*.md" --fail-fast
|
|
371
513
|
```
|
|
372
514
|
|
|
515
|
+
### Annotations on the pull request
|
|
516
|
+
|
|
517
|
+
`--format sarif` writes a SARIF 2.1.0 document, which GitHub turns into a
|
|
518
|
+
comment on the offending line:
|
|
519
|
+
|
|
520
|
+
```yaml
|
|
521
|
+
- run: npx @descent-vtt/spec-guard "docs/**/*.md" --format sarif > spec-guard.sarif || true
|
|
522
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
523
|
+
with:
|
|
524
|
+
sarif_file: spec-guard.sarif
|
|
525
|
+
category: spec-guard
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
That job needs `permissions: security-events: write`. One alert per broken rule
|
|
529
|
+
rather than per match - the thing that broke is the rule - anchored on the first
|
|
530
|
+
offending line, with the directive as a related location because that is often
|
|
531
|
+
where the fix goes. Alerts carry a fingerprint derived from what the assertion
|
|
532
|
+
is about rather than where its matches landed, so inserting a line above a
|
|
533
|
+
violation does not close the alert and open a new one.
|
|
534
|
+
|
|
535
|
+
**There is no language server, and that is a decision rather than a gap.**
|
|
536
|
+
spec-guard's claims are about a whole repository - "this symbol appears nowhere
|
|
537
|
+
in `src`" - and a language server is handed one buffer at a time. Answering a
|
|
538
|
+
repository-wide question on every keystroke means rescanning the tree on every
|
|
539
|
+
keystroke; the alternative is answering a smaller question and calling it the
|
|
540
|
+
same one. The CLI does a full run of this repository in about 130 ms, so a
|
|
541
|
+
pre-commit hook or a watch loop already closes the feedback gap without a
|
|
542
|
+
daemon, an extension per editor, or a protocol version matrix.
|
|
543
|
+
|
|
373
544
|
## How it works
|
|
374
545
|
|
|
375
546
|
```text
|
|
@@ -392,45 +563,69 @@ parser ──▶ Directive[] ──▶ runner ──▶ Assertion[] ──▶ e
|
|
|
392
563
|
|
|
393
564
|
Scanning a tree costs about the same whether you look for one symbol or twenty,
|
|
394
565
|
so spec-guard groups assertions by target set and flags and answers each group
|
|
395
|
-
in one pass.
|
|
396
|
-
2,000-file, 5.1 MB tree with 8 assertions:
|
|
397
|
-
|
|
398
|
-
| | one pass per assertion | batched (current) |
|
|
399
|
-
| --- | --- | --- |
|
|
400
|
-
| ripgrep engine | ~280 ms | **~72 ms** |
|
|
401
|
-
| JavaScript fallback | ~780 ms | **~283 ms** |
|
|
566
|
+
in one pass.
|
|
402
567
|
|
|
403
568
|
`auto` then picks between the two engines **per search group**, because they
|
|
404
569
|
have different shapes of cost: ripgrep is dominated by process startup and
|
|
405
570
|
barely notices tree size, while the built-in scanner has no startup cost and
|
|
406
571
|
grows linearly. Starting a process to search a handful of files is a bad trade.
|
|
572
|
+
Re-measured for 0.4.0 on Windows 11 / Node 24, median of three, one symbol over
|
|
573
|
+
a synthetic tree:
|
|
574
|
+
|
|
575
|
+
| files | scanner | ripgrep | |
|
|
576
|
+
| --- | --- | --- | --- |
|
|
577
|
+
| 100 | **17 ms** | 214 ms | scanner, 12.9x |
|
|
578
|
+
| 500 | **70 ms** | 263 ms | scanner, 3.8x |
|
|
579
|
+
| 1,000 | 216 ms | **181 ms** | ripgrep, 1.2x |
|
|
580
|
+
| 3,000 | 735 ms | **222 ms** | ripgrep, 3.3x |
|
|
581
|
+
| 5,000 | 1,046 ms | **241 ms** | ripgrep, 4.3x |
|
|
407
582
|
|
|
408
583
|
The decision uses a bounded enumeration as its probe: spec-guard walks the
|
|
409
584
|
target set until it either finishes - in which case the file list is already in
|
|
410
585
|
hand and the scanner runs against it, with no process and no second walk - or
|
|
411
|
-
exceeds a budget, in which case ripgrep takes over.
|
|
412
|
-
|
|
413
|
-
|
|
586
|
+
exceeds a budget, in which case ripgrep takes over. The crossover above sits
|
|
587
|
+
between 500 and 1,000 files, which is what the Windows budget of 512 encodes;
|
|
588
|
+
on Linux it is far lower, because what is really being measured is process
|
|
589
|
+
spawn cost. `scripts/bench-engines.mjs` reproduces this, and
|
|
590
|
+
[ADR-0004](docs/adr/0004-adaptive-engine.md) has the full tables.
|
|
591
|
+
|
|
592
|
+
Treat single measurements from one machine with suspicion. On the development
|
|
593
|
+
machine used here, an absence assertion over 2,000 files measures 303 ms with
|
|
594
|
+
the scanner and 399 ms with ripgrep, but ripgrep's own spread across five runs
|
|
595
|
+
was 379-1039 ms - wide enough that the two are not really distinguishable at
|
|
596
|
+
that size. The order-of-magnitude differences at the ends of the table are the
|
|
597
|
+
part worth trusting.
|
|
598
|
+
|
|
599
|
+
0.5.0 added four language analysers, a scope probe on every assertion and a
|
|
600
|
+
baseline pass, so the obvious question is what that cost. Both versions were run
|
|
601
|
+
against the same synthetic 1,000-file tree, alternating rounds inside one
|
|
602
|
+
process so that this machine's load - which varies by a factor of two over a
|
|
603
|
+
session - falls on both equally:
|
|
604
|
+
|
|
605
|
+
| | median | min |
|
|
414
606
|
| --- | --- | --- |
|
|
415
|
-
|
|
|
416
|
-
|
|
|
417
|
-
| `--engine auto` | **19.1 ms** | **171.9 ms** |
|
|
607
|
+
| 0.4.0 | 908.8 ms | 493.7 ms |
|
|
608
|
+
| 0.5.0 | 913.4 ms | 496.6 ms |
|
|
418
609
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
610
|
+
0.5% apart, against a run-to-run spread far wider than that. The two versions
|
|
611
|
+
also returned identical counts for all five assertions, which is the more
|
|
612
|
+
useful half of the result.
|
|
422
613
|
|
|
423
614
|
Two honest caveats:
|
|
424
615
|
|
|
425
616
|
- **The reported engine is the one that ran, not the one available.** On a small
|
|
426
617
|
repository `--json` reports `"engine": "javascript"` even with ripgrep
|
|
427
618
|
installed. That is the optimisation working, not a failure to find `rg`.
|
|
428
|
-
- **
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
619
|
+
- **The engines are one implementation, not two that agree.** This used to say
|
|
620
|
+
that ripgrep was the reference and the scanner merely matched it "on
|
|
621
|
+
everything the test suite covers", with a note that the two treated
|
|
622
|
+
`.gitignore` and ignored directories differently and that you should pin an
|
|
623
|
+
engine if it mattered. It did matter: on a tree with eight copies of a symbol
|
|
624
|
+
the scanner found two and ripgrep found four. Since 0.4.0 ripgrep answers only
|
|
625
|
+
*which files contain this text*, and the scanner does all the counting,
|
|
626
|
+
classification and reporting for both. `--engine` now changes how long a run
|
|
627
|
+
takes and nothing about its verdict, and the suite asserts that on trees built
|
|
628
|
+
from every case that used to split them.
|
|
434
629
|
|
|
435
630
|
## Programmatic API
|
|
436
631
|
|
|
@@ -490,12 +685,14 @@ process spawn (~27 ms on Windows) on the critical path of every run, including
|
|
|
490
685
|
runs where ripgrep is missing. Discovering its absence from the first real
|
|
491
686
|
search is free.
|
|
492
687
|
|
|
493
|
-
**Assertions
|
|
494
|
-
|
|
495
|
-
`PrimaryButton`) and dovetailing (`abc` / `cd`
|
|
496
|
-
both and
|
|
497
|
-
|
|
498
|
-
|
|
688
|
+
**Assertions that share a target list share one pass over the tree.** This used
|
|
689
|
+
to need a proof: merging literals into a ripgrep alternation can lose matches
|
|
690
|
+
through containment (`Primary` / `PrimaryButton`) and dovetailing (`abc` / `cd`
|
|
691
|
+
in `abcd`), so spec-guard checked for both and fell back to separate passes.
|
|
692
|
+
Since 0.4.0 it does not need the proof, because ripgrep no longer counts
|
|
693
|
+
anything - it answers only *which files contain this text*, and the scanner
|
|
694
|
+
counts each pattern separately over the shared file contents. The batching
|
|
695
|
+
checks were deleted along with the risk they guarded.
|
|
499
696
|
|
|
500
697
|
**Exit code 2 exists.** "Your specs failed" and "spec-guard could not run" are
|
|
501
698
|
different facts, and CI should be able to tell them apart.
|
|
@@ -506,12 +703,42 @@ nothing, and reported success — the exact shape of a green check that verified
|
|
|
506
703
|
nothing. `--allow-missing-targets` restores the old behaviour for repositories
|
|
507
704
|
where a path is legitimately optional.
|
|
508
705
|
|
|
706
|
+
**Hidden directories are searched, and `.gitignore` is not consulted.** An
|
|
707
|
+
audit of 0.3.0 found the tool reporting a clean pass on a repository whose
|
|
708
|
+
forbidden symbol sat in `.github/workflows/ci.yml`; the scanner and ripgrep also
|
|
709
|
+
disagreed with each other, finding two matches and four on the same tree. Scope
|
|
710
|
+
is now one policy that both engines are driven by, the skip list is four names
|
|
711
|
+
long, and anything spec-guard could not inspect is reported rather than assumed
|
|
712
|
+
clean ([ADR-0007](docs/adr/0007-search-scope.md)).
|
|
713
|
+
|
|
509
714
|
**Comments are excluded by default, and the exclusion is reported.** Counting
|
|
510
715
|
the note that records a deletion as an occurrence of the thing deleted punishes
|
|
511
716
|
the documentation this tool exists to keep honest ([ADR-0006](docs/adr/0006-comment-classification.md)).
|
|
512
717
|
The reverse risk — a rule that quietly stops checking anything because every
|
|
513
718
|
match now sits in a comment — is why every run says how many matches it dropped.
|
|
514
719
|
|
|
720
|
+
**An assertion that inspected no files fails.** It is the same defect as a
|
|
721
|
+
missing `target` seen from a different angle: a rule whose scope is empty passes
|
|
722
|
+
forever and reads exactly like a rule that found nothing. Turning this on found
|
|
723
|
+
a vacuous assertion inside this repository's own test suite on the first run.
|
|
724
|
+
`allow-empty="true"` covers the honest case of a rule written before the code it
|
|
725
|
+
guards.
|
|
726
|
+
|
|
727
|
+
**Four more languages, and no parser.** Import assertions read Python, Go, Rust
|
|
728
|
+
and C# as well as JavaScript. Not by adding four tokenizers - by reusing the
|
|
729
|
+
comment and string lexer that already existed, masking the source with it, and
|
|
730
|
+
reading statements off what is left. Tree-sitter would have been the modern
|
|
731
|
+
answer and costs 94 MB unpacked against this package's 0.33 MB; the four things
|
|
732
|
+
a real parser would genuinely see that this cannot are listed in
|
|
733
|
+
[ADR-0008](docs/adr/0008-polyglot-imports.md) rather than glossed over.
|
|
734
|
+
|
|
735
|
+
**There is no `--fix`.** Every edit a machine can make to a failing boundary
|
|
736
|
+
assertion is an edit that records the rule no longer holding: widen the bound,
|
|
737
|
+
add an exclusion, append to the baseline, insert an ignore comment. A one-flag
|
|
738
|
+
path from red to green is a bad button for a person and a much worse one for an
|
|
739
|
+
agent whose loop terminates on a green build. `--print-baseline` prints what you
|
|
740
|
+
could paste; it does not paste it ([ADR-0009](docs/adr/0009-debt-baselines.md)).
|
|
741
|
+
|
|
515
742
|
## spec-guard checks itself
|
|
516
743
|
|
|
517
744
|
The invariants in [`docs/adr/0001-invariants.md`](docs/adr/0001-invariants.md)
|
|
@@ -532,7 +759,7 @@ npm install
|
|
|
532
759
|
npm run build # tsc -> dist/
|
|
533
760
|
npm test # vitest
|
|
534
761
|
npm run test:coverage
|
|
535
|
-
npm run test:mutation # stryker (~16 min in CI;
|
|
762
|
+
npm run test:mutation # stryker (~16 min in CI; over two hours locally)
|
|
536
763
|
npm run lint # tsc --noEmit
|
|
537
764
|
npm run selfcheck # run spec-guard on its own docs
|
|
538
765
|
```
|
|
@@ -546,8 +773,8 @@ prebuilt binary and is never a runtime dependency.
|
|
|
546
773
|
### Mutation testing
|
|
547
774
|
|
|
548
775
|
Coverage says a line ran. It does not say an assertion would notice if the line
|
|
549
|
-
behaved differently. This repository measures the difference: **
|
|
550
|
-
|
|
776
|
+
behaved differently. This repository measures the difference: **87.21%** of
|
|
777
|
+
4,512 mutants are killed in CI, against high line coverage.
|
|
551
778
|
|
|
552
779
|
That gap is the point. The first run scored 77.23%, and the weakest file was the
|
|
553
780
|
reporter at 65.48% - not because it lacked tests, but because its tests were
|
|
@@ -557,17 +784,33 @@ test still passed. Exact whole-output comparison took it to 87.30%. The engine
|
|
|
557
784
|
and the walker were then rewritten for testability rather than papered over with
|
|
558
785
|
more tests, which is what moved them from 75%/78% to 83%/93%.
|
|
559
786
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
787
|
+
CI runs it in **two tiers**, both gated at 85%. Branches and pull requests run
|
|
788
|
+
Stryker incrementally, reusing the verdict for any mutant whose source and
|
|
789
|
+
covering tests are both unchanged. Pushes to `main`, the weekly schedule and
|
|
790
|
+
manual runs do the full sweep, which is the authoritative number and the one
|
|
791
|
+
quoted above; a full sweep is also what publishes the cache the branches start
|
|
792
|
+
from, so an incremental verdict can never be built on another incremental
|
|
793
|
+
verdict.
|
|
794
|
+
|
|
795
|
+
The full sweep was 6m54s at 2,118 mutants, 15m51s at 3,493, and is 22m11s at
|
|
796
|
+
4,512. That growth is why the tiers exist: a check that gets quietly more
|
|
797
|
+
expensive every release is a check somebody eventually proposes lowering.
|
|
798
|
+
|
|
799
|
+
Run it locally with `npm run test:mutation` if you like, but do not calibrate
|
|
800
|
+
anything on the result: on the Windows machine this was developed on the same
|
|
801
|
+
suite takes over two hours against 22 minutes hosted, and it scores *higher*,
|
|
802
|
+
because far more mutants hang there and Stryker counts a hang as a kill. Linux
|
|
803
|
+
CI is the measurement.
|
|
804
|
+
|
|
805
|
+
Six cautionary tales are in [ADR-0003](docs/adr/0003-mutation-testing.md): a
|
|
567
806
|
run whose score was pure fiction because the mutants were never activated, a
|
|
568
807
|
tuning knob that lifted the score six points without adding a test, the platform
|
|
569
|
-
gap above,
|
|
570
|
-
|
|
808
|
+
gap above, the baseline being re-anchored when a tokenizer arrived, and a score
|
|
809
|
+
that rose partly because code carrying hard-to-kill mutants was deleted rather
|
|
810
|
+
than because tests improved, and the 0.5.0 sweep that failed the build at 83.77%
|
|
811
|
+
because a suite can be thorough about the thing it was written to test and
|
|
812
|
+
silent about the machinery underneath it - along with the real bug that chasing
|
|
813
|
+
the gate uncovered.
|
|
571
814
|
|
|
572
815
|
## Requirements
|
|
573
816
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -17,15 +17,22 @@ export interface CliIO {
|
|
|
17
17
|
cwd: string;
|
|
18
18
|
isTTY: boolean;
|
|
19
19
|
}
|
|
20
|
+
/** How a finished run is written out. */
|
|
21
|
+
export type OutputFormat = 'human' | 'json' | 'sarif';
|
|
20
22
|
export interface CliOptions {
|
|
21
23
|
patterns: string[];
|
|
22
24
|
root: string;
|
|
23
25
|
verbose: boolean;
|
|
24
26
|
failFast: boolean;
|
|
27
|
+
format: OutputFormat;
|
|
28
|
+
/** Kept as its own field so `--json` remains exactly what it always was. */
|
|
25
29
|
json: boolean;
|
|
26
30
|
engine: EnginePreference;
|
|
27
31
|
allowMissingTargets: boolean;
|
|
32
|
+
defaultSkips: boolean;
|
|
28
33
|
strictTargets: boolean;
|
|
34
|
+
allowEmptyScope: boolean;
|
|
35
|
+
printBaseline: boolean;
|
|
29
36
|
includeSpecs: boolean;
|
|
30
37
|
allowEmpty: boolean;
|
|
31
38
|
maxSnippets: number;
|
|
@@ -36,7 +43,8 @@ export interface CliOptions {
|
|
|
36
43
|
}
|
|
37
44
|
export declare class UsageError extends Error {
|
|
38
45
|
}
|
|
39
|
-
export declare
|
|
46
|
+
export declare function version(): string;
|
|
47
|
+
export declare const HELP = "spec-guard - Executable architecture assertions for Markdown specs & ADRs\n\nUsage\n spec-guard [patterns...] [options]\n\nPatterns\n Globs or paths to the Markdown specs to execute. A directory expands to the\n Markdown files inside it. Defaults to \"docs/**/*.md\" when omitted.\n\nOptions\n -r, --root <path> Codebase root that assertions are resolved against (default: cwd)\n -v, --verbose Print passing assertions too\n --fail-fast Stop at the first failing assertion\n --json Emit a machine-readable JSON report (same as --format json)\n --format <name> human | json | sarif (sarif uploads to GitHub code scanning)\n --engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)\n --strict Treat analysis that could not be completed as a failure\n --allow-missing-targets\n Tolerate target paths that do not exist (they fail by default)\n --allow-empty-scope Tolerate assertions whose scope holds no files (they fail by default)\n --print-baseline Print the baseline=\"...\" that would exempt today's violations, and exit\n --no-default-skips Search .git, .hg, .svn and node_modules too\n --include-specs Also count matches inside the spec files themselves\n --max-snippets <n> Failure snippets per assertion (default: 5)\n --concurrency <n> Assertions executed in parallel (default: 8)\n --allow-empty Exit 0 when no spec files matched (about the run, not an assertion)\n --color/--no-color Force colour on or off (NO_COLOR is honoured)\n -h, --help Show this help\n --version Print the version\n\nDirectives\n <!-- @assert-absence target=\"src/\" symbol=\"LegacyGateway\" exclude=\"src/legacy/**\" -->\n <!-- @assert-count target=\"src/\" symbol=\"SessionManager\" expected=\"1\" -->\n <!-- @assert-present file=\"SECURITY.md\" -->\n\n Matches inside comments do not count; add comments=\"include\" to count them.\n An assertion whose scope holds no files fails; add allow-empty=\"true\" to allow it.\n\nExit codes\n 0 all assertions passed 1 an assertion failed 2 spec-guard could not run";
|
|
40
48
|
/** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
|
|
41
49
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
42
50
|
/** Runs the CLI and resolves to the process exit code. */
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,gBAAgB,CAAC;IACzB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,UAAW,SAAQ,KAAK;CAAG;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,yCAAyC;AACzC,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,YAAY,CAAC;IACrB,4EAA4E;IAC5E,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,gBAAgB,CAAC;IACzB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,wBAAgB,OAAO,IAAI,MAAM,CAShC;AAED,eAAO,MAAM,IAAI,4sEAuC8D,CAAC;AAgChF,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CA8H1E;AAYD,0DAA0D;AAC1D,wBAAsB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,EAAE,EAAE,GAAE,KAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CA2EpH"}
|