@descent-vtt/spec-guard 0.11.0 → 0.12.1

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 (157) hide show
  1. package/CHANGELOG.md +443 -31
  2. package/README.md +403 -41
  3. package/dist/cites.d.ts +192 -0
  4. package/dist/cites.d.ts.map +1 -0
  5. package/dist/cites.js +599 -0
  6. package/dist/cites.js.map +1 -0
  7. package/dist/cli.d.ts +14 -6
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +315 -22
  10. package/dist/cli.js.map +1 -1
  11. package/dist/comments.d.ts +1 -1
  12. package/dist/comments.js +1 -1
  13. package/dist/config.d.ts +3 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +46 -5
  16. package/dist/config.js.map +1 -1
  17. package/dist/engine.d.ts.map +1 -1
  18. package/dist/engine.js +38 -14
  19. package/dist/engine.js.map +1 -1
  20. package/dist/glob.d.ts +128 -24
  21. package/dist/glob.d.ts.map +1 -1
  22. package/dist/glob.js +304 -61
  23. package/dist/glob.js.map +1 -1
  24. package/dist/impact.d.ts +179 -0
  25. package/dist/impact.d.ts.map +1 -0
  26. package/dist/impact.js +360 -0
  27. package/dist/impact.js.map +1 -0
  28. package/dist/index.d.ts +15 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +8 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/mcp.d.ts +22 -75
  33. package/dist/mcp.d.ts.map +1 -1
  34. package/dist/mcp.js +206 -370
  35. package/dist/mcp.js.map +1 -1
  36. package/dist/overlay.d.ts +39 -0
  37. package/dist/overlay.d.ts.map +1 -0
  38. package/dist/overlay.js +133 -0
  39. package/dist/overlay.js.map +1 -0
  40. package/dist/parser.d.ts +41 -24
  41. package/dist/parser.d.ts.map +1 -1
  42. package/dist/parser.js +217 -160
  43. package/dist/parser.js.map +1 -1
  44. package/dist/prove.d.ts +46 -0
  45. package/dist/prove.d.ts.map +1 -0
  46. package/dist/prove.js +549 -0
  47. package/dist/prove.js.map +1 -0
  48. package/dist/query.d.ts +18 -1
  49. package/dist/query.d.ts.map +1 -1
  50. package/dist/query.js +9 -3
  51. package/dist/query.js.map +1 -1
  52. package/dist/reporter.d.ts +139 -1
  53. package/dist/reporter.d.ts.map +1 -1
  54. package/dist/reporter.js +593 -21
  55. package/dist/reporter.js.map +1 -1
  56. package/dist/rules.js +3 -3
  57. package/dist/rules.js.map +1 -1
  58. package/dist/runner.d.ts +31 -2
  59. package/dist/runner.d.ts.map +1 -1
  60. package/dist/runner.js +85 -21
  61. package/dist/runner.js.map +1 -1
  62. package/dist/specs.d.ts +27 -1
  63. package/dist/specs.d.ts.map +1 -1
  64. package/dist/specs.js +45 -10
  65. package/dist/specs.js.map +1 -1
  66. package/dist/structure.d.ts.map +1 -1
  67. package/dist/structure.js +7 -6
  68. package/dist/structure.js.map +1 -1
  69. package/dist/types.d.ts +193 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/vendor/spec-core/jsonrpc/index.d.ts +3 -0
  72. package/dist/vendor/spec-core/jsonrpc/index.d.ts.map +1 -0
  73. package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
  74. package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
  75. package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +158 -0
  76. package/dist/vendor/spec-core/jsonrpc/mcp.d.ts.map +1 -0
  77. package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
  78. package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
  79. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +104 -0
  80. package/dist/vendor/spec-core/markdown/frontmatter.d.ts.map +1 -0
  81. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  82. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  83. package/dist/vendor/spec-core/markdown/headings.d.ts +46 -0
  84. package/dist/vendor/spec-core/markdown/headings.d.ts.map +1 -0
  85. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  86. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  87. package/dist/vendor/spec-core/markdown/index.d.ts +6 -0
  88. package/dist/vendor/spec-core/markdown/index.d.ts.map +1 -0
  89. package/dist/vendor/spec-core/markdown/index.js +4 -0
  90. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  91. package/dist/vendor/spec-core/markdown/layout.d.ts +40 -0
  92. package/dist/vendor/spec-core/markdown/layout.d.ts.map +1 -0
  93. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  94. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  95. package/dist/vendor/spec-core/markdown/links.d.ts +26 -0
  96. package/dist/vendor/spec-core/markdown/links.d.ts.map +1 -0
  97. package/dist/vendor/spec-core/markdown/links.js +394 -0
  98. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  99. package/dist/vendor/spec-core/markdown/lists.d.ts +23 -0
  100. package/dist/vendor/spec-core/markdown/lists.d.ts.map +1 -0
  101. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  102. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  103. package/dist/vendor/spec-core/markdown/scan.d.ts +37 -0
  104. package/dist/vendor/spec-core/markdown/scan.d.ts.map +1 -0
  105. package/dist/vendor/spec-core/markdown/scan.js +463 -0
  106. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  107. package/dist/vendor/spec-core/markdown/syntax.d.ts +76 -0
  108. package/dist/vendor/spec-core/markdown/syntax.d.ts.map +1 -0
  109. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  110. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  111. package/dist/vendor/spec-core/markdown/tables.d.ts +19 -0
  112. package/dist/vendor/spec-core/markdown/tables.d.ts.map +1 -0
  113. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  114. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  115. package/dist/vendor/spec-core/markdown/types.d.ts +291 -0
  116. package/dist/vendor/spec-core/markdown/types.d.ts.map +1 -0
  117. package/dist/vendor/spec-core/markdown/types.js +9 -0
  118. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  119. package/dist/vendor/spec-core/path/index.d.ts +2 -0
  120. package/dist/vendor/spec-core/path/index.d.ts.map +1 -0
  121. package/dist/vendor/spec-core/path/index.js +2 -0
  122. package/dist/vendor/spec-core/path/index.js.map +1 -0
  123. package/dist/vendor/spec-core/path/posix.d.ts +84 -0
  124. package/dist/vendor/spec-core/path/posix.d.ts.map +1 -0
  125. package/dist/vendor/spec-core/path/posix.js +150 -0
  126. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  127. package/dist/vendor/spec-core/pattern/automaton.d.ts +130 -0
  128. package/dist/vendor/spec-core/pattern/automaton.d.ts.map +1 -0
  129. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  130. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  131. package/dist/vendor/spec-core/pattern/charset.d.ts +59 -0
  132. package/dist/vendor/spec-core/pattern/charset.d.ts.map +1 -0
  133. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  134. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  135. package/dist/vendor/spec-core/pattern/glob.d.ts +122 -0
  136. package/dist/vendor/spec-core/pattern/glob.d.ts.map +1 -0
  137. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  138. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  139. package/dist/vendor/spec-core/pattern/index.d.ts +7 -0
  140. package/dist/vendor/spec-core/pattern/index.d.ts.map +1 -0
  141. package/dist/vendor/spec-core/pattern/index.js +4 -0
  142. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  143. package/dist/vendor/spec-core/pattern/regex.d.ts +68 -0
  144. package/dist/vendor/spec-core/pattern/regex.d.ts.map +1 -0
  145. package/dist/vendor/spec-core/pattern/regex.js +813 -0
  146. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  147. package/dist/vendor/spec-core/text/index.d.ts +3 -0
  148. package/dist/vendor/spec-core/text/index.d.ts.map +1 -0
  149. package/dist/vendor/spec-core/text/index.js +2 -0
  150. package/dist/vendor/spec-core/text/index.js.map +1 -0
  151. package/dist/vendor/spec-core/text/lines.d.ts +73 -0
  152. package/dist/vendor/spec-core/text/lines.d.ts.map +1 -0
  153. package/dist/vendor/spec-core/text/lines.js +156 -0
  154. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  155. package/package.json +9 -2
  156. package/src/vendor/spec-core/LICENSE +21 -0
  157. package/src/vendor/spec-core/README.md +8 -0
