@descent-vtt/spec-guard 0.1.0 → 0.3.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 ADDED
@@ -0,0 +1,90 @@
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.3.0
9
+
10
+ ### Added
11
+
12
+ - **`exclude` attribute.** Most real rules are "nowhere except one place", not
13
+ "not here": `exclude="src/config/**"` expresses that in one assertion instead
14
+ of a hand-maintained list of every directory that is not `src/config`. Follows
15
+ gitignore semantics — deliberately different from `glob`'s basename matching —
16
+ and a parity matrix asserts both engines agree.
17
+ - **Import assertions:** `@assert-import-absence` and `@assert-import-count`.
18
+ "The UI layer must not depend on the database layer" is a dependency claim,
19
+ not a text claim, and approximating it with a string search fails in both
20
+ directions. A zero-dependency tokenizer resolves `import`, `export … from`,
21
+ `import type`, dynamic `import()` with a literal, and `require()`. Module
22
+ references it cannot resolve statically are reported, never counted as clean.
23
+ See [ADR-0005](docs/adr/0005-import-assertions.md).
24
+ - **`comments` attribute.** See Changed, below.
25
+ - **`--allow-missing-targets`** to restore the previous handling of a `target`
26
+ path that does not exist.
27
+ - **`commentMatches` and `unclassifiedFiles`** on every result in `--json`.
28
+
29
+ ### Changed
30
+
31
+ - **Matches inside comments no longer count**, so the note recording that a
32
+ symbol was deleted is no longer read as an occurrence of that symbol. This is
33
+ the defect where documenting a decision defeated the assertion enforcing it.
34
+ Comment syntax is known for 59 extensions across 9 families; strings are
35
+ tracked too, since `//` inside a URL is not a comment. Where classification is
36
+ uncertain the text counts as code, and every run reports how many matches it
37
+ excluded — a pass caused by comment exclusion is never silent.
38
+ `comments="include"` restores counting per assertion.
39
+ See [ADR-0006](docs/adr/0006-comment-classification.md).
40
+ - **A `target` path that does not exist now fails the run** instead of warning
41
+ and searching what remained. An assertion pointed at a renamed directory used
42
+ to search nothing, find nothing, and report success. `--allow-missing-targets`
43
+ restores the old behaviour.
44
+ - **`--strict`** now means "treat analysis that could not be completed as a
45
+ failure" — it no longer carries the missing-target meaning, which is now the
46
+ default.
47
+
48
+ ### Fixed
49
+
50
+ - `search()` on the ripgrep engine was not comment-aware while `searchBatch()`
51
+ was, so a spec containing exactly one assertion kept its comment matches.
52
+ - The two engines reported different columns for the same match on a line
53
+ containing non-ASCII text: ripgrep counts bytes, the scanner counts
54
+ characters, and editors count characters. ripgrep's column is now converted,
55
+ so a reported location points at the match in both engines.
56
+ - Leftover `*.test.ts` files in scratch fixture directories were collected by
57
+ Vitest, breaking a run with "no test suite found" because of debris from a
58
+ previous one.
59
+
60
+ ### Notes
61
+
62
+ The version in `package.json` was bumped to 0.3.0 during development but never
63
+ published; npm went 0.1.0 -> 0.2.0. Everything above ships together as the
64
+ first published 0.3.0.
65
+
66
+ ## 0.2.0
67
+
68
+ ### Added
69
+
70
+ - **Adaptive engine selection.** `--engine auto` now measures rather than
71
+ guesses: a budgeted, stat-only enumeration decides whether a tree is small
72
+ enough for the JavaScript scanner to beat a ripgrep process spawn. The probe
73
+ is the work — the enumeration it performs is reused by the scanner, so
74
+ choosing costs nothing when the scanner wins.
75
+ See [ADR-0004](docs/adr/0004-adaptive-engine.md).
76
+ - **Mutation testing** with Stryker, wired into CI on every push, plus the tests
77
+ that closed the gaps it exposed. See [ADR-0003](docs/adr/0003-mutation-testing.md).
78
+
79
+ ### Changed
80
+
81
+ - Published as `@descent-vtt/spec-guard`. The binary is still plain
82
+ `spec-guard`.
83
+
84
+ ## 0.1.0
85
+
86
+ Initial release: `@assert-absence`, `@assert-count` and `@assert-present`,
87
+ written as HTML comments that are invisible in every Markdown renderer. ripgrep
88
+ primary with a pure-JavaScript fallback of identical semantics, safe assertion
89
+ batching, `--json`, and exit code 2 for "spec-guard could not run" as distinct
90
+ from "your specs failed".
package/README.md CHANGED
@@ -120,6 +120,85 @@ may use double or single quotes, and a bare attribute means `="true"`.
120
120
  Fails when the symbol occurs more than `expected` times (default `0`).
