@descent-vtt/spec-guard 0.8.0 → 0.9.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +119 -19
  3. package/dist/cli.d.ts +37 -2
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +274 -59
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +66 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +165 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/engine.d.ts +22 -8
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +118 -67
  14. package/dist/engine.js.map +1 -1
  15. package/dist/facts.d.ts +106 -0
  16. package/dist/facts.d.ts.map +1 -0
  17. package/dist/facts.js +188 -0
  18. package/dist/facts.js.map +1 -0
  19. package/dist/glob.d.ts +9 -21
  20. package/dist/glob.d.ts.map +1 -1
  21. package/dist/glob.js +12 -24
  22. package/dist/glob.js.map +1 -1
  23. package/dist/imports.d.ts +6 -1
  24. package/dist/imports.d.ts.map +1 -1
  25. package/dist/imports.js +9 -5
  26. package/dist/imports.js.map +1 -1
  27. package/dist/index.d.ts +18 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +10 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/io.d.ts +51 -0
  32. package/dist/io.d.ts.map +1 -0
  33. package/dist/io.js +44 -0
  34. package/dist/io.js.map +1 -0
  35. package/dist/mcp.d.ts +11 -0
  36. package/dist/mcp.d.ts.map +1 -1
  37. package/dist/mcp.js +21 -17
  38. package/dist/mcp.js.map +1 -1
  39. package/dist/memo.d.ts +53 -0
  40. package/dist/memo.d.ts.map +1 -0
  41. package/dist/memo.js +67 -0
  42. package/dist/memo.js.map +1 -0
  43. package/dist/query.d.ts +5 -2
  44. package/dist/query.d.ts.map +1 -1
  45. package/dist/query.js +7 -3
  46. package/dist/query.js.map +1 -1
  47. package/dist/reporter.d.ts +10 -0
  48. package/dist/reporter.d.ts.map +1 -1
  49. package/dist/reporter.js +20 -0
  50. package/dist/reporter.js.map +1 -1
  51. package/dist/runner.d.ts +39 -2
  52. package/dist/runner.d.ts.map +1 -1
  53. package/dist/runner.js +85 -72
  54. package/dist/runner.js.map +1 -1
  55. package/dist/specs.d.ts +3 -1
  56. package/dist/specs.d.ts.map +1 -1
  57. package/dist/specs.js +6 -5
  58. package/dist/specs.js.map +1 -1
  59. package/dist/structure.d.ts +2 -2
  60. package/dist/structure.d.ts.map +1 -1
  61. package/dist/structure.js +5 -4
  62. package/dist/structure.js.map +1 -1
  63. package/dist/types.d.ts +17 -2
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/watch.d.ts +125 -0
  66. package/dist/watch.d.ts.map +1 -0
  67. package/dist/watch.js +321 -0
  68. package/dist/watch.js.map +1 -0
  69. package/package.json +8 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,96 @@ 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.9.0
