@descent-vtt/spec-guard 0.10.3 → 0.12.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 (162) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/README.md +385 -27
  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 +51 -2
  12. package/dist/comments.d.ts.map +1 -1
  13. package/dist/comments.js +353 -57
  14. package/dist/comments.js.map +1 -1
  15. package/dist/config.d.ts +3 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +46 -5
  18. package/dist/config.js.map +1 -1
  19. package/dist/engine.d.ts.map +1 -1
  20. package/dist/engine.js +38 -14
  21. package/dist/engine.js.map +1 -1
  22. package/dist/glob.d.ts +128 -24
  23. package/dist/glob.d.ts.map +1 -1
  24. package/dist/glob.js +304 -61
  25. package/dist/glob.js.map +1 -1
  26. package/dist/impact.d.ts +179 -0
  27. package/dist/impact.d.ts.map +1 -0
  28. package/dist/impact.js +360 -0
  29. package/dist/impact.js.map +1 -0
  30. package/dist/imports.d.ts.map +1 -1
  31. package/dist/imports.js +5 -58
  32. package/dist/imports.js.map +1 -1
  33. package/dist/index.d.ts +13 -5
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +7 -3
  36. package/dist/index.js.map +1 -1
  37. package/dist/mcp.d.ts +16 -75
  38. package/dist/mcp.d.ts.map +1 -1
  39. package/dist/mcp.js +198 -370
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/overlay.d.ts +39 -0
  42. package/dist/overlay.d.ts.map +1 -0
  43. package/dist/overlay.js +133 -0
  44. package/dist/overlay.js.map +1 -0
  45. package/dist/parser.d.ts +41 -24
  46. package/dist/parser.d.ts.map +1 -1
  47. package/dist/parser.js +217 -160
  48. package/dist/parser.js.map +1 -1
  49. package/dist/prove.d.ts +46 -0
  50. package/dist/prove.d.ts.map +1 -0
  51. package/dist/prove.js +549 -0
  52. package/dist/prove.js.map +1 -0
  53. package/dist/query.d.ts +11 -0
  54. package/dist/query.d.ts.map +1 -1
  55. package/dist/query.js +9 -3
  56. package/dist/query.js.map +1 -1
  57. package/dist/reporter.d.ts +139 -1
  58. package/dist/reporter.d.ts.map +1 -1
  59. package/dist/reporter.js +593 -21
  60. package/dist/reporter.js.map +1 -1
  61. package/dist/rules.js +3 -3
  62. package/dist/rules.js.map +1 -1
  63. package/dist/runner.d.ts +24 -1
  64. package/dist/runner.d.ts.map +1 -1
  65. package/dist/runner.js +84 -21
  66. package/dist/runner.js.map +1 -1
  67. package/dist/specs.d.ts +5 -1
  68. package/dist/specs.d.ts.map +1 -1
  69. package/dist/specs.js +5 -1
  70. package/dist/specs.js.map +1 -1
  71. package/dist/structure.d.ts.map +1 -1
  72. package/dist/structure.js +7 -6
  73. package/dist/structure.js.map +1 -1
  74. package/dist/types.d.ts +193 -0
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/vendor/spec-core/jsonrpc/index.d.ts +3 -0
  77. package/dist/vendor/spec-core/jsonrpc/index.d.ts.map +1 -0
  78. package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
  79. package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
  80. package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +158 -0
  81. package/dist/vendor/spec-core/jsonrpc/mcp.d.ts.map +1 -0
  82. package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
  83. package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
  84. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +104 -0
  85. package/dist/vendor/spec-core/markdown/frontmatter.d.ts.map +1 -0
  86. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  87. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  88. package/dist/vendor/spec-core/markdown/headings.d.ts +46 -0
  89. package/dist/vendor/spec-core/markdown/headings.d.ts.map +1 -0
  90. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  91. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  92. package/dist/vendor/spec-core/markdown/index.d.ts +6 -0
  93. package/dist/vendor/spec-core/markdown/index.d.ts.map +1 -0
  94. package/dist/vendor/spec-core/markdown/index.js +4 -0
  95. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  96. package/dist/vendor/spec-core/markdown/layout.d.ts +40 -0
  97. package/dist/vendor/spec-core/markdown/layout.d.ts.map +1 -0
  98. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  99. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  100. package/dist/vendor/spec-core/markdown/links.d.ts +23 -0
  101. package/dist/vendor/spec-core/markdown/links.d.ts.map +1 -0
  102. package/dist/vendor/spec-core/markdown/links.js +364 -0
  103. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  104. package/dist/vendor/spec-core/markdown/lists.d.ts +23 -0
  105. package/dist/vendor/spec-core/markdown/lists.d.ts.map +1 -0
  106. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  107. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  108. package/dist/vendor/spec-core/markdown/scan.d.ts +37 -0
  109. package/dist/vendor/spec-core/markdown/scan.d.ts.map +1 -0
  110. package/dist/vendor/spec-core/markdown/scan.js +439 -0
  111. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  112. package/dist/vendor/spec-core/markdown/syntax.d.ts +76 -0
  113. package/dist/vendor/spec-core/markdown/syntax.d.ts.map +1 -0
  114. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  115. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  116. package/dist/vendor/spec-core/markdown/tables.d.ts +19 -0
  117. package/dist/vendor/spec-core/markdown/tables.d.ts.map +1 -0
  118. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  119. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  120. package/dist/vendor/spec-core/markdown/types.d.ts +288 -0
  121. package/dist/vendor/spec-core/markdown/types.d.ts.map +1 -0
  122. package/dist/vendor/spec-core/markdown/types.js +9 -0
  123. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  124. package/dist/vendor/spec-core/path/index.d.ts +2 -0
  125. package/dist/vendor/spec-core/path/index.d.ts.map +1 -0
  126. package/dist/vendor/spec-core/path/index.js +2 -0
  127. package/dist/vendor/spec-core/path/index.js.map +1 -0
  128. package/dist/vendor/spec-core/path/posix.d.ts +84 -0
  129. package/dist/vendor/spec-core/path/posix.d.ts.map +1 -0
  130. package/dist/vendor/spec-core/path/posix.js +150 -0
  131. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  132. package/dist/vendor/spec-core/pattern/automaton.d.ts +130 -0
  133. package/dist/vendor/spec-core/pattern/automaton.d.ts.map +1 -0
  134. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  135. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  136. package/dist/vendor/spec-core/pattern/charset.d.ts +59 -0
  137. package/dist/vendor/spec-core/pattern/charset.d.ts.map +1 -0
  138. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  139. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  140. package/dist/vendor/spec-core/pattern/glob.d.ts +122 -0
  141. package/dist/vendor/spec-core/pattern/glob.d.ts.map +1 -0
  142. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  143. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  144. package/dist/vendor/spec-core/pattern/index.d.ts +7 -0
  145. package/dist/vendor/spec-core/pattern/index.d.ts.map +1 -0
  146. package/dist/vendor/spec-core/pattern/index.js +4 -0
  147. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  148. package/dist/vendor/spec-core/pattern/regex.d.ts +68 -0
  149. package/dist/vendor/spec-core/pattern/regex.d.ts.map +1 -0
  150. package/dist/vendor/spec-core/pattern/regex.js +813 -0
  151. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  152. package/dist/vendor/spec-core/text/index.d.ts +3 -0
  153. package/dist/vendor/spec-core/text/index.d.ts.map +1 -0
  154. package/dist/vendor/spec-core/text/index.js +2 -0
  155. package/dist/vendor/spec-core/text/index.js.map +1 -0
  156. package/dist/vendor/spec-core/text/lines.d.ts +73 -0
  157. package/dist/vendor/spec-core/text/lines.d.ts.map +1 -0
  158. package/dist/vendor/spec-core/text/lines.js +156 -0
  159. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  160. package/package.json +8 -1
  161. package/src/vendor/spec-core/LICENSE +21 -0
  162. package/src/vendor/spec-core/README.md +8 -0