121
121
  `max="..."` is accepted as a synonym for `expected`.
122
122
 
123
+ ### `exclude` - everywhere except
124
+
125
+ Most real rules are "nowhere except one place", not "not here":
126
+
127
+ ```md
128
+ <!-- @assert-absence target="src" symbol="process.env" exclude="src/config/**" -->
129
+ ```
130
+
131
+ That is the rule this README opens with - *no secret is ever read from
132
+ `process.env` outside `src/config`* - written as one assertion instead of a
133
+ hand-maintained list of every directory that is not `src/config`.
134
+
135
+ `exclude` follows gitignore rules, which are **not** the same as `glob`'s:
136
+
137
+ | Pattern | Excludes |
138
+ | --- | --- |
139
+ | `tests` | any directory or file named `tests`, at any depth, and everything under it |
140
+ | `src/config` | that directory and everything under it |
141
+ | `src/config/**` | the same, written explicitly |
142
+ | `*.test.ts` | any file with that name shape, at any depth |
143
+
144
+ The difference is deliberate. `glob="*.ts"` filters files, so basename matching
145
+ is what you want; `exclude="tests"` means the directory, because that is what
146
+ people mean when they write it - and it is what `rg -g '!tests'` does. Both
147
+ engines implement the same rule, and a parity matrix asserts they agree on it.
148
+
149
+ List attributes accept commas or whitespace, so both of these work:
150
+
151
+ ```md
152
+ <!-- @assert-absence target="src" symbol="TODO" exclude="src/legacy/** tests" -->
153
+ <!-- @assert-absence target="src" symbol="TODO" exclude="src/legacy/**,tests" -->
154
+ ```
155
+
156
+ (A path containing a space therefore cannot be written; there is no quoting
157
+ inside an attribute value.)
158
+
159
+ ### `comments` - the note about a deletion is not the deletion
160
+
161
+ You delete a symbol and leave the explanation where the next person will look:
162
+
163
+ ```ts
164
+ // LegacyThing was removed in ADR-398; do not reintroduce it.
165
+ ```
166
+
167
+ A plain text search reads that comment as an occurrence, so the assertion that
168
+ keeps the symbol deleted fails on the sentence proving it was deleted. Both
169
+ fixes are bad: delete the note and lose the reason, or delete the rule.
170
+
171
+ So matches inside comments do not count:
172
+
173
+ ```md
174
+ <!-- comments are ignored by default -->
175
+ <!-- @assert-absence target="src" symbol="LegacyThing" -->
176
+
177
+ <!-- ...unless you ask for them -->
178
+ <!-- @assert-absence target="src" symbol="Copyright" comments="include" -->
179
+ ```
180
+
181
+ `comments="include"` is right for assertions that really are about text — a
182
+ licence header, or a name that must appear nowhere in the repository at all.
183
+
184
+ Because this is the one thing that can turn a failing run green without anyone
185
+ touching code, a run that passed this way says so:
186
+
187
+ ```
188
+ ⚠ 1 match inside comments was not counted; add comments="include" to count it
189
+
190
+ 1 passed · 7ms
191
+ ✔ every spec assertion holds
192
+ ```
193
+
194
+ Comment syntax is known for around 59 extensions across 9 families (JS/TS, C,
195
+ C#, Rust, Go, Python-style `#`, SQL-style `--`, markup, and formats with no
196
+ comments at all). Strings are tracked too, because `//` inside a URL is not a
197
+ comment and reading it as one would hide real code. Where spec-guard is unsure —
198
+ an unknown extension, an unterminated literal — the text counts as code, and the
199
+ report says which files it could not classify. A match wrongly kept is a visible
200
+ failure you can argue with; a match wrongly dropped is a lie.
201
+
123
202
  ### `@assert-count` - this symbol occurs exactly / at least / at most N times
124
203
 