9
+
10
+ A project's policy can live in its `package.json`, and `spec-guard --watch`
11
+ reports again as the tree changes, re-executing only the rules a change
12
+ affected. [ADR-0014](docs/adr/0014-configuration-and-watch.md).
13
+
14
+ ### Added
15
+
16
+ - **Options in `package.json`, under `"specGuard"`:** `specs`, `engine`,
17
+ `strict`, `allowMissingTargets`, `allowEmptyScope`, `ignoreStatus`,
18
+ `includeSpecs`, `defaultSkips`, `maxSnippets` and `concurrency`.
19
+ - Read from the root's `package.json` only, never a parent directory's.
20
+ - Validated before anything runs. An unknown key, an option that belongs to one
21
+ invocation (`format`, `verbose`, `watch`, ...), a string where a boolean goes
22
+ or a count out of range is exit 2, naming the file and the key. `--help` and
23
+ `--version` still work under a broken `package.json`.
24
+ - The command line wins: patterns replace `specs`, and every on/off option the
25
+ file can set gained its opposite - `--no-strict`,
26
+ `--no-allow-missing-targets`, `--no-allow-empty-scope`, `--no-ignore-status`,
27
+ `--no-include-specs` and `--default-skips`.
28
+ - A report that took anything from the file names it in a line above its
29
+ summary, with what the command line overrode; the JSON report carries
30
+ `config`.
31
+ - `query` applies the keys a query reads. The MCP server reads the file again
32
+ for every request, as it reads the specs.
33
+ - **`spec-guard --watch`.** Reports, then reports again whenever something under
34
+ the root changes, until Ctrl+C, which exits 130.
35
+ - Saves are batched: 50 ms of quiet, or 500 ms after the first change. Enter
36
+ re-runs everything. A change no rule reads updates the status line only.
37
+ - Only the rules whose directive or inputs changed are re-executed. On this
38
+ repository a save in `src` re-executes 21 of 60 rules in a median of 21 ms -
39
+ over the 15 ms it was built to, as ADR-0014 reports - and a save elsewhere
40
+ takes 2-3 ms.
41
+ - One recursive `fs.watch` on the root, and no dependency.
42
+ - Refuses `--engine` - it scans in-process, where it sees what each rule reads -
43
+ and `--json`, `--format json|sarif`, `--print-baseline`, `--fail-fast` and
44
+ `--allow-empty`.
45
+ - A test changes trees at random and requires every report a session gives to
46
+ equal a fresh run's, however the change is reported, and six deliberately
47
+ broken sessions have to fail it.
48
+ - **One door for every read** (`Io`, `nodeIo`, `readText`, `watchTree`), and the
49
+ session behind watch mode (`createSession`, `runWatch`, `createFactCache`,
50
+ `createMemo`) in the programmatic API. `runSpecGuard` is `planRun`, an
51
+ execution and `reportRun`, and those two are exported.
52
+ `createJavaScriptEngine(io, memo)` builds a scanner over a door, and
53
+ `createCachedEngine(engine, fallback)` names what it falls back to.
54
+ - `scripts/bench-watch.mjs`, which measures a session on a copy of this
55
+ repository.
56
+
57
+ ### Changed
58
+
59
+ - **API:** `defaultDirectoryReader` and `statOrNull` are gone for `nodeIo`.
60
+ `WalkOptions.readDirectory` is `WalkOptions.io`, `enumerateCandidates` takes an
61
+ `Io` as its third argument, and `createTreeIndex(root, io)` takes a whole door.
62
+ `Assertion.missingTargets` is gone: only execution ever wrote to it.
63
+ - This repository keeps its specs in `package.json`, and its selfcheck, CI run
64
+ and SARIF upload run `spec-guard` without patterns.
65
+ - **Mutation testing in CI runs in four parallel shards.** The full sweep took
66
+ 42m39s against a 45-minute job limit, and a slower runner was cancelled on the
67
+ same source. Hosted runners vary by a fifth or more on identical work.
68
+ - Each shard mutates its own files, balanced on per-file minutes read from
69
+ sweep logs (`scripts/mutation-timeline.mjs`).
70
+ - A final job merges the reports (`scripts/mutation-shards.mjs`). It refuses a
71
+ missing shard, a file mutated twice or by the wrong shard, and shards that ran
72
+ different tests, then applies the 97% gate to the merged score.
73
+ - Every shard runs every test: Stryker's vitest runner now has `related: false`.
74
+ - A full sweep starts from no incremental file, and a branch's shard from only
75
+ its own part of main's. Stryker reports the verdicts an incremental file holds
76
+ for files a run does not mutate, and on the first sharded sweep every shard
77
+ reported every other shard's files with the previous sweep's verdicts. The
78
+ merge refused that sweep.
79
+ - No mutants were left out, the per-mutant timeout is unchanged, and the gate
80
+ is unchanged. [ADR-0003](docs/adr/0003-mutation-testing.md).
81
+
82
+ ### Fixed
83
+
84
+ - **A binary file was a gap for every rule sharing its pass.** A plain run scans
85
+ every rule over one scope together, and kept one ledger for them: a binary file
86
+ holding one rule's symbol was reported against all of them with their matches
87
+ summed, so `--strict` failed rules over files that did not hold their symbol.
88
+ The count of files whose comments could not be classified leaked the same way.
89
+ Each rule now has its own.
90
+ - **Two runs of one tree could list skipped files in different orders**, and past
91
+ the ledger's cap of 100, name different ones: files entered the ledger as their
92
+ reads finished. They now enter in walk order.
93
+ - Executing one resolved assertion twice named each missing target twice. Nothing
94
+ did so before watch mode.
95
+ - A test of the MCP server's durations that failed about one run in 53, on
96
+ floating-point error in the test.
97
+
8
98
  ## 0.8.0