package/dist/cli.d.ts CHANGED
@@ -10,7 +10,7 @@ import type { Readable } from 'node:stream';
10
10
  import { type ConfigKey, type ProjectConfig } from './config.js';
11
11
  import { watchTree } from './io.js';
12
12
  import type { EnginePreference } from './engine.js';
13
- import type { ConfigUse } from './types.js';
13
+ import type { CiteFamily, ConfigUse } from './types.js';
14
14
  export declare const EXIT_OK = 0;
15
15
  export declare const EXIT_FAILED = 1;
16
16
  export declare const EXIT_ERROR = 2;
@@ -32,15 +32,23 @@ export interface CliIO {
32
32
  /** Registers what Ctrl+C and SIGTERM do, and returns how to unregister it. */
33
33
  onInterrupt?: (handler: () => void) => () => void;
34
34
  }
35
- /** What the command line asked for: a run, a query, or a server. */
36
- export type Command = 'check' | 'query' | 'mcp';
35
+ /**
36
+ * What the command line asked for: a run, a query, a server, a proof that the
37
+ * rules can fail, the citations of specs in code comments, or who depends on a
38
+ * path.
39
+ */
40
+ export type Command = 'check' | 'query' | 'mcp' | 'prove' | 'cites' | 'impact';
37
41
  /** How a finished run is written out. */