125
204
  ```md
@@ -132,6 +211,67 @@ Fails when the symbol occurs more than `expected` times (default `0`).
132
211
  Requires `expected`, or `min` and/or `max`. `expected` cannot be combined with
133
212
  `min`/`max`.
134
213
 
214
+ An exact count is the most brittle assertion in the set: a test that merely
215
+ mentions the symbol will break it. spec-guard does not silently skip test files
216
+ for you - an assertion that quietly ignores part of the tree is the failure mode
217
+ this tool exists to prevent - so say what you mean:
218
+
219
+ ```md
220
+ <!-- @assert-count target="src" symbol="UserSessionManager" expected="1" exclude="*.test.ts *.spec.ts" -->
221
+ ```
222
+
223
+ `min="1"` is often the better rule anyway: it says "this exists" without
224
+ breaking every time someone writes a second test.
225
+
226
+ ### `@assert-import-absence` / `@assert-import-count` - dependencies, not text
227
+
228
+ The rules architecture documents actually contain are usually about
229
+ dependencies, and a text search answers those badly in both directions - it
230
+ matches a mention in a comment, and it misses `export * from '../db'`.
231
+
232
+ ```md
233
+ <!-- @assert-import-absence target="src/ui" module="src/db" reason="the UI talks to services, not storage" -->
234
+ <!-- @assert-import-count target="src" module="axios" max="1" -->
235
+ ```
236
+
237
+ These read the dependency rather than the text. A JavaScript/TypeScript
238
+ tokenizer - comments, strings, template substitutions and regular expressions,
239
+ no parser and no new dependency - extracts module references and counts the
240
+ **files** that depend on the module. It understands:
241
+
242
+ | Form | Counted |
243
+ | --- | --- |
244
+ | `import { A } from 'x'`, `import 'x'`, `import * as ns from 'x'` | yes |
245
+ | `import type { A } from 'x'` | yes, unless `types="ignore"` |
246
+ | `export * from 'x'`, `export { A } from 'x'` | yes - a re-export is a dependency, and a barrel file is how layering rules usually get broken |
247
+ | `require('x')` | yes, with a literal argument |
248
+ | `import('x')` | yes, with a literal argument |
249
+ | `import(name)`, `require(expr)` | **no - reported, see below** |
250
+
251
+ Relative specifiers are resolved by path arithmetic, so `module="src/db"`
252
+ matches `../db/client.js` seen from `src/ui/`. There is deliberately no
253
+ filesystem resolution: tsconfig `paths` aliases and package `exports` maps are
254
+ not followed, so an alias is written out as itself (`module="@app/db"`).
255
+
256
+ **It tells you what it could not see.** Roughly 1% of module references in real
257
+ code are dynamic, and about 0.09% of files defeat the tokenizer outright. Both
258
+ are reported rather than counted as clean:
259
+
260
+ ```text
261
+ ✔ docs/adr.md:1 @assert-import-absence "src/db" (0 matches) in src/core
262
+ ⚠ 1 module reference could not be resolved statically
263
+ ⚠ src/core/plugin.ts:1 import(name)
264
+ ```
265
+
266
+ The count is still true of everything that could be seen; the warning is what
267
+ stops it being mistaken for a complete answer. `--strict` turns those warnings
268
+ 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 1 of 3
270
+ files" rather than quietly passing.
271
+
272
+ [ADR-0005](docs/adr/0005-import-assertions.md) has the measurements and the
273
+ reasoning behind each boundary.
274
+
135
275
  ### `@assert-present` - this file exists
136
276
 
137
277
  ```md
@@ -150,10 +290,14 @@ Passes when every listed path exists relative to `--root`. Directories count.
150
290
  | `file` | present | Comma-separated paths that must exist |
151
291
  | `expected` | absence, count | Upper bound for absence; exact count for count |
152
292
  | `min` / `max` | count (`max` also on absence) | Inclusive bounds |
153
- | `glob` | absence, count | Comma-separated file filters, e.g. `*.ts,*.tsx` (ripgrep `-g` semantics) |
293
+ | `glob` | absence, count | Include-only file filters, e.g. `*.ts,*.tsx` (ripgrep `-g` semantics) |
294
+ | `exclude` | absence, count | Paths to leave out, gitignore-style: `src/config/**`, `tests`, `*.test.ts` |
154
295
  | `regex` | absence, count | Treat `symbol` as a regular expression |
155
296
  | `word` | absence, count | Require word boundaries, so `Primary` does not match `PrimaryButton` |
156
297
  | `ignore-case` | absence, count | Case-insensitive matching |
298
+ | `comments` | absence, count | `ignore` (default) or `include` for matches inside comments |
299
+ | `module` | import assertions | Which dependency, matched like `exclude` |
300
+ | `types` | import assertions | `include` (default) or `ignore` for `import type` |
157
301
  | `reason` | all | Human-readable justification, printed on failure |
158
302
 
159
303
  Unknown attributes are an error, not a shrug: `expct="1"` fails the run instead
@@ -171,8 +315,9 @@ spec-guard [patterns...] [options]
171
315
  | `-v, --verbose` | Print passing assertions too |
172
316
  | `--fail-fast` | Stop at the first failing assertion |
173
317
  | `--json` | Machine-readable report on stdout |