9
99
 
10
100
  An architecture document can now state conventions about names and layout: what
package/README.md CHANGED
@@ -758,6 +758,7 @@ sources they follow, and what is deliberately not implemented.
758
758
 
759
759
  ```bash
760
760
  spec-guard [patterns...] [options] # execute the directives
761
+ spec-guard --watch [patterns...] # execute them again whenever the tree changes
761
762
  spec-guard query <paths...> [options] # the rules in force for files or directories
762
763
  spec-guard mcp [options] # serve the rules over MCP on stdio
763
764
  ```
@@ -767,17 +768,18 @@ spec-guard mcp [options] # serve the rules over MCP on stdio
767
768
  | `-r, --root <path>` | Codebase root that assertions resolve against (default: cwd) |
768
769
  | `--spec <pattern>` | A spec glob or path, repeatable. `query` and `mcp` take their specs only from here |
769
770
  | `-v, --verbose` | Print passing assertions too |
771
+ | `--watch` | Report, then report again as the tree changes, until Ctrl+C |
770
772
  | `--fail-fast` | Stop at the first failing assertion |
771
773
  | `--json` | Machine-readable report on stdout (same as `--format json`) |
772
774
  | `--format <human\|json\|sarif>` | Output format. `sarif` uploads to GitHub code scanning |
773
775
  | `--engine <auto\|rg\|js>` | Search engine (default `auto`: scanner for small trees, ripgrep for big ones) |
774
- | `--strict` | Treat analysis that could not be completed as a failure |
775
- | `--allow-missing-targets` | Warn instead of failing when a `target` path does not exist |
776
- | `--allow-empty-scope` | Warn instead of failing when an assertion inspects no files |
776
+ | `--strict` / `--no-strict` | Treat analysis that could not be completed as a failure |
777
+ | `--allow-missing-targets` / `--no-allow-missing-targets` | Warn instead of failing when a `target` path does not exist |
778
+ | `--allow-empty-scope` / `--no-allow-empty-scope` | Warn instead of failing when an assertion inspects no files |
777
779
  | `--print-baseline` | Print the `baseline="..."` that would exempt today's violations, and exit |
778
- | `--no-default-skips` | Search `.git`, `.hg`, `.svn` and `node_modules` too |
779
- | `--ignore-status` | Execute directives in draft, proposed and superseded documents too |
780
- | `--include-specs` | Also count matches inside the spec files themselves |
780
+ | `--no-default-skips` / `--default-skips` | Search `.git`, `.hg`, `.svn` and `node_modules` too |
781
+ | `--ignore-status` / `--no-ignore-status` | Execute directives in draft, proposed and superseded documents too |
782
+ | `--include-specs` / `--no-include-specs` | Also count matches inside the spec files themselves |
781
783
  | `--max-snippets <n>` | Failure snippets per assertion (default 5) |
782
784
  | `--concurrency <n>` | Search passes in flight at once (default 8) |
783
785
  | `--allow-empty` | Exit 0 when no spec file matched the patterns (about the run, not an assertion) |
@@ -785,6 +787,93 @@ spec-guard mcp [options] # serve the rules over MCP on stdio
785
787
 
786
788
  Patterns are expanded by spec-guard itself, so quoted globs behave identically
787
789
  on Windows, macOS and Linux. A directory expands to the Markdown files in it.