package/dist/mcp.js CHANGED
@@ -2,167 +2,118 @@
2
2
  * `spec-guard mcp`: the rules, served to an agent over the Model Context
3
3
  * Protocol on stdio.
4
4
  *
5
- * No SDK. MCP over stdio is JSON-RPC 2.0, one message per line, and what a
6
- * server has to get right fits in this file: framing, the two eras of the
7
- * protocol, and four methods' worth of results. The official SDK brings a
8
- * schema library and a dependency tree to do the same, and spec-guard's first
9
- * invariant is that it has no runtime dependencies at all. ADR-0012 has the
10
- * rest, including what this file deliberately does not implement.
11
- *
12
- * Two eras, served side by side. Revisions up to 2025-11-25 open with an
13
- * `initialize` handshake; 2026-07-28 has none, and every request carries its
14
- * protocol version in `_meta`. Clients of both are in use, so each request is
15
- * classified on its own - by the rule the TypeScript SDK's server applies, not
16
- * by one invented here: a request that claims a version in `_meta` is modern
17
- * and its claim is validated; anything else, `initialize` included, is legacy.
5
+ * No SDK. The protocol - framing, the two eras, classifying each request,
6
+ * dispatching it and shaping its result - is spec-core's `jsonrpc` module,
7
+ * copied into `src/vendor` and verified by hash (ADR-0015). It was written
8
+ * here first and moved there so that a second server in the family does not
9
+ * relearn it. What is spec-guard's, and stays here, is what the server says:
10
+ * its instructions, its three tools, and the rules and documents it serves as
11
+ * resources. ADR-0012 has the rest, including what this server deliberately
12
+ * does not implement; ADR-0018 has `get_dependents`.
18
13
  */
19
14
  import { toPosix } from './glob.js';
15
+ import { formatImpact, ImpactError, impactDocument, impactOf } from './impact.js';
20
16
  import { nodeIo, readText } from './io.js';
21
17
  import { formatReport } from './reporter.js';
22
18
  import { elapsed, runSpecGuard } from './runner.js';
23
19
  import { answerQuery, formatQuery, inQueriedPaths, loadRuleSet, QueryPathError, resolveQueryPath, viewDocument, } from './query.js';