38
- export type OutputFormat = 'human' | 'json' | 'sarif';
42
+ export type OutputFormat = 'human' | 'json' | 'sarif' | 'github' | 'gitlab';
39
43
  export interface CliOptions {
40
44
  command: Command;
41
45
  patterns: string[];
42
- /** The paths `spec-guard query` asks about. */
46
+ /** The paths `spec-guard query` and `spec-guard impact` ask about, and the ones `spec-guard cites` reads. */
43
47
  paths: string[];
48
+ /** How many imports away `spec-guard impact` follows dependents; all of them when absent. */
49
+ depth?: number;
50
+ /** The documents comments cite, from a configuration; derived from the specs when absent. */
51
+ cites?: CiteFamily[];
44
52
  root: string;
45
53
  verbose: boolean;
46
54
  failFast: boolean;
@@ -84,7 +92,7 @@ export declare class UsageError extends Error {
84
92
  * fallback nobody knows still works.
85
93
  */
86
94
  export declare function version(manifest?: string): string;
87
- 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 --exclude <globs> Paths no assertion looks at, beside each directive's exclude; repeatable\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, exclude, engine, strict,\n allowMissingTargets, allowEmptyScope, ignoreStatus, includeSpecs, defaultSkips,\n maxSnippets and concurrency under \"specGuard\"; a root with no package.json can\n keep them in .spec-guard.json instead, but not in both. A flag wins over the\n file, every on/off option there also takes its opposite (--no-strict,\n --default-skips, ...), and --exclude= with nothing clears exclude.\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";
95
+ 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 spec-guard prove [patterns...] [options]\n show each rule a violation of itself, in memory, and report any that pass\n spec-guard cites [paths...] [options] check that each spec a code comment cites exists and is in force\n spec-guard impact <paths...> [options] the files that depend on each path, and the rules in play there\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,\n mcp, cites and impact 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 | github | gitlab (sarif uploads to code scanning,\n github annotates a pull request from the log, gitlab is Code Quality)\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 --exclude <globs> Paths no assertion looks at, beside each directive's exclude; repeatable\n --ignore-status Execute directives in documents not in force too (see Directives)\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 --depth <n> impact: follow dependents at most n imports away (default: all)\n --allow-empty Exit 0 when no spec files matched (a run, prove or impact; 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, exclude, engine, strict,\n allowMissingTargets, allowEmptyScope, ignoreStatus, includeSpecs, defaultSkips,\n maxSnippets and concurrency under \"specGuard\"; a root with no package.json can\n keep them in .spec-guard.json instead, but not in both. A flag wins over the\n file, every on/off option there also takes its opposite (--no-strict,\n --default-skips, ...), and --exclude= with nothing clears exclude.\n \"cites\": [{ \"id\": \"ADR-{n}\", \"files\": \"docs/adr/{n}-*.md\" }] names the\n documents code comments cite; without it, cites reads them off the titles of\n numbered specs.\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, superseded\n or archived is reported and not executed; --ignore-status runs it anyway.\n\nprove adds, changes or removes files in memory only - never on disk - to make\nthe violation each rule forbids, and runs the rule over that tree. A rule that\nstill passes survived, and exits 1; one no violation could be made for is\nunprovable, and exits 1 under --strict.\n\ncites reads comment text only - not strings, not Markdown - for ids such as\nADR-0007, ADR-7 and ADR-007, which are one document. A ghost citation names no\ndocument, and exits 1; a stale one names a document superseded, deprecated,\nrejected or archived, and exits 1 under --strict. A family whose files match no\ndocument exits 2.\n\nimpact follows the import graph backwards: relative JavaScript, TypeScript and\nPython imports that name a file. Go, Rust, C# and absolute Python imports name\nmodules, and are counted rather than followed; an import that names no file is\nlisted, never guessed at.\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, a check and\nimpact's, as the tools get_architectural_rules, check_architecture and\nget_dependents.\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";
88
96
  /** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
89
97
  export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
90
98
  /**
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -8,17 +8,40 @@
8
8
  */
9
9
  import { createRequire } from 'node:module';
10
10
  import path from 'node:path';
11
+ import { CitesError, findCitations } from './cites.js';
11
12
  import { CONFIG_KEYS, ConfigError, engineNamed, findConfig } from './config.js';
12
- import { excludeListError } from './glob.js';
13
+ import { formatImpact, formatImpactJson, impactOf } from './impact.js';
14
+ import { excludeListError, patternListError, specPatternError } from './glob.js';
13
15
  import { nodeIo, readText, watchTree } from './io.js';
14
16
  import { createMcpHandler, serveStdio } from './mcp.js';
15
- import { formatQuery, formatQueryJson, queryRules } from './query.js';
16
- import { formatBaselines, formatConfigUse, formatJson, formatReport, formatSarif, shouldUseAscii, shouldUseColor } from './reporter.js';
17
+ import { formatQuery, formatQueryJson, queryRules, resolveQueryPath } from './query.js';
18
+ import { proveSpecGuard } from './prove.js';
19
+ import { citesAnnotations, formatBaselines, formatCites, formatCitesJson, formatCitesSarif, formatConfigUse, formatGithub, formatGitlab, formatJson, formatProve, formatProveJson, formatProveSarif, formatReport, formatSarif, proveAnnotations, runAnnotations, shouldUseAscii, shouldUseColor, } from './reporter.js';
17
20
  import { DEFAULT_CONCURRENCY, DEFAULT_MAX_SNIPPETS, runSpecGuard, splitList } from './runner.js';
18
21
  import { createSession, runWatch } from './watch.js';
19
22
  export const EXIT_OK = 0;
20
23
  export const EXIT_FAILED = 1;
21
24
  export const EXIT_ERROR = 2;
25
+ /** The words that name a command when they come first. */
26
+ const COMMANDS = new Set(['query', 'mcp', 'prove', 'cites', 'impact']);
27
+ /**
28
+ * The formats each command writes. A command that lists rules rather than
29
+ * findings has nothing to put on a line of code, so it has no SARIF, GitHub or
30
+ * GitLab form; `mcp` writes the protocol, and refuses `--format` altogether.
31
+ */
32
+ const FORMATS = {
33
+ check: ['human', 'json', 'sarif', 'github', 'gitlab'],
34
+ prove: ['human', 'json', 'sarif', 'github', 'gitlab'],
35
+ cites: ['human', 'json', 'sarif', 'github', 'gitlab'],
36
+ query: ['human', 'json'],
37
+ impact: ['human', 'json'],
38
+ };
39
+ /** What a command without line-level formats lists instead of results, for the message refusing one. */
40
+ const LISTS = { query: 'rules', impact: 'files and rules' };
41
+ /** `a or b`, `a, b or c`: every command that takes a format has at least two. */
42
+ function either(words) {
43
+ return `${words.slice(0, -1).join(', ')} or ${words[words.length - 1]}`;
44
+ }
22
45
  export class UsageError extends Error {
23
46
  }
24
47
  /**
@@ -44,11 +67,15 @@ Usage
44
67
  spec-guard [patterns...] [options] execute the directives in the specs
45
68
  spec-guard query <paths...> [options] list the rules in force for files or directories
46
69
  spec-guard mcp [options] serve the rules to an AI agent over MCP on stdio
70
+ spec-guard prove [patterns...] [options]
71
+ show each rule a violation of itself, in memory, and report any that pass
72
+ spec-guard cites [paths...] [options] check that each spec a code comment cites exists and is in force
73
+ spec-guard impact <paths...> [options] the files that depend on each path, and the rules in play there
47
74
 
48
75
  Patterns
49
76
  Globs or paths to the Markdown specs to execute. A directory expands to the
50
- Markdown files inside it. Defaults to "docs/**/*.md" when omitted. query and
51
- mcp take theirs from --spec.
77
+ Markdown files inside it. Defaults to "docs/**/*.md" when omitted. query,
78
+ mcp, cites and impact take theirs from --spec.
52
79
 
53
80
  Options
54
81
  -r, --root <path> Codebase root that assertions are resolved against (default: cwd)
@@ -57,7 +84,8 @@ Options
57
84
  --watch Run again as the tree changes, until Ctrl+C (human output only)
58
85
  --fail-fast Stop at the first failing assertion
59
86
  --json Emit a machine-readable JSON report (same as --format json)
60
- --format <name> human | json | sarif (sarif uploads to GitHub code scanning)
87
+ --format <name> human | json | sarif | github | gitlab (sarif uploads to code scanning,
88
+ github annotates a pull request from the log, gitlab is Code Quality)
61
89
  --engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)
62
90
  --strict Treat analysis that could not be completed as a failure
63
91
  --allow-missing-targets
@@ -66,11 +94,12 @@ Options
66
94
  --print-baseline Print the baseline="..." that would exempt today's violations, and exit
67
95
  --no-default-skips Search .git, .hg, .svn and node_modules too
68
96
  --exclude <globs> Paths no assertion looks at, beside each directive's exclude; repeatable
69
- --ignore-status Execute directives in draft, proposed and superseded documents too
97
+ --ignore-status Execute directives in documents not in force too (see Directives)
70
98
  --include-specs Also count matches inside the spec files themselves
71
99
  --max-snippets <n> Failure snippets per assertion (default: ${DEFAULT_MAX_SNIPPETS})
72
100
  --concurrency <n> Assertions executed in parallel (default: ${DEFAULT_CONCURRENCY})
73
- --allow-empty Exit 0 when no spec files matched (about the run, not an assertion)
101
+ --depth <n> impact: follow dependents at most n imports away (default: all)
102
+ --allow-empty Exit 0 when no spec files matched (a run, prove or impact; not an assertion)
74
103
  --color/--no-color Force colour on or off (NO_COLOR is honoured)
75
104
  -h, --help Show this help
76
105
  --version Print the version
@@ -82,6 +111,9 @@ Configuration
82
111
  keep them in .spec-guard.json instead, but not in both. A flag wins over the
83
112
  file, every on/off option there also takes its opposite (--no-strict,
84
113
  --default-skips, ...), and --exclude= with nothing clears exclude.
114
+ "cites": [{ "id": "ADR-{n}", "files": "docs/adr/{n}-*.md" }] names the
115
+ documents code comments cite; without it, cites reads them off the titles of
116
+ numbered specs.
85
117
 
86
118
  Directives
87
119
  <!-- @assert-absence target="src/" symbol="LegacyGateway" exclude="src/legacy/**" -->
@@ -93,12 +125,29 @@ Directives
93
125
 
94
126
  Matches inside comments do not count; add comments="include" to count them.
95
127
  An assertion whose scope holds no files fails; add allow-empty="true" to allow it.
96
- A document whose status is draft, proposed, rejected, deprecated or superseded
97
- is reported and not executed; --ignore-status runs it anyway.
128
+ A document whose status is draft, proposed, rejected, deprecated, superseded
129
+ or archived is reported and not executed; --ignore-status runs it anyway.
130
+
131
+ prove adds, changes or removes files in memory only - never on disk - to make
132
+ the violation each rule forbids, and runs the rule over that tree. A rule that
133
+ still passes survived, and exits 1; one no violation could be made for is
134
+ unprovable, and exits 1 under --strict.
135
+
136
+ cites reads comment text only - not strings, not Markdown - for ids such as
137
+ ADR-0007, ADR-7 and ADR-007, which are one document. A ghost citation names no
138
+ document, and exits 1; a stale one names a document superseded, deprecated,
139
+ rejected or archived, and exits 1 under --strict. A family whose files match no
140
+ document exits 2.
141
+
142
+ impact follows the import graph backwards: relative JavaScript, TypeScript and
143
+ Python imports that name a file. Go, Rust, C# and absolute Python imports name
144
+ modules, and are counted rather than followed; an import that names no file is
145
+ listed, never guessed at.
98
146
 
99
147
  query answers from the specs alone, without reading the codebase, so it works
100
- for a file that does not exist yet. mcp offers the same answer, and a check,
101
- as the tools get_architectural_rules and check_architecture.
148
+ for a file that does not exist yet. mcp offers the same answer, a check and
149
+ impact's, as the tools get_architectural_rules, check_architecture and
150
+ get_dependents.
102
151
 
103
152
  Exit codes
104
153
  0 all assertions passed 1 an assertion failed 2 spec-guard could not run
@@ -150,7 +199,50 @@ const NOT_FOR = {
150
199
  '--concurrency',
151
200
  '--color',
152
201
  ]),
