@apidiff/core 1.0.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 (249) hide show
  1. package/dist/ast/hash.d.ts +2 -0
  2. package/dist/ast/hash.d.ts.map +1 -0
  3. package/dist/ast/index.d.ts +3 -0
  4. package/dist/ast/index.d.ts.map +1 -0
  5. package/dist/ast/traverse.d.ts +13 -0
  6. package/dist/ast/traverse.d.ts.map +1 -0
  7. package/dist/config/index.d.ts +9 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/diff/auth-differ.d.ts +4 -0
  10. package/dist/diff/auth-differ.d.ts.map +1 -0
  11. package/dist/diff/endpoint-differ.d.ts +3 -0
  12. package/dist/diff/endpoint-differ.d.ts.map +1 -0
  13. package/dist/diff/index.d.ts +3 -0
  14. package/dist/diff/index.d.ts.map +1 -0
  15. package/dist/diff/schema-differ.d.ts +3 -0
  16. package/dist/diff/schema-differ.d.ts.map +1 -0
  17. package/dist/diff/server-differ.d.ts +3 -0
  18. package/dist/diff/server-differ.d.ts.map +1 -0
  19. package/dist/index.cjs +2902 -0
  20. package/dist/index.d.ts +15 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +2855 -0
  23. package/dist/loader/file-loader.d.ts +5 -0
  24. package/dist/loader/file-loader.d.ts.map +1 -0
  25. package/dist/loader/git-loader.d.ts +5 -0
  26. package/dist/loader/git-loader.d.ts.map +1 -0
  27. package/dist/loader/index.d.ts +7 -0
  28. package/dist/loader/index.d.ts.map +1 -0
  29. package/dist/loader/url-loader.d.ts +5 -0
  30. package/dist/loader/url-loader.d.ts.map +1 -0
  31. package/dist/output/html.d.ts +3 -0
  32. package/dist/output/html.d.ts.map +1 -0
  33. package/dist/output/index.d.ts +3 -0
  34. package/dist/output/index.d.ts.map +1 -0
  35. package/dist/output/json.d.ts +3 -0
  36. package/dist/output/json.d.ts.map +1 -0
  37. package/dist/output/markdown.d.ts +3 -0
  38. package/dist/output/markdown.d.ts.map +1 -0
  39. package/dist/output/terminal.d.ts +3 -0
  40. package/dist/output/terminal.d.ts.map +1 -0
  41. package/dist/parsers/base.d.ts +8 -0
  42. package/dist/parsers/base.d.ts.map +1 -0
  43. package/dist/parsers/graphql/index.d.ts +13 -0
  44. package/dist/parsers/graphql/index.d.ts.map +1 -0
  45. package/dist/parsers/index.d.ts +7 -0
  46. package/dist/parsers/index.d.ts.map +1 -0
  47. package/dist/parsers/openapi2/index.d.ts +9 -0
  48. package/dist/parsers/openapi2/index.d.ts.map +1 -0
  49. package/dist/parsers/openapi3/index.d.ts +9 -0
  50. package/dist/parsers/openapi3/index.d.ts.map +1 -0
  51. package/dist/parsers/openapi3/ref-resolver.d.ts +2 -0
  52. package/dist/parsers/openapi3/ref-resolver.d.ts.map +1 -0
  53. package/dist/parsers/openapi3/schema-normalizer.d.ts +3 -0
  54. package/dist/parsers/openapi3/schema-normalizer.d.ts.map +1 -0
  55. package/dist/parsers/openapi3/security-normalizer.d.ts +3 -0
  56. package/dist/parsers/openapi3/security-normalizer.d.ts.map +1 -0
  57. package/dist/parsers/protobuf/index.d.ts +14 -0
  58. package/dist/parsers/protobuf/index.d.ts.map +1 -0
  59. package/dist/rules/auth/oauth-scope-removed.d.ts +9 -0
  60. package/dist/rules/auth/oauth-scope-removed.d.ts.map +1 -0
  61. package/dist/rules/auth/security-added.d.ts +9 -0
  62. package/dist/rules/auth/security-added.d.ts.map +1 -0
  63. package/dist/rules/auth/security-removed.d.ts +9 -0
  64. package/dist/rules/auth/security-removed.d.ts.map +1 -0
  65. package/dist/rules/auth/security-scheme-type-changed.d.ts +9 -0
  66. package/dist/rules/auth/security-scheme-type-changed.d.ts.map +1 -0
  67. package/dist/rules/base.d.ts +19 -0
  68. package/dist/rules/base.d.ts.map +1 -0
  69. package/dist/rules/endpoint/endpoint-added.d.ts +9 -0
  70. package/dist/rules/endpoint/endpoint-added.d.ts.map +1 -0
  71. package/dist/rules/endpoint/endpoint-deprecated.d.ts +9 -0
  72. package/dist/rules/endpoint/endpoint-deprecated.d.ts.map +1 -0
  73. package/dist/rules/endpoint/endpoint-removed.d.ts +9 -0
  74. package/dist/rules/endpoint/endpoint-removed.d.ts.map +1 -0
  75. package/dist/rules/endpoint/http-method-changed.d.ts +9 -0
  76. package/dist/rules/endpoint/http-method-changed.d.ts.map +1 -0
  77. package/dist/rules/endpoint/path-changed.d.ts +9 -0
  78. package/dist/rules/endpoint/path-changed.d.ts.map +1 -0
  79. package/dist/rules/index.d.ts +4 -0
  80. package/dist/rules/index.d.ts.map +1 -0
  81. package/dist/rules/meta/server-removed.d.ts +9 -0
  82. package/dist/rules/meta/server-removed.d.ts.map +1 -0
  83. package/dist/rules/param/param-added.d.ts +9 -0
  84. package/dist/rules/param/param-added.d.ts.map +1 -0
  85. package/dist/rules/param/param-deprecated.d.ts +9 -0
  86. package/dist/rules/param/param-deprecated.d.ts.map +1 -0
  87. package/dist/rules/param/param-enum-value-added.d.ts +9 -0
  88. package/dist/rules/param/param-enum-value-added.d.ts.map +1 -0
  89. package/dist/rules/param/param-enum-value-removed.d.ts +9 -0
  90. package/dist/rules/param/param-enum-value-removed.d.ts.map +1 -0
  91. package/dist/rules/param/param-location-changed.d.ts +9 -0
  92. package/dist/rules/param/param-location-changed.d.ts.map +1 -0
  93. package/dist/rules/param/param-removed.d.ts +9 -0
  94. package/dist/rules/param/param-removed.d.ts.map +1 -0
  95. package/dist/rules/param/param-required-added.d.ts +9 -0
  96. package/dist/rules/param/param-required-added.d.ts.map +1 -0
  97. package/dist/rules/param/param-required-true-to-false.d.ts +9 -0
  98. package/dist/rules/param/param-required-true-to-false.d.ts.map +1 -0
  99. package/dist/rules/param/param-type-changed.d.ts +9 -0
  100. package/dist/rules/param/param-type-changed.d.ts.map +1 -0
  101. package/dist/rules/request/request-body-added-required.d.ts +9 -0
  102. package/dist/rules/request/request-body-added-required.d.ts.map +1 -0
  103. package/dist/rules/request/request-body-removed.d.ts +9 -0
  104. package/dist/rules/request/request-body-removed.d.ts.map +1 -0
  105. package/dist/rules/request/request-content-type-added.d.ts +9 -0
  106. package/dist/rules/request/request-content-type-added.d.ts.map +1 -0
  107. package/dist/rules/request/request-content-type-removed.d.ts +9 -0
  108. package/dist/rules/request/request-content-type-removed.d.ts.map +1 -0
  109. package/dist/rules/request/request-field-added-required.d.ts +9 -0
  110. package/dist/rules/request/request-field-added-required.d.ts.map +1 -0
  111. package/dist/rules/request/request-field-removed.d.ts +9 -0
  112. package/dist/rules/request/request-field-removed.d.ts.map +1 -0
  113. package/dist/rules/request/request-field-type-changed.d.ts +9 -0
  114. package/dist/rules/request/request-field-type-changed.d.ts.map +1 -0
  115. package/dist/rules/request/request-required-false-to-true.d.ts +9 -0
  116. package/dist/rules/request/request-required-false-to-true.d.ts.map +1 -0
  117. package/dist/rules/response/response-field-added.d.ts +9 -0
  118. package/dist/rules/response/response-field-added.d.ts.map +1 -0
  119. package/dist/rules/response/response-field-removed.d.ts +9 -0
  120. package/dist/rules/response/response-field-removed.d.ts.map +1 -0
  121. package/dist/rules/response/response-field-type-changed.d.ts +9 -0
  122. package/dist/rules/response/response-field-type-changed.d.ts.map +1 -0
  123. package/dist/rules/response/response-header-added-required.d.ts +9 -0
  124. package/dist/rules/response/response-header-added-required.d.ts.map +1 -0
  125. package/dist/rules/response/response-header-removed.d.ts +9 -0
  126. package/dist/rules/response/response-header-removed.d.ts.map +1 -0
  127. package/dist/rules/response/response-media-type-added.d.ts +9 -0
  128. package/dist/rules/response/response-media-type-added.d.ts.map +1 -0
  129. package/dist/rules/response/response-media-type-removed.d.ts +9 -0
  130. package/dist/rules/response/response-media-type-removed.d.ts.map +1 -0
  131. package/dist/rules/response/response-status-added.d.ts +9 -0
  132. package/dist/rules/response/response-status-added.d.ts.map +1 -0
  133. package/dist/rules/response/response-status-removed.d.ts +9 -0
  134. package/dist/rules/response/response-status-removed.d.ts.map +1 -0
  135. package/dist/types/ast.d.ts +140 -0
  136. package/dist/types/ast.d.ts.map +1 -0
  137. package/dist/types/config.d.ts +17 -0
  138. package/dist/types/config.d.ts.map +1 -0
  139. package/dist/types/diff.d.ts +43 -0
  140. package/dist/types/diff.d.ts.map +1 -0
  141. package/dist/types/errors.d.ts +21 -0
  142. package/dist/types/errors.d.ts.map +1 -0
  143. package/dist/types/index.d.ts +6 -0
  144. package/dist/types/index.d.ts.map +1 -0
  145. package/dist/types/semantic.d.ts +45 -0
  146. package/dist/types/semantic.d.ts.map +1 -0
  147. package/package.json +31 -0
  148. package/src/ast/hash.ts +26 -0
  149. package/src/ast/index.ts +2 -0
  150. package/src/ast/traverse.ts +59 -0
  151. package/src/config/index.ts +44 -0
  152. package/src/diff/auth-differ.ts +72 -0
  153. package/src/diff/endpoint-differ.ts +144 -0
  154. package/src/diff/index.ts +13 -0
  155. package/src/diff/schema-differ.ts +70 -0
  156. package/src/diff/server-differ.ts +22 -0
  157. package/src/index.ts +92 -0
  158. package/src/loader/file-loader.ts +19 -0
  159. package/src/loader/git-loader.ts +22 -0
  160. package/src/loader/index.ts +32 -0
  161. package/src/loader/url-loader.ts +25 -0
  162. package/src/output/html.ts +177 -0
  163. package/src/output/index.ts +16 -0
  164. package/src/output/json.ts +5 -0
  165. package/src/output/markdown.ts +29 -0
  166. package/src/output/terminal.ts +37 -0
  167. package/src/parsers/base.ts +8 -0
  168. package/src/parsers/graphql/index.ts +181 -0
  169. package/src/parsers/index.ts +61 -0
  170. package/src/parsers/openapi2/index.ts +218 -0
  171. package/src/parsers/openapi3/index.ts +223 -0
  172. package/src/parsers/openapi3/ref-resolver.ts +101 -0
  173. package/src/parsers/openapi3/schema-normalizer.ts +52 -0
  174. package/src/parsers/openapi3/security-normalizer.ts +18 -0
  175. package/src/parsers/protobuf/index.ts +208 -0
  176. package/src/rules/auth/oauth-scope-removed.ts +34 -0
  177. package/src/rules/auth/security-added.ts +32 -0
  178. package/src/rules/auth/security-removed.ts +47 -0
  179. package/src/rules/auth/security-scheme-type-changed.ts +30 -0
  180. package/src/rules/base.ts +29 -0
  181. package/src/rules/endpoint/endpoint-added.ts +26 -0
  182. package/src/rules/endpoint/endpoint-deprecated.ts +29 -0
  183. package/src/rules/endpoint/endpoint-removed.ts +26 -0
  184. package/src/rules/endpoint/http-method-changed.ts +29 -0
  185. package/src/rules/endpoint/path-changed.ts +29 -0
  186. package/src/rules/index.ts +83 -0
  187. package/src/rules/meta/server-removed.ts +26 -0
  188. package/src/rules/param/param-added.ts +52 -0
  189. package/src/rules/param/param-deprecated.ts +34 -0
  190. package/src/rules/param/param-enum-value-added.ts +32 -0
  191. package/src/rules/param/param-enum-value-removed.ts +32 -0
  192. package/src/rules/param/param-location-changed.ts +43 -0
  193. package/src/rules/param/param-removed.ts +52 -0
  194. package/src/rules/param/param-required-added.ts +34 -0
  195. package/src/rules/param/param-required-true-to-false.ts +34 -0
  196. package/src/rules/param/param-type-changed.ts +32 -0
  197. package/src/rules/request/request-body-added-required.ts +33 -0
  198. package/src/rules/request/request-body-removed.ts +30 -0
  199. package/src/rules/request/request-content-type-added.ts +31 -0
  200. package/src/rules/request/request-content-type-removed.ts +31 -0
  201. package/src/rules/request/request-field-added-required.ts +41 -0
  202. package/src/rules/request/request-field-removed.ts +35 -0
  203. package/src/rules/request/request-field-type-changed.ts +37 -0
  204. package/src/rules/request/request-required-false-to-true.ts +56 -0
  205. package/src/rules/response/response-field-added.ts +34 -0
  206. package/src/rules/response/response-field-removed.ts +34 -0
  207. package/src/rules/response/response-field-type-changed.ts +37 -0
  208. package/src/rules/response/response-header-added-required.ts +47 -0
  209. package/src/rules/response/response-header-removed.ts +32 -0
  210. package/src/rules/response/response-media-type-added.ts +32 -0
  211. package/src/rules/response/response-media-type-removed.ts +32 -0
  212. package/src/rules/response/response-status-added.ts +31 -0
  213. package/src/rules/response/response-status-removed.ts +31 -0
  214. package/src/types/ast.ts +164 -0
  215. package/src/types/config.ts +31 -0
  216. package/src/types/diff.ts +49 -0
  217. package/src/types/errors.ts +26 -0
  218. package/src/types/index.ts +5 -0
  219. package/src/types/semantic.ts +60 -0
  220. package/test/integration/stripe-v1.yaml +36 -0
  221. package/test/integration/stripe-v2.yaml +35 -0
  222. package/tests/ast.test.ts +60 -0
  223. package/tests/config.test.ts +43 -0
  224. package/tests/diff/auth-differ.test.ts +55 -0
  225. package/tests/diff/endpoint-differ.test.ts +42 -0
  226. package/tests/diff/schema-differ.test.ts +46 -0
  227. package/tests/diff/server-differ.test.ts +23 -0
  228. package/tests/diff.test.ts +116 -0
  229. package/tests/fixtures/openapi3/auth-added/v1.yaml +11 -0
  230. package/tests/fixtures/openapi3/auth-added/v2.yaml +18 -0
  231. package/tests/integration.test.ts +21 -0
  232. package/tests/loader-more.test.ts +75 -0
  233. package/tests/loader.test.ts +61 -0
  234. package/tests/output.test.ts +58 -0
  235. package/tests/parsers/openapi3-coverage.test.ts +77 -0
  236. package/tests/parsers/openapi3.test.ts +41 -0
  237. package/tests/parsers/parsers-other.test.ts +135 -0
  238. package/tests/parsers/ref-resolver.test.ts +78 -0
  239. package/tests/parsers/schema-normalizer.test.ts +30 -0
  240. package/tests/rules/auth-meta.test.ts +87 -0
  241. package/tests/rules/base.test.ts +44 -0
  242. package/tests/rules/endpoint.test.ts +122 -0
  243. package/tests/rules/param.test.ts +242 -0
  244. package/tests/rules/request.test.ts +147 -0
  245. package/tests/rules/response.test.ts +161 -0
  246. package/tests/rules/rules-coverage.test.ts +64 -0
  247. package/tests/types-errors.test.ts +22 -0
  248. package/tsconfig.json +8 -0
  249. package/tsconfig.tsbuildinfo +1 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,2902 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ApidiffError: () => ApidiffError,