24
20
  import { governs, viewRule } from './rules.js';
25
- /* ----------------------------------------------------------------- protocol */
26
- /** Revisions that carry their version on every request. Newest first. */
27
- export const MODERN_PROTOCOL_VERSIONS = ['2026-07-28'];
28
- /** Revisions negotiated by `initialize`, newest first - the SDK's own list. */
29
- export const LEGACY_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07'];
30
- export const PROTOCOL_VERSION_KEY = 'io.modelcontextprotocol/protocolVersion';
31
- export const CLIENT_CAPABILITIES_KEY = 'io.modelcontextprotocol/clientCapabilities';
32
- export const CLIENT_INFO_KEY = 'io.modelcontextprotocol/clientInfo';
33
- export const SERVER_INFO_KEY = 'io.modelcontextprotocol/serverInfo';
34
- export const PARSE_ERROR = -32700;
35
- export const INVALID_REQUEST = -32600;
36
- export const METHOD_NOT_FOUND = -32601;
37
- export const INVALID_PARAMS = -32602;
38
- export const INTERNAL_ERROR = -32603;
39
- /** Resource not found before 2026-07-28, which forbids it in favour of -32602. */
40
- export const LEGACY_RESOURCE_NOT_FOUND = -32002;
41
- export const UNSUPPORTED_PROTOCOL_VERSION = -32022;
42
- class ProtocolError extends Error {
43
- code;
44
- data;
45
- constructor(code, message, data) {
46
- super(message);
47
- this.code = code;
48
- this.data = data;
49
- }
50
- }
51
- function isObject(value) {
52
- return typeof value === 'object' && value !== null && !Array.isArray(value);
53
- }
54
- /** JSON-RPC allows a null id; MCP does not, and requires an integer if a number. */
55
- function isRequestId(value) {
56
- return typeof value === 'string' || Number.isInteger(value);
57
- }
58
- /**
59
- * What is wrong with a request's `_meta`, once it claims a protocol version.
60
- *
61
- * Only what the server depends on or the revision requires: a version that is a
62
- * string, capabilities that are an object, and client info shaped as client info
63
- * when it is sent. A claim is never silently ignored - a malformed one is an
64
- * error, not a reason to fall back to legacy handling.
65
- */
66
- export function envelopeIssue(meta) {
67
- if (!(CLIENT_CAPABILITIES_KEY in meta))
68
- return `${CLIENT_CAPABILITIES_KEY}: missing`;
69
- if (typeof meta[PROTOCOL_VERSION_KEY] !== 'string')
70
- return `${PROTOCOL_VERSION_KEY}: expected a string`;
71
- if (!isObject(meta[CLIENT_CAPABILITIES_KEY]))
72
- return `${CLIENT_CAPABILITIES_KEY}: expected an object`;
73
- const info = meta[CLIENT_INFO_KEY];
74
- if (info !== undefined && !(isObject(info) && typeof info['name'] === 'string' && typeof info['version'] === 'string')) {
75
- return `${CLIENT_INFO_KEY}: expected an object with a string name and version`;
76
- }
77
- return undefined;
78
- }
79
- /**
80
- * The era a request belongs to, or the error that answers it instead.
81
- *
82
- * `initialize` is the legacy handshake unless it carries a valid modern claim.
83
- * Any other request is modern exactly when `_meta` holds the version key - its
84
- * presence is the claim, whatever its value - and legacy otherwise.
85
- */
86
- export function classifyRequest(method, params) {
87
- const meta = params?.['_meta'];
88
- const claimed = isObject(meta) && PROTOCOL_VERSION_KEY in meta;
89
- if (!claimed)
90
- return 'legacy';
91
- const issue = envelopeIssue(meta);
92
- const version = meta[PROTOCOL_VERSION_KEY];
93
- const supported = issue === undefined && MODERN_PROTOCOL_VERSIONS.includes(version);
94
- if (method === 'initialize' && !supported)
95
- return 'legacy';
96
- if (issue !== undefined)
97
- throw new ProtocolError(INVALID_PARAMS, `Invalid _meta envelope: ${issue}`);
98
- if (!supported) {
99
- throw new ProtocolError(UNSUPPORTED_PROTOCOL_VERSION, 'Unsupported protocol version', {
100
- supported: [...MODERN_PROTOCOL_VERSIONS],
101
- requested: version,
102
- });
103
- }
104
- return 'modern';
105
- }
106
- /** The version `initialize` settles on: the one asked for, or the newest legacy one. */
107
- export function negotiateLegacyVersion(requested) {
108
- // No typeof check: a list of strings includes nothing that is not one.
109
- return LEGACY_PROTOCOL_VERSIONS.includes(requested) ? requested : LEGACY_PROTOCOL_VERSIONS[0];
110
- }
21
+ import { createMcpServer, serveLines, toolError, unknownArguments, } from './vendor/spec-core/jsonrpc/index.js';
22
+ // The protocol's names, re-exported from where they now live, so that a
23
+ // caller of the API who imported them from here still finds them.
24
+ export { classifyRequest, envelopeIssue, negotiateLegacyVersion, CLIENT_CAPABILITIES_KEY, CLIENT_INFO_KEY, INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, LEGACY_PROTOCOL_VERSIONS, LEGACY_RESOURCE_NOT_FOUND, METHOD_NOT_FOUND, MODERN_PROTOCOL_VERSIONS, PARSE_ERROR, PROTOCOL_VERSION_KEY, SERVER_INFO_KEY, UNSUPPORTED_PROTOCOL_VERSION, } from './vendor/spec-core/jsonrpc/index.js';
111
25
  /* -------------------------------------------------------------- the surface */