202
+ // Who depends on a path is answered from the graph, and the rules from the
203
+ // specs, as a query answers them: nothing is run, so nothing about running
204
+ // applies. `--allow-empty` does: the dependents are an answer without specs.
205
+ impact: new Set([
206
+ '--verbose',
207
+ '--watch',
208
+ '--fail-fast',
209
+ '--engine',
210
+ '--strict',
211
+ '--no-strict',
212
+ '--allow-missing-targets',
213
+ '--no-allow-missing-targets',
214
+ '--allow-empty-scope',
215
+ '--no-allow-empty-scope',
216
+ '--print-baseline',
217
+ '--max-snippets',
218
+ '--concurrency',
219
+ '--color',
220
+ ]),
153
221
  mcp: new Set(['--verbose', '--watch', '--fail-fast', '--json', '--format', '--print-baseline', '--allow-empty', '--color', '--no-color']),
222
+ // A proof runs every rule, one at a time, with the scanner reading through a
223
+ // door ripgrep cannot see through, and prints no snippets.
224
+ prove: new Set(['--watch', '--fail-fast', '--print-baseline', '--engine', '--concurrency', '--max-snippets']),
225
+ // Citations are read from comments, not from rules: nothing is executed, so
226
+ // nothing about how rules run applies, and a document's status is the
227
+ // answer rather than something to ignore.
228
+ cites: new Set([
229
+ '--verbose',
230
+ '--watch',
231
+ '--fail-fast',
232
+ '--engine',
233
+ '--allow-missing-targets',
234
+ '--no-allow-missing-targets',
235
+ '--allow-empty-scope',
236
+ '--no-allow-empty-scope',
237
+ '--print-baseline',
238
+ '--ignore-status',
239
+ '--no-ignore-status',
240
+ '--include-specs',
241
+ '--no-include-specs',
242
+ '--max-snippets',
243
+ '--concurrency',
244
+ '--allow-empty',
245
+ ]),
154
246
  };