174
- | `--engine <auto\|rg\|js>` | Search engine (default `auto`: ripgrep when available) |
175
- | `--strict` | Treat a `target` that does not exist as a failure, not a warning |
318
+ | `--engine <auto\|rg\|js>` | Search engine (default `auto`: scanner for small trees, ripgrep for big ones) |
319
+ | `--strict` | Treat analysis that could not be completed as a failure |
320
+ | `--allow-missing-targets` | Warn instead of failing when a `target` path does not exist |
176
321
  | `--include-specs` | Also count matches inside the spec files themselves |
177
322
  | `--max-snippets <n>` | Failure snippets per assertion (default 5) |
178
323
  | `--concurrency <n>` | Search passes in flight at once (default 8) |
@@ -255,19 +400,37 @@ in one pass. Measured on Windows 11 / Node 24 / ripgrep 15 against a synthetic
255
400
  | ripgrep engine | ~280 ms | **~72 ms** |
256
401
  | JavaScript fallback | ~780 ms | **~283 ms** |
257
402
 
403
+ `auto` then picks between the two engines **per search group**, because they
404
+ have different shapes of cost: ripgrep is dominated by process startup and
405
+ barely notices tree size, while the built-in scanner has no startup cost and
406
+ grows linearly. Starting a process to search a handful of files is a bad trade.
407
+
408
+ The decision uses a bounded enumeration as its probe: spec-guard walks the
409
+ target set until it either finishes - in which case the file list is already in
410
+ 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. Measured end to end:
412
+
413
+ | | this repo's own specs | 2,000 files |
414
+ | --- | --- | --- |
415
+ | `--engine rg` | 793.7 ms | 195.8 ms |
416
+ | `--engine js` | 19.5 ms | 330.6 ms |
417
+ | `--engine auto` | **19.1 ms** | **171.9 ms** |
418
+
419
+ The crossover is ~575 files on Windows and ~25 on Linux, because what is really
420
+ being measured is process spawn cost. `scripts/bench-engines.mjs` reproduces
421
+ both; [ADR-0004](docs/adr/0004-adaptive-engine.md) has the full tables.
422
+
258
423
  Two honest caveats:
259
424
 
260
- - **Process spawning is expensive on Windows** (~27 ms each). On a small tree
261
- the JavaScript engine can beat ripgrep outright - spec-guard checks its own
262
- repository in ~15 ms with `--engine js` versus ~170 ms with ripgrep. On Linux
263
- and macOS, where spawning costs a few milliseconds, ripgrep wins at every
264
- size. If your repository is small and you care about the last millisecond,
265
- `--engine js` is a legitimate choice.
266
- - **ripgrep is the reference implementation.** The fallback matches it on
425
+ - **The reported engine is the one that ran, not the one available.** On a small
426
+ repository `--json` reports `"engine": "javascript"` even with ripgrep
427
+ installed. That is the optimisation working, not a failure to find `rg`.
428
+ - **ripgrep is the reference implementation.** The scanner matches it on
267
429
  everything the test suite covers - counts, snippets, word boundaries, globs,
268
430
  binary skipping, file-size limits - but ripgrep also honours `.gitignore`,
269
- while the fallback uses a fixed ignore list (`node_modules`, `dist`, `build`,
270
- `coverage`, `.git`, dotfiles, and friends).
431
+ while the scanner uses a fixed ignore list (`node_modules`, `dist`, `build`,
432
+ `coverage`, `.git`, dotfiles, and friends). On a repository where those differ
433
+ materially, pin the engine with `--engine rg`.
271
434
 
272
435
  ## Programmatic API
273
436
 
@@ -299,7 +462,10 @@ implementation departs from the obvious reading of the brief, here is why.
299
462
  "`LegacyGateway` must not appear" contains the string `LegacyGateway`. Without
300
463
  this rule, absence assertions would fail on the document asserting them - the
301
464
  single most confusing possible first-run experience. `--include-specs` restores
302
- the naive behaviour.
465
+ the naive behaviour, and counts the prose of your specs; the directives
466
+ themselves stay uncounted, because a directive is an HTML comment and a rule
467
+ whose own text trips it can never be satisfied. `comments="include"` counts even
468
+ those.
303
469
 
304
470
  **Fenced code and inline code are masked before parsing.** A README documenting
305
471
  the syntax must not execute it. Masking preserves byte offsets, so reported line
@@ -334,6 +500,18 @@ count.
334
500
  **Exit code 2 exists.** "Your specs failed" and "spec-guard could not run" are
335
501
  different facts, and CI should be able to tell them apart.
336
502
 