790
+ The second form of each on/off option exists to override a configuration.
791
+
792
+ ### Configuration
793
+
794
+ The options that are a project's policy can live in `package.json`, under
795
+ `"specGuard"`, so CI, a pre-commit hook and the MCP server an agent starts all
796
+ hold the same rules the same way:
797
+
798
+ ```json
799
+ {
800
+ "specGuard": {
801
+ "specs": ["docs/**/*.md", "README.md"],
802
+ "strict": true
803
+ }
804
+ }
805
+ ```
806
+
807
+ | Key | Command line | Value |
808
+ | --- | --- | --- |
809
+ | `specs` | patterns, `--spec` | a non-empty array of globs |
810
+ | `engine` | `--engine` | `"auto"`, `"rg"` or `"js"` |
811
+ | `strict`, `allowMissingTargets`, `allowEmptyScope`, `ignoreStatus`, `includeSpecs`, `defaultSkips` | the flag of that name | `true` or `false` |
812
+ | `maxSnippets` | `--max-snippets` | an integer, 0 or more |
813
+ | `concurrency` | `--concurrency` | an integer, 1 or more |
814
+
815
+ - **Only the root's `package.json`** is read: the `--root` directory, or the
816
+ working directory. Nothing is inherited from a parent directory.
817
+ - **The command line wins, both ways.** Patterns replace `specs`, and
818
+ `--no-strict` beats `"strict": true`.
819
+ - **A malformed configuration is exit 2, before anything runs**, naming the file
820
+ and the key: an unknown key, `"true"` where `true` goes, or an option that
821
+ belongs to one invocation, such as `format` or `verbose`.
822
+ - **Every report says what it took from the file**: a line above the summary
823
+ such as `options from package.json: specs, strict`, and a `config` field in
824
+ JSON. An
825
+ option nobody can see should never decide a result.
826
+ - `query` applies what a query reads, and the MCP server reads the file again
827
+ for every request.
828
+
829
+ ### Watch mode
830
+
831
+ ```bash
832
+ spec-guard --watch
833
+ ```
834
+
835
+ Prints the report, then prints it again whenever something under the root
836
+ changes, until Ctrl+C. Saves are batched: a batch starts after 50 ms without a
837
+ new change, or 500 ms after its first. On a terminal the screen is redrawn;
838
+ otherwise each report is appended under the time. A change that no rule reads
839
+ updates the status line and nothing else:
840
+
841
+ ```text
842
+ watching 15 specs · 1 change · 21 of 60 rules re-executed · 21 ms · Enter re-runs everything, Ctrl+C stops
843
+ ```
844
+
845
+ A session re-executes only the rules whose directive or inputs changed. Every
846
+ filesystem read goes through one module, so the session can record which rule
847
+ read what. A watcher's event only evicts what it may have changed, and the
848
+ evicted facts are read again and compared, so noise costs one directory read,
849
+ and a renamed directory's contents need no events of their own. Work that is a
850
+ pure function of a file's bytes - tokenizing, comment masking, scanning - is
851
+ remembered by the bytes' hash. Each of those is a way to report a tree that no
852
+ longer exists, so a test changes trees at random and requires every report a
853
+ session gives to equal a fresh run's, with six deliberately broken sessions
854
+ that it has to catch. [ADR-0014](docs/adr/0014-configuration-and-watch.md) has
855
+ the design, the measurements and the limits.
856
+
857
+ On this repository, from a change to the report, in the quietest of four runs
858
+ (a plain warm run took 69 ms in it):
859
+
860
+ | Edit | Rules re-executed | Median |
861
+ | --- | --- | --- |
862
+ | a file in `src` | 21 of 60 | 21 ms |
863
+ | a test file | 1 of 60 | 3.3 ms |
864
+ | a file no rule reads | 0 | 2.0 ms |
865
+ | an ADR's prose | 0 | 2.4 ms |
866
+
867
+ The 15 ms this was built to is missed for a save in `src`, which re-executes the
868
+ two layer rules and every text rule over `src`; ADR-0014 says where the time
869
+ goes.
870
+
871
+ `--watch` scans in-process, where it can see what each rule reads, so it refuses
872
+ `--engine`, and `--json`, `--format json|sarif`, `--print-baseline`,
873
+ `--fail-fast` and `--allow-empty`, which each describe a single run. It exits
874
+ 130 when stopped, since a session is neither a pass nor a failure. What it
875
+ cannot see: a change the operating system never reports, a change behind a
876
+ symbolic link, and anything outside the root. CI stays the authority.
788
877
 