112
26
  export const SERVER_NAME = 'spec-guard';
113
27
  export const INSTRUCTIONS = 'spec-guard enforces the architecture decisions written in this project\'s Markdown specs and ADRs. ' +
114
28
  'Before creating or changing a file, call get_architectural_rules with its path to learn the rules in force there: ' +
115
29
  'imports it must not make, the layer it belongs to, what it must be named and the files it needs beside it, text it must not contain. ' +
30
+ 'Before changing a file that other files import - renaming or removing an export, changing what a function takes or returns - ' +
31
+ 'call get_dependents with its path to learn every file the change can reach and the rules in force over them. ' +
116
32
  'After changing files, call check_architecture with their paths to find violations before CI does. ' +
117
- 'Rules in draft, proposed, rejected, deprecated or superseded documents are not in force and are only counted.';
33
+ 'Rules in draft, proposed, rejected, deprecated, superseded or archived documents are not in force and are only counted.';
118
34
  const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
119
- export const TOOLS = [
120
- {
121
- name: 'get_architectural_rules',
122
- title: 'Architectural rules for a path',
123
- description: 'Lists the architecture rules in force for a file or directory, grouped by the ADR or spec that states them: ' +
124
- 'modules it must not import, the layer it belongs to and the layers it must not depend on, import cycles, ' +
125
- 'what it must be named and the partner files it needs, ' +
126
- 'and text or symbols it must not contain, each with the reason the document gives. ' +
127
- 'Answers from the specs without reading the codebase, so it works for a file that does not exist yet. ' +
128
- 'Rules in documents that are not in force are counted and named but not listed unless include_inactive is true.',
129
- inputSchema: {
130
- type: 'object',
131
- properties: {
132
- path: {
133
- type: 'string',
134
- description: 'A file or directory, relative to the project root or absolute inside it.',
135
- },
136
- include_inactive: {
137
- type: 'boolean',
138
- description: 'Also list rules from draft, proposed, rejected, deprecated and superseded documents.',
139
- },
35
+ const GET_RULES = {
36
+ name: 'get_architectural_rules',
37
+ title: 'Architectural rules for a path',
38
+ description: 'Lists the architecture rules in force for a file or directory, grouped by the ADR or spec that states them: ' +
39
+ 'modules it must not import, the layer it belongs to and the layers it must not depend on, import cycles, ' +
40
+ 'what it must be named and the partner files it needs, ' +
41
+ 'and text or symbols it must not contain, each with the reason the document gives. ' +
42
+ 'Answers from the specs without reading the codebase, so it works for a file that does not exist yet. ' +
43
+ 'Rules in documents that are not in force are counted and named but not listed unless include_inactive is true.',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ path: {
48
+ type: 'string',
49
+ description: 'A file or directory, relative to the project root or absolute inside it.',
50
+ },
51
+ include_inactive: {
52
+ type: 'boolean',
53
+ description: 'Also list rules from draft, proposed, rejected, deprecated, superseded and archived documents.',
140
54
  },
141
- required: ['path'],
142
- additionalProperties: false,
143
55
  },
144
- annotations: READ_ONLY,
56
+ required: ['path'],
57
+ additionalProperties: false,
145
58
  },
146
- {
147
- name: 'check_architecture',
148
- title: 'Check the architecture rules',
149
- description: 'Runs the architecture rules in force against the files on disk and reports every violation, as CI would. ' +
150
- 'Given paths, runs only the rules that govern them - each over its whole scope, so a count or a cycle is judged ' +
151
- 'exactly as CI judges it - and marks which violations lie in those paths. Save your edits before calling.',
152
- inputSchema: {
153
- type: 'object',
154
- properties: {
155
- paths: {
156
- type: 'array',
157
- items: { type: 'string' },
158
- description: 'Files or directories to check the rules of, relative to the project root. Omit to run every rule.',
159
- },
59
+ annotations: READ_ONLY,
60
+ };
61
+ const CHECK_ARCHITECTURE = {
62
+ name: 'check_architecture',
63
+ title: 'Check the architecture rules',
64
+ description: 'Runs the architecture rules in force against the files on disk and reports every violation, as CI would. ' +
65
+ 'Given paths, runs only the rules that govern them - each over its whole scope, so a count or a cycle is judged ' +
66
+ 'exactly as CI judges it - and marks which violations lie in those paths. Save your edits before calling.',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ paths: {
71
+ type: 'array',
72
+ items: { type: 'string' },
73
+ description: 'Files or directories to check the rules of, relative to the project root. Omit to run every rule.',
160
74
  },
161
- additionalProperties: false,
162
75
  },
163
- annotations: READ_ONLY,
76
+ additionalProperties: false,
164
77
  },
165
- ];
78
+ annotations: READ_ONLY,
79
+ };
80
+ const GET_DEPENDENTS = {
81
+ name: 'get_dependents',
82
+ title: 'Files that depend on a path',
83
+ description: 'Lists the files that import each given file or directory, directly or through other files, ' +
84
+ 'each with how many imports away it is and the import that leads there, ' +
85
+ 'and the architecture rules in force that govern the paths or any of those files. ' +
86
+ 'Call it before changing a file that other files import - renaming or removing an export, changing a signature - ' +
87
+ 'to see everything the change can reach. ' +
88
+ 'Relative JavaScript, TypeScript and Python imports are followed; an import that cannot be resolved is listed, ' +
89
+ 'and Go, Rust, C# and absolute Python imports, which name modules rather than files, are counted, never guessed, ' +
90
+ 'so a short list is not mistaken for a complete one. Reads the codebase on disk, so every path must exist.',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ paths: {
95
+ type: 'array',
96
+ items: { type: 'string' },
97
+ minItems: 1,
98
+ description: 'Files or directories that exist, relative to the project root or absolute inside it.',
99
+ },
100
+ depth: {
101
+ type: 'integer',
102
+ minimum: 1,
103
+ description: 'Follow dependents at most this many imports away. Omit to follow every one.',
104
+ },
105
+ include_inactive: {
106
+ type: 'boolean',
107
+ description: 'Also list rules from draft, proposed, rejected, deprecated, superseded and archived documents.',
108
+ },
109
+ },
110
+ required: ['paths'],
111
+ additionalProperties: false,
112
+ },
113
+ annotations: READ_ONLY,
114
+ };
115
+ /** What `tools/list` answers, in the order it lists them. */
116
+ export const TOOLS = [GET_RULES, CHECK_ARCHITECTURE, GET_DEPENDENTS];
166
117
  export const RULES_URI = 'spec://rules';