34
+ ConfigError: () => ConfigError,
35
+ DEFAULT_CONFIG: () => DEFAULT_CONFIG2,
36
+ FormatError: () => FormatError,
37
+ LoadError: () => LoadError,
38
+ ParseError: () => ParseError,
39
+ RefError: () => RefError,
40
+ formatOutput: () => formatOutput,
41
+ loadConfig: () => loadConfig,
42
+ run: () => run,
43
+ runContent: () => runContent
44
+ });
45
+ module.exports = __toCommonJS(index_exports);
46
+
47
+ // src/loader/index.ts
48
+ var import_node_fs2 = require("fs");
49
+
50
+ // src/loader/file-loader.ts
51
+ var import_node_fs = require("fs");
52
+ var import_node_path = require("path");
53
+
54
+ // src/types/errors.ts
55
+ var ApidiffError = class extends Error {
56
+ constructor(message, cause) {
57
+ super(message);
58
+ this.cause = cause;
59
+ this.name = this.constructor.name;
60
+ }
61
+ cause;
62
+ };
63
+ var LoadError = class extends ApidiffError {
64
+ };
65
+ var ParseError = class extends ApidiffError {
66
+ constructor(message, line, col, filePath, cause) {
67
+ super(message, cause);
68
+ this.line = line;
69
+ this.col = col;
70
+ this.filePath = filePath;
71
+ }
72
+ line;
73
+ col;
74
+ filePath;
75
+ };
76
+ var RefError = class extends ApidiffError {
77
+ constructor(message, ref, cause) {
78
+ super(message, cause);
79
+ this.ref = ref;
80
+ }
81
+ ref;
82
+ };
83
+ var ConfigError = class extends ApidiffError {
84
+ };
85
+ var FormatError = class extends ApidiffError {
86
+ };
87
+
88
+ // src/loader/file-loader.ts
89
+ function loadFile(source) {
90
+ try {
91
+ const sourcePath = (0, import_node_path.resolve)(source);
92
+ const content = (0, import_node_fs.readFileSync)(sourcePath, "utf8");
93
+ return { content, sourcePath };
94
+ } catch (err) {
95
+ if (err.code === "ENOENT") {
96
+ throw new LoadError(`file not found: ${source}`, err);
97
+ }
98
+ if (err.code === "EACCES") {
99
+ throw new LoadError(`permission denied: ${source}`, err);
100
+ }
101
+ throw new LoadError(`failed to read file: ${source}`, err);
102
+ }
103
+ }
104
+
105
+ // src/loader/url-loader.ts
106
+ async function loadUrl(source) {
107
+ try {
108
+ const controller = new AbortController();
109
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
110
+ const response = await fetch(source, { signal: controller.signal });
111
+ clearTimeout(timeoutId);
112
+ if (!response.ok) {
113
+ throw new LoadError(`failed to fetch url: ${source} (status: ${response.status})`);
114
+ }
115
+ const content = await response.text();
116
+ return { content, sourcePath: source };
117
+ } catch (err) {
118
+ if (err instanceof LoadError) {
119
+ throw err;
120
+ }
121
+ if (err.name === "AbortError") {
122
+ throw new LoadError(`timeout fetching url: ${source}`, err);
123
+ }
124
+ throw new LoadError(`network error fetching url: ${source}`, err instanceof Error ? err : void 0);
125
+ }
126
+ }
127
+
128
+ // src/loader/git-loader.ts
129
+ var import_node_child_process = require("child_process");
130
+ function loadGit(source) {
131
+ const match = source.match(/^git:([^:]+):(.+)$/);
132
+ if (!match) {
133
+ throw new LoadError(`invalid git source format: ${source}`);
134
+ }
135
+ const [, ref, filePath] = match;
136
+ try {
137
+ const content = (0, import_node_child_process.execSync)(`git show ${ref}:${filePath}`, { encoding: "utf-8", stdio: "pipe" });
138
+ return { content, sourcePath: filePath };
139
+ } catch (err) {
140
+ const stderr = err.stderr?.toString() || "";
141
+ if (stderr.includes("Not a valid object name") || stderr.includes("does not exist")) {
142
+ throw new LoadError(`git ref or path not found: ${ref}:${filePath}`, err);
143
+ }
144
+ throw new LoadError(`git command failed: ${source}`, err);
145
+ }
146
+ }
147
+
148
+ // src/loader/index.ts
149
+ async function loadSpec(source) {
150
+ if (source === "-") {
151
+ try {
152
+ const content = (0, import_node_fs2.readFileSync)(0, "utf-8");
153
+ return { content, sourcePath: void 0 };
154
+ } catch (err) {
155
+ throw new LoadError("failed to read from stdin", err);
156
+ }
157
+ }
158
+ if (source.startsWith("http://") || source.startsWith("https://")) {
159
+ return await loadUrl(source);
160
+ }
161
+ if (source.startsWith("git:")) {
162
+ return loadGit(source);
163
+ }
164
+ return loadFile(source);
165
+ }
166
+
167
+ // src/parsers/index.ts
168
+ var import_js_yaml4 = __toESM(require("js-yaml"), 1);
169
+
170
+ // src/parsers/openapi3/ref-resolver.ts
171
+ var import_node_fs3 = require("fs");
172
+ var import_node_path2 = require("path");
173
+ var import_js_yaml = __toESM(require("js-yaml"), 1);
174
+ var fileCache = /* @__PURE__ */ new Map();
175
+ function resolveRefs(obj, root, sourcePath) {
176
+ if (!obj || typeof obj !== "object") return obj;
177
+ const stack = [
178
+ { node: obj, parent: null, key: null }
179
+ ];
180
+ const visitedRefs = /* @__PURE__ */ new Set();
181
+ const visitedNodes = /* @__PURE__ */ new WeakSet();
182
+ while (stack.length > 0) {
183
+ const current = stack.pop();
184
+ const { node, parent, key } = current;
185
+ if (!node || typeof node !== "object") continue;
186
+ if (visitedNodes.has(node)) continue;
187
+ visitedNodes.add(node);
188
+ if (Array.isArray(node)) {
189
+ for (let i = node.length - 1; i >= 0; i--) {
190
+ stack.push({ node: node[i], parent: node, key: i.toString() });
191
+ }
192
+ continue;
193
+ }
194
+ if ("$ref" in node && typeof node.$ref === "string") {
195
+ const ref = node.$ref;
196
+ if (visitedRefs.has(ref)) {
197
+ if (parent && key !== null) {
198
+ parent[key] = { ...node, $circular: true };
199
+ }
200
+ continue;
201
+ }
202
+ visitedRefs.add(ref);
203
+ let resolved;
204
+ if (ref.startsWith("#")) {
205
+ resolved = resolveLocalRef(ref, root);
206
+ } else if (ref.startsWith("./") || ref.startsWith("../")) {
207
+ if (!sourcePath) {
208
+ throw new RefError(`Cannot resolve relative ref without sourcePath`, ref);
209
+ }
210
+ const refPath = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(sourcePath), ref.split("#")[0]);
211
+ try {
212
+ let parsed = fileCache.get(refPath);
213
+ if (!parsed) {
214
+ const content = (0, import_node_fs3.readFileSync)(refPath, "utf8");
215
+ parsed = refPath.endsWith(".json") ? JSON.parse(content) : import_js_yaml.default.load(content);
216
+ fileCache.set(refPath, parsed);
217
+ }
218
+ resolved = ref.includes("#") ? resolveLocalRef("#" + ref.split("#")[1], parsed) : parsed;
219
+ } catch (err) {
220
+ throw new RefError(`Failed to load external ref`, ref, err);
221
+ }
222
+ } else if (ref.startsWith("http://") || ref.startsWith("https://")) {
223
+ throw new RefError(`URL refs not implemented synchronously`, ref);
224
+ } else {
225
+ throw new RefError(`Unsupported ref format`, ref);
226
+ }
227
+ if (parent && key !== null) {
228
+ parent[key] = resolved;
229
+ stack.push({ node: resolved, parent, key });
230
+ }
231
+ continue;
232
+ }
233
+ const keys = Object.keys(node);
234
+ for (let i = keys.length - 1; i >= 0; i--) {
235
+ const k = keys[i];
236
+ if (k !== "$ref" && typeof node[k] === "object" && node[k] !== null) {
237
+ stack.push({ node: node[k], parent: node, key: k });
238
+ }
239
+ }
240
+ }
241
+ return obj;
242
+ }
243
+ function resolveLocalRef(ref, root) {
244
+ const parts = ref.replace(/^#\/?/, "").split("/");
245
+ let current = root;
246
+ for (const part of parts) {
247
+ if (!part) continue;
248
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
249
+ if (current && typeof current === "object" && key in current) {
250
+ current = current[key];
251
+ } else {
252
+ throw new RefError(`Local ref not found`, ref);
253
+ }
254
+ }
255
+ return current;
256
+ }
257
+
258
+ // src/parsers/openapi3/schema-normalizer.ts
259
+ function normalizeSchema(schema) {
260
+ if (!schema || typeof schema !== "object") return schema;
261
+ if (Array.isArray(schema.allOf)) {
262
+ const merged = flattenAllOf(schema.allOf);
263
+ schema = { ...schema, ...merged };
264
+ delete schema.allOf;
265
+ }
266
+ if (schema.nullable === true) {
267
+ if (typeof schema.type === "string") {
268
+ schema.type = [schema.type, "null"];
269
+ } else if (Array.isArray(schema.type) && !schema.type.includes("null")) {
270
+ schema.type = [...schema.type, "null"];
271
+ }
272
+ delete schema.nullable;
273
+ }
274
+ if (schema.properties) {
275
+ for (const key of Object.keys(schema.properties)) {
276
+ schema.properties[key] = normalizeSchema(schema.properties[key]);
277
+ }
278
+ }
279
+ if (schema.items) {
280
+ schema.items = normalizeSchema(schema.items);
281
+ }
282
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
283
+ schema.additionalProperties = normalizeSchema(schema.additionalProperties);
284
+ }
285
+ if (Array.isArray(schema.oneOf)) {
286
+ schema.oneOf = schema.oneOf.map(normalizeSchema);
287
+ }
288
+ if (Array.isArray(schema.anyOf)) {
289
+ schema.anyOf = schema.anyOf.map(normalizeSchema);
290
+ }
291
+ return schema;
292
+ }
293
+ function flattenAllOf(schemas) {
294
+ return schemas.reduce((merged, subSchema) => {
295
+ const normalized = normalizeSchema(subSchema);
296
+ return {
297
+ ...merged,
298
+ ...normalized,
299
+ properties: { ...merged.properties || {}, ...normalized.properties || {} },
300
+ required: [.../* @__PURE__ */ new Set([...merged.required || [], ...normalized.required || []])]
301
+ };
302
+ }, {});
303
+ }
304
+
305
+ // src/parsers/openapi3/security-normalizer.ts
306
+ function normalizeSecurity(securityArray) {
307
+ if (!securityArray || !Array.isArray(securityArray)) {
308
+ return [];
309
+ }
310
+ const result = [];
311
+ for (const req of securityArray) {
312
+ for (const [schemeId, scopes] of Object.entries(req)) {
313
+ result.push({
314
+ schemeId,
315
+ scopes: Array.isArray(scopes) ? scopes.map(String) : []
316
+ });
317
+ }
318
+ }
319
+ return result;
320
+ }
321
+
322
+ // src/parsers/openapi3/index.ts
323
+ var import_js_yaml2 = __toESM(require("js-yaml"), 1);
324
+ var OpenApi3Parser = class {
325
+ format = "openapi3";
326
+ canParse(raw) {
327
+ return raw.format === "openapi3";
328
+ }
329
+ async parse(raw) {
330
+ let parsed;
331
+ try {
332
+ parsed = JSON.parse(raw.content);
333
+ } catch {
334
+ try {
335
+ parsed = import_js_yaml2.default.load(raw.content);
336
+ } catch (err) {
337
+ throw new ParseError("Failed to parse OpenAPI 3.x spec", void 0, void 0, raw.sourcePath, err);
338
+ }
339
+ }
340
+ if (!parsed || typeof parsed !== "object" || !parsed.openapi || !parsed.openapi.startsWith("3.")) {
341
+ throw new ParseError("Not a valid OpenAPI 3.x spec", void 0, void 0, raw.sourcePath);
342
+ }
343
+ const resolved = resolveRefs(parsed, parsed, raw.sourcePath);
344
+ const meta = {
345
+ title: resolved.info?.title || "Unknown API",
346
+ version: resolved.info?.version || "1.0.0",
347
+ format: "openapi3",
348
+ rawVersion: resolved.openapi
349
+ };
350
+ const servers = (resolved.servers || []).map((s) => ({
351
+ url: s.url,
352
+ description: s.description,
353
+ variables: s.variables
354
+ }));
355
+ const components = {
356
+ schemas: {},
357
+ securitySchemes: {},
358
+ parameters: {},
359
+ responses: {},
360
+ headers: {},
361
+ requestBodies: {}
362
+ };
363
+ if (resolved.components?.schemas) {
364
+ for (const [k, v] of Object.entries(resolved.components.schemas)) {
365
+ components.schemas[k] = normalizeSchema(v);
366
+ }
367
+ }
368
+ const securitySchemes = resolved.components?.securitySchemes || {};
369
+ for (const [k, v] of Object.entries(securitySchemes)) {
370
+ components.securitySchemes[k] = v;
371
+ }
372
+ const endpoints = [];
373
+ const paths = resolved.paths || {};
374
+ const globalSecurity = resolved.security;
375
+ const allowedMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
376
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
377
+ if (!pathItem || typeof pathItem !== "object") continue;
378
+ const pathParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
379
+ for (const methodStr of Object.keys(pathItem)) {
380
+ if (!allowedMethods.includes(methodStr)) continue;
381
+ const op = pathItem[methodStr];
382
+ if (!op || typeof op !== "object") continue;
383
+ const method = methodStr.toUpperCase();
384
+ const id = `${method}:${normalizePathForId(pathStr)}`;
385
+ const opParams = Array.isArray(op.parameters) ? op.parameters : [];
386
+ const parameters = buildParameters(pathParams, opParams);
387
+ const security = op.security !== void 0 ? normalizeSecurity(op.security) : normalizeSecurity(globalSecurity);
388
+ const endpoint = {
389
+ id,
390
+ path: pathStr,
391
+ method,
392
+ summary: op.summary,
393
+ description: op.description,
394
+ operationId: op.operationId,
395
+ tags: Array.isArray(op.tags) ? op.tags : [],
396
+ deprecated: !!op.deprecated,
397
+ security,
398
+ parameters,
399
+ requestBody: op.requestBody ? buildRequestBody(op.requestBody) : void 0,
400
+ responses: buildResponses(op.responses),
401
+ extensions: extractExtensions(op)
402
+ };
403
+ endpoints.push(endpoint);
404
+ }
405
+ }
406
+ return {
407
+ meta,
408
+ servers,
409
+ endpoints,
410
+ components,
411
+ security: Object.values(components.securitySchemes)
412
+ };
413
+ }
414
+ };
415
+ function normalizePathForId(path) {
416
+ return path.toLowerCase().replace(/\/$/, "").replace(/\{[^}]+\}/g, "{*}");
417
+ }
418
+ function buildParameters(pathParams, opParams) {
419
+ const paramMap = /* @__PURE__ */ new Map();
420
+ for (const p of pathParams) {
421
+ paramMap.set(`${p.name}:${p.in}`, {
422
+ name: p.name,
423
+ in: p.in,
424
+ required: !!p.required,
425
+ deprecated: !!p.deprecated,
426
+ description: p.description,
427
+ schema: normalizeSchema(p.schema || {}),
428
+ example: p.example
429
+ });
430
+ }
431
+ for (const p of opParams) {
432
+ paramMap.set(`${p.name}:${p.in}`, {
433
+ name: p.name,
434
+ in: p.in,
435
+ required: !!p.required,
436
+ deprecated: !!p.deprecated,
437
+ description: p.description,
438
+ schema: normalizeSchema(p.schema || {}),
439
+ example: p.example
440
+ });
441
+ }
442
+ return Array.from(paramMap.values());
443
+ }
444
+ function buildRequestBody(rb) {
445
+ const content = {};
446
+ if (rb.content) {
447
+ for (const [k, v] of Object.entries(rb.content)) {
448
+ content[k] = {
449
+ schema: normalizeSchema(v.schema || {}),
450
+ example: v.example
451
+ };
452
+ }
453
+ }
454
+ return {
455
+ required: !!rb.required,
456
+ description: rb.description,
457
+ content
458
+ };
459
+ }
460
+ function buildResponses(responses) {
461
+ if (!responses || typeof responses !== "object") return [];
462
+ const res = [];
463
+ for (const [code, r] of Object.entries(responses)) {
464
+ const content = {};
465
+ if (r.content) {
466
+ for (const [k, v] of Object.entries(r.content)) {
467
+ content[k] = {
468
+ schema: normalizeSchema(v.schema || {}),
469
+ example: v.example
470
+ };
471
+ }
472
+ }
473
+ const headers = {};
474
+ if (r.headers) {
475
+ for (const [k, v] of Object.entries(r.headers)) {
476
+ headers[k] = {
477
+ required: !!v.required,
478
+ deprecated: !!v.deprecated,
479
+ schema: normalizeSchema(v.schema || {}),
480
+ description: v.description
481
+ };
482
+ }
483
+ }
484
+ res.push({
485
+ statusCode: code,
486
+ description: r.description,
487
+ content,
488
+ headers
489
+ });
490
+ }
491
+ return res;
492
+ }
493
+ function extractExtensions(obj) {
494
+ const ext = {};
495
+ if (obj && typeof obj === "object") {
496
+ for (const k of Object.keys(obj)) {
497
+ if (k.startsWith("x-")) {
498
+ ext[k] = obj[k];
499
+ }
500
+ }
501
+ }
502
+ return ext;
503
+ }
504
+
505
+ // src/parsers/openapi2/index.ts
506
+ var import_js_yaml3 = __toESM(require("js-yaml"), 1);
507
+ var OpenApi2Parser = class {
508
+ format = "openapi2";
509
+ canParse(raw) {
510
+ return raw.format === "openapi2";
511
+ }
512
+ async parse(raw) {
513
+ let parsed;
514
+ try {
515
+ parsed = JSON.parse(raw.content);
516
+ } catch {
517
+ try {
518
+ parsed = import_js_yaml3.default.load(raw.content);
519
+ } catch (err) {
520
+ throw new ParseError("Failed to parse OpenAPI 2.x spec", void 0, void 0, raw.sourcePath, err);
521
+ }
522
+ }
523
+ if (!parsed || typeof parsed !== "object" || !parsed.swagger || !parsed.swagger.startsWith("2.")) {
524
+ throw new ParseError("Not a valid OpenAPI 2.x spec", void 0, void 0, raw.sourcePath);
525
+ }
526
+ const resolved = parsed;
527
+ const meta = {
528
+ title: resolved.info?.title || "Unknown API",
529
+ version: resolved.info?.version || "1.0.0",
530
+ format: "openapi2",
531
+ rawVersion: resolved.swagger
532
+ };
533
+ const servers = [];
534
+ if (resolved.host) {
535
+ const schemes = Array.isArray(resolved.schemes) && resolved.schemes.length > 0 ? resolved.schemes : ["https"];
536
+ const basePath = resolved.basePath || "";
537
+ for (const scheme of schemes) {
538
+ servers.push({ url: `${scheme}://${resolved.host}${basePath}` });
539
+ }
540
+ }
541
+ const components = {
542
+ schemas: {},
543
+ securitySchemes: {},
544
+ parameters: {},
545
+ responses: {},
546
+ headers: {},
547
+ requestBodies: {}
548
+ };
549
+ if (resolved.definitions) {
550
+ for (const [k, v] of Object.entries(resolved.definitions)) {
551
+ components.schemas[k] = normalizeSchema(v);
552
+ }
553
+ }
554
+ const securitySchemes = resolved.securityDefinitions || {};
555
+ for (const [k, v] of Object.entries(securitySchemes)) {
556
+ components.securitySchemes[k] = v;
557
+ }
558
+ const endpoints = [];
559
+ const paths = resolved.paths || {};
560
+ const globalSecurity = resolved.security || [];
561
+ const allowedMethods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
562
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
563
+ if (!pathItem || typeof pathItem !== "object") continue;
564
+ const pathParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
565
+ for (const methodStr of Object.keys(pathItem)) {
566
+ if (!allowedMethods.includes(methodStr)) continue;
567
+ const op = pathItem[methodStr];
568
+ if (!op || typeof op !== "object") continue;
569
+ const method = methodStr.toUpperCase();
570
+ const id = `${method}:${normalizePathForId2(pathStr)}`;
571
+ const opParams = Array.isArray(op.parameters) ? op.parameters : [];
572
+ const { parameters, requestBody } = buildParametersAndBody(pathParams, opParams, resolved.consumes || op.consumes || ["application/json"]);
573
+ const endpoint = {
574
+ id,
575
+ path: pathStr,
576
+ method,
577
+ summary: op.summary,
578
+ description: op.description,
579
+ operationId: op.operationId,
580
+ tags: Array.isArray(op.tags) ? op.tags : [],
581
+ deprecated: !!op.deprecated,
582
+ security: op.security || globalSecurity,
583
+ parameters,
584
+ requestBody,
585
+ responses: buildResponses2(op.responses, resolved.produces || op.produces || ["application/json"]),
586
+ extensions: extractExtensions2(op)
587
+ };
588
+ endpoints.push(endpoint);
589
+ }
590
+ }
591
+ return {
592
+ meta,
593
+ servers,
594
+ endpoints,
595
+ components,
596
+ security: Object.values(components.securitySchemes)
597
+ };
598
+ }
599
+ };
600
+ function normalizePathForId2(path) {
601
+ return path.toLowerCase().replace(/\/$/, "").replace(/\{[^}]+\}/g, "{*}");
602
+ }
603
+ function buildParametersAndBody(pathParams, opParams, consumes) {
604
+ const paramMap = /* @__PURE__ */ new Map();
605
+ let requestBody = void 0;
606
+ const allParams = [...pathParams, ...opParams];
607
+ for (const p of allParams) {
608
+ if (p.in === "body") {
609
+ const content = {};
610
+ for (const mime of consumes) {
611
+ content[mime] = { schema: normalizeSchema(p.schema || {}) };
612
+ }
613
+ requestBody = {
614
+ required: !!p.required,
615
+ description: p.description,
616
+ content
617
+ };
618
+ } else if (p.in === "formData") {
619
+ if (!requestBody) {
620
+ requestBody = { required: false, content: {} };
621
+ }
622
+ const mime = consumes.includes("multipart/form-data") ? "multipart/form-data" : "application/x-www-form-urlencoded";
623
+ if (!requestBody.content[mime]) {
624
+ requestBody.content[mime] = { schema: { type: "object", properties: {} } };
625
+ }
626
+ const schema = requestBody.content[mime].schema;
627
+ if (!schema.properties) schema.properties = {};
628
+ schema.properties[p.name] = normalizeSchema(p);
629
+ if (p.required) {
630
+ if (!schema.required) schema.required = [];
631
+ schema.required.push(p.name);
632
+ }
633
+ } else {
634
+ paramMap.set(`${p.name}:${p.in}`, {
635
+ name: p.name,
636
+ in: p.in,
637
+ required: !!p.required,
638
+ deprecated: false,
639
+ description: p.description,
640
+ schema: normalizeSchema(p)
641
+ });
642
+ }
643
+ }
644
+ return { parameters: Array.from(paramMap.values()), requestBody };
645
+ }
646
+ function buildResponses2(responses, produces) {
647
+ if (!responses || typeof responses !== "object") return [];
648
+ const res = [];
649
+ for (const [code, r] of Object.entries(responses)) {
650
+ const content = {};
651
+ if (r.schema) {
652
+ for (const mime of produces) {
653
+ content[mime] = { schema: normalizeSchema(r.schema) };
654
+ }
655
+ }
656
+ const headers = {};
657
+ if (r.headers) {
658
+ for (const [k, v] of Object.entries(r.headers)) {
659
+ headers[k] = {
660
+ schema: normalizeSchema(v),
661
+ description: v.description
662
+ };
663
+ }
664
+ }
665
+ res.push({
666
+ statusCode: code,
667
+ description: r.description,
668
+ content,
669
+ headers
670
+ });
671
+ }
672
+ return res;
673
+ }
674
+ function extractExtensions2(obj) {
675
+ const ext = {};
676
+ if (obj && typeof obj === "object") {
677
+ for (const k of Object.keys(obj)) {
678
+ if (k.startsWith("x-")) {
679
+ ext[k] = obj[k];
680
+ }
681
+ }
682
+ }
683
+ return ext;
684
+ }
685
+
686
+ // src/parsers/protobuf/index.ts
687
+ var import_protobufjs = __toESM(require("protobufjs"), 1);
688
+ var ProtobufParser = class {
689
+ format = "protobuf";
690
+ canParse(raw) {
691
+ return raw.format === "protobuf" || raw.content.trim().startsWith('syntax = "proto');
692
+ }
693
+ async parse(raw) {
694
+ let root;
695
+ try {
696
+ const parsed = import_protobufjs.default.parse(raw.content, { keepCase: true });
697
+ root = parsed.root;
698
+ } catch (err) {
699
+ throw new ParseError("Failed to parse Protobuf spec", err.line, err.column, raw.sourcePath, err);
700
+ }
701
+ const meta = {
702
+ title: "Protobuf API",
703
+ version: "1.0.0",
704
+ // Protobuf doesn't have a standard version field
705
+ format: "protobuf",
706
+ rawVersion: "3"
707
+ // Assume proto3 for now
708
+ };
709
+ const components = {
710
+ schemas: {},
711
+ securitySchemes: {},
712
+ parameters: {},
713
+ responses: {},
714
+ headers: {},
715
+ requestBodies: {}
716
+ };
717
+ const endpoints = [];
718
+ this.walkRoot(root, "", components, endpoints);
719
+ return {
720
+ meta,
721
+ servers: [],
722
+ endpoints,
723
+ components,
724
+ security: []
725
+ };
726
+ }
727
+ walkRoot(node, namespace, components, endpoints) {
728
+ if (node instanceof import_protobufjs.default.Type) {
729
+ components.schemas[`${namespace}${node.name}`] = this.messageToSchema(node);
730
+ } else if (node instanceof import_protobufjs.default.Enum) {
731
+ components.schemas[`${namespace}${node.name}`] = this.enumToSchema(node);
732
+ } else if (node instanceof import_protobufjs.default.Service) {
733
+ for (const method of node.methodsArray) {
734
+ endpoints.push(this.methodToEndpoint(method, `${namespace}${node.name}`));
735
+ }
736
+ } else if (node instanceof import_protobufjs.default.Namespace) {
737
+ const newNamespace = node.name ? `${namespace}${node.name}.` : namespace;
738
+ for (const child of node.nestedArray) {
739
+ this.walkRoot(child, newNamespace, components, endpoints);
740
+ }
741
+ }
742
+ }
743
+ messageToSchema(message) {
744
+ const properties = {};
745
+ const required = [];
746
+ for (const field of message.fieldsArray) {
747
+ const fieldSchema = this.typeToSchema(field);
748
+ fieldSchema.$protobufNumber = field.id;
749
+ properties[field.name] = fieldSchema;
750
+ if (field.required) {
751
+ required.push(field.name);
752
+ }
753
+ }
754
+ const schema = {
755
+ type: "object",
756
+ properties
757
+ };
758
+ if (required.length > 0) {
759
+ schema.required = required;
760
+ }
761
+ return schema;
762
+ }
763
+ enumToSchema(enumObj) {
764
+ return {
765
+ type: "string",
766
+ enum: Object.keys(enumObj.values)
767
+ };
768
+ }
769
+ typeToSchema(field) {
770
+ let schema = {};
771
+ switch (field.type) {
772
+ case "double":
773
+ case "float":
774
+ schema.type = "number";
775
+ schema.format = field.type;
776
+ break;
777
+ case "int32":
778
+ case "uint32":
779
+ case "sint32":
780
+ case "fixed32":
781
+ case "sfixed32":
782
+ schema.type = "integer";
783
+ schema.format = field.type;
784
+ break;
785
+ case "int64":
786
+ case "uint64":
787
+ case "sint64":
788
+ case "fixed64":
789
+ case "sfixed64":
790
+ schema.type = "integer";
791
+ schema.format = "int64";
792
+ break;
793
+ case "bool":
794
+ schema.type = "boolean";
795
+ break;
796
+ case "string":
797
+ schema.type = "string";
798
+ break;
799
+ case "bytes":
800
+ schema.type = "string";
801
+ schema.format = "byte";
802
+ break;
803
+ default:
804
+ schema.type = "object";
805
+ schema.$ref = `#/components/schemas/${field.type}`;
806
+ break;
807
+ }
808
+ if (field.repeated) {
809
+ return {
810
+ type: "array",
811
+ items: schema
812
+ };
813
+ }
814
+ return schema;
815
+ }
816
+ methodToEndpoint(method, servicePath) {
817
+ const id = `RPC:${servicePath}/${method.name}`;
818
+ const path = `${servicePath}/${method.name}`;
819
+ const requestBody = {
820
+ required: true,
821
+ content: {
822
+ "application/protobuf": {
823
+ schema: {
824
+ $ref: `#/components/schemas/${method.requestType}`
825
+ }
826
+ }
827
+ }
828
+ };
829
+ const responses = [
830
+ {
831
+ statusCode: "200",
832
+ description: "Successful response",
833
+ content: {
834
+ "application/protobuf": {
835
+ schema: {
836
+ $ref: `#/components/schemas/${method.responseType}`
837
+ }
838
+ }
839
+ },
840
+ headers: {}
841
+ }
842
+ ];
843
+ return {
844
+ id,
845
+ path,
846
+ method: "RPC",
847
+ summary: method.comment || void 0,
848
+ tags: [servicePath],
849
+ deprecated: !!method.options?.deprecated,
850
+ security: [],
851
+ // Protobuf doesn't specify security natively
852
+ parameters: [],
853
+ requestBody,
854
+ responses,
855
+ extensions: {}
856
+ };
857
+ }
858
+ };
859
+
860
+ // src/parsers/graphql/index.ts
861
+ var import_graphql = require("graphql");
862
+ var GraphqlParser = class {
863
+ format = "graphql";
864
+ canParse(raw) {
865
+ return raw.format === "graphql" || raw.content.includes("type Query") || raw.content.includes("type Mutation");
866
+ }
867
+ async parse(raw) {
868
+ let ast;
869
+ try {
870
+ ast = (0, import_graphql.parse)(raw.content);
871
+ } catch (err) {
872
+ throw new ParseError("Failed to parse GraphQL spec", err.locations?.[0]?.line, err.locations?.[0]?.column, raw.sourcePath, err);
873
+ }
874
+ const meta = {
875
+ title: "GraphQL API",
876
+ version: "1.0.0",
877
+ format: "graphql",
878
+ rawVersion: "1"
879
+ };
880
+ const components = {
881
+ schemas: {},
882
+ securitySchemes: {},
883
+ parameters: {},
884
+ responses: {},
885
+ headers: {},
886
+ requestBodies: {}
887
+ };
888
+ const endpoints = [];
889
+ for (const def of ast.definitions) {
890
+ if (def.kind === "ObjectTypeDefinition" || def.kind === "InputObjectTypeDefinition" || def.kind === "InterfaceTypeDefinition") {
891
+ components.schemas[def.name.value] = this.typeDefToSchema(def);
892
+ } else if (def.kind === "EnumTypeDefinition") {
893
+ components.schemas[def.name.value] = this.enumDefToSchema(def);
894
+ }
895
+ }
896
+ for (const def of ast.definitions) {
897
+ if (def.kind === "ObjectTypeDefinition" && ["Query", "Mutation", "Subscription"].includes(def.name.value)) {
898
+ const typeName = def.name.value;
899
+ if (def.fields) {
900
+ for (const field of def.fields) {
901
+ endpoints.push(this.fieldToEndpoint(field, typeName));
902
+ }
903
+ }
904
+ }
905
+ }
906
+ return {
907
+ meta,
908
+ servers: [],
909
+ endpoints,
910
+ components,
911
+ security: []
912
+ };
913
+ }
914
+ typeDefToSchema(def) {
915
+ const properties = {};
916
+ const required = [];
917
+ if (def.fields) {
918
+ for (const field of def.fields) {
919
+ const fieldName = field.name.value;
920
+ properties[fieldName] = this.resolveTypeNode(field.type);
921
+ if (field.type.kind === "NonNullType") {
922
+ required.push(fieldName);
923
+ }
924
+ }
925
+ }
926
+ const schema = {
927
+ type: "object",
928
+ properties
929
+ };
930
+ if (required.length > 0) {
931
+ schema.required = required;
932
+ }
933
+ return schema;
934
+ }
935
+ enumDefToSchema(def) {
936
+ const values = def.values?.map((v) => v.name.value) || [];
937
+ return {
938
+ type: "string",
939
+ enum: values
940
+ };
941
+ }
942
+ resolveTypeNode(typeNode) {
943
+ if (typeNode.kind === "NonNullType") {
944
+ return this.resolveTypeNode(typeNode.type);
945
+ }
946
+ if (typeNode.kind === "ListType") {
947
+ return {
948
+ type: "array",
949
+ items: this.resolveTypeNode(typeNode.type)
950
+ };
951
+ }
952
+ const typeName = typeNode.name.value;
953
+ switch (typeName) {
954
+ case "Int":
955
+ return { type: "integer" };
956
+ case "Float":
957
+ return { type: "number" };
958
+ case "String":
959
+ case "ID":
960
+ return { type: "string" };
961
+ case "Boolean":
962
+ return { type: "boolean" };
963
+ default:
964
+ return {
965
+ type: "object",
966
+ $ref: `#/components/schemas/${typeName}`
967
+ };
968
+ }
969
+ }
970
+ fieldToEndpoint(field, parentType) {
971
+ const path = `${parentType}.${field.name.value}`;
972
+ const id = `POST:${path.toLowerCase()}`;
973
+ const parameters = [];
974
+ if (field.arguments) {
975
+ for (const arg of field.arguments) {
976
+ parameters.push({
977
+ name: arg.name.value,
978
+ in: "query",
979
+ // Map GraphQL args to 'query'
980
+ required: arg.type.kind === "NonNullType",
981
+ deprecated: !!arg.directives?.some((d) => d.name.value === "deprecated"),
982
+ description: arg.description?.value,
983
+ schema: this.resolveTypeNode(arg.type)
984
+ });
985
+ }
986
+ }
987
+ const responses = [
988
+ {
989
+ statusCode: "200",
990
+ description: "Successful response",
991
+ content: {
992
+ "application/graphql": {
993
+ schema: this.resolveTypeNode(field.type)
994
+ }
995
+ },
996
+ headers: {}
997
+ }
998
+ ];
999
+ return {
1000
+ id,
1001
+ path,
1002
+ method: "POST",
1003
+ summary: field.description?.value,
1004
+ tags: [parentType],
1005
+ deprecated: !!field.directives?.some((d) => d.name.value === "deprecated"),
1006
+ security: [],
1007
+ parameters,
1008
+ responses,
1009
+ extensions: {}
1010
+ };
1011
+ }
1012
+ };
1013
+
1014
+ // src/parsers/index.ts
1015
+ var parsers = [
1016
+ new OpenApi3Parser(),
1017
+ new OpenApi2Parser(),
1018
+ new ProtobufParser(),
1019
+ new GraphqlParser()
1020
+ ];
1021
+ function detectFormat(raw) {
1022
+ if (raw.format) return raw.format;
1023
+ const content = raw.content.trim();
1024
+ if (content.startsWith('syntax = "proto')) return "protobuf";
1025
+ if (content.includes("type Query") || content.includes("type Mutation")) return "graphql";
1026
+ let parsed;
1027
+ try {
1028
+ parsed = JSON.parse(content);
1029
+ } catch {
1030
+ try {
1031
+ parsed = import_js_yaml4.default.load(content);
1032
+ } catch {
1033
+ }
1034
+ }
1035
+ if (parsed && typeof parsed === "object") {
1036
+ if (parsed.openapi && typeof parsed.openapi === "string" && parsed.openapi.startsWith("3.")) {
1037
+ return "openapi3";
1038
+ }
1039
+ if (parsed.swagger && typeof parsed.swagger === "string" && parsed.swagger.startsWith("2.")) {
1040
+ return "openapi2";
1041
+ }
1042
+ }
1043
+ throw new FormatError("Could not detect format. Use --format openapi3|openapi2|protobuf|graphql");
1044
+ }
1045
+ async function parseSpec(raw) {
1046
+ const format = detectFormat(raw);
1047
+ const parser = parsers.find((p) => p.format === format);
1048
+ if (!parser) {
1049
+ throw new FormatError(`Parser for format '${format}' not implemented yet.`);
1050
+ }
1051
+ return parser.parse(raw);
1052
+ }
1053
+
1054
+ // src/diff/schema-differ.ts
1055
+ function diffSchema(oldSchema, newSchema, path, changes) {
1056
+ if (!oldSchema && !newSchema) return;
1057
+ if (!oldSchema && newSchema) {
1058
+ changes.push({ fieldPath: path, changeType: "added", newValue: newSchema });
1059
+ return;
1060
+ }
1061
+ if (oldSchema && !newSchema) {
1062
+ changes.push({ fieldPath: path, changeType: "removed", oldValue: oldSchema });
1063
+ return;
1064
+ }
1065
+ if (oldSchema && newSchema) {
1066
+ if (oldSchema.type !== newSchema.type) {
1067
+ const oldTypes = Array.isArray(oldSchema.type) ? oldSchema.type : [oldSchema.type].filter(Boolean);
1068
+ const newTypes = Array.isArray(newSchema.type) ? newSchema.type : [newSchema.type].filter(Boolean);
1069
+ const oldTypeStr = oldTypes.sort().join(",");
1070
+ const newTypeStr = newTypes.sort().join(",");
1071
+ if (oldTypeStr !== newTypeStr) {
1072
+ changes.push({ fieldPath: [...path, "type"], changeType: "changed", oldValue: oldSchema.type, newValue: newSchema.type });
1073
+ }
1074
+ }
1075
+ if (oldSchema.nullable !== newSchema.nullable) {
1076
+ changes.push({ fieldPath: [...path, "nullable"], changeType: "changed", oldValue: oldSchema.nullable, newValue: newSchema.nullable });
1077
+ }
1078
+ const oldReq = new Set(oldSchema.required || []);
1079
+ const newReq = new Set(newSchema.required || []);
1080
+ for (const req of newReq) {
1081
+ if (!oldReq.has(req)) {
1082
+ changes.push({ fieldPath: [...path, "required", req], changeType: "added", newValue: req });
1083
+ }
1084
+ }
1085
+ for (const req of oldReq) {
1086
+ if (!newReq.has(req)) {
1087
+ changes.push({ fieldPath: [...path, "required", req], changeType: "removed", oldValue: req });
1088
+ }
1089
+ }
1090
+ const oldEnum = new Set(oldSchema.enum || []);
1091
+ const newEnum = new Set(newSchema.enum || []);
1092
+ for (const val of newEnum) {
1093
+ if (!oldEnum.has(val)) {
1094
+ changes.push({ fieldPath: [...path, "enum", String(val)], changeType: "added", newValue: val });
1095
+ }
1096
+ }
1097
+ for (const val of oldEnum) {
1098
+ if (!newEnum.has(val)) {
1099
+ changes.push({ fieldPath: [...path, "enum", String(val)], changeType: "removed", oldValue: val });
1100
+ }
1101
+ }
1102
+ const oldProps = oldSchema.properties || {};
1103
+ const newProps = newSchema.properties || {};
1104
+ const allProps = /* @__PURE__ */ new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
1105
+ for (const prop of allProps) {
1106
+ diffSchema(oldProps[prop], newProps[prop], [...path, "properties", prop], changes);
1107
+ }
1108
+ if (oldSchema.items || newSchema.items) {
1109
+ diffSchema(oldSchema.items, newSchema.items, [...path, "items"], changes);
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ // src/diff/auth-differ.ts
1115
+ function diffSecuritySchemes(oldSchemes, newSchemes) {
1116
+ const diffs = [];
1117
+ const oldMap = new Map(oldSchemes.map((s) => [s.id, s]));
1118
+ const newMap = new Map(newSchemes.map((s) => [s.id, s]));
1119
+ const allIds = /* @__PURE__ */ new Set([...oldMap.keys(), ...newMap.keys()]);
1120
+ for (const id of allIds) {
1121
+ const oldScheme = oldMap.get(id);
1122
+ const newScheme = newMap.get(id);
1123
+ if (oldScheme && !newScheme) {
1124
+ diffs.push({ schemeId: id, changeType: "removed", fieldChanges: [] });
1125
+ } else if (!oldScheme && newScheme) {
1126
+ diffs.push({ schemeId: id, changeType: "added", fieldChanges: [] });
1127
+ } else if (oldScheme && newScheme) {
1128
+ const fieldChanges = [];
1129
+ if (oldScheme.type !== newScheme.type) {
1130
+ fieldChanges.push({ fieldPath: ["type"], changeType: "changed", oldValue: oldScheme.type, newValue: newScheme.type });
1131
+ }
1132
+ if (oldScheme.scheme !== newScheme.scheme) {
1133
+ fieldChanges.push({ fieldPath: ["scheme"], changeType: "changed", oldValue: oldScheme.scheme, newValue: newScheme.scheme });
1134
+ }
1135
+ if (oldScheme.in !== newScheme.in) {
1136
+ fieldChanges.push({ fieldPath: ["in"], changeType: "changed", oldValue: oldScheme.in, newValue: newScheme.in });
1137
+ }
1138
+ if (fieldChanges.length > 0) {
1139
+ diffs.push({ schemeId: id, changeType: "changed", fieldChanges });
1140
+ }
1141
+ }
1142
+ }
1143
+ return diffs;
1144
+ }
1145
+ function diffSecurityRequirements(oldReqs, newReqs, path, changes) {
1146
+ const oldSet = new Set(oldReqs.map((r) => r.schemeId));
1147
+ const newSet = new Set(newReqs.map((r) => r.schemeId));
1148
+ for (const schemeId of newSet) {
1149
+ if (!oldSet.has(schemeId)) {
1150
+ changes.push({ fieldPath: [...path, schemeId], changeType: "added", newValue: schemeId });
1151
+ }
1152
+ }
1153
+ for (const schemeId of oldSet) {
1154
+ if (!newSet.has(schemeId)) {
1155
+ changes.push({ fieldPath: [...path, schemeId], changeType: "removed", oldValue: schemeId });
1156
+ }
1157
+ }
1158
+ const oldMap = new Map(oldReqs.map((r) => [r.schemeId, new Set(r.scopes)]));
1159
+ const newMap = new Map(newReqs.map((r) => [r.schemeId, new Set(r.scopes)]));
1160
+ for (const [schemeId, oldScopes] of oldMap.entries()) {
1161
+ const newScopes = newMap.get(schemeId);
1162
+ if (newScopes) {
1163
+ for (const scope of newScopes) {
1164
+ if (!oldScopes.has(scope)) {
1165
+ changes.push({ fieldPath: [...path, schemeId, "scopes", scope], changeType: "added", newValue: scope });
1166
+ }
1167
+ }
1168
+ for (const scope of oldScopes) {
1169
+ if (!newScopes.has(scope)) {
1170
+ changes.push({ fieldPath: [...path, schemeId, "scopes", scope], changeType: "removed", oldValue: scope });
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ // src/diff/endpoint-differ.ts
1178
+ function diffEndpoints(oldEndpoints, newEndpoints) {
1179
+ const diffs = [];
1180
+ const oldMap = new Map(oldEndpoints.map((e) => [e.id, e]));
1181
+ const newMap = new Map(newEndpoints.map((e) => [e.id, e]));
1182
+ const allIds = /* @__PURE__ */ new Set([...oldMap.keys(), ...newMap.keys()]);
1183
+ for (const id of allIds) {
1184
+ const oldE = oldMap.get(id);
1185
+ const newE = newMap.get(id);
1186
+ if (oldE && !newE) {
1187
+ diffs.push({ type: "removed", endpointId: id, path: oldE.path, method: oldE.method, oldEndpoint: oldE, fieldChanges: [] });
1188
+ } else if (!oldE && newE) {
1189
+ diffs.push({ type: "added", endpointId: id, path: newE.path, method: newE.method, newEndpoint: newE, fieldChanges: [] });
1190
+ } else if (oldE && newE) {
1191
+ const fieldChanges = [];
1192
+ if (oldE.method !== newE.method) {
1193
+ fieldChanges.push({ fieldPath: ["method"], changeType: "changed", oldValue: oldE.method, newValue: newE.method });
1194
+ }
1195
+ if (oldE.path !== newE.path) {
1196
+ fieldChanges.push({ fieldPath: ["path"], changeType: "changed", oldValue: oldE.path, newValue: newE.path });
1197
+ }
1198
+ if (!!oldE.deprecated !== !!newE.deprecated) {
1199
+ fieldChanges.push({ fieldPath: ["deprecated"], changeType: "changed", oldValue: !!oldE.deprecated, newValue: !!newE.deprecated });
1200
+ }
1201
+ diffSecurityRequirements(oldE.security, newE.security, ["security"], fieldChanges);
1202
+ diffParameters(oldE.parameters, newE.parameters, ["parameters"], fieldChanges);
1203
+ diffRequestBody(oldE.requestBody, newE.requestBody, ["requestBody"], fieldChanges);
1204
+ diffResponses(oldE.responses, newE.responses, ["responses"], fieldChanges);
1205
+ if (fieldChanges.length > 0) {
1206
+ diffs.push({ type: "changed", endpointId: id, path: newE.path, method: newE.method, oldEndpoint: oldE, newEndpoint: newE, fieldChanges });
1207
+ }
1208
+ }
1209
+ }
1210
+ return diffs;
1211
+ }
1212
+ function diffParameters(oldParams, newParams, path, changes) {
1213
+ const oldMap = new Map(oldParams.map((p) => [`${p.name}:${p.in}`, p]));
1214
+ const newMap = new Map(newParams.map((p) => [`${p.name}:${p.in}`, p]));
1215
+ const allIds = /* @__PURE__ */ new Set([...oldMap.keys(), ...newMap.keys()]);
1216
+ for (const id of allIds) {
1217
+ const o = oldMap.get(id);
1218
+ const n = newMap.get(id);
1219
+ if (o && !n) {
1220
+ changes.push({ fieldPath: [...path, id], changeType: "removed", oldValue: o });
1221
+ } else if (!o && n) {
1222
+ changes.push({ fieldPath: [...path, id], changeType: "added", newValue: n });
1223
+ } else if (o && n) {
1224
+ if (!!o.required !== !!n.required) {
1225
+ changes.push({ fieldPath: [...path, id, "required"], changeType: "changed", oldValue: !!o.required, newValue: !!n.required });
1226
+ }
1227
+ if (!!o.deprecated !== !!n.deprecated) {
1228
+ changes.push({ fieldPath: [...path, id, "deprecated"], changeType: "changed", oldValue: !!o.deprecated, newValue: !!n.deprecated });
1229
+ }
1230
+ diffSchema(o.schema, n.schema, [...path, id, "schema"], changes);
1231
+ }
1232
+ }
1233
+ }
1234
+ function diffRequestBody(oldReq, newReq, path, changes) {
1235
+ if (!oldReq && !newReq) return;
1236
+ if (oldReq && !newReq) {
1237
+ changes.push({ fieldPath: path, changeType: "removed", oldValue: oldReq });
1238
+ return;
1239
+ }
1240
+ if (!oldReq && newReq) {
1241
+ changes.push({ fieldPath: path, changeType: "added", newValue: newReq });
1242
+ return;
1243
+ }
1244
+ if (oldReq && newReq) {
1245
+ if (!!oldReq.required !== !!newReq.required) {
1246
+ changes.push({ fieldPath: [...path, "required"], changeType: "changed", oldValue: !!oldReq.required, newValue: !!newReq.required });
1247
+ }
1248
+ const oldMedia = new Set(Object.keys(oldReq.content));
1249
+ const newMedia = new Set(Object.keys(newReq.content));
1250
+ for (const m of newMedia) {
1251
+ if (!oldMedia.has(m)) changes.push({ fieldPath: [...path, "content", m], changeType: "added", newValue: newReq.content[m] });
1252
+ }
1253
+ for (const m of oldMedia) {
1254
+ if (!newMedia.has(m)) {
1255
+ changes.push({ fieldPath: [...path, "content", m], changeType: "removed", oldValue: oldReq.content[m] });
1256
+ } else {
1257
+ diffSchema(oldReq.content[m].schema, newReq.content[m].schema, [...path, "content", m, "schema"], changes);
1258
+ }
1259
+ }
1260
+ }
1261
+ }
1262
+ function diffResponses(oldRes, newRes, path, changes) {
1263
+ const oldMap = new Map(oldRes.map((r) => [r.statusCode, r]));
1264
+ const newMap = new Map(newRes.map((r) => [r.statusCode, r]));
1265
+ const allCodes = /* @__PURE__ */ new Set([...oldMap.keys(), ...newMap.keys()]);
1266
+ for (const code of allCodes) {
1267
+ const o = oldMap.get(code);
1268
+ const n = newMap.get(code);
1269
+ if (o && !n) {
1270
+ changes.push({ fieldPath: [...path, code], changeType: "removed", oldValue: o });
1271
+ } else if (!o && n) {
1272
+ changes.push({ fieldPath: [...path, code], changeType: "added", newValue: n });
1273
+ } else if (o && n) {
1274
+ const oldMedia = new Set(Object.keys(o.content || {}));
1275
+ const newMedia = new Set(Object.keys(n.content || {}));
1276
+ for (const m of newMedia) {
1277
+ if (!oldMedia.has(m)) changes.push({ fieldPath: [...path, code, "content", m], changeType: "added", newValue: n.content[m] });
1278
+ }
1279
+ for (const m of oldMedia) {
1280
+ if (!newMedia.has(m)) {
1281
+ changes.push({ fieldPath: [...path, code, "content", m], changeType: "removed", oldValue: o.content[m] });
1282
+ } else {
1283
+ diffSchema(o.content[m].schema, n.content[m].schema, [...path, code, "content", m, "schema"], changes);
1284
+ }
1285
+ }
1286
+ const oldHeaders = new Set(Object.keys(o.headers || {}));
1287
+ const newHeaders = new Set(Object.keys(n.headers || {}));
1288
+ for (const h of newHeaders) {
1289
+ if (!oldHeaders.has(h)) changes.push({ fieldPath: [...path, code, "headers", h], changeType: "added", newValue: n.headers[h] });
1290
+ }
1291
+ for (const h of oldHeaders) {
1292
+ if (!newHeaders.has(h)) {
1293
+ changes.push({ fieldPath: [...path, code, "headers", h], changeType: "removed", oldValue: o.headers[h] });
1294
+ } else {
1295
+ if (!!o.headers[h].required !== !!n.headers[h].required) {
1296
+ changes.push({ fieldPath: [...path, code, "headers", h, "required"], changeType: "changed", oldValue: !!o.headers[h].required, newValue: !!n.headers[h].required });
1297
+ }
1298
+ diffSchema(o.headers[h].schema, n.headers[h].schema, [...path, code, "headers", h, "schema"], changes);
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+ }
1304
+
1305
+ // src/diff/server-differ.ts
1306
+ function diffServers(oldServers, newServers) {
1307
+ const diffs = [];
1308
+ const oldUrls = new Set(oldServers.map((s) => s.url));
1309
+ const newUrls = new Set(newServers.map((s) => s.url));
1310
+ for (const server of oldServers) {
1311
+ if (!newUrls.has(server.url)) {
1312
+ diffs.push({ changeType: "removed", oldServer: server });
1313
+ }
1314
+ }
1315
+ for (const server of newServers) {
1316
+ if (!oldUrls.has(server.url)) {
1317
+ diffs.push({ changeType: "added", newServer: server });
1318
+ }
1319
+ }
1320
+ return diffs;
1321
+ }
1322
+
1323
+ // src/diff/index.ts
1324
+ function computeDiff(oldAST, newAST) {
1325
+ return {
1326
+ endpointDiffs: diffEndpoints(oldAST.endpoints, newAST.endpoints),
1327
+ schemaDiffs: [],
1328
+ securityDiffs: diffSecuritySchemes(oldAST.security, newAST.security),
1329
+ serverDiffs: diffServers(oldAST.servers, newAST.servers)
1330
+ };
1331
+ }
1332
+
1333
+ // src/rules/base.ts
1334
+ var BaseRule = class {
1335
+ isIgnored(path, context) {
1336
+ for (const glob of context.config.ignorePaths) {
1337
+ if (this.matchGlob(path, glob)) return true;
1338
+ }
1339
+ return false;
1340
+ }
1341
+ matchGlob(path, glob) {
1342
+ const regex = glob.replace(/\*/g, ".*").replace(/\//g, "\\/");
1343
+ return new RegExp(`^${regex}$`).test(path);
1344
+ }
1345
+ makeChange(details, context) {
1346
+ const { ruleId, ...rest } = details;
1347
+ return {
1348
+ ruleId: ruleId || this.id,
1349
+ ...rest
1350
+ };
1351
+ }
1352
+ };
1353
+
1354
+ // src/rules/auth/security-added.ts
1355
+ var SecurityAddedRule = class extends BaseRule {
1356
+ id = "SECURITY_ADDED";
1357
+ description = "Authentication added to previously public endpoint";
1358
+ severity = "breaking";
1359
+ apply(diff, context) {
1360
+ const changes = [];
1361
+ for (const ed of diff.endpointDiffs) {
1362
+ if (ed.type !== "changed") continue;
1363
+ if (this.isIgnored(ed.path, context)) continue;
1364
+ const oldSec = ed.oldEndpoint?.security ?? [];
1365
+ const newSec = ed.newEndpoint?.security ?? [];
1366
+ if (oldSec.length === 0 && newSec.length > 0) {
1367
+ const schemes = newSec.map((s) => s.schemeId).join(", ");
1368
+ changes.push(this.makeChange({
1369
+ severity: "breaking",
1370
+ category: "authentication",
1371
+ message: `${ed.method} ${ed.path} now requires authentication (${schemes}).`,
1372
+ consequence: "Clients without credentials receive 401 Unauthorized.",
1373
+ migration: `Add Authorization header with required credentials to all ${ed.method} ${ed.path} requests.`,
1374
+ location: { path: ed.path, method: ed.method }
1375
+ }, context));
1376
+ }
1377
+ }
1378
+ return changes;
1379
+ }
1380
+ };
1381
+
1382
+ // src/rules/endpoint/endpoint-removed.ts
1383
+ var EndpointRemovedRule = class extends BaseRule {
1384
+ id = "ENDPOINT_REMOVED";
1385
+ description = "An existing endpoint was removed entirely";
1386
+ severity = "breaking";
1387
+ apply(diff, context) {
1388
+ const changes = [];
1389
+ for (const ed of diff.endpointDiffs) {
1390
+ if (ed.type !== "removed") continue;
1391
+ if (this.isIgnored(ed.path, context)) continue;
1392
+ changes.push(this.makeChange({
1393
+ severity: "breaking",
1394
+ category: "endpoint",
1395
+ message: `Endpoint ${ed.method} ${ed.path} was removed.`,
1396
+ consequence: "Clients calling this endpoint will receive a 404 Not Found error.",
1397
+ migration: "Migrate clients to use an alternative endpoint before removing this one.",
1398
+ location: { path: ed.path, method: ed.method }
1399
+ }, context));
1400
+ }
1401
+ return changes;
1402
+ }
1403
+ };
1404
+
1405
+ // src/rules/endpoint/endpoint-added.ts
1406
+ var EndpointAddedRule = class extends BaseRule {
1407
+ id = "ENDPOINT_ADDED";
1408
+ description = "A new endpoint was added";
1409
+ severity = "info";
1410
+ apply(diff, context) {
1411
+ const changes = [];
1412
+ for (const ed of diff.endpointDiffs) {
1413
+ if (ed.type !== "added") continue;
1414
+ if (this.isIgnored(ed.path, context)) continue;
1415
+ changes.push(this.makeChange({
1416
+ severity: "info",
1417
+ category: "endpoint",
1418
+ message: `Endpoint ${ed.method} ${ed.path} was added.`,
1419
+ consequence: "Clients can now use this new endpoint.",
1420
+ migration: "No migration required.",
1421
+ location: { path: ed.path, method: ed.method }
1422
+ }, context));
1423
+ }
1424
+ return changes;
1425
+ }
1426
+ };
1427
+
1428
+ // src/rules/endpoint/endpoint-deprecated.ts
1429
+ var EndpointDeprecatedRule = class extends BaseRule {
1430
+ id = "ENDPOINT_DEPRECATED";
1431
+ description = "An existing endpoint was marked as deprecated";
1432
+ severity = "warning";
1433
+ apply(diff, context) {
1434
+ const changes = [];
1435
+ for (const ed of diff.endpointDiffs) {
1436
+ if (ed.type !== "changed") continue;
1437
+ if (this.isIgnored(ed.path, context)) continue;
1438
+ const deprecatedChange = ed.fieldChanges.find((c) => c.fieldPath[0] === "deprecated" && c.newValue === true && c.oldValue === false);
1439
+ if (deprecatedChange) {
1440
+ changes.push(this.makeChange({
1441
+ severity: "warning",
1442
+ category: "endpoint",
1443
+ message: `Endpoint ${ed.method} ${ed.path} was deprecated.`,
1444
+ consequence: "This endpoint is slated for future removal.",
1445
+ migration: "Plan to migrate away from this endpoint to its recommended alternative.",
1446
+ location: { path: ed.path, method: ed.method }
1447
+ }, context));
1448
+ }
1449
+ }
1450
+ return changes;
1451
+ }
1452
+ };
1453
+
1454
+ // src/rules/endpoint/http-method-changed.ts
1455
+ var HttpMethodChangedRule = class extends BaseRule {
1456
+ id = "HTTP_METHOD_CHANGED";
1457
+ description = "The HTTP method for an endpoint changed";
1458
+ severity = "breaking";
1459
+ apply(diff, context) {
1460
+ const changes = [];
1461
+ for (const ed of diff.endpointDiffs) {
1462
+ if (ed.type !== "changed") continue;
1463
+ if (this.isIgnored(ed.path, context)) continue;
1464
+ const methodChange = ed.fieldChanges.find((c) => c.fieldPath[0] === "method");
1465
+ if (methodChange) {
1466
+ changes.push(this.makeChange({
1467
+ severity: "breaking",
1468
+ category: "endpoint",
1469
+ message: `Endpoint HTTP method changed from ${methodChange.oldValue} to ${methodChange.newValue}.`,
1470
+ consequence: "Clients using the old HTTP method will fail.",
1471
+ migration: `Update client requests to use the ${methodChange.newValue} method.`,
1472
+ location: { path: ed.path, method: ed.method }
1473
+ }, context));
1474
+ }
1475
+ }
1476
+ return changes;
1477
+ }
1478
+ };
1479
+
1480
+ // src/rules/endpoint/path-changed.ts
1481
+ var PathChangedRule = class extends BaseRule {
1482
+ id = "PATH_CHANGED";
1483
+ description = "The path string for an endpoint changed (e.g. parameter renamed)";
1484
+ severity = "info";
1485
+ apply(diff, context) {
1486
+ const changes = [];
1487
+ for (const ed of diff.endpointDiffs) {
1488
+ if (ed.type !== "changed") continue;
1489
+ if (this.isIgnored(ed.path, context)) continue;
1490
+ const pathChange = ed.fieldChanges.find((c) => c.fieldPath[0] === "path");
1491
+ if (pathChange) {
1492
+ changes.push(this.makeChange({
1493
+ severity: "info",
1494
+ category: "endpoint",
1495
+ message: `Endpoint path changed from ${pathChange.oldValue} to ${pathChange.newValue}.`,
1496
+ consequence: "Clients may need to update routing or parameter names.",
1497
+ migration: "Review the path syntax changes.",
1498
+ location: { path: ed.path, method: ed.method }
1499
+ }, context));
1500
+ }
1501
+ }
1502
+ return changes;
1503
+ }
1504
+ };
1505
+
1506
+ // src/rules/response/response-field-removed.ts
1507
+ var ResponseFieldRemovedRule = class extends BaseRule {
1508
+ id = "RESPONSE_FIELD_REMOVED";
1509
+ description = "A field was removed from a response payload";
1510
+ severity = "breaking";
1511
+ apply(diff, context) {
1512
+ const changes = [];
1513
+ for (const ed of diff.endpointDiffs) {
1514
+ if (ed.type !== "changed") continue;
1515
+ if (this.isIgnored(ed.path, context)) continue;
1516
+ for (const fc of ed.fieldChanges) {
1517
+ if (fc.fieldPath[0] === "responses" && fc.changeType === "removed") {
1518
+ if (fc.fieldPath.includes("properties")) {
1519
+ const fieldName = fc.fieldPath[fc.fieldPath.length - 1];
1520
+ const statusCode = fc.fieldPath[1] || "unknown";
1521
+ changes.push(this.makeChange({
1522
+ severity: "breaking",
1523
+ category: "response",
1524
+ message: `Field '${fieldName}' was removed from the response.`,
1525
+ consequence: `Clients depending on '${fieldName}' will encounter missing data errors.`,
1526
+ migration: `Ensure clients no longer require '${fieldName}' before removing it.`,
1527
+ location: { path: ed.path, method: ed.method, field: fieldName, statusCode }
1528
+ }, context));
1529
+ }
1530
+ }
1531
+ }
1532
+ }
1533
+ return changes;
1534
+ }
1535
+ };
1536
+
1537
+ // src/rules/param/param-required-added.ts
1538
+ var ParamRequiredAddedRule = class extends BaseRule {
1539
+ id = "PARAM_REQUIRED_FALSE_TO_TRUE";
1540
+ description = "An optional parameter was made required";
1541
+ severity = "breaking";
1542
+ apply(diff, context) {
1543
+ const changes = [];
1544
+ for (const ed of diff.endpointDiffs) {
1545
+ if (ed.type !== "changed") continue;
1546
+ if (this.isIgnored(ed.path, context)) continue;
1547
+ for (const fc of ed.fieldChanges) {
1548
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath[2] === "required" && fc.changeType === "changed") {
1549
+ if (fc.oldValue === false && fc.newValue === true) {
1550
+ const paramId = fc.fieldPath[1];
1551
+ const [paramName, inLoc] = paramId.split(":");
1552
+ changes.push(this.makeChange({
1553
+ severity: "breaking",
1554
+ category: "parameter",
1555
+ message: `Optional parameter '${paramName}' in ${inLoc} is now required.`,
1556
+ consequence: `Requests missing '${paramName}' will be rejected with a 400 Bad Request error.`,
1557
+ migration: `Update all clients to include '${paramName}' in their requests.`,
1558
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1559
+ }, context));
1560
+ }
1561
+ }
1562
+ }
1563
+ }
1564
+ return changes;
1565
+ }
1566
+ };
1567
+
1568
+ // src/rules/param/param-removed.ts
1569
+ var ParamRemovedRule = class extends BaseRule {
1570
+ id = "PARAM_REMOVED";
1571
+ description = "A parameter was removed";
1572
+ severity = "breaking";
1573
+ apply(diff, context) {
1574
+ const changes = [];
1575
+ for (const ed of diff.endpointDiffs) {
1576
+ if (ed.type !== "changed") continue;
1577
+ if (this.isIgnored(ed.path, context)) continue;
1578
+ for (const fc of ed.fieldChanges) {
1579
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath.length === 2 && fc.changeType === "removed") {
1580
+ const paramId = fc.fieldPath[1];
1581
+ const [paramName, inLoc] = paramId.split(":");
1582
+ const isMoved = ed.fieldChanges.some((c) => c.fieldPath[0] === "parameters" && c.fieldPath.length === 2 && c.changeType === "added" && c.fieldPath[1].startsWith(`${paramName}:`));
1583
+ if (isMoved) continue;
1584
+ const oldParam = fc.oldValue;
1585
+ if (oldParam.required) {
1586
+ changes.push(this.makeChange({
1587
+ severity: "breaking",
1588
+ ruleId: "PARAM_REMOVED",
1589
+ category: "parameter",
1590
+ message: `Required parameter '${paramName}' in ${inLoc} was removed.`,
1591
+ consequence: "Clients sending this parameter may experience errors or unexpected behavior if the server rejects unknown parameters.",
1592
+ migration: `Update clients to stop sending the '${paramName}' parameter.`,
1593
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1594
+ }, context));
1595
+ } else {
1596
+ changes.push(this.makeChange({
1597
+ severity: "warning",
1598
+ ruleId: "PARAM_OPTIONAL_REMOVED",
1599
+ category: "parameter",
1600
+ message: `Optional parameter '${paramName}' in ${inLoc} was removed.`,
1601
+ consequence: "Clients sending this parameter will have it ignored, or may receive errors if the server strictly validates inputs.",
1602
+ migration: `Update clients to stop sending the '${paramName}' parameter.`,
1603
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1604
+ }, context));
1605
+ }
1606
+ }
1607
+ }
1608
+ }
1609
+ return changes;
1610
+ }
1611
+ };
1612
+
1613
+ // src/rules/param/param-added.ts
1614
+ var ParamAddedRule = class extends BaseRule {
1615
+ id = "PARAM_ADDED";
1616
+ description = "A new parameter was added";
1617
+ severity = "info";
1618
+ apply(diff, context) {
1619
+ const changes = [];
1620
+ for (const ed of diff.endpointDiffs) {
1621
+ if (ed.type !== "changed") continue;
1622
+ if (this.isIgnored(ed.path, context)) continue;
1623
+ for (const fc of ed.fieldChanges) {
1624
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath.length === 2 && fc.changeType === "added") {
1625
+ const paramId = fc.fieldPath[1];
1626
+ const [paramName, inLoc] = paramId.split(":");
1627
+ const isMoved = ed.fieldChanges.some((c) => c.fieldPath[0] === "parameters" && c.fieldPath.length === 2 && c.changeType === "removed" && c.fieldPath[1].startsWith(`${paramName}:`));
1628
+ if (isMoved) continue;
1629
+ const newParam = fc.newValue;
1630
+ if (newParam.required) {
1631
+ changes.push(this.makeChange({
1632
+ severity: "breaking",
1633
+ ruleId: "PARAM_ADDED_REQUIRED",
1634
+ category: "parameter",
1635
+ message: `Required parameter '${paramName}' in ${inLoc} was added.`,
1636
+ consequence: "Existing clients failing to send this new required parameter will receive 400 Bad Request errors.",
1637
+ migration: `Update clients to include the '${paramName}' parameter.`,
1638
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1639
+ }, context));
1640
+ } else {
1641
+ changes.push(this.makeChange({
1642
+ severity: "info",
1643
+ ruleId: "PARAM_ADDED",
1644
+ category: "parameter",
1645
+ message: `Optional parameter '${paramName}' in ${inLoc} was added.`,
1646
+ consequence: "Clients can optionally provide this new parameter for extended functionality.",
1647
+ migration: "No immediate action required.",
1648
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1649
+ }, context));
1650
+ }
1651
+ }
1652
+ }
1653
+ }
1654
+ return changes;
1655
+ }
1656
+ };
1657
+
1658
+ // src/rules/param/param-deprecated.ts
1659
+ var ParamDeprecatedRule = class extends BaseRule {
1660
+ id = "PARAM_DEPRECATED";
1661
+ description = "A parameter was marked as deprecated";
1662
+ severity = "warning";
1663
+ apply(diff, context) {
1664
+ const changes = [];
1665
+ for (const ed of diff.endpointDiffs) {
1666
+ if (ed.type !== "changed") continue;
1667
+ if (this.isIgnored(ed.path, context)) continue;
1668
+ for (const fc of ed.fieldChanges) {
1669
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath[2] === "deprecated" && fc.changeType === "changed") {
1670
+ if (fc.oldValue === false && fc.newValue === true) {
1671
+ const paramId = fc.fieldPath[1];
1672
+ const [paramName, inLoc] = paramId.split(":");
1673
+ changes.push(this.makeChange({
1674
+ severity: "warning",
1675
+ category: "parameter",
1676
+ message: `Parameter '${paramName}' in ${inLoc} was deprecated.`,
1677
+ consequence: "This parameter is slated for future removal.",
1678
+ migration: `Plan to migrate away from using '${paramName}'.`,
1679
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1680
+ }, context));
1681
+ }
1682
+ }
1683
+ }
1684
+ }
1685
+ return changes;
1686
+ }
1687
+ };
1688
+
1689
+ // src/rules/param/param-type-changed.ts
1690
+ var ParamTypeChangedRule = class extends BaseRule {
1691
+ id = "PARAM_TYPE_CHANGED";
1692
+ description = "The data type of a parameter changed";
1693
+ severity = "breaking";
1694
+ apply(diff, context) {
1695
+ const changes = [];
1696
+ for (const ed of diff.endpointDiffs) {
1697
+ if (ed.type !== "changed") continue;
1698
+ if (this.isIgnored(ed.path, context)) continue;
1699
+ for (const fc of ed.fieldChanges) {
1700
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath[2] === "schema" && fc.fieldPath[3] === "type" && fc.changeType === "changed") {
1701
+ const paramId = fc.fieldPath[1];
1702
+ const [paramName, inLoc] = paramId.split(":");
1703
+ changes.push(this.makeChange({
1704
+ severity: "breaking",
1705
+ category: "parameter",
1706
+ message: `Parameter '${paramName}' in ${inLoc} changed type from ${fc.oldValue} to ${fc.newValue}.`,
1707
+ consequence: "Clients sending the old type will receive validation errors.",
1708
+ migration: `Update clients to send the new type (${fc.newValue}) for '${paramName}'.`,
1709
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1710
+ }, context));
1711
+ }
1712
+ }
1713
+ }
1714
+ return changes;
1715
+ }
1716
+ };
1717
+
1718
+ // src/rules/param/param-location-changed.ts
1719
+ var ParamLocationChangedRule = class extends BaseRule {
1720
+ id = "PARAM_LOCATION_CHANGED";
1721
+ description = "A parameter was moved to a different location (e.g. query to header)";
1722
+ severity = "breaking";
1723
+ apply(diff, context) {
1724
+ const changes = [];
1725
+ for (const ed of diff.endpointDiffs) {
1726
+ if (ed.type !== "changed") continue;
1727
+ if (this.isIgnored(ed.path, context)) continue;
1728
+ const removedParams = /* @__PURE__ */ new Map();
1729
+ const addedParams = /* @__PURE__ */ new Map();
1730
+ for (const fc of ed.fieldChanges) {
1731
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath.length === 2) {
1732
+ const paramId = fc.fieldPath[1];
1733
+ const [paramName, inLoc] = paramId.split(":");
1734
+ if (fc.changeType === "removed") removedParams.set(paramName, inLoc);
1735
+ if (fc.changeType === "added") addedParams.set(paramName, inLoc);
1736
+ }
1737
+ }
1738
+ for (const [name, oldLoc] of removedParams.entries()) {
1739
+ const newLoc = addedParams.get(name);
1740
+ if (newLoc) {
1741
+ changes.push(this.makeChange({
1742
+ severity: "breaking",
1743
+ category: "parameter",
1744
+ message: `Parameter '${name}' moved from ${oldLoc} to ${newLoc}.`,
1745
+ consequence: `Clients sending '${name}' in the ${oldLoc} will fail.`,
1746
+ migration: `Update clients to send '${name}' in the ${newLoc} instead.`,
1747
+ location: { path: ed.path, method: ed.method, paramName: name, field: oldLoc }
1748
+ }, context));
1749
+ }
1750
+ }
1751
+ }
1752
+ return changes;
1753
+ }
1754
+ };
1755
+
1756
+ // src/rules/param/param-enum-value-removed.ts
1757
+ var ParamEnumValueRemovedRule = class extends BaseRule {
1758
+ id = "PARAM_ENUM_VALUE_REMOVED";
1759
+ description = "An allowed enum value was removed from a parameter";
1760
+ severity = "breaking";
1761
+ apply(diff, context) {
1762
+ const changes = [];
1763
+ for (const ed of diff.endpointDiffs) {
1764
+ if (ed.type !== "changed") continue;
1765
+ if (this.isIgnored(ed.path, context)) continue;
1766
+ for (const fc of ed.fieldChanges) {
1767
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath[2] === "schema" && fc.fieldPath.includes("enum") && fc.changeType === "removed") {
1768
+ const paramId = fc.fieldPath[1];
1769
+ const [paramName, inLoc] = paramId.split(":");
1770
+ changes.push(this.makeChange({
1771
+ severity: "breaking",
1772
+ category: "parameter",
1773
+ message: `Enum value '${fc.oldValue}' was removed from parameter '${paramName}' in ${inLoc}.`,
1774
+ consequence: `Clients sending '${fc.oldValue}' will now receive validation errors.`,
1775
+ migration: `Update clients to use one of the remaining allowed enum values for '${paramName}'.`,
1776
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1777
+ }, context));
1778
+ }
1779
+ }
1780
+ }
1781
+ return changes;
1782
+ }
1783
+ };
1784
+
1785
+ // src/rules/param/param-enum-value-added.ts
1786
+ var ParamEnumValueAddedRule = class extends BaseRule {
1787
+ id = "PARAM_ENUM_VALUE_ADDED";
1788
+ description = "A new allowed enum value was added to a parameter";
1789
+ severity = "info";
1790
+ apply(diff, context) {
1791
+ const changes = [];
1792
+ for (const ed of diff.endpointDiffs) {
1793
+ if (ed.type !== "changed") continue;
1794
+ if (this.isIgnored(ed.path, context)) continue;
1795
+ for (const fc of ed.fieldChanges) {
1796
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath[2] === "schema" && fc.fieldPath.includes("enum") && fc.changeType === "added") {
1797
+ const paramId = fc.fieldPath[1];
1798
+ const [paramName, inLoc] = paramId.split(":");
1799
+ changes.push(this.makeChange({
1800
+ severity: "info",
1801
+ category: "parameter",
1802
+ message: `Enum value '${fc.newValue}' was added to parameter '${paramName}' in ${inLoc}.`,
1803
+ consequence: `Clients can now optionally send '${fc.newValue}' for this parameter.`,
1804
+ migration: "No immediate action required.",
1805
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1806
+ }, context));
1807
+ }
1808
+ }
1809
+ }
1810
+ return changes;
1811
+ }
1812
+ };
1813
+
1814
+ // src/rules/param/param-required-true-to-false.ts
1815
+ var ParamRequiredTrueToFalseRule = class extends BaseRule {
1816
+ id = "PARAM_REQUIRED_TRUE_TO_FALSE";
1817
+ description = "A required parameter was made optional";
1818
+ severity = "info";
1819
+ apply(diff, context) {
1820
+ const changes = [];
1821
+ for (const ed of diff.endpointDiffs) {
1822
+ if (ed.type !== "changed") continue;
1823
+ if (this.isIgnored(ed.path, context)) continue;
1824
+ for (const fc of ed.fieldChanges) {
1825
+ if (fc.fieldPath[0] === "parameters" && fc.fieldPath[2] === "required" && fc.changeType === "changed") {
1826
+ if (fc.oldValue === true && fc.newValue === false) {
1827
+ const paramId = fc.fieldPath[1];
1828
+ const [paramName, inLoc] = paramId.split(":");
1829
+ changes.push(this.makeChange({
1830
+ severity: "info",
1831
+ category: "parameter",
1832
+ message: `Required parameter '${paramName}' in ${inLoc} is now optional.`,
1833
+ consequence: "Clients can omit this parameter in requests.",
1834
+ migration: "Clients may stop sending this parameter if desired.",
1835
+ location: { path: ed.path, method: ed.method, paramName, field: inLoc }
1836
+ }, context));
1837
+ }
1838
+ }
1839
+ }
1840
+ }
1841
+ return changes;
1842
+ }
1843
+ };
1844
+
1845
+ // src/rules/request/request-body-added-required.ts
1846
+ var RequestBodyAddedRequiredRule = class extends BaseRule {
1847
+ id = "REQUEST_BODY_ADDED_REQUIRED";
1848
+ description = "A required request body was added";
1849
+ severity = "breaking";
1850
+ apply(diff, context) {
1851
+ const changes = [];
1852
+ for (const ed of diff.endpointDiffs) {
1853
+ if (ed.type !== "changed") continue;
1854
+ if (this.isIgnored(ed.path, context)) continue;
1855
+ for (const fc of ed.fieldChanges) {
1856
+ if (fc.fieldPath[0] === "requestBody" && fc.fieldPath.length === 1 && fc.changeType === "added") {
1857
+ const body = fc.newValue;
1858
+ if (body.required) {
1859
+ changes.push(this.makeChange({
1860
+ severity: "breaking",
1861
+ category: "request-body",
1862
+ message: "A required request body was added.",
1863
+ consequence: "Clients not sending a request body will now receive 400 Bad Request errors.",
1864
+ migration: "Update clients to send the required request body.",
1865
+ location: { path: ed.path, method: ed.method, field: "requestBody" }
1866
+ }, context));
1867
+ }
1868
+ }
1869
+ }
1870
+ }
1871
+ return changes;
1872
+ }
1873
+ };
1874
+
1875
+ // src/rules/request/request-body-removed.ts
1876
+ var RequestBodyRemovedRule = class extends BaseRule {
1877
+ id = "REQUEST_BODY_REMOVED";
1878
+ description = "A request body was removed";
1879
+ severity = "breaking";
1880
+ apply(diff, context) {
1881
+ const changes = [];
1882
+ for (const ed of diff.endpointDiffs) {
1883
+ if (ed.type !== "changed") continue;
1884
+ if (this.isIgnored(ed.path, context)) continue;
1885
+ for (const fc of ed.fieldChanges) {
1886
+ if (fc.fieldPath[0] === "requestBody" && fc.fieldPath.length === 1 && fc.changeType === "removed") {
1887
+ changes.push(this.makeChange({
1888
+ severity: "breaking",
1889
+ category: "request-body",
1890
+ message: "The request body was removed.",
1891
+ consequence: "Clients sending a request body may experience errors if the server strictly validates requests.",
1892
+ migration: "Update clients to stop sending a request body.",
1893
+ location: { path: ed.path, method: ed.method, field: "requestBody" }
1894
+ }, context));
1895
+ }
1896
+ }
1897
+ }
1898
+ return changes;
1899
+ }
1900
+ };
1901
+
1902
+ // src/rules/request/request-content-type-added.ts
1903
+ var RequestContentTypeAddedRule = class extends BaseRule {
1904
+ id = "REQUEST_CONTENT_TYPE_ADDED";
1905
+ description = "A new content type was added to the request body";
1906
+ severity = "info";
1907
+ apply(diff, context) {
1908
+ const changes = [];
1909
+ for (const ed of diff.endpointDiffs) {
1910
+ if (ed.type !== "changed") continue;
1911
+ if (this.isIgnored(ed.path, context)) continue;
1912
+ for (const fc of ed.fieldChanges) {
1913
+ if (fc.fieldPath[0] === "requestBody" && fc.fieldPath[1] === "content" && fc.fieldPath.length === 3 && fc.changeType === "added") {
1914
+ const mimeType = fc.fieldPath[2];
1915
+ changes.push(this.makeChange({
1916
+ severity: "info",
1917
+ category: "request-body",
1918
+ message: `Request content type '${mimeType}' was added.`,
1919
+ consequence: "Clients can now use this new content type when sending requests.",
1920
+ migration: "No immediate action required.",
1921
+ location: { path: ed.path, method: ed.method, field: `requestBody.content['${mimeType}']` }
1922
+ }, context));
1923
+ }
1924
+ }
1925
+ }
1926
+ return changes;
1927
+ }
1928
+ };
1929
+
1930
+ // src/rules/request/request-content-type-removed.ts
1931
+ var RequestContentTypeRemovedRule = class extends BaseRule {
1932
+ id = "REQUEST_CONTENT_TYPE_REMOVED";
1933
+ description = "A content type was removed from the request body";
1934
+ severity = "breaking";
1935
+ apply(diff, context) {
1936
+ const changes = [];
1937
+ for (const ed of diff.endpointDiffs) {
1938
+ if (ed.type !== "changed") continue;
1939
+ if (this.isIgnored(ed.path, context)) continue;
1940
+ for (const fc of ed.fieldChanges) {
1941
+ if (fc.fieldPath[0] === "requestBody" && fc.fieldPath[1] === "content" && fc.fieldPath.length === 3 && fc.changeType === "removed") {
1942
+ const mimeType = fc.fieldPath[2];
1943
+ changes.push(this.makeChange({
1944
+ severity: "breaking",
1945
+ category: "request-body",
1946
+ message: `Request content type '${mimeType}' was removed.`,
1947
+ consequence: `Clients sending requests with Content-Type '${mimeType}' will receive 415 Unsupported Media Type errors.`,
1948
+ migration: `Update clients to use a supported content type instead of '${mimeType}'.`,
1949
+ location: { path: ed.path, method: ed.method, field: `requestBody.content['${mimeType}']` }
1950
+ }, context));
1951
+ }
1952
+ }
1953
+ }
1954
+ return changes;
1955
+ }
1956
+ };
1957
+
1958
+ // src/rules/request/request-field-removed.ts
1959
+ var RequestFieldRemovedRule = class extends BaseRule {
1960
+ id = "REQUEST_FIELD_REMOVED";
1961
+ description = "A field was removed from the request body";
1962
+ severity = "breaking";
1963
+ apply(diff, context) {
1964
+ const changes = [];
1965
+ for (const ed of diff.endpointDiffs) {
1966
+ if (ed.type !== "changed") continue;
1967
+ if (this.isIgnored(ed.path, context)) continue;
1968
+ for (const fc of ed.fieldChanges) {
1969
+ if (fc.fieldPath[0] === "requestBody" && fc.changeType === "removed") {
1970
+ const propsIdx = fc.fieldPath.lastIndexOf("properties");
1971
+ if (propsIdx !== -1 && fc.fieldPath.length === propsIdx + 2) {
1972
+ const fieldName = fc.fieldPath[propsIdx + 1];
1973
+ changes.push(this.makeChange({
1974
+ severity: "breaking",
1975
+ category: "request-body",
1976
+ message: `Request body field '${fieldName}' was removed.`,
1977
+ consequence: "Clients sending this field may experience errors if the server strictly validates requests.",
1978
+ migration: `Update clients to stop sending the '${fieldName}' field.`,
1979
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join(".") }
1980
+ }, context));
1981
+ }
1982
+ }
1983
+ }
1984
+ }
1985
+ return changes;
1986
+ }
1987
+ };
1988
+
1989
+ // src/rules/request/request-field-added-required.ts
1990
+ var RequestFieldAddedRequiredRule = class extends BaseRule {
1991
+ id = "REQUEST_FIELD_ADDED_REQUIRED";
1992
+ description = "A required field was added to the request body";
1993
+ severity = "breaking";
1994
+ apply(diff, context) {
1995
+ const changes = [];
1996
+ for (const ed of diff.endpointDiffs) {
1997
+ if (ed.type !== "changed") continue;
1998
+ if (this.isIgnored(ed.path, context)) continue;
1999
+ for (const fc of ed.fieldChanges) {
2000
+ if (fc.fieldPath[0] === "requestBody" && fc.changeType === "added") {
2001
+ const requiredIdx = fc.fieldPath.lastIndexOf("required");
2002
+ if (requiredIdx !== -1 && fc.fieldPath.length === requiredIdx + 2) {
2003
+ const fieldName = fc.fieldPath[requiredIdx + 1];
2004
+ const propsPath = fc.fieldPath.slice(0, requiredIdx).concat(["properties", fieldName]).join(".");
2005
+ const propAdded = ed.fieldChanges.some((c) => c.changeType === "added" && c.fieldPath.join(".") === propsPath);
2006
+ if (propAdded) {
2007
+ changes.push(this.makeChange({
2008
+ severity: "breaking",
2009
+ category: "request-body",
2010
+ message: `Required request body field '${fieldName}' was added.`,
2011
+ consequence: `Clients not sending the new '${fieldName}' field will receive validation errors.`,
2012
+ migration: `Update clients to include the '${fieldName}' field in requests.`,
2013
+ location: { path: ed.path, method: ed.method, field: propsPath }
2014
+ }, context));
2015
+ }
2016
+ }
2017
+ }
2018
+ }
2019
+ }
2020
+ return changes;
2021
+ }
2022
+ };
2023
+
2024
+ // src/rules/request/request-field-type-changed.ts
2025
+ var RequestFieldTypeChangedRule = class extends BaseRule {
2026
+ id = "REQUEST_FIELD_TYPE_CHANGED";
2027
+ description = "The data type of a request body field changed";
2028
+ severity = "breaking";
2029
+ apply(diff, context) {
2030
+ const changes = [];
2031
+ for (const ed of diff.endpointDiffs) {
2032
+ if (ed.type !== "changed") continue;
2033
+ if (this.isIgnored(ed.path, context)) continue;
2034
+ for (const fc of ed.fieldChanges) {
2035
+ if (fc.fieldPath[0] === "requestBody" && fc.changeType === "changed") {
2036
+ const typeIdx = fc.fieldPath.lastIndexOf("type");
2037
+ if (typeIdx !== -1 && typeIdx === fc.fieldPath.length - 1) {
2038
+ const propsIdx = fc.fieldPath.lastIndexOf("properties");
2039
+ if (propsIdx !== -1 && typeIdx > propsIdx + 1) {
2040
+ const fieldName = fc.fieldPath[propsIdx + 1];
2041
+ changes.push(this.makeChange({
2042
+ severity: "breaking",
2043
+ category: "request-body",
2044
+ message: `Request body field '${fieldName}' changed type from ${fc.oldValue} to ${fc.newValue}.`,
2045
+ consequence: "Clients sending the old type will receive validation errors.",
2046
+ migration: `Update clients to send the new type (${fc.newValue}) for '${fieldName}'.`,
2047
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.slice(0, -1).join(".") }
2048
+ }, context));
2049
+ }
2050
+ }
2051
+ }
2052
+ }
2053
+ }
2054
+ return changes;
2055
+ }
2056
+ };
2057
+
2058
+ // src/rules/request/request-required-false-to-true.ts
2059
+ var RequestRequiredFalseToTrueRule = class extends BaseRule {
2060
+ id = "REQUEST_REQUIRED_FALSE_TO_TRUE";
2061
+ description = "An optional request body field was made required";
2062
+ severity = "breaking";
2063
+ apply(diff, context) {
2064
+ const changes = [];
2065
+ for (const ed of diff.endpointDiffs) {
2066
+ if (ed.type !== "changed") continue;
2067
+ if (this.isIgnored(ed.path, context)) continue;
2068
+ for (const fc of ed.fieldChanges) {
2069
+ if (fc.fieldPath[0] === "requestBody") {
2070
+ if (fc.fieldPath.length === 2 && fc.fieldPath[1] === "required" && fc.changeType === "changed" && fc.oldValue === false && fc.newValue === true) {
2071
+ changes.push(this.makeChange({
2072
+ severity: "breaking",
2073
+ category: "request-body",
2074
+ message: "The request body was made required.",
2075
+ consequence: "Clients not sending a request body will now receive validation errors.",
2076
+ migration: "Update clients to always send a request body.",
2077
+ location: { path: ed.path, method: ed.method, field: "requestBody.required" }
2078
+ }, context));
2079
+ }
2080
+ if (fc.changeType === "added") {
2081
+ const requiredIdx = fc.fieldPath.lastIndexOf("required");
2082
+ if (requiredIdx !== -1 && fc.fieldPath.length === requiredIdx + 2) {
2083
+ const fieldName = fc.fieldPath[requiredIdx + 1];
2084
+ const propsPath = fc.fieldPath.slice(0, requiredIdx).concat(["properties", fieldName]).join(".");
2085
+ const propAdded = ed.fieldChanges.some((c) => c.changeType === "added" && c.fieldPath.join(".") === propsPath);
2086
+ if (!propAdded) {
2087
+ changes.push(this.makeChange({
2088
+ severity: "breaking",
2089
+ category: "request-body",
2090
+ message: `Optional request body field '${fieldName}' is now required.`,
2091
+ consequence: `Requests missing '${fieldName}' will now be rejected.`,
2092
+ migration: `Update clients to always include the '${fieldName}' field.`,
2093
+ location: { path: ed.path, method: ed.method, field: propsPath }
2094
+ }, context));
2095
+ }
2096
+ }
2097
+ }
2098
+ }
2099
+ }
2100
+ }
2101
+ return changes;
2102
+ }
2103
+ };
2104
+
2105
+ // src/rules/response/response-field-added.ts
2106
+ var ResponseFieldAddedRule = class extends BaseRule {
2107
+ id = "RESPONSE_FIELD_ADDED";
2108
+ description = "A field was added to the response body";
2109
+ severity = "info";
2110
+ apply(diff, context) {
2111
+ const changes = [];
2112
+ for (const ed of diff.endpointDiffs) {
2113
+ if (ed.type !== "changed") continue;
2114
+ if (this.isIgnored(ed.path, context)) continue;
2115
+ for (const fc of ed.fieldChanges) {
2116
+ if (fc.fieldPath[0] === "responses" && fc.changeType === "added") {
2117
+ const propsIdx = fc.fieldPath.lastIndexOf("properties");
2118
+ if (propsIdx !== -1 && fc.fieldPath.length === propsIdx + 2) {
2119
+ const fieldName = fc.fieldPath[propsIdx + 1];
2120
+ changes.push(this.makeChange({
2121
+ severity: "info",
2122
+ category: "response",
2123
+ message: `Response field '${fieldName}' was added to status code ${fc.fieldPath[1]}.`,
2124
+ consequence: "Clients parsing responses strictly might fail if they do not ignore unknown fields.",
2125
+ migration: "Clients should ensure their JSON parsers ignore unknown fields.",
2126
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join(".") }
2127
+ }, context));
2128
+ }
2129
+ }
2130
+ }
2131
+ }
2132
+ return changes;
2133
+ }
2134
+ };
2135
+
2136
+ // src/rules/response/response-field-type-changed.ts
2137
+ var ResponseFieldTypeChangedRule = class extends BaseRule {
2138
+ id = "RESPONSE_FIELD_TYPE_CHANGED";
2139
+ description = "The data type of a response body field changed";
2140
+ severity = "breaking";
2141
+ apply(diff, context) {
2142
+ const changes = [];
2143
+ for (const ed of diff.endpointDiffs) {
2144
+ if (ed.type !== "changed") continue;
2145
+ if (this.isIgnored(ed.path, context)) continue;
2146
+ for (const fc of ed.fieldChanges) {
2147
+ if (fc.fieldPath[0] === "responses" && fc.changeType === "changed") {
2148
+ const typeIdx = fc.fieldPath.lastIndexOf("type");
2149
+ if (typeIdx !== -1 && typeIdx === fc.fieldPath.length - 1) {
2150
+ const propsIdx = fc.fieldPath.lastIndexOf("properties");
2151
+ if (propsIdx !== -1 && typeIdx > propsIdx + 1) {
2152
+ const fieldName = fc.fieldPath[propsIdx + 1];
2153
+ changes.push(this.makeChange({
2154
+ severity: "breaking",
2155
+ category: "response",
2156
+ message: `Response body field '${fieldName}' changed type from ${fc.oldValue} to ${fc.newValue} for status code ${fc.fieldPath[1]}.`,
2157
+ consequence: "Clients expecting the old type will fail to parse the response.",
2158
+ migration: `Update clients to expect the new type (${fc.newValue}) for '${fieldName}'.`,
2159
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.slice(0, -1).join(".") }
2160
+ }, context));
2161
+ }
2162
+ }
2163
+ }
2164
+ }
2165
+ }
2166
+ return changes;
2167
+ }
2168
+ };
2169
+
2170
+ // src/rules/response/response-status-removed.ts
2171
+ var ResponseStatusRemovedRule = class extends BaseRule {
2172
+ id = "RESPONSE_STATUS_REMOVED";
2173
+ description = "A response status code was removed";
2174
+ severity = "breaking";
2175
+ apply(diff, context) {
2176
+ const changes = [];
2177
+ for (const ed of diff.endpointDiffs) {
2178
+ if (ed.type !== "changed") continue;
2179
+ if (this.isIgnored(ed.path, context)) continue;
2180
+ for (const fc of ed.fieldChanges) {
2181
+ if (fc.fieldPath[0] === "responses" && fc.fieldPath.length === 2 && fc.changeType === "removed") {
2182
+ const statusCode = fc.fieldPath[1];
2183
+ changes.push(this.makeChange({
2184
+ severity: "breaking",
2185
+ category: "response",
2186
+ message: `Response status code ${statusCode} was removed.`,
2187
+ consequence: `Clients explicitly expecting a ${statusCode} response might fail or behave unexpectedly.`,
2188
+ migration: `Update clients to no longer rely on the ${statusCode} response.`,
2189
+ location: { path: ed.path, method: ed.method, field: `responses['${statusCode}']` }
2190
+ }, context));
2191
+ }
2192
+ }
2193
+ }
2194
+ return changes;
2195
+ }
2196
+ };
2197
+
2198
+ // src/rules/response/response-status-added.ts
2199
+ var ResponseStatusAddedRule = class extends BaseRule {
2200
+ id = "RESPONSE_STATUS_ADDED";
2201
+ description = "A response status code was added";
2202
+ severity = "info";
2203
+ apply(diff, context) {
2204
+ const changes = [];
2205
+ for (const ed of diff.endpointDiffs) {
2206
+ if (ed.type !== "changed") continue;
2207
+ if (this.isIgnored(ed.path, context)) continue;
2208
+ for (const fc of ed.fieldChanges) {
2209
+ if (fc.fieldPath[0] === "responses" && fc.fieldPath.length === 2 && fc.changeType === "added") {
2210
+ const statusCode = fc.fieldPath[1];
2211
+ changes.push(this.makeChange({
2212
+ severity: "info",
2213
+ category: "response",
2214
+ message: `Response status code ${statusCode} was added.`,
2215
+ consequence: `Clients may receive a ${statusCode} response they weren't previously expecting.`,
2216
+ migration: `Update clients to gracefully handle the ${statusCode} response.`,
2217
+ location: { path: ed.path, method: ed.method, field: `responses['${statusCode}']` }
2218
+ }, context));
2219
+ }
2220
+ }
2221
+ }
2222
+ return changes;
2223
+ }
2224
+ };
2225
+
2226
+ // src/rules/response/response-media-type-removed.ts
2227
+ var ResponseMediaTypeRemovedRule = class extends BaseRule {
2228
+ id = "RESPONSE_MEDIA_TYPE_REMOVED";
2229
+ description = "A response media type was removed";
2230
+ severity = "breaking";
2231
+ apply(diff, context) {
2232
+ const changes = [];
2233
+ for (const ed of diff.endpointDiffs) {
2234
+ if (ed.type !== "changed") continue;
2235
+ if (this.isIgnored(ed.path, context)) continue;
2236
+ for (const fc of ed.fieldChanges) {
2237
+ if (fc.fieldPath[0] === "responses" && fc.fieldPath[2] === "content" && fc.fieldPath.length === 4 && fc.changeType === "removed") {
2238
+ const statusCode = fc.fieldPath[1];
2239
+ const mediaType = fc.fieldPath[3];
2240
+ changes.push(this.makeChange({
2241
+ severity: "breaking",
2242
+ category: "response",
2243
+ message: `Response media type '${mediaType}' was removed for status code ${statusCode}.`,
2244
+ consequence: `Clients requesting '${mediaType}' will no longer receive it and may fail to process the response.`,
2245
+ migration: `Update clients to accept one of the remaining supported media types.`,
2246
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join(".") }
2247
+ }, context));
2248
+ }
2249
+ }
2250
+ }
2251
+ return changes;
2252
+ }
2253
+ };
2254
+
2255
+ // src/rules/response/response-media-type-added.ts
2256
+ var ResponseMediaTypeAddedRule = class extends BaseRule {
2257
+ id = "RESPONSE_MEDIA_TYPE_ADDED";
2258
+ description = "A response media type was added";
2259
+ severity = "info";
2260
+ apply(diff, context) {
2261
+ const changes = [];
2262
+ for (const ed of diff.endpointDiffs) {
2263
+ if (ed.type !== "changed") continue;
2264
+ if (this.isIgnored(ed.path, context)) continue;
2265
+ for (const fc of ed.fieldChanges) {
2266
+ if (fc.fieldPath[0] === "responses" && fc.fieldPath[2] === "content" && fc.fieldPath.length === 4 && fc.changeType === "added") {
2267
+ const statusCode = fc.fieldPath[1];
2268
+ const mediaType = fc.fieldPath[3];
2269
+ changes.push(this.makeChange({
2270
+ severity: "info",
2271
+ category: "response",
2272
+ message: `Response media type '${mediaType}' was added to status code ${statusCode}.`,
2273
+ consequence: "Clients can now request the new media type.",
2274
+ migration: `Update clients to request '${mediaType}' if desired.`,
2275
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join(".") }
2276
+ }, context));
2277
+ }
2278
+ }
2279
+ }
2280
+ return changes;
2281
+ }
2282
+ };
2283
+
2284
+ // src/rules/response/response-header-removed.ts
2285
+ var ResponseHeaderRemovedRule = class extends BaseRule {
2286
+ id = "RESPONSE_HEADER_REMOVED";
2287
+ description = "A response header was removed";
2288
+ severity = "breaking";
2289
+ apply(diff, context) {
2290
+ const changes = [];
2291
+ for (const ed of diff.endpointDiffs) {
2292
+ if (ed.type !== "changed") continue;
2293
+ if (this.isIgnored(ed.path, context)) continue;
2294
+ for (const fc of ed.fieldChanges) {
2295
+ if (fc.fieldPath[0] === "responses" && fc.fieldPath[2] === "headers" && fc.fieldPath.length === 4 && fc.changeType === "removed") {
2296
+ const statusCode = fc.fieldPath[1];
2297
+ const headerName = fc.fieldPath[3];
2298
+ changes.push(this.makeChange({
2299
+ severity: "breaking",
2300
+ category: "response",
2301
+ message: `Response header '${headerName}' was removed from status code ${statusCode}.`,
2302
+ consequence: `Clients depending on the '${headerName}' header will no longer receive it.`,
2303
+ migration: `Update clients to not expect the '${headerName}' header.`,
2304
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join(".") }
2305
+ }, context));
2306
+ }
2307
+ }
2308
+ }
2309
+ return changes;
2310
+ }
2311
+ };
2312
+
2313
+ // src/rules/response/response-header-added-required.ts
2314
+ var ResponseHeaderAddedRequiredRule = class extends BaseRule {
2315
+ id = "RESPONSE_HEADER_ADDED_REQUIRED";
2316
+ description = "A required response header was added";
2317
+ severity = "breaking";
2318
+ apply(diff, context) {
2319
+ const changes = [];
2320
+ for (const ed of diff.endpointDiffs) {
2321
+ if (ed.type !== "changed") continue;
2322
+ if (this.isIgnored(ed.path, context)) continue;
2323
+ for (const fc of ed.fieldChanges) {
2324
+ if (fc.fieldPath[0] === "responses" && fc.fieldPath[2] === "headers" && fc.changeType === "added" && fc.fieldPath.length === 4) {
2325
+ const statusCode = fc.fieldPath[1];
2326
+ const headerName = fc.fieldPath[3];
2327
+ const headerDef = fc.newValue;
2328
+ if (headerDef.required) {
2329
+ changes.push(this.makeChange({
2330
+ severity: "breaking",
2331
+ category: "response",
2332
+ message: `Required response header '${headerName}' was added to status code ${statusCode}.`,
2333
+ consequence: `Clients not designed to handle the new required header may fail or reject the response.`,
2334
+ migration: `Update clients to accept and process the new '${headerName}' header.`,
2335
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join(".") }
2336
+ }, context));
2337
+ }
2338
+ } else if (fc.fieldPath[0] === "responses" && fc.fieldPath[2] === "headers" && fc.fieldPath[4] === "required" && fc.changeType === "changed" && fc.oldValue === false && fc.newValue === true) {
2339
+ const statusCode = fc.fieldPath[1];
2340
+ const headerName = fc.fieldPath[3];
2341
+ changes.push(this.makeChange({
2342
+ severity: "breaking",
2343
+ category: "response",
2344
+ message: `Optional response header '${headerName}' was made required for status code ${statusCode}.`,
2345
+ consequence: `Clients not designed to handle the required header may fail or reject the response.`,
2346
+ migration: `Update clients to accept and process the '${headerName}' header.`,
2347
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join(".") }
2348
+ }, context));
2349
+ }
2350
+ }
2351
+ }
2352
+ return changes;
2353
+ }
2354
+ };
2355
+
2356
+ // src/rules/auth/security-removed.ts
2357
+ var SecurityRemovedRule = class extends BaseRule {
2358
+ id = "SECURITY_REMOVED";
2359
+ description = "A global security scheme was removed";
2360
+ severity = "breaking";
2361
+ apply(diff, context) {
2362
+ const changes = [];
2363
+ for (const sd of diff.securityDiffs) {
2364
+ if (sd.changeType === "removed") {
2365
+ changes.push(this.makeChange({
2366
+ severity: "breaking",
2367
+ category: "authentication",
2368
+ message: `Security scheme '${sd.schemeId}' was removed.`,
2369
+ consequence: `Clients using '${sd.schemeId}' for authentication will fail to authenticate.`,
2370
+ migration: `Update clients to use a different, supported security scheme.`,
2371
+ location: { field: `securitySchemes['${sd.schemeId}']` }
2372
+ }, context));
2373
+ }
2374
+ }
2375
+ for (const ed of diff.endpointDiffs) {
2376
+ if (ed.type !== "changed") continue;
2377
+ if (this.isIgnored(ed.path, context)) continue;
2378
+ for (const fc of ed.fieldChanges) {
2379
+ if (fc.fieldPath[0] === "security" && fc.fieldPath.length === 2 && fc.changeType === "removed") {
2380
+ const schemeId = fc.oldValue;
2381
+ changes.push(this.makeChange({
2382
+ severity: "breaking",
2383
+ category: "authentication",
2384
+ message: `Security requirement '${schemeId}' was removed from the endpoint.`,
2385
+ consequence: `Clients trying to authenticate with '${schemeId}' might fail if the server no longer accepts it.`,
2386
+ migration: `Update clients to use one of the remaining security requirements.`,
2387
+ location: { path: ed.path, method: ed.method, field: `security['${schemeId}']` }
2388
+ }, context));
2389
+ }
2390
+ }
2391
+ }
2392
+ return changes;
2393
+ }
2394
+ };
2395
+
2396
+ // src/rules/auth/security-scheme-type-changed.ts
2397
+ var SecuritySchemeTypeChangedRule = class extends BaseRule {
2398
+ id = "SECURITY_SCHEME_TYPE_CHANGED";
2399
+ description = "The type of a security scheme was changed";
2400
+ severity = "breaking";
2401
+ apply(diff, context) {
2402
+ const changes = [];
2403
+ for (const sd of diff.securityDiffs) {
2404
+ if (sd.changeType === "changed") {
2405
+ for (const fc of sd.fieldChanges) {
2406
+ if (fc.fieldPath[0] === "type") {
2407
+ changes.push(this.makeChange({
2408
+ severity: "breaking",
2409
+ category: "authentication",
2410
+ message: `Security scheme '${sd.schemeId}' changed type from '${fc.oldValue}' to '${fc.newValue}'.`,
2411
+ consequence: `Clients using the old authentication type will fail to authenticate.`,
2412
+ migration: `Update clients to authenticate using the new type '${fc.newValue}'.`,
2413
+ location: { field: `securitySchemes['${sd.schemeId}'].type` }
2414
+ }, context));
2415
+ }
2416
+ }
2417
+ }
2418
+ }
2419
+ return changes;
2420
+ }
2421
+ };
2422
+
2423
+ // src/rules/auth/oauth-scope-removed.ts
2424
+ var OauthScopeRemovedRule = class extends BaseRule {
2425
+ id = "OAUTH_SCOPE_REMOVED";
2426
+ description = "An OAuth scope was removed from a security requirement";
2427
+ severity = "breaking";
2428
+ apply(diff, context) {
2429
+ const changes = [];
2430
+ for (const ed of diff.endpointDiffs) {
2431
+ if (ed.type !== "changed") continue;
2432
+ if (this.isIgnored(ed.path, context)) continue;
2433
+ for (const fc of ed.fieldChanges) {
2434
+ if (fc.fieldPath[0] === "security" && fc.fieldPath[2] === "scopes" && fc.changeType === "removed") {
2435
+ const schemeId = fc.fieldPath[1];
2436
+ const scope = fc.oldValue;
2437
+ changes.push(this.makeChange({
2438
+ severity: "breaking",
2439
+ category: "authentication",
2440
+ message: `OAuth scope '${scope}' was removed from security requirement '${schemeId}'.`,
2441
+ consequence: `Tokens granted with only the '${scope}' scope may no longer be accepted.`,
2442
+ migration: `Ensure clients request one of the remaining supported scopes.`,
2443
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join(".") }
2444
+ }, context));
2445
+ }
2446
+ }
2447
+ }
2448
+ return changes;
2449
+ }
2450
+ };
2451
+
2452
+ // src/rules/meta/server-removed.ts
2453
+ var ServerRemovedRule = class extends BaseRule {
2454
+ id = "SERVER_REMOVED";
2455
+ description = "A server was removed from the API";
2456
+ severity = "breaking";
2457
+ apply(diff, context) {
2458
+ const changes = [];
2459
+ for (const sd of diff.serverDiffs) {
2460
+ if (sd.changeType === "removed" && sd.oldServer) {
2461
+ changes.push(this.makeChange({
2462
+ severity: "breaking",
2463
+ category: "server",
2464
+ message: `Server URL '${sd.oldServer.url}' was removed.`,
2465
+ consequence: `Clients routing traffic to '${sd.oldServer.url}' will eventually fail if the server is decommissioned.`,
2466
+ migration: `Update clients to use one of the remaining server URLs.`,
2467
+ location: { field: `servers` }
2468
+ }, context));
2469
+ }
2470
+ }
2471
+ return changes;
2472
+ }
2473
+ };
2474
+
2475
+ // src/rules/index.ts
2476
+ var BUILT_IN_RULES = [
2477
+ new SecurityAddedRule(),
2478
+ new EndpointRemovedRule(),
2479
+ new EndpointAddedRule(),
2480
+ new EndpointDeprecatedRule(),
2481
+ new HttpMethodChangedRule(),
2482
+ new PathChangedRule(),
2483
+ new ResponseFieldRemovedRule(),
2484
+ new ParamRequiredAddedRule(),
2485
+ new ParamRemovedRule(),
2486
+ new ParamAddedRule(),
2487
+ new ParamDeprecatedRule(),
2488
+ new ParamTypeChangedRule(),
2489
+ new ParamLocationChangedRule(),
2490
+ new ParamEnumValueRemovedRule(),
2491
+ new ParamEnumValueAddedRule(),
2492
+ new ParamRequiredTrueToFalseRule(),
2493
+ new RequestBodyAddedRequiredRule(),
2494
+ new RequestBodyRemovedRule(),
2495
+ new RequestContentTypeAddedRule(),
2496
+ new RequestContentTypeRemovedRule(),
2497
+ new RequestFieldRemovedRule(),
2498
+ new RequestFieldAddedRequiredRule(),
2499
+ new RequestFieldTypeChangedRule(),
2500
+ new RequestRequiredFalseToTrueRule(),
2501
+ new ResponseFieldAddedRule(),
2502
+ new ResponseFieldTypeChangedRule(),
2503
+ new ResponseStatusRemovedRule(),
2504
+ new ResponseStatusAddedRule(),
2505
+ new ResponseMediaTypeRemovedRule(),
2506
+ new ResponseMediaTypeAddedRule(),
2507
+ new ResponseHeaderRemovedRule(),
2508
+ new ResponseHeaderAddedRequiredRule(),
2509
+ new SecurityRemovedRule(),
2510
+ new SecuritySchemeTypeChangedRule(),
2511
+ new OauthScopeRemovedRule(),
2512
+ new ServerRemovedRule()
2513
+ ];
2514
+ function runRules(diff, context) {
2515
+ const enabledRules = BUILT_IN_RULES.filter(
2516
+ (r) => !context.config.disabledRules.includes(r.id)
2517
+ );
2518
+ return enabledRules.flatMap((r) => r.apply(diff, context));
2519
+ }
2520
+
2521
+ // src/config/index.ts
2522
+ var import_node_fs4 = require("fs");
2523
+ var import_node_path3 = require("path");
2524
+ var DEFAULT_CONFIG = {
2525
+ failOn: "breaking",
2526
+ ignorePaths: [],
2527
+ ruleSeverityOverrides: {},
2528
+ disabledRules: [],
2529
+ customRules: [],
2530
+ output: {
2531
+ format: "terminal",
2532
+ color: true,
2533
+ summary: false,
2534
+ quiet: false
2535
+ }
2536
+ };
2537
+ async function loadConfig(options) {
2538
+ let config = { ...DEFAULT_CONFIG, output: { ...DEFAULT_CONFIG.output } };
2539
+ if (options.config) {
2540
+ const configPath = (0, import_node_path3.resolve)(process.cwd(), options.config);
2541
+ if ((0, import_node_fs4.existsSync)(configPath)) {
2542
+ const fileContent = (0, import_node_fs4.readFileSync)(configPath, "utf8");
2543
+ try {
2544
+ const fileConfig = JSON.parse(fileContent);
2545
+ config = { ...config, ...fileConfig, output: { ...config.output, ...fileConfig.output || {} } };
2546
+ } catch (e) {
2547
+ throw new Error(`Failed to parse config file at ${configPath}`);
2548
+ }
2549
+ } else {
2550
+ throw new Error(`Config file not found at ${configPath}`);
2551
+ }
2552
+ }
2553
+ if (options.format) config.output.format = options.format;
2554
+ if (options.failOn) config.failOn = options.failOn;
2555
+ if (options.ignorePath && options.ignorePath.length > 0) {
2556
+ config.ignorePaths = [...config.ignorePaths, ...options.ignorePath];
2557
+ }
2558
+ return config;
2559
+ }
2560
+
2561
+ // src/types/config.ts
2562
+ var DEFAULT_CONFIG2 = {
2563
+ failOn: "breaking",
2564
+ ignorePaths: [],
2565
+ ruleSeverityOverrides: {},
2566
+ disabledRules: [],
2567
+ customRules: [],
2568
+ output: {
2569
+ format: "terminal",
2570
+ color: true,
2571
+ summary: false,
2572
+ quiet: false
2573
+ }
2574
+ };
2575
+
2576
+ // src/output/terminal.ts
2577
+ var import_chalk = __toESM(require("chalk"), 1);
2578
+ var SEVERITY_ICONS = {
2579
+ breaking: import_chalk.default.red("\u274C"),
2580
+ warning: import_chalk.default.yellow("\u26A0\uFE0F "),
2581
+ info: import_chalk.default.blue("\u2139 ")
2582
+ };
2583
+ var SEVERITY_COLORS = {
2584
+ breaking: import_chalk.default.red,
2585
+ warning: import_chalk.default.yellow,
2586
+ info: import_chalk.default.blue
2587
+ };
2588
+ function formatTerminal(changes) {
2589
+ if (changes.length === 0) {
2590
+ return import_chalk.default.green("No changes detected.\n");
2591
+ }
2592
+ let output = "";
2593
+ for (const c of changes) {
2594
+ const icon = SEVERITY_ICONS[c.severity] || SEVERITY_ICONS.info;
2595
+ const color = SEVERITY_COLORS[c.severity] || SEVERITY_COLORS.info;
2596
+ let locStr = `${c.location.method} ${c.location.path}`;
2597
+ if (c.location.paramName) locStr += ` (param: ${c.location.paramName})`;
2598
+ if (c.location.field) locStr += ` (field: ${c.location.field})`;
2599
+ output += `${icon} ${color(c.message)}
2600
+ `;
2601
+ output += ` Location: ${locStr}
2602
+ `;
2603
+ output += ` Consequence: ${c.consequence}
2604
+ `;
2605
+ output += ` Migration: ${c.migration}
2606
+
2607
+ `;
2608
+ }
2609
+ return output;
2610
+ }
2611
+
2612
+ // src/output/json.ts
2613
+ function formatJson(changes) {
2614
+ return JSON.stringify({ changes }, null, 2);
2615
+ }
2616
+
2617
+ // src/output/markdown.ts
2618
+ var SEVERITY_EMOJI = {
2619
+ breaking: "\u274C",
2620
+ warning: "\u26A0\uFE0F",
2621
+ info: "\u2139\uFE0F"
2622
+ };
2623
+ function formatMarkdown(changes) {
2624
+ if (changes.length === 0) {
2625
+ return "No changes detected.\n";
2626
+ }
2627
+ let md = "## API Changes\n\n";
2628
+ for (const c of changes) {
2629
+ const emoji = SEVERITY_EMOJI[c.severity] || SEVERITY_EMOJI.info;
2630
+ let locStr = `**${c.location.method}** \`${c.location.path}\``;
2631
+ if (c.location.paramName) locStr += ` (param: \`${c.location.paramName}\`)`;
2632
+ if (c.location.field) locStr += ` (field: \`${c.location.field}\`)`;
2633
+ md += `### ${emoji} ${c.message}
2634
+ `;
2635
+ md += `- **Location:** ${locStr}
2636
+ `;
2637
+ md += `- **Consequence:** ${c.consequence}
2638
+ `;
2639
+ md += `- **Migration:** ${c.migration}
2640
+
2641
+ `;
2642
+ }
2643
+ return md;
2644
+ }
2645
+
2646
+ // src/output/html.ts
2647
+ function formatHtml(changes) {
2648
+ const breaking = changes.filter((c) => c.severity === "breaking");
2649
+ const warning = changes.filter((c) => c.severity === "warning");
2650
+ const info = changes.filter((c) => c.severity === "info");
2651
+ return `
2652
+ <!DOCTYPE html>
2653
+ <html lang="en">
2654
+ <head>
2655
+ <meta charset="UTF-8">
2656
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2657
+ <title>Semantic API Diff Report</title>
2658
+ <style>
2659
+ body {
2660
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
2661
+ line-height: 1.6;
2662
+ color: #333;
2663
+ max-width: 900px;
2664
+ margin: 0 auto;
2665
+ padding: 2rem;
2666
+ background: #f9f9f9;
2667
+ }
2668
+ h1 {
2669
+ color: #222;
2670
+ border-bottom: 2px solid #eaeaea;
2671
+ padding-bottom: 0.5rem;
2672
+ }
2673
+ .summary {
2674
+ display: flex;
2675
+ gap: 1rem;
2676
+ margin: 2rem 0;
2677
+ }
2678
+ .stat-card {
2679
+ background: white;
2680
+ padding: 1.5rem;
2681
+ border-radius: 8px;
2682
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
2683
+ flex: 1;
2684
+ text-align: center;
2685
+ border-top: 4px solid #ccc;
2686
+ }
2687
+ .stat-card.breaking { border-top-color: #e53e3e; }
2688
+ .stat-card.warning { border-top-color: #d69e2e; }
2689
+ .stat-card.info { border-top-color: #3182ce; }
2690
+ .stat-card .value {
2691
+ font-size: 2rem;
2692
+ font-weight: bold;
2693
+ margin: 0.5rem 0;
2694
+ }
2695
+ .stat-card .label {
2696
+ text-transform: uppercase;
2697
+ font-size: 0.8rem;
2698
+ letter-spacing: 0.05em;
2699
+ color: #666;
2700
+ }
2701
+ .change-list {
2702
+ list-style: none;
2703
+ padding: 0;
2704
+ }
2705
+ .change-item {
2706
+ background: white;
2707
+ margin-bottom: 1rem;
2708
+ border-radius: 6px;
2709
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
2710
+ overflow: hidden;
2711
+ }
2712
+ .change-header {
2713
+ padding: 1rem;
2714
+ cursor: pointer;
2715
+ display: flex;
2716
+ justify-content: space-between;
2717
+ align-items: center;
2718
+ background: #fff;
2719
+ border-left: 4px solid #ccc;
2720
+ }
2721
+ .change-item.breaking .change-header { border-left-color: #e53e3e; }
2722
+ .change-item.warning .change-header { border-left-color: #d69e2e; }
2723
+ .change-item.info .change-header { border-left-color: #3182ce; }
2724
+
2725
+ .change-title {
2726
+ font-weight: 600;
2727
+ }
2728
+ .change-body {
2729
+ padding: 1rem;
2730
+ border-top: 1px solid #eaeaea;
2731
+ background: #fafafa;
2732
+ display: none;
2733
+ }
2734
+ .open .change-body {
2735
+ display: block;
2736
+ }
2737
+ .badge {
2738
+ padding: 0.25rem 0.5rem;
2739
+ border-radius: 9999px;
2740
+ font-size: 0.75rem;
2741
+ font-weight: bold;
2742
+ background: #eee;
2743
+ }
2744
+ .badge.breaking { background: #fee2e2; color: #991b1b; }
2745
+ .badge.warning { background: #fef3c7; color: #92400e; }
2746
+ .badge.info { background: #dbeafe; color: #1e40af; }
2747
+
2748
+ .detail-row {
2749
+ margin-bottom: 0.5rem;
2750
+ }
2751
+ .detail-label {
2752
+ font-weight: 600;
2753
+ color: #555;
2754
+ width: 100px;
2755
+ display: inline-block;
2756
+ }
2757
+ code {
2758
+ background: #edf2f7;
2759
+ padding: 0.1rem 0.3rem;
2760
+ border-radius: 3px;
2761
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
2762
+ font-size: 0.9em;
2763
+ }
2764
+ </style>
2765
+ <script>
2766
+ function toggleChange(el) {
2767
+ el.parentElement.classList.toggle('open');
2768
+ }
2769
+ </script>
2770
+ </head>
2771
+ <body>
2772
+
2773
+ <h1>API Diff Report</h1>
2774
+
2775
+ <div class="summary">
2776
+ <div class="stat-card breaking">
2777
+ <div class="label">Breaking</div>
2778
+ <div class="value">${breaking.length}</div>
2779
+ </div>
2780
+ <div class="stat-card warning">
2781
+ <div class="label">Warnings</div>
2782
+ <div class="value">${warning.length}</div>
2783
+ </div>
2784
+ <div class="stat-card info">
2785
+ <div class="label">Info</div>
2786
+ <div class="value">${info.length}</div>
2787
+ </div>
2788
+ </div>
2789
+
2790
+ <h2>All Changes</h2>
2791
+ <ul class="change-list">
2792
+ ${changes.map(renderChange).join("\n")}
2793
+ </ul>
2794
+
2795
+ </body>
2796
+ </html>
2797
+ `.trim();
2798
+ }
2799
+ function renderChange(c) {
2800
+ let locStr = `<code>${c.location.method} ${c.location.path}</code>`;
2801
+ if (c.location.paramName) locStr += ` (param: <code>${c.location.paramName}</code>)`;
2802
+ if (c.location.field) locStr += ` (field: <code>${c.location.field}</code>)`;
2803
+ return `
2804
+ <li class="change-item ${c.severity}">
2805
+ <div class="change-header" onclick="toggleChange(this)">
2806
+ <div class="change-title">${c.message}</div>
2807
+ <span class="badge ${c.severity}">${c.severity}</span>
2808
+ </div>
2809
+ <div class="change-body">
2810
+ <div class="detail-row"><span class="detail-label">Location:</span> ${locStr}</div>
2811
+ <div class="detail-row"><span class="detail-label">Category:</span> ${c.category}</div>
2812
+ <div class="detail-row"><span class="detail-label">Rule:</span> <code>${c.ruleId}</code></div>
2813
+ ${c.consequence ? `<div class="detail-row"><span class="detail-label">Impact:</span> ${c.consequence}</div>` : ""}
2814
+ ${c.migration ? `<div class="detail-row"><span class="detail-label">Migration:</span> ${c.migration}</div>` : ""}
2815
+ </div>
2816
+ </li>
2817
+ `;
2818
+ }
2819
+
2820
+ // src/output/index.ts
2821
+ function formatOutput(changes, format) {
2822
+ switch (format) {
2823
+ case "json":
2824
+ return formatJson(changes);
2825
+ case "markdown":
2826
+ return formatMarkdown(changes);
2827
+ case "html":
2828
+ return formatHtml(changes);
2829
+ case "terminal":
2830
+ default:
2831
+ return formatTerminal(changes);
2832
+ }
2833
+ }
2834
+
2835
+ // src/index.ts
2836
+ async function runContent(oldContent, newContent, config = {}) {
2837
+ const fullConfig = { ...DEFAULT_CONFIG, ...config };
2838
+ const startMs = Date.now();
2839
+ const [oldAST, newAST] = await Promise.all([
2840
+ parseSpec({ content: oldContent }),
2841
+ parseSpec({ content: newContent })
2842
+ ]);
2843
+ const diffSet = computeDiff(oldAST, newAST);
2844
+ const allChanges = runRules(diffSet, { oldAST, newAST, config: fullConfig });
2845
+ const changes = filterChanges(allChanges, fullConfig);
2846
+ return {
2847
+ changes,
2848
+ stats: computeStats(changes),
2849
+ oldSpecMeta: oldAST.meta,
2850
+ newSpecMeta: newAST.meta,
2851
+ durationMs: Date.now() - startMs
2852
+ };
2853
+ }
2854
+ async function run(oldSource, newSource, config = {}) {
2855
+ const fullConfig = { ...DEFAULT_CONFIG, ...config };
2856
+ const startMs = Date.now();
2857
+ const [oldRaw, newRaw] = await Promise.all([
2858
+ loadSpec(oldSource),
2859
+ loadSpec(newSource)
2860
+ ]);
2861
+ const [oldAST, newAST] = await Promise.all([
2862
+ parseSpec(oldRaw),
2863
+ parseSpec(newRaw)
2864
+ ]);
2865
+ const diffSet = computeDiff(oldAST, newAST);
2866
+ const allChanges = runRules(diffSet, { oldAST, newAST, config: fullConfig });
2867
+ const changes = filterChanges(allChanges, fullConfig);
2868
+ return {
2869
+ changes,
2870
+ stats: computeStats(changes),
2871
+ oldSpecMeta: oldAST.meta,
2872
+ newSpecMeta: newAST.meta,
2873
+ durationMs: Date.now() - startMs
2874
+ };
2875
+ }
2876
+ function filterChanges(changes, config) {
2877
+ return changes;
2878
+ }
2879
+ function computeStats(changes) {
2880
+ const stats = { breaking: 0, warning: 0, info: 0, total: 0 };
2881
+ for (const c of changes) {
2882
+ if (c.severity === "breaking") stats.breaking++;
2883
+ else if (c.severity === "warning") stats.warning++;
2884
+ else stats.info++;
2885
+ stats.total++;
2886
+ }
2887
+ return stats;
2888
+ }
2889
+ // Annotate the CommonJS export names for ESM import in node:
2890
+ 0 && (module.exports = {
2891
+ ApidiffError,
2892
+ ConfigError,
2893
+ DEFAULT_CONFIG,
2894
+ FormatError,
2895
+ LoadError,
2896
+ ParseError,
2897
+ RefError,
2898
+ formatOutput,
2899
+ loadConfig,
2900
+ run,
2901
+ runContent
2902
+ });