789
878
  ### Exit codes
790
879
 
@@ -792,7 +881,8 @@ on Windows, macOS and Linux. A directory expands to the Markdown files in it.
792
881
  | --- | --- |
793
882
  | `0` | Every assertion held |
794
883
  | `1` | An assertion failed, or a directive was malformed |
795
- | `2` | spec-guard could not run: bad usage, no spec files matched, `--engine rg` with no ripgrep |
884
+ | `2` | spec-guard could not run: bad usage, a malformed configuration, no spec files matched, `--engine rg` with no ripgrep, a watch that could not start |
885
+ | `130` | A `--watch` session was stopped |
796
886
 
797
887
  ## CI integration
798
888
 
@@ -854,9 +944,10 @@ spec-guard's claims are about a whole repository - "this symbol appears nowhere
854
944
  in `src`" - and a language server is handed one buffer at a time. Answering a
855
945
  repository-wide question on every keystroke means rescanning the tree on every
856
946
  keystroke; the alternative is answering a smaller question and calling it the
857
- same one. The CLI does a full run of this repository in about 130 ms, so a
858
- pre-commit hook or a watch loop already closes the feedback gap without a
859
- daemon, an extension per editor, or a protocol version matrix.
947
+ same one. `--watch` re-evaluates this repository in milliseconds after a save
948
+ by re-executing only what the save affected, and a pre-commit hook closes the
949
+ rest of the gap, without a daemon, an extension per editor, or a protocol
950
+ version matrix.
860
951
 
861
952
  ## How it works
862
953
 
@@ -1099,7 +1190,7 @@ npm install
1099
1190
  npm run build # tsc -> dist/
1100
1191
  npm test # vitest
1101
1192
  npm run test:coverage
1102
- npm run test:mutation # stryker (~16 min in CI; over two hours locally)
1193
+ npm run test:mutation # stryker (four parallel jobs in CI; hours locally)
1103
1194
  npm run lint # tsc --noEmit
1104
1195
  npm run selfcheck # run spec-guard on its own docs