155
247
  /**
156
248
  * Options a watch session refuses, and why.
@@ -171,7 +263,7 @@ const NOT_WITH_WATCH = [
171
263
  const LONG_NAMES = { '-v': '--verbose' };
172
264
  /** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
173
265
  export function parseArgs(argv, cwd) {
174
- const command = argv[0] === 'query' || argv[0] === 'mcp' ? argv[0] : 'check';
266
+ const command = COMMANDS.has(argv[0]) ? argv[0] : 'check';
175
267
  const specs = [];
176
268
  const options = {
177
269
  command,
@@ -208,7 +300,7 @@ export function parseArgs(argv, cwd) {
208
300
  if (onlyPositional || !argument.startsWith('-') || argument === '-') {
209
301
  if (command === 'mcp')
210
302
  throw new UsageError(`spec-guard mcp takes no arguments, got "${argument}". Name specs with --spec.`);
211
- (command === 'query' ? options.paths : options.patterns).push(argument);
303
+ (command === 'query' || command === 'cites' || command === 'impact' ? options.paths : options.patterns).push(argument);
212
304
  continue;
213
305
  }
214
306
  if (argument === '--') {
@@ -252,11 +344,14 @@ export function parseArgs(argv, cwd) {
252
344
  break;
253
345
  case '--format': {
254
346
  const value = nextValue().toLowerCase();
255
- if (command === 'query' && value === 'sarif') {
256
- throw new UsageError('spec-guard query has no sarif format: it lists rules, not results. Expected human or json.');
257
- }
258
- if (value !== 'human' && value !== 'json' && value !== 'sarif') {
259
- throw new UsageError(`Unknown format "${value}". Expected human, json or sarif.`);
347
+ // mcp refused --format above, so every command that gets here writes one.
348
+ const formats = FORMATS[command];
349
+ if (!formats.includes(value)) {
350
+ // Only a command that lists leaves out one of check's formats, so
351
+ // one refused here is refused by a command LISTS names.
352
+ throw new UsageError(FORMATS.check.includes(value)
353
+ ? `spec-guard ${command} has no ${value} format: it lists ${LISTS[command]}, not results. Expected ${either(formats)}.`
354
+ : `Unknown format "${value}". Expected ${either(formats)}.`);
260
355
  }
261
356
  options.format = value;
262
357
  options.json = value === 'json';
@@ -341,6 +436,15 @@ export function parseArgs(argv, cwd) {
341
436
  options.concurrency = Math.max(1, positiveInteger(name, nextValue()));
342
437
  set.add('concurrency');
343
438
  break;
439
+ case '--depth': {
440
+ if (command !== 'impact')
441
+ throw new UsageError('Option --depth applies only to spec-guard impact.');
442
+ const depth = positiveInteger(name, nextValue());
443
+ if (depth === 0)
444
+ throw new UsageError('Option --depth expects 1 or more: a depth of 0 would follow no import.');
445
+ options.depth = depth;
446
+ break;
447
+ }
344
448
  default:
345
449
  throw new UsageError(`Unknown option "${name}". Run spec-guard --help.`);
346
450
  }
@@ -360,10 +464,26 @@ export function parseArgs(argv, cwd) {
360
464
  if (command === 'query' && options.paths.length === 0 && !options.help && !options.version) {
361
465
  throw new UsageError('spec-guard query needs a path to ask about, e.g. spec-guard query src/domain/user.ts.');
362
466
  }
467
+ if (command === 'impact' && options.paths.length === 0 && !options.help && !options.version) {
468
+ throw new UsageError('spec-guard impact needs a path to ask about, e.g. spec-guard impact src/db/client.ts.');
469
+ }
363
470
  return options;
364
471
  }
365
472
  /** The configurable options a query reads; the rest are about running rules. */