167
118
  export const DOCUMENT_URI_PREFIX = 'spec://doc/';
168
119
  export const RESOURCE_TEMPLATES = [
@@ -189,15 +140,24 @@ export function documentPath(uri) {
189
140
  return null;
190
141
  }
191
142
  }
192
- function toolError(text) {
193
- return { text, isError: true };
194
- }
195
- /** Arguments a tool does not declare, which the model should hear about. */
196
- function unknownArguments(args, allowed) {
197
- const unknown = Object.keys(args).filter((name) => !allowed.includes(name));
198
- return unknown.length === 0
199
- ? undefined
200
- : toolError(`Unknown argument${unknown.length === 1 ? '' : 's'} ${unknown.map((name) => `"${name}"`).join(', ')}; this tool takes ${allowed.join(' and ')}.`);
143
+ /**
144
+ * One of the server's tools.
145
+ *
146
+ * A path argument outside the root, or one that must exist and does not, is
147
+ * the model's to fix, so it hears the message alone. Anything else a tool
148
+ * throws is caught by the protocol layer and reported after the server's name,
149
+ * "spec-guard failed: ...", which is still better than a protocol failure the
150
+ * model cannot read.
151
+ */
152
+ function tool(descriptor, run) {
153
+ return {
154
+ descriptor,
155
+ call: (args) => run(args).catch((error) => {
156
+ if (error instanceof QueryPathError || error instanceof ImpactError)
157
+ return toolError(error.message);
158
+ throw error;
159
+ }),
160
+ };
201
161
  }
202
162
  /**
203
163
  * Builds the function that answers one parsed JSON-RPC message.
@@ -206,8 +166,6 @@ function unknownArguments(args, allowed) {
206
166
  * notification, or a response the client should not have sent.
207
167
  */
208
168
  export function createMcpHandler(options) {
209
- const serverInfo = { name: SERVER_NAME, version: options.version };
210
- const capabilities = { tools: {}, resources: {} };
211
169
  const readFile = options.readFile ?? ((file) => readText(nodeIo, file));
212
170
  /** The settings a request is answered under, read when the request arrives. */
213
171
  const current = async () => options.settings ? options.settings() : { patterns: options.patterns, run: options.run };
@@ -292,6 +250,13 @@ export function createMcpHandler(options) {
292
250
  })),
293
251
  inactiveSpecs: report.inactiveSpecs,
294
252
  warnings: report.warnings,
253
+ // A document read differently from how it was written: see ADR-0010 and ADR-0002.
254
+ // A run always reports them; the type leaves them out only for a result a caller built.
255
+ specWarnings: report.specWarnings.map((warning) => ({
256
+ file: warning.location.relativeFile,
257
+ line: warning.location.line,
258
+ message: warning.message,
259
+ })),
295
260
  exclude: report.exclude,
296
261
  config: settings.config,
297
262
  durationMs: Math.round(report.durationMs * 1000) / 1000,