1105
1196
  ```
@@ -1114,10 +1205,13 @@ prebuilt binary and is never a runtime dependency.
1114
1205
 
1115
1206
  Coverage says a line ran. It does not say an assertion would notice if the line
1116
1207
  behaved differently. This repository measures the difference: line coverage is
1117
- **100%**, and **97.65%** of 4,299 mutants are killed in CI, with no file below
1118
- 95%. The second number is the one worth reading - and it is the ceiling, not a
1119
- way-point: every surviving mutant has been checked individually and the 101 that
1120
- are left produce byte-identical output.
1208
+ **100%**, and **98.84%** of 7,763 mutants are killed in CI, with 87 survivors and
1209
+ no file below 95%. The second number is the one worth reading, and what was done
1210
+ about the survivors matters more than the score. At 0.5.1 every surviving mutant
1211
+ was checked individually, and the 101 left then produced byte-identical output.
1212
+ Each release since has replayed the survivors its own code added before it
1213
+ shipped: they were killed, deleted as dead code, or shown equivalent and
1214
+ recorded in [ADR-0003](docs/adr/0003-mutation-testing.md) with the evidence.
1121
1215
 
1122
1216
  That gap is the point. The first run scored 77.23%, and the weakest file was the
1123
1217
  reporter at 65.48% - not because it lacked tests, but because its tests were
@@ -1158,13 +1252,19 @@ quoted above; a full sweep is also what publishes the cache the branches start
1158
1252
  from, so an incremental verdict can never be built on another incremental
1159
1253
  verdict.
1160
1254
 
1161
- The full sweep was 6m54s at 2,118 mutants, 15m51s at 3,493, and is 17m at
1162
- 4,299. That growth is why the tiers exist: a check that gets quietly more
1163
- expensive every release is a check somebody eventually proposes lowering.
1255
+ The full sweep was 6m54s at 2,118 mutants, 15m51s at 3,493, and 42m39s at 7,763.
1256
+ That growth is why the tiers exist: a check that gets quietly more expensive
1257
+ every release is a check somebody eventually proposes lowering. It is also why
1258
+ both tiers now run in four parallel shards. One runner
1259
+ could no longer finish the full sweep reliably inside the job's limit, and the
1260
+ answer was to split the sweep rather than raise the limit or drop mutants.
1261
+ Each shard mutates its own files against every test, and a final job merges the
1262
+ reports. The merge refuses anything that is not exactly one sweep, and applies
1263
+ the 97% gate to the merged score with the same library Stryker's gate uses.
1164
1264
 
1165
1265
  Run it locally with `npm run test:mutation` if you like, but do not calibrate
1166
1266
  anything on the result: on the Windows machine this was developed on the same
1167
- suite takes over two hours against 21 minutes hosted, and it scores *higher*,
1267
+ suite takes hours against 43 minutes of hosted time, and it scores *higher*,
1168
1268
  because far more mutants hang there and Stryker counts a hang as a kill. Linux
1169
1269
  CI is the measurement.
1170
1270
 
package/dist/cli.d.ts CHANGED
@@ -7,7 +7,10 @@
7
7
  * 2 - spec-guard could not run (bad usage, no spec files, missing engine)
8
8
  */
9
9
  import type { Readable } from 'node:stream';
10
+ import { type ConfigKey, type ProjectConfig } from './config.js';
11
+ import { watchTree } from './io.js';
10
12
  import type { EnginePreference } from './engine.js';
13
+ import type { ConfigUse } from './types.js';
11
14
  export declare const EXIT_OK = 0;
12
15
  export declare const EXIT_FAILED = 1;
13
16
  export declare const EXIT_ERROR = 2;
@@ -17,8 +20,17 @@ export interface CliIO {
17
20
  env: NodeJS.ProcessEnv;
18
21
  cwd: string;
19
22
  isTTY: boolean;
20
- /** Where `spec-guard mcp` reads its messages. Only that command reads input. */
23
+ /**
24
+ * Where `spec-guard mcp` reads its messages, and where a watch session hears
25
+ * Enter. Nothing else reads input.
26
+ */
21
27
  stdin?: Readable;
28
+ /** Writes to stdout as it is, with no newline added: a watch session redraws. */
29
+ write?: (text: string) => void;
30
+ /** Starts a recursive watch on a directory; a test passes one it drives itself. */
31
+ watch?: typeof watchTree;
32
+ /** Registers what Ctrl+C and SIGTERM do, and returns how to unregister it. */
33
+ onInterrupt?: (handler: () => void) => () => void;
22
34
  }
23
35
  /** What the command line asked for: a run, a query, or a server. */
24
36
  export type Command = 'check' | 'query' | 'mcp';
@@ -47,8 +59,18 @@ export interface CliOptions {
47
59
  maxSnippets: number;
48
60
  concurrency: number;
49
61
  color?: boolean;
62
+ /** Re-run as the tree changes, until stopped. ADR-0014. */
63
+ watch: boolean;
50
64
  help: boolean;
51
65
  version: boolean;
66
+ /**
67
+ * The configurable options the command line set, either way.
68
+ *
69
+ * A configuration fills in only what is not here, so a flag always wins - and
70
+ * `--strict` given alongside `"strict": true` is still reported as the command
71
+ * line's doing, not the file's.
72
+ */
73
+ fromCommandLine: Set<ConfigKey>;
52
74
  }
53
75
  export declare class UsageError extends Error {
54
76
  }
@@ -60,9 +82,22 @@ export declare class UsageError extends Error {
60
82
  * fallback nobody knows still works.
61
83
  */
62
84
  export declare function version(manifest?: string): string;
63
- export declare const HELP = "spec-guard - Executable architecture assertions for Markdown specs & ADRs\n\nUsage\n spec-guard [patterns...] [options] execute the directives in the specs\n spec-guard query <paths...> [options] list the rules in force for files or directories\n spec-guard mcp [options] serve the rules to an AI agent over MCP on stdio\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. query and\n mcp take theirs from --spec.\n\nOptions\n -r, --root <path> Codebase root that assertions are resolved against (default: cwd)\n --spec <pattern> A spec glob or path; repeatable (default: \"docs/**/*.md\")\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 --ignore-status Execute directives in draft, proposed and superseded documents 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 <!-- @assert-import-cycle target=\"src/\" types=\"ignore\" -->\n <!-- @assert-layers target=\"src/\" order=\"domain, application, infrastructure\" -->\n <!-- @assert-structure target=\"src/\" exclude=\"*.test.ts\" partner=\"[name].test.ts\" -->\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 A document whose status is draft, proposed, rejected, deprecated or superseded\n is reported and not executed; --ignore-status runs it anyway.\n\nquery answers from the specs alone, without reading the codebase, so it works\nfor a file that does not exist yet. mcp offers the same answer, and a check,\nas the tools get_architectural_rules and check_architecture.\n\nExit codes\n 0 all assertions passed 1 an assertion failed 2 spec-guard could not run";
85
+ export declare const HELP = "spec-guard - Executable architecture assertions for Markdown specs & ADRs\n\nUsage\n spec-guard [patterns...] [options] execute the directives in the specs\n spec-guard query <paths...> [options] list the rules in force for files or directories\n spec-guard mcp [options] serve the rules to an AI agent over MCP on stdio\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. query and\n mcp take theirs from --spec.\n\nOptions\n -r, --root <path> Codebase root that assertions are resolved against (default: cwd)\n --spec <pattern> A spec glob or path; repeatable (default: \"docs/**/*.md\")\n -v, --verbose Print passing assertions too\n --watch Run again as the tree changes, until Ctrl+C (human output only)\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 --ignore-status Execute directives in draft, proposed and superseded documents 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\nConfiguration\n package.json in the root can hold specs, engine, strict, allowMissingTargets,\n allowEmptyScope, ignoreStatus, includeSpecs, defaultSkips, maxSnippets and\n concurrency under \"specGuard\". A flag wins over the file, and every on/off\n option there also takes its opposite (--no-strict, --default-skips, ...).\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 <!-- @assert-import-cycle target=\"src/\" types=\"ignore\" -->\n <!-- @assert-layers target=\"src/\" order=\"domain, application, infrastructure\" -->\n <!-- @assert-structure target=\"src/\" exclude=\"*.test.ts\" partner=\"[name].test.ts\" -->\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 A document whose status is draft, proposed, rejected, deprecated or superseded\n is reported and not executed; --ignore-status runs it anyway.\n\nquery answers from the specs alone, without reading the codebase, so it works\nfor a file that does not exist yet. mcp offers the same answer, and a check,\nas the tools get_architectural_rules and check_architecture.\n\nExit codes\n 0 all assertions passed 1 an assertion failed 2 spec-guard could not run\n 130 a --watch session was stopped";
64
86
  /** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
65
87
  export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
88
+ /**
89
+ * Fills in what the command line left unset from a configuration, and says what
90
+ * it took.
91
+ *
92
+ * A key the command does not read is neither applied nor overridden: `strict`
93
+ * is written for the runs, and a query that ignores it has not been told
94
+ * anything about strictness. A watch session scans in-process, so `engine` is
95
+ * not one of its keys either. Returns nothing when the configuration had
96
+ * nothing to say to this command, so a report says nothing about it.
97
+ */
98
+ export declare function applyConfig(options: CliOptions, config: ProjectConfig, file?: string): ConfigUse | undefined;
99
+ /** The process's own streams, signals and filesystem watcher. */
100
+ export declare function defaultIO(): CliIO;
66
101
  /** Runs the CLI and resolves to the process exit code. */
67
102
  export declare function main(argv?: readonly string[], io?: CliIO): Promise<number>;
68
103
  //# sourceMappingURL=cli.d.ts.map
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;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAM5C,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;IACf,gFAAgF;IAChF,KAAK,CAAC,EAAE,QAAQ,CAAC;CAClB;AAED,oEAAoE;AACpE,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,CAAC;AAEhD,yCAAyC;AACzC,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,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,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;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,QAAQ,SAAoB,GAAG,MAAM,CAQ5D;AAED,eAAO,MAAM,IAAI,mvGAqD8D,CAAC;AA2DhF,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAqJ1E;AAoED,0DAA0D;AAC1D,wBAAsB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAA0B,EAAE,EAAE,GAAE,KAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8EpH"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}