503
+ **A missing `target` fails the run.** It used to warn and search what was left,
504
+ which meant an assertion pointed at a renamed directory searched nothing, found
505
+ nothing, and reported success — the exact shape of a green check that verified
506
+ nothing. `--allow-missing-targets` restores the old behaviour for repositories
507
+ where a path is legitimately optional.
508
+
509
+ **Comments are excluded by default, and the exclusion is reported.** Counting
510
+ the note that records a deletion as an occurrence of the thing deleted punishes
511
+ the documentation this tool exists to keep honest ([ADR-0006](docs/adr/0006-comment-classification.md)).
512
+ The reverse risk — a rule that quietly stops checking anything because every
513
+ match now sits in a comment — is why every run says how many matches it dropped.
514
+
337
515
  ## spec-guard checks itself
338
516
 
339
517
  The invariants in [`docs/adr/0001-invariants.md`](docs/adr/0001-invariants.md)
@@ -345,7 +523,7 @@ build fails if they stop being true.
345
523
 
346
524
  This README is executable too:
347
525
 
348
- <!-- @assert-present file="src/parser.ts,src/engine.ts,src/reporter.ts,src/runner.ts,src/cli.ts" -->
526
+ <!-- @assert-present file="src/parser.ts,src/engine.ts,src/reporter.ts,src/runner.ts,src/cli.ts,src/comments.ts" -->
349
527
 
350
528
  ## Development
351
529
 
@@ -354,7 +532,7 @@ npm install
354
532
  npm run build # tsc -> dist/
355
533
  npm test # vitest
356
534
  npm run test:coverage
357
- npm run test:mutation # stryker (~20 minutes)
535
+ npm run test:mutation # stryker (~16 min in CI; much slower locally)
358
536
  npm run lint # tsc --noEmit
359
537
  npm run selfcheck # run spec-guard on its own docs
360
538
  ```
@@ -368,20 +546,28 @@ prebuilt binary and is never a runtime dependency.
368
546
  ### Mutation testing
369
547
 
370
548
  Coverage says a line ran. It does not say an assertion would notice if the line
371
- behaved differently. This repository measures the difference: **83.16%** of
372
- 2,060 mutants are killed, against 98.98% line coverage.
549
+ behaved differently. This repository measures the difference: **84.63%** of
550
+ 2,949 mutants are killed in CI, against high line coverage.
373
551
 
374
- That gap is the point. The first run scored 77.23%, and the weakest file was
375
- the reporter at 65.48% - not because it lacked tests, but because its tests were
552
+ That gap is the point. The first run scored 77.23%, and the weakest file was the
553
+ reporter at 65.48% - not because it lacked tests, but because its tests were
376
554
  almost all `toContain` against colourless output. A mutant could prepend a junk
377
- line to the output, drop a colour, or turn `remaining > 0` into `remaining >= 0`
378
- and every test still passed. Seventy-five tests later - exact whole-output
379
- comparison instead of substring matching - the reporter is at 87.30%.
380
-
381
- `npm run test:mutation` runs it. CI runs it weekly, on demand, and on pull
382
- requests that touch `src/` or `tests/`, with the score gated at 80%. The full
383
- story, including a run whose score turned out to be fiction, is in
384
- [ADR-0003](docs/adr/0003-mutation-testing.md).
555
+ line, drop a colour, or turn `remaining > 0` into `remaining >= 0` and every
556
+ test still passed. Exact whole-output comparison took it to 87.30%. The engine
557
+ and the walker were then rewritten for testability rather than papered over with
558
+ more tests, which is what moved them from 75%/78% to 83%/93%.
559
+
560
+ `npm run test:mutation` runs it locally and takes about an hour; CI runs it on
561
+ **every push**, gated at 84%. The CI figure is the one
562
+ quoted above, and it is the lower of the two: the same commit scores 88.76% on
563
+ Windows, where three times as many mutants hang and Stryker counts a hang as a
564
+ kill.
565
+
566
+ Four cautionary tales are in [ADR-0003](docs/adr/0003-mutation-testing.md): a
567
+ run whose score was pure fiction because the mutants were never activated, a
568
+ tuning knob that lifted the score six points without adding a test, the platform
569
+ gap above, and the baseline being re-anchored when a tokenizer arrived - along
570
+ with the real bug that chasing the gate uncovered.
385
571
 
386
572
  ## Requirements
387
573
 
package/dist/cli.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface CliOptions {
24
24
  failFast: boolean;
25
25
  json: boolean;
26
26
  engine: EnginePreference;
27
+ allowMissingTargets: boolean;
27
28
  strictTargets: boolean;
28
29
  includeSpecs: boolean;
29
30
  allowEmpty: boolean;
@@ -35,7 +36,7 @@ export interface CliOptions {
35
36
  }
36
37
  export declare class UsageError extends Error {
37
38
  }
38
- 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\n --engine <name> auto | rg | js (default: auto - ripgrep when available)\n --strict Treat a target path that does not exist as a failure\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\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\" -->\n <!-- @assert-count target=\"src/\" symbol=\"SessionManager\" expected=\"1\" -->\n <!-- @assert-present file=\"SECURITY.md\" -->\n\nExit codes\n 0 all assertions passed 1 an assertion failed 2 spec-guard could not run";
39
+ 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\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 --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\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\nExit codes\n 0 all assertions passed 1 an assertion failed 2 spec-guard could not run";
39
40
  /** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
40
41
  export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
41
42
  /** 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,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;AAaxC,eAAO,MAAM,IAAI,o9CA8B8D,CAAC;AAuBhF,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAmG1E;AAYD,0DAA0D;AAC1D,wBAAsB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,EAAE,EAAE,GAAE,KAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+DpH"}
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;AAaxC,eAAO,MAAM,IAAI,otDAkC8D,CAAC;AAgChF,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAuG1E;AAYD,0DAA0D;AAC1D,wBAAsB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,EAAE,EAAE,GAAE,KAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpH"}
package/dist/cli.js CHANGED
@@ -40,8 +40,10 @@ Options
40
40
  -v, --verbose Print passing assertions too
41
41
  --fail-fast Stop at the first failing assertion
42
42
  --json Emit a machine-readable JSON report
43
- --engine <name> auto | rg | js (default: auto - ripgrep when available)
44
- --strict Treat a target path that does not exist as a failure
43
+ --engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)
44
+ --strict Treat analysis that could not be completed as a failure
45
+ --allow-missing-targets
46
+ Tolerate target paths that do not exist (they fail by default)
45
47
  --include-specs Also count matches inside the spec files themselves
46
48
  --max-snippets <n> Failure snippets per assertion (default: ${DEFAULT_MAX_SNIPPETS})
47
49
  --concurrency <n> Assertions executed in parallel (default: ${DEFAULT_CONCURRENCY})
@@ -51,10 +53,12 @@ Options
51
53
  --version Print the version
52
54
 
53
55
  Directives
54
- <!-- @assert-absence target="src/" symbol="LegacyGateway" -->
56
+ <!-- @assert-absence target="src/" symbol="LegacyGateway" exclude="src/legacy/**" -->
55
57
  <!-- @assert-count target="src/" symbol="SessionManager" expected="1" -->
56
58
  <!-- @assert-present file="SECURITY.md" -->
57
59
 
60
+ Matches inside comments do not count; add comments="include" to count them.
61
+
58
62
  Exit codes
59
63
  0 all assertions passed 1 an assertion failed 2 spec-guard could not run`;