@@ -299,36 +264,62 @@ export function createMcpHandler(options) {
299
264
  const text = `Checked ${scope}.\n\n${formatReport({ ...report, config: settings.config }, { color: false, verbose: false, ascii: true }, settings.run?.maxSnippets)}`;
300
265
  return { text, structured };
301
266
  }
302
- async function callTool(params) {
303
- const name = params['name'];
304
- if (typeof name !== 'string')
305
- throw new ProtocolError(INVALID_PARAMS, 'tools/call needs the name of a tool.');
306
- const tool = name === 'get_architectural_rules' ? getRules : name === 'check_architecture' ? checkArchitecture : undefined;
307
- if (tool === undefined)
308
- throw new ProtocolError(INVALID_PARAMS, `Unknown tool: ${name}`);
309
- const args = params['arguments'] ?? {};
310
- if (!isObject(args))
311
- throw new ProtocolError(INVALID_PARAMS, 'Tool arguments must be an object.');
312
- let outcome;
313
- try {
314
- outcome = await tool(args);
267
+ /**
268
+ * `spec-guard impact`, answered as `impact --json` answers: the same
269
+ * document, and the human report as the text. Unlike the other two tools it
270
+ * answers without specs, as the command does, since who imports a file does
271
+ * not depend on them; the report says no rules are shown.
272
+ */
273
+ async function getDependents(args) {
274
+ const unknown = unknownArguments(args, ['paths', 'depth', 'include_inactive']);
275
+ if (unknown)
276
+ return unknown;
277
+ const paths = args['paths'];
278
+ if (!Array.isArray(paths) || paths.length === 0 || !paths.every((entry) => typeof entry === 'string')) {
279
+ return toolError('"paths" is required and must be a non-empty array of strings.');
315
280
  }
316
- catch (error) {
317
- // A path outside the root is the model's to fix; anything else is still
318
- // better reported to the model than turned into a protocol failure it
319
- // cannot read.
320
- outcome = toolError(error instanceof QueryPathError ? error.message : `spec-guard failed: ${error instanceof Error ? error.message : String(error)}`);
281
+ const depth = args['depth'];
282
+ if (depth !== undefined && !(Number.isInteger(depth) && depth >= 1)) {
283
+ return toolError('"depth" must be a whole number of 1 or more: a depth of 0 would follow no import.');
321
284
  }
322
- return {
323
- content: [{ type: 'text', text: outcome.text }],
324
- ...(outcome.structured === undefined ? {} : { structuredContent: outcome.structured }),
325
- ...(outcome.isError ? { isError: true } : {}),
285
+ const settings = await current();
286
+ const includeInactive = args['include_inactive'] ?? settings.run?.ignoreStatus ?? false;
287
+ if (typeof includeInactive !== 'boolean')
288
+ return toolError('"include_inactive" must be true or false.');
289
+ const report = {
290
+ ...(await impactOf({
291
+ ...ruleSetOptions(settings),
292
+ paths: paths,
293
+ // Absent and undefined are one answer to impactOf: every dependent.
294
+ depth: depth,
295
+ includeInactive,
296
+ })),
297
+ config: settings.config,
326
298
  };
299
+ return { text: formatImpact(report), structured: { ...impactDocument(report) } };
327
300
  }
328
- async function listResources() {
329
- const ruleSet = await loadRuleSet(ruleSetOptions(await current()));
301
+ function rulesResource(ruleSet) {
330
302
  return {
331
- resources: [
303
+ root: toPosix(options.root),
304
+ specFiles: ruleSet.specFiles,
305
+ rules: ruleSet.rules
306
+ .filter(({ document }) => document.inForce)
307
+ .map(({ assertion, document }) => viewRule(assertion, document)),
308
+ notInForce: ruleSet.documents
309
+ .filter((document) => !document.inForce)
310
+ .map((document) => ({ ...viewDocument(document), rules: document.directives.length })),
311
+ errors: ruleSet.errors.map((error) => ({
312
+ file: error.location.relativeFile,
313
+ line: error.location.line,
314
+ message: error.message,
315
+ })),
316
+ };
317
+ }
318
+ const resources = {
319
+ templates: RESOURCE_TEMPLATES,
320
+ async list() {
321
+ const ruleSet = await loadRuleSet(ruleSetOptions(await current()));
322
+ return [
332
323
  {
333
324
  uri: RULES_URI,
334
325
  name: 'rules',
@@ -346,207 +337,44 @@ export function createMcpHandler(options) {
346
337
  title: document.title ?? document.relativeFile,
347
338
  mimeType: 'text/markdown',
348
339
  })),
349
- ],
350
- };
351
- }
352
- function rulesResource(ruleSet) {
353
- return {
354
- root: toPosix(options.root),
355
- specFiles: ruleSet.specFiles,
356
- rules: ruleSet.rules
357
- .filter(({ document }) => document.inForce)
358
- .map(({ assertion, document }) => viewRule(assertion, document)),
359
- notInForce: ruleSet.documents
360
- .filter((document) => !document.inForce)
361
- .map((document) => ({ ...viewDocument(document), rules: document.directives.length })),
362
- errors: ruleSet.errors.map((error) => ({
363
- file: error.location.relativeFile,
364
- line: error.location.line,
365
- message: error.message,
366
- })),
367
- };
368
- }
369
- async function readResource(params, era) {
370
- const uri = params['uri'];
371
- if (typeof uri !== 'string')
372
- throw new ProtocolError(INVALID_PARAMS, 'resources/read needs a uri.');
373
- const ruleSet = await loadRuleSet(ruleSetOptions(await current()));
374
- if (uri === RULES_URI) {
375
- return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(rulesResource(ruleSet), null, 2) }] };
376
- }
377
- // Membership, never a path join: the only files this reads are the ones the
378
- // spec patterns matched, so no URI can reach anything else on disk.
379
- const wanted = documentPath(uri);
380
- const document = ruleSet.documents.find((candidate) => candidate.relativeFile === wanted);
381
- const text = document === undefined ? undefined : await readFile(document.file).catch(() => undefined);
382
- if (text === undefined) {
383
- throw new ProtocolError(era === 'modern' ? INVALID_PARAMS : LEGACY_RESOURCE_NOT_FOUND, 'Resource not found', { uri });
384
- }
385
- return { contents: [{ uri, mimeType: 'text/markdown', text }] };
386
- }
387
- /** A list request's cursor. This server never issues one, so any is invalid. */
388
- function refuseCursor(params) {
389
- if (params?.['cursor'] !== undefined)
390
- throw new ProtocolError(INVALID_PARAMS, 'Invalid cursor: this server does not paginate.');
391
- }
392
- async function dispatch(method, params, era) {
393
- const given = params ?? {};
394
- if (era === 'legacy') {
395
- if (method === 'initialize') {
396
- return {
397
- protocolVersion: negotiateLegacyVersion(given['protocolVersion']),
398
- capabilities,
399
- serverInfo,
400
- instructions: INSTRUCTIONS,
401
- };
340
+ ];
341
+ },
342
+ async read(uri) {
343
+ const ruleSet = await loadRuleSet(ruleSetOptions(await current()));
344
+ if (uri === RULES_URI) {
345
+ return [{ uri, mimeType: 'application/json', text: JSON.stringify(rulesResource(ruleSet), null, 2) }];
402
346
  }
403
- if (method === 'ping')
404
- return {};
405
- }
406
- else if (method === 'server/discover') {
407
- return { supportedVersions: [...MODERN_PROTOCOL_VERSIONS], capabilities, instructions: INSTRUCTIONS };
408
- }
409
- switch (method) {
410
- case 'tools/list':
411
- refuseCursor(params);
412
- return { tools: TOOLS };
413
- case 'tools/call':
414
- return callTool(given);
415
- case 'resources/list':
416
- refuseCursor(params);
417
- return listResources();
418
- case 'resources/templates/list':
419
- refuseCursor(params);
420
- return { resourceTemplates: RESOURCE_TEMPLATES };
421
- case 'resources/read':
422
- return readResource(given, era);
423
- default:
424
- throw new ProtocolError(METHOD_NOT_FOUND, 'Method not found');
425
- }
426
- }
427
- /** The 2026-07-28 shape: a result type, caching hints where required, and identity. */
428
- function modernResult(method, result) {
429
- // Every cacheable result is marked stale at once and private. The rules are
430
- // read fresh on each request, and a client serving an ADR edited a minute ago
431
- // from cache would be doing the one thing this tool exists to stop.
432
- const cacheable = method !== 'tools/call';
433
- return {
434
- resultType: 'complete',
435
- ...result,
436
- ...(cacheable ? { ttlMs: 0, cacheScope: 'private' } : {}),
437
- _meta: { [SERVER_INFO_KEY]: serverInfo },
438
- };
439
- }
440
- return async (message) => {
441
- if (!isObject(message)) {
442
- return {
443
- jsonrpc: '2.0',
444
- error: {
445
- code: INVALID_REQUEST,
446
- message: Array.isArray(message) ? 'Invalid Request: batches are not supported' : 'Invalid Request',
447
- },
448
- };
449
- }
450
- const { id, method, params } = message;
451
- if (message['jsonrpc'] === '2.0' && method === undefined && isRequestId(id) && ('result' in message || 'error' in message)) {
452
- return null;
453
- }
454
- if (message['jsonrpc'] !== '2.0' || typeof method !== 'string' || (id !== undefined && !isRequestId(id))) {
455
- return { jsonrpc: '2.0', ...(isRequestId(id) ? { id } : {}), error: { code: INVALID_REQUEST, message: 'Invalid Request' } };
456
- }
457
- if (id === undefined)
458
- return null;
459
- try {
460
- if (params !== undefined && !isObject(params))
461
- throw new ProtocolError(INVALID_PARAMS, 'params must be an object.');
462
- const era = classifyRequest(method, params);
463
- const result = await dispatch(method, params, era);
464
- return { jsonrpc: '2.0', id, result: era === 'modern' ? modernResult(method, result) : result };
465
- }
466
- catch (error) {
467
- const failure = error instanceof ProtocolError
468
- ? { code: error.code, message: error.message, data: error.data }
469
- : { code: INTERNAL_ERROR, message: `Internal error: ${error instanceof Error ? error.message : String(error)}` };
470
- return { jsonrpc: '2.0', id, error: failure };
471
- }
347
+ // Membership, never a path join: the only files this reads are the ones the
348
+ // spec patterns matched, so no URI can reach anything else on disk.
349
+ const wanted = documentPath(uri);
350
+ const document = ruleSet.documents.find((candidate) => candidate.relativeFile === wanted);
351
+ const text = document === undefined ? undefined : await readFile(document.file).catch(() => undefined);
352
+ return text === undefined ? null : [{ uri, mimeType: 'text/markdown', text }];
353
+ },
472
354
  };
355
+ // Every result a modern client may cache is marked stale at once and
356
+ // private, which is the protocol layer's default and the one this server
357
+ // needs: the rules are read fresh on each request, and a client serving an
358
+ // ADR edited a minute ago from its cache would be doing the one thing this
359
+ // tool exists to stop. No prompts are declared, so prompts/list is answered
360
+ // "Method not found", as it is for any method the server does not have.
361
+ return createMcpServer({
362
+ name: SERVER_NAME,
363
+ version: options.version,
364
+ instructions: INSTRUCTIONS,
365
+ tools: [tool(GET_RULES, getRules), tool(CHECK_ARCHITECTURE, checkArchitecture), tool(GET_DEPENDENTS, getDependents)],
366
+ resources,
367
+ });
473
368
  }