366
473
  const QUERY_KEYS = new Set(['specs', 'exclude', 'ignoreStatus', 'includeSpecs', 'defaultSkips']);
474
+ /** The configurable options a citation check reads: which specs, what to leave out, and how strictly. */
475
+ const CITES_KEYS = new Set(['specs', 'exclude', 'strict', 'defaultSkips', 'cites']);
476
+ /** The configurable options a proof reads: what decides whether a rule passes, and not how a run is scheduled. */
477
+ const PROVE_KEYS = new Set([
478
+ 'specs',
479
+ 'exclude',
480
+ 'strict',
481
+ 'allowMissingTargets',
482
+ 'allowEmptyScope',
483
+ 'ignoreStatus',
484
+ 'includeSpecs',
485
+ 'defaultSkips',
486
+ ]);
367
487
  /** Where each key of a configuration lands on the command line's options. */
368
488
  const SETTERS = {
369
489
  specs: (options, value) => {
@@ -399,6 +519,9 @@ const SETTERS = {
399
519
  concurrency: (options, value) => {
400
520
  options.concurrency = value;
401
521
  },
522
+ cites: (options, value) => {
523
+ options.cites = value.map((family) => ({ ...family }));
524
+ },
402
525
  };
403
526
  /**
404
527
  * Fills in what the command line left unset from a configuration, and says what
@@ -417,7 +540,14 @@ export function applyConfig(options, config, file = 'package.json') {
417
540
  const value = config[key];
418
541
  if (value === undefined)
419
542
  continue;
420
- if (options.command === 'query' ? !QUERY_KEYS.has(key) : options.watch && key === 'engine')
543
+ const reads = options.command === 'query' || options.command === 'impact'
544
+ ? QUERY_KEYS.has(key)
545
+ : options.command === 'prove'
546
+ ? PROVE_KEYS.has(key)
547
+ : options.command === 'cites'
548
+ ? CITES_KEYS.has(key)
549
+ : key !== 'cites' && !(options.watch && key === 'engine');
550
+ if (!reads)
421
551
  continue;
422
552
  if (options.fromCommandLine.has(key)) {
423
553
  overridden.push(key);
@@ -592,6 +722,144 @@ async function runMcp(options, commandLine, io, use) {
592
722
  await serveStdio(io.stdin, io.stdout, handler);
593
723
  return EXIT_OK;
594
724
  }
725
+ /**
726
+ * `spec-guard prove`: each rule shown a violation of itself, in memory. ADR-0016.
727
+ *
728
+ * Exit 1 when a rule survived, or a directive could not be read; under
729
+ * `--strict`, when a rule is unprovable too, since a rule nobody can show
730
+ * failing is analysis that could not be completed.
731
+ */
732
+ async function runProve(options, io, use) {
733
+ let report;
734
+ try {
735
+ report = {
736
+ ...(await proveSpecGuard({
737
+ patterns: options.patterns,
738
+ root: options.root,
739
+ allowMissingTargets: options.allowMissingTargets,
740
+ strictTargets: options.strictTargets,
741
+ allowEmptyScope: options.allowEmptyScope,
742
+ includeSpecs: options.includeSpecs,
743
+ defaultSkips: options.defaultSkips,
744
+ exclude: options.exclude,
745
+ ignoreStatus: options.ignoreStatus,
746
+ })),
747
+ config: use,
748
+ };
749
+ }
750
+ catch (error) {
751
+ io.stderr(`spec-guard: ${error instanceof Error ? error.message : String(error)}`);
752
+ return EXIT_ERROR;
753
+ }
754
+ if (report.summary.specs === 0 && (options.format === 'human' || options.format === 'github')) {
755
+ io.stderr(`spec-guard: no spec files matched ${options.patterns.map((p) => `"${p}"`).join(', ')}`);
756
+ return options.allowEmpty ? EXIT_OK : EXIT_ERROR;
757
+ }
758
+ const written = options.format === 'sarif'
759
+ ? formatProveSarif(report, { version: version() })
760
+ : options.format === 'json'
761
+ ? formatProveJson(report)
762
+ : options.format === 'gitlab'
763
+ ? formatGitlab(proveAnnotations(report))
764
+ : options.format === 'github'
765
+ ? formatGithub(proveAnnotations(report))
766
+ : formatProve(report, {
767
+ color: shouldUseColor({ isTTY: io.isTTY }, options.color, io.env),
768
+ verbose: options.verbose,
769
+ ascii: shouldUseAscii(io.env),
770
+ });
771
+ // GitHub's commands are one per finding, and a proof with none has no line to write.
772
+ if (written !== '')
773
+ io.stdout(written);
774
+ if (report.summary.specs === 0)
775
+ return options.allowEmpty ? EXIT_OK : EXIT_ERROR;
776
+ return report.ok && !(options.strictTargets && report.summary.unprovable > 0) ? EXIT_OK : EXIT_FAILED;
777
+ }
778
+ /**
779
+ * `spec-guard impact`: the files that depend on each path, and the rules in
780
+ * play there. ADR-0018. Exit 2 for a path outside the root or not there, and
781
+ * when no spec matched, as a query does: the dependents are written, but "no
782
+ * rules" over no specs is nothing measured, which the family contract says is
783
+ * not clean. `--allow-empty` asks for the dependents alone.
784
+ */
785
+ async function runImpact(options, io, use) {
786
+ let report;
787
+ try {
788
+ report = await impactOf({
789
+ patterns: options.patterns,
790
+ root: options.root,
791
+ paths: options.paths,
792
+ // Absent and undefined are one answer to impactOf: every dependent.
793
+ depth: options.depth,
794
+ includeInactive: options.ignoreStatus,
795
+ includeSpecs: options.includeSpecs,
796
+ defaultSkips: options.defaultSkips,
797
+ exclude: options.exclude,
798
+ });
799
+ }
800
+ catch (error) {
801
+ io.stderr(`spec-guard: ${error instanceof Error ? error.message : String(error)}`);
802
+ return EXIT_ERROR;
803
+ }
804
+ const answered = { ...report, config: use };
805
+ io.stdout(options.format === 'json' ? formatImpactJson(answered) : formatImpact(answered));
806
+ if (report.specFiles.length > 0)
807
+ return EXIT_OK;
808
+ if (options.format !== 'json')
809
+ io.stderr(`spec-guard: no spec files matched ${options.patterns.map((p) => `"${p}"`).join(', ')}`);
810
+ return options.allowEmpty ? EXIT_OK : EXIT_ERROR;
811
+ }
812
+ /**
813
+ * `spec-guard cites`: the specs code comments cite, and whether each is still
814
+ * there and in force. ADR-0017.
815
+ *
816
+ * Exit 1 for a ghost, and under `--strict` for a stale citation or a file
817
+ * whose comments could not all be read; 2 when the answer could not be
818
+ * trusted - a family whose files match no document, a path outside the root.
819
+ */
820
+ async function runCites(options, io, use) {
821
+ let report;
822
+ try {
823
+ const paths = await Promise.all(options.paths.map((entry) => resolveQueryPath(entry, options.root)));
824
+ for (const entry of paths) {
825
+ if (!entry.exists)
826
+ throw new CitesError(`"${entry.path}" does not exist, so there are no comments in it to read`);
827
+ }
828
+ report = {
829
+ ...(await findCitations({
830
+ root: options.root,
831
+ patterns: options.patterns,
832
+ // Undefined, as absent, derives the families from the specs.
833
+ families: options.cites,
834
+ ...(paths.length === 0 ? {} : { paths: paths.map((entry) => entry.path) }),
835
+ exclude: options.exclude,
836
+ defaultSkips: options.defaultSkips,
837
+ strict: options.strictTargets,
838
+ })),
839
+ config: use,
840
+ };
841
+ }
842
+ catch (error) {
843
+ io.stderr(`spec-guard: ${error instanceof Error ? error.message : String(error)}`);
844
+ return EXIT_ERROR;
845
+ }
846
+ const written = options.format === 'json'
847
+ ? formatCitesJson(report)
848
+ : options.format === 'sarif'
849
+ ? formatCitesSarif(report, { version: version() })
850
+ : options.format === 'gitlab'
851
+ ? formatGitlab(citesAnnotations(report))
852
+ : options.format === 'github'
853
+ ? formatGithub(citesAnnotations(report))
854
+ : formatCites(report, {
855
+ color: shouldUseColor({ isTTY: io.isTTY }, options.color, io.env),
856
+ verbose: false,
857
+ ascii: shouldUseAscii(io.env),
858
+ });
859
+ if (written !== '')
860
+ io.stdout(written);
861
+ return report.ok ? EXIT_OK : EXIT_FAILED;
862
+ }
595
863
  /** Runs the CLI and resolves to the process exit code. */
596
864
  export async function main(argv = process.argv.slice(2), io = defaultIO()) {
597
865
  let options;
@@ -612,6 +880,14 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
612
880
  io.stdout(version());
613
881
  return EXIT_OK;
614
882
  }
883
+ // Before any command starts, so a server, a watch session and a proof refuse
884
+ // a pattern as a run does, rather than each answering with fewer documents.
885
+ // The configuration's are refused where it is read, naming the file.
886
+ const patternError = patternListError(options.patterns, specPatternError);
887
+ if (patternError !== null) {
888
+ io.stderr(`spec-guard: ${patternError}`);
889
+ return EXIT_ERROR;
890
+ }
615
891
  // Read after --help and --version, which must work in a project whose
616
892
  // package.json is broken, and before anything that runs a rule.
617
893
  const commandLine = { ...options, patterns: [...options.patterns] };
@@ -621,8 +897,14 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
621
897
  const { use } = configured;
622
898
  if (options.command === 'query')
623
899
  return runQuery(options, io, use);
900
+ if (options.command === 'cites')
901
+ return runCites(options, io, use);
902
+ if (options.command === 'impact')
903
+ return runImpact(options, io, use);
624
904
  if (options.command === 'mcp')
625
905
  return runMcp(options, commandLine, io, use);
906
+ if (options.command === 'prove')
907
+ return runProve(options, io, use);
626
908
  if (options.watch)
627
909
  return runWatchSession(commandLine, io);
628
910
  let report;
@@ -639,8 +921,11 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
639
921
  return EXIT_ERROR;
640
922
  }
641
923
  if (report.summary.specs === 0) {
642
- if (options.format !== 'human') {
643
- io.stdout(options.format === 'sarif' ? formatSarif(report, { version: version() }) : formatJson(report));
924
+ // A document a script reads is still written, empty, for the script to
925
+ // read. GitHub's commands have nothing to annotate, and the person reading
926
+ // the job's log is told why, as a person at a terminal is.
927
+ if (options.format === 'sarif' || options.format === 'json' || options.format === 'gitlab') {
928
+ io.stdout(options.format === 'sarif' ? formatSarif(report, { version: version() }) : options.format === 'gitlab' ? formatGitlab([]) : formatJson(report));
644
929
  }
645
930
  else {
646
931
  io.stderr(`spec-guard: no spec files matched ${options.patterns.map((p) => `"${p}"`).join(', ')}`);
@@ -659,6 +944,14 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
659
944
  else if (options.format === 'json') {
660
945
  io.stdout(formatJson(report));
661
946
  }
947
+ else if (options.format === 'gitlab') {
948
+ io.stdout(formatGitlab(runAnnotations(report)));
949
+ }
950
+ else if (options.format === 'github') {
951
+ const annotations = formatGithub(runAnnotations(report));
952
+ if (annotations !== '')
953
+ io.stdout(annotations);
954
+ }
662
955
  else {
663
956
  io.stdout(formatReport(report, {
664
957
  color: shouldUseColor({ isTTY: io.isTTY }, options.color, io.env),