60
64
  const ENGINE_ALIASES = {
@@ -65,6 +69,15 @@ const ENGINE_ALIASES = {
65
69
  javascript: 'javascript',
66
70
  node: 'javascript',
67
71
  };
72
+ /**
73
+ * Takes the next argv element as a value.
74
+ *
75
+ * A leading dash means the value was omitted and the next option got swallowed
76
+ * ("--root --verbose"). That heuristic only applies here: in the `--opt=value`
77
+ * form there is nothing ambiguous about a leading dash, so "--max-snippets=-1"
78
+ * should be told it wants a non-negative integer rather than that it is missing
79
+ * a value.
80
+ */
68
81
  function requireValue(name, value) {
69
82
  if (value === undefined || value.startsWith('-')) {
70
83
  throw new UsageError(`Option ${name} requires a value.`);
@@ -85,6 +98,7 @@ export function parseArgs(argv, cwd) {
85
98
  failFast: false,
86
99
  json: false,
87
100
  engine: 'auto',
101
+ allowMissingTargets: false,
88
102
  strictTargets: false,
89
103
  includeSpecs: false,
90
104
  allowEmpty: false,
@@ -109,7 +123,7 @@ export function parseArgs(argv, cwd) {
109
123
  const inlineValue = equals === -1 ? undefined : argument.slice(equals + 1);
110
124
  const nextValue = () => {
111
125
  if (inlineValue !== undefined)
112
- return requireValue(name, inlineValue);
126
+ return inlineValue;
113
127
  index += 1;
114
128
  return requireValue(name, argv[index]);
115
129
  };
@@ -134,6 +148,9 @@ export function parseArgs(argv, cwd) {
134
148
  case '--strict':
135
149
  options.strictTargets = true;
136
150
  break;
151
+ case '--allow-missing-targets':
152
+ options.allowMissingTargets = true;
153
+ break;
137
154
  case '--include-specs':
138
155
  options.includeSpecs = true;
139
156
  break;
@@ -209,6 +226,7 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
209
226
  root: options.root,
210
227
  engine: options.engine,
211
228
  failFast: options.failFast,
229
+ allowMissingTargets: options.allowMissingTargets,
212
230
  strictTargets: options.strictTargets,
213
231
  includeSpecs: options.includeSpecs,
214
232
  concurrency: options.concurrency,
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGtF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC;AACzB,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC;AA2B5B,MAAM,OAAO,UAAW,SAAQ,KAAK;CAAG;AAExC,SAAS,OAAO;IACd,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAyB,CAAC;QAC/D,OAAO,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;QAC9D,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;qEAiBiD,oBAAoB;sEACnB,mBAAmB;;;;;;;;;;;;+EAYV,CAAC;AAEhF,MAAM,cAAc,GAAqC;IACvD,IAAI,EAAE,MAAM;IACZ,EAAE,EAAE,SAAS;IACb,OAAO,EAAE,SAAS;IAClB,EAAE,EAAE,YAAY;IAChB,UAAU,EAAE,YAAY;IACxB,IAAI,EAAE,YAAY;CACnB,CAAC;AAEF,SAAS,YAAY,CAAC,IAAY,EAAE,KAAyB;IAC3D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,UAAU,CAAC,UAAU,IAAI,oBAAoB,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,eAAe,CAAC,IAAY,EAAE,KAAa;IAClD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,UAAU,IAAI,yCAAyC,KAAK,IAAI,CAAC,CAAC;IACjH,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,SAAS,CAAC,IAAuB,EAAE,GAAW;IAC5D,MAAM,OAAO,GAAe;QAC1B,QAAQ,EAAE,EAAE;QACZ,IAAI,EAAE,GAAG;QACT,OAAO,EAAE,KAAK;QACd,QAAQ,EAAE,KAAK;QACf,IAAI,EAAE,KAAK;QACX,MAAM,EAAE,MAAM;QACd,aAAa,EAAE,KAAK;QACpB,YAAY,EAAE,KAAK;QACnB,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,oBAAoB;QACjC,WAAW,EAAE,mBAAmB;QAChC,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,KAAK;KACf,CAAC;IAEF,IAAI,cAAc,GAAG,KAAK,CAAC;IAE3B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QACjD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAW,CAAC;QAEvC,IAAI,cAAc,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;YACpE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAChC,SAAS;QACX,CAAC;QACD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,cAAc,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAClE,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3E,MAAM,SAAS,GAAG,GAAW,EAAE;YAC7B,IAAI,WAAW,KAAK,SAAS;gBAAE,OAAO,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;YACtE,KAAK,IAAI,CAAC,CAAC;YACX,OAAO,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QACzC,CAAC,CAAC;QAEF,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,IAAI,CAAC;YACV,KAAK,QAAQ;gBACX,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;gBACpB,MAAM;YACR,KAAK,WAAW;gBACd,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;gBACvB,MAAM;YACR,KAAK,IAAI,CAAC;YACV,KAAK,WAAW;gBACd,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;gBACvB,MAAM;YACR,KAAK,aAAa;gBAChB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;gBACxB,MAAM;YACR,KAAK,QAAQ;gBACX,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;gBACpB,MAAM;YACR,KAAK,UAAU;gBACb,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;gBAC7B,MAAM;YACR,KAAK,iBAAiB;gBACpB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;gBAC5B,MAAM;YACR,KAAK,eAAe;gBAClB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;gBAC1B,MAAM;YACR,KAAK,SAAS;gBACZ,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;gBACrB,MAAM;YACR,KAAK,YAAY;gBACf,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;gBACtB,MAAM;YACR,KAAK,IAAI,CAAC;YACV,KAAK,QAAQ;gBACX,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;gBAC9C,MAAM;YACR,KAAK,UAAU,EAAE,CAAC;gBAChB,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC,WAAW,EAAE,CAAC;gBACxC,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;gBACrC,IAAI,CAAC,MAAM,EAAE,CAAC;oBACZ,MAAM,IAAI,UAAU,CAAC,mBAAmB,KAAK,6BAA6B,CAAC,CAAC;gBAC9E,CAAC;gBACD,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;gBACxB,MAAM;YACR,CAAC;YACD,KAAK,gBAAgB;gBACnB,OAAO,CAAC,WAAW,GAAG,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;gBACzD,MAAM;YACR,KAAK,eAAe;gBAClB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;gBACtE,MAAM;YACR;gBACE,MAAM,IAAI,UAAU,CAAC,mBAAmB,IAAI,2BAA2B,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,QAAQ,GAAG,CAAC,cAAc,CAAC,CAAC;IACvE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,SAAS;IAChB,OAAO;QACL,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;QACnD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;QACnD,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;QAClB,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;KACrC,CAAC;AACJ,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAI,GAAsB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,GAAU,SAAS,EAAE;IACjG,IAAI,OAAmB,CAAC;IACxB,IAAI,CAAC;QACH,OAAO,GAAG,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,EAAE,CAAC,MAAM,CAAC,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACvE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACd,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChB,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QACrB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,YAAY,CAAC;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,WAAW,EAAE,OAAO,CAAC,WAAW;SACjC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,EAAE,CAAC,MAAM,CAAC,eAAe,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACnF,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,MAAM,CAAC,qCAAqC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrG,CAAC;QACD,OAAO,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC;IACnD,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAChC,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,MAAM,CACP,YAAY,CACV,MAAM,EACN;YACE,KAAK,EAAE,cAAc,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC;YACjE,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,KAAK,EAAE,cAAc,CAAC,EAAE,CAAC,GAAG,CAAC;SAC9B,EACD,OAAO,CAAC,WAAW,CACpB,CACF,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC;AAC3C,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Comment classification.
3
+ *
4
+ * A documented codebase defeats a plain text search in the most ironic way
5
+ * available: the comment recording that a symbol was deleted is itself a match
6
+ * for that symbol. `@assert-absence symbol="LegacyThing"` then fails on
7
+ *
8
+ * // LegacyThing was removed in ADR-398
9
+ *
10
+ * which is the sentence proving the assertion true. So matches are classified
11
+ * and, by default, those inside comments do not count.
12
+ *
13
+ * The scanner tracks strings as well as comments, and it has to: a `//` inside
14
+ * a string literal is not a comment, and misreading one is the dangerous
15
+ * direction. Consider
16
+ *
17
+ * const url = "http://example.com"; const x = LegacyThing;
18
+ *
19
+ * Treating `//` in that URL as a comment start would hide a real use of
20
+ * LegacyThing and turn a violation into a silent pass. Every uncertainty here
21
+ * therefore resolves the same way: **when in doubt, it is code**. A match
22
+ * wrongly kept is a visible failure someone can argue with; a match wrongly
23
+ * dropped is a lie.
24
+ *
25
+ * Languages it does not recognise get no comment ranges at all, so their
26
+ * matches all count - and the result says so rather than implying the file was
27
+ * understood.
28
+ */
29
+ interface StringRule {
30
+ open: string;
31
+ close: string;
32
+ /** Backslash escapes apply inside. */
33
+ escape: boolean;
34
+ }
35
+ export interface CommentSyntax {
36
+ name: string;
37
+ /** Prefixes that comment out the rest of the line. */
38
+ line: readonly string[];
39
+ /** Delimiter pairs for block comments. */
40
+ block: ReadonlyArray<readonly [string, string]>;
41
+ /** Block comments nest, as in Rust. */
42
+ nested: boolean;
43
+ /** Literal forms that may contain comment-looking text. */
44
+ strings: readonly StringRule[];
45
+ }
46
+ /** The comment syntax for a path, or null when the language is unknown. */
47
+ export declare function syntaxFor(filePath: string): CommentSyntax | null;
48
+ /** Half-open [start, end) offsets that are comment text. */
49
+ export type CommentRange = readonly [number, number];
50
+ /**
51
+ * Finds every comment in the source.
52
+ *
53
+ * One pass, tracking strings so that comment markers inside them are ignored.
54
+ * An unterminated comment or string runs to the end of the file, which matches
55
+ * how a compiler would read it.
56
+ *
57
+ * The scan is written so that it cannot stand still: whatever the branches
58
+ * below decide, the loop advances by at least one character. That guarantee is
59
+ * the difference between a wrong answer and a hang, and this loop appends as it
60
+ * goes - standing still here would not spin, it would eat memory until the
61
+ * process died, on somebody's file, in somebody's CI.
62
+ */
63
+ export declare function commentRanges(source: string, syntax: CommentSyntax): CommentRange[];
64
+ /**
65
+ * Decides whether a byte offset falls inside a comment.
66
+ *
67
+ * `classified` is false when the language is unknown, in which case nothing is
68
+ * treated as a comment. Callers report that rather than implying the file was
69
+ * understood.
70
+ */
71
+ export interface CommentMask {
72
+ readonly classified: boolean;
73
+ readonly syntax: string | null;
74
+ isComment(offset: number): boolean;
75
+ }
76
+ export declare function createCommentMask(source: string, filePath: string): CommentMask;
77
+ export {};
78
+ //# sourceMappingURL=comments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,0CAA0C;IAC1C,KAAK,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,uCAAuC;IACvC,MAAM,EAAE,OAAO,CAAC;IAChB,2DAA2D;IAC3D,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;CAChC;AAuHD,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAEhE;AAED,4DAA4D;AAC5D,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAqCrD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,YAAY,EAAE,CAkCnF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAQD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAqB/E"}