474
369
  /* -------------------------------------------------------------------- stdio */
475
- /** The key a request is tracked under: `1` and `"1"` are different ids. */
476
- function trackingKey(id) {
477
- return `${typeof id}:${id}`;
478
- }
479
370
  /**
480
371
  * Serves a handler over newline-delimited JSON-RPC until the input ends.
481
372
  *
482
- * The whole stdio binding. Messages are split on `\n` with a trailing `\r`
483
- * dropped, blank lines are skipped, and every response is written as a single
484
- * line - `JSON.stringify` never emits a raw newline, which is what the binding
485
- * requires. Requests are answered concurrently and in whatever order they
486
- * finish. A request the client cancels gets no response at all. When the input
487
- * closes, requests still running are allowed to finish and then this resolves.
373
+ * spec-core's `serveLines`, under the name this module has always exported:
374
+ * one message per line, answered concurrently, a cancelled request answered
375
+ * with nothing, and the input's end awaited until the last request finishes.
488
376
  */
489
377
  export function serveStdio(input, write, handle) {
490
- return new Promise((resolve, reject) => {
491
- // Decoded here rather than with setEncoding, which cannot be told apart
492
- // from not setting one: Node reads an empty encoding name as UTF-8 too.
493
- const decoder = new TextDecoder();
494
- let buffer = '';
495
- let running = 0;
496
- let ended = false;
497
- const inFlight = new Set();
498
- const cancelled = new Set();
499
- // Resolves once the input has ended and the last request has answered. A
500
- // count rather than a set of promises, so that getting it wrong either hangs
501
- // or ends early - both of which a test sees.
502
- const settle = () => {
503
- if (ended && running === 0)
504
- resolve();
505
- };
506
- const receive = (line) => {
507
- // A CRLF line keeps its \r: JSON allows it as whitespace, and a line of
508
- // nothing else is blank.
509
- if (line.trim() === '')
510
- return;
511
- let message;
512
- try {
513
- message = JSON.parse(line);
514
- }
515
- catch {
516
- write(JSON.stringify({ jsonrpc: '2.0', error: { code: PARSE_ERROR, message: 'Parse error' } }));
517
- return;
518
- }
519
- if (isObject(message) && message['method'] === 'notifications/cancelled') {
520
- const requestId = isObject(message['params']) ? message['params']['requestId'] : undefined;
521
- if (isRequestId(requestId) && inFlight.has(trackingKey(requestId)))
522
- cancelled.add(trackingKey(requestId));
523
- return;
524
- }
525
- const key = isObject(message) && typeof message['method'] === 'string' ? trackingKey(message['id']) : undefined;
526
- inFlight.add(key);
527
- running += 1;
528
- void handle(message).then((response) => {
529
- running -= 1;
530
- inFlight.delete(key);
531
- if (!cancelled.delete(key) && response !== null)
532
- write(JSON.stringify(response));
533
- settle();
534
- });
535
- };
536
- input.on('data', (chunk) => {
537
- buffer += decoder.decode(chunk, { stream: true });
538
- for (let newline = buffer.indexOf('\n'); newline !== -1; newline = buffer.indexOf('\n')) {
539
- const line = buffer.slice(0, newline);
540
- buffer = buffer.slice(newline + 1);
541
- receive(line);
542
- }
543
- });
544
- input.once('end', () => {
545
- ended = true;
546
- receive(buffer);
547
- settle();
548
- });
549
- input.once('error', reject);
550
- });
378
+ return serveLines(input, write, handle);
551
379
  }
552
380
  //# sourceMappingURL=mcp.js.map