api-tests-coverage 1.0.22 → 1.0.24

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 (175) hide show
  1. package/README.md +39 -0
  2. package/config.yaml.example +35 -0
  3. package/dist/dashboard/dist/assets/_basePickBy-BamgEusj.js +1 -0
  4. package/dist/dashboard/dist/assets/_basePickBy-D4Hl8chy.js +1 -0
  5. package/dist/dashboard/dist/assets/_baseUniq-BBhq12Ja.js +1 -0
  6. package/dist/dashboard/dist/assets/_baseUniq-BSUUnV_V.js +1 -0
  7. package/dist/dashboard/dist/assets/arc-Dh-qL1ea.js +1 -0
  8. package/dist/dashboard/dist/assets/arc-DhDluTY5.js +1 -0
  9. package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-BxQ_anmt.js +36 -0
  10. package/dist/dashboard/dist/assets/architectureDiagram-VXUJARFQ-DGlUU7dC.js +36 -0
  11. package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-CgXi3kEZ.js +122 -0
  12. package/dist/dashboard/dist/assets/blockDiagram-VD42YOAC-Krm3lc7z.js +122 -0
  13. package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-Cfd4OeWg.js +10 -0
  14. package/dist/dashboard/dist/assets/c4Diagram-YG6GDRKO-Cr3xB15y.js +10 -0
  15. package/dist/dashboard/dist/assets/channel-DYAie-7m.js +1 -0
  16. package/dist/dashboard/dist/assets/channel-a4t2URTe.js +1 -0
  17. package/dist/dashboard/dist/assets/chunk-4BX2VUAB-BaW3__pI.js +1 -0
  18. package/dist/dashboard/dist/assets/chunk-4BX2VUAB-ljBQ5lHA.js +1 -0
  19. package/dist/dashboard/dist/assets/chunk-55IACEB6-Cikrdc3Q.js +1 -0
  20. package/dist/dashboard/dist/assets/chunk-55IACEB6-DyYevfEQ.js +1 -0
  21. package/dist/dashboard/dist/assets/chunk-B4BG7PRW-C2bwZFec.js +165 -0
  22. package/dist/dashboard/dist/assets/chunk-B4BG7PRW-dtHgbkmj.js +165 -0
  23. package/dist/dashboard/dist/assets/chunk-DI55MBZ5-Cv3hm2Ke.js +220 -0
  24. package/dist/dashboard/dist/assets/chunk-DI55MBZ5-DO0T2xne.js +220 -0
  25. package/dist/dashboard/dist/assets/chunk-FMBD7UC4-CCYA4j_f.js +15 -0
  26. package/dist/dashboard/dist/assets/chunk-FMBD7UC4-Ds1_OqKH.js +15 -0
  27. package/dist/dashboard/dist/assets/chunk-QN33PNHL-B6zkzIAo.js +1 -0
  28. package/dist/dashboard/dist/assets/chunk-QN33PNHL-Cdhqs7xo.js +1 -0
  29. package/dist/dashboard/dist/assets/chunk-QZHKN3VN-BzHw38Ki.js +1 -0
  30. package/dist/dashboard/dist/assets/chunk-QZHKN3VN-C7xuA6tl.js +1 -0
  31. package/dist/dashboard/dist/assets/chunk-TZMSLE5B-D_ea_wdP.js +1 -0
  32. package/dist/dashboard/dist/assets/chunk-TZMSLE5B-dkJ0rsgF.js +1 -0
  33. package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-B6SxXE6T.js +1 -0
  34. package/dist/dashboard/dist/assets/classDiagram-2ON5EDUG-DiIv5Pho.js +1 -0
  35. package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-B6SxXE6T.js +1 -0
  36. package/dist/dashboard/dist/assets/classDiagram-v2-WZHVMYZB-DiIv5Pho.js +1 -0
  37. package/dist/dashboard/dist/assets/clone-B4LorrSy.js +1 -0
  38. package/dist/dashboard/dist/assets/clone-kcKg1tUH.js +1 -0
  39. package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-DasyAK5c.js +1 -0
  40. package/dist/dashboard/dist/assets/cose-bilkent-S5V4N54A-jzGbyPIS.js +1 -0
  41. package/dist/dashboard/dist/assets/dagre-6UL2VRFP-D7rgvBx1.js +4 -0
  42. package/dist/dashboard/dist/assets/dagre-6UL2VRFP-m-5bs635.js +4 -0
  43. package/dist/dashboard/dist/assets/diagram-PSM6KHXK-2rYklqon.js +24 -0
  44. package/dist/dashboard/dist/assets/diagram-PSM6KHXK-CYFwwEdy.js +24 -0
  45. package/dist/dashboard/dist/assets/diagram-QEK2KX5R-CGrvALqm.js +43 -0
  46. package/dist/dashboard/dist/assets/diagram-QEK2KX5R-m4Fda1GA.js +43 -0
  47. package/dist/dashboard/dist/assets/diagram-S2PKOQOG-BDVk4AKU.js +24 -0
  48. package/dist/dashboard/dist/assets/diagram-S2PKOQOG-DA3c-QP4.js +24 -0
  49. package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-3-jAbxQ6.js +60 -0
  50. package/dist/dashboard/dist/assets/erDiagram-Q2GNP2WA-BsYH8cLH.js +60 -0
  51. package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-Cfv1hkQB.js +162 -0
  52. package/dist/dashboard/dist/assets/flowDiagram-NV44I4VS-Da_JhBCy.js +162 -0
  53. package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-B7GZPGck.js +267 -0
  54. package/dist/dashboard/dist/assets/ganttDiagram-JELNMOA3-D8FTswNn.js +267 -0
  55. package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-BFJR-ITH.js +65 -0
  56. package/dist/dashboard/dist/assets/gitGraphDiagram-V2S2FVAM-K8X-_4av.js +65 -0
  57. package/dist/dashboard/dist/assets/graph-CIvnjOQQ.js +1 -0
  58. package/dist/dashboard/dist/assets/graph-CfuGK9GG.js +1 -0
  59. package/dist/dashboard/dist/assets/index-BWX0sSZn.css +1 -0
  60. package/dist/dashboard/dist/assets/index-CbAFWEor.js +777 -0
  61. package/dist/dashboard/dist/assets/index-DS-KIxwV.js +777 -0
  62. package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-CaIaIUhT.js +2 -0
  63. package/dist/dashboard/dist/assets/infoDiagram-HS3SLOUP-OcK0Lxgi.js +2 -0
  64. package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-D6dwPswq.js +139 -0
  65. package/dist/dashboard/dist/assets/journeyDiagram-XKPGCS4Q-DTJukVOY.js +139 -0
  66. package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-CERyhhrH.js +89 -0
  67. package/dist/dashboard/dist/assets/kanban-definition-3W4ZIXB7-Di65fNuD.js +89 -0
  68. package/dist/dashboard/dist/assets/layout-DAt24RVX.js +1 -0
  69. package/dist/dashboard/dist/assets/layout-v7cCi3Fl.js +1 -0
  70. package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-BvNtTz8N.js +68 -0
  71. package/dist/dashboard/dist/assets/mindmap-definition-VGOIOE7T-DxI8MXCF.js +68 -0
  72. package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-BafKx3_Y.js +30 -0
  73. package/dist/dashboard/dist/assets/pieDiagram-ADFJNKIX-Cjg80C_b.js +30 -0
  74. package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-BcZsArkk.js +7 -0
  75. package/dist/dashboard/dist/assets/quadrantDiagram-AYHSOK5B-YtFFUYGD.js +7 -0
  76. package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-CqFAO2t6.js +64 -0
  77. package/dist/dashboard/dist/assets/requirementDiagram-UZGBJVZJ-DLV2LTE5.js +64 -0
  78. package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-C6_Urrii.js +10 -0
  79. package/dist/dashboard/dist/assets/sankeyDiagram-TZEHDZUN-CqSaCg-3.js +10 -0
  80. package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-6IXD1uqW.js +145 -0
  81. package/dist/dashboard/dist/assets/sequenceDiagram-WL72ISMW-D33UwAtz.js +145 -0
  82. package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-DSp83t9D.js +1 -0
  83. package/dist/dashboard/dist/assets/stateDiagram-FKZM4ZOC-DvSVQAfp.js +1 -0
  84. package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-BMFdt0QQ.js +1 -0
  85. package/dist/dashboard/dist/assets/stateDiagram-v2-4FDKWEC3-OTWrEpQO.js +1 -0
  86. package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-Cll7Nvth.js +61 -0
  87. package/dist/dashboard/dist/assets/timeline-definition-IT6M3QCI-D5Bb3Jj7.js +61 -0
  88. package/dist/dashboard/dist/assets/treemap-GDKQZRPO-CKbkkwye.js +162 -0
  89. package/dist/dashboard/dist/assets/treemap-GDKQZRPO-DtqX8zNC.js +162 -0
  90. package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-C_Tlzchx.js +7 -0
  91. package/dist/dashboard/dist/assets/xychartDiagram-PRI3JC2R-zxwS9i0A.js +7 -0
  92. package/dist/dashboard/dist/index.html +2 -2
  93. package/dist/dashboard/dist/reports/coverage-summary.json +75 -1
  94. package/dist/dashboard/dist/reports/security-full.json +157 -0
  95. package/dist/src/compatibilityCoverage.d.ts +34 -15
  96. package/dist/src/compatibilityCoverage.d.ts.map +1 -1
  97. package/dist/src/compatibilityCoverage.js +387 -85
  98. package/dist/src/config/defaultConfig.d.ts.map +1 -1
  99. package/dist/src/config/defaultConfig.js +62 -0
  100. package/dist/src/config/schema.d.ts.map +1 -1
  101. package/dist/src/config/schema.js +1 -1
  102. package/dist/src/config/types.d.ts +81 -1
  103. package/dist/src/config/types.d.ts.map +1 -1
  104. package/dist/src/config/validateConfig.d.ts.map +1 -1
  105. package/dist/src/config/validateConfig.js +126 -0
  106. package/dist/src/contracts/compatibilityMatrix.d.ts +20 -0
  107. package/dist/src/contracts/compatibilityMatrix.d.ts.map +1 -0
  108. package/dist/src/contracts/compatibilityMatrix.js +198 -0
  109. package/dist/src/contracts/pactBrokerClient.d.ts +10 -0
  110. package/dist/src/contracts/pactBrokerClient.d.ts.map +1 -0
  111. package/dist/src/contracts/pactBrokerClient.js +117 -0
  112. package/dist/src/contracts/schemaEvolutionChecker.d.ts +17 -0
  113. package/dist/src/contracts/schemaEvolutionChecker.d.ts.map +1 -0
  114. package/dist/src/contracts/schemaEvolutionChecker.js +95 -0
  115. package/dist/src/contracts/springCloudContractParser.d.ts +10 -0
  116. package/dist/src/contracts/springCloudContractParser.d.ts.map +1 -0
  117. package/dist/src/contracts/springCloudContractParser.js +144 -0
  118. package/dist/src/discovery/fileClassifier.d.ts.map +1 -1
  119. package/dist/src/discovery/fileClassifier.js +25 -0
  120. package/dist/src/discovery/projectDiscovery.d.ts +2 -0
  121. package/dist/src/discovery/projectDiscovery.d.ts.map +1 -1
  122. package/dist/src/discovery/projectDiscovery.js +25 -25
  123. package/dist/src/index.js +233 -16
  124. package/dist/src/inference/routeInference.d.ts +10 -2
  125. package/dist/src/inference/routeInference.d.ts.map +1 -1
  126. package/dist/src/inference/routeInference.js +363 -62
  127. package/dist/src/languageDetection.d.ts.map +1 -1
  128. package/dist/src/languageDetection.js +21 -4
  129. package/dist/src/lib/index.d.ts +3 -0
  130. package/dist/src/lib/index.d.ts.map +1 -1
  131. package/dist/src/lib/index.js +3 -1
  132. package/dist/src/pipeline/stages/tia/parameterizedTestExpander.js +152 -79
  133. package/dist/src/pipeline/stages/tia/testEndpointMapper.d.ts +5 -1
  134. package/dist/src/pipeline/stages/tia/testEndpointMapper.d.ts.map +1 -1
  135. package/dist/src/pipeline/stages/tia/testEndpointMapper.js +356 -42
  136. package/dist/src/pipeline/stages/tia/testLayerClassifier.d.ts.map +1 -1
  137. package/dist/src/pipeline/stages/tia/testLayerClassifier.js +20 -5
  138. package/dist/src/pipeline/stages/tia/tiaStage.d.ts.map +1 -1
  139. package/dist/src/pipeline/stages/tia/tiaStage.js +3 -1
  140. package/dist/src/pipeline/stages/tia/types.d.ts +11 -2
  141. package/dist/src/pipeline/stages/tia/types.d.ts.map +1 -1
  142. package/dist/src/projectDefaults.d.ts +6 -0
  143. package/dist/src/projectDefaults.d.ts.map +1 -0
  144. package/dist/src/projectDefaults.js +43 -0
  145. package/dist/src/security/hub.d.ts +81 -0
  146. package/dist/src/security/hub.d.ts.map +1 -0
  147. package/dist/src/security/hub.js +420 -0
  148. package/dist/src/security/index.d.ts +1 -0
  149. package/dist/src/security/index.d.ts.map +1 -1
  150. package/dist/src/security/index.js +8 -2
  151. package/dist/src/security/normalizers/gitleaks.d.ts +7 -0
  152. package/dist/src/security/normalizers/gitleaks.d.ts.map +1 -0
  153. package/dist/src/security/normalizers/gitleaks.js +32 -0
  154. package/dist/src/security/scanners/gitleaks.d.ts +3 -0
  155. package/dist/src/security/scanners/gitleaks.d.ts.map +1 -0
  156. package/dist/src/security/scanners/gitleaks.js +105 -0
  157. package/dist/src/security/scanners/semgrep.d.ts.map +1 -1
  158. package/dist/src/security/scanners/semgrep.js +24 -2
  159. package/dist/src/security/scanners/trivy.d.ts.map +1 -1
  160. package/dist/src/security/scanners/trivy.js +24 -2
  161. package/dist/src/security/scanners/zap.d.ts.map +1 -1
  162. package/dist/src/security/scanners/zap.js +27 -2
  163. package/dist/src/security/types.d.ts +15 -1
  164. package/dist/src/security/types.d.ts.map +1 -1
  165. package/dist/src/streaming/schema/index.d.ts +23 -0
  166. package/dist/src/streaming/schema/index.d.ts.map +1 -0
  167. package/dist/src/streaming/schema/index.js +196 -0
  168. package/dist/src/summary/markdownRenderer.d.ts.map +1 -1
  169. package/dist/src/summary/markdownRenderer.js +15 -1
  170. package/dist/src/summary/summaryTypes.d.ts.map +1 -1
  171. package/dist/src/summary/summaryTypes.js +1 -0
  172. package/dist/src/unitAnalysis.d.ts +145 -0
  173. package/dist/src/unitAnalysis.d.ts.map +1 -0
  174. package/dist/src/unitAnalysis.js +1392 -0
  175. package/package.json +1 -1
@@ -42,6 +42,7 @@ exports.parseContractFiles = parseContractFiles;
42
42
  exports.verifyContracts = verifyContracts;
43
43
  exports.computeCompatibilityPercent = computeCompatibilityPercent;
44
44
  exports.computeContractCoveragePercent = computeContractCoveragePercent;
45
+ exports.computeCanDeploy = computeCanDeploy;
45
46
  exports.buildCompatibilityReport = buildCompatibilityReport;
46
47
  exports.generateCompatibilityReports = generateCompatibilityReports;
47
48
  const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
@@ -77,6 +78,183 @@ function getParameters(op) {
77
78
  function paramKey(p) {
78
79
  return `${p.in}:${p.name}`;
79
80
  }
81
+ function createEndpointChange(change) {
82
+ var _a;
83
+ const breaking = (_a = change.breaking) !== null && _a !== void 0 ? _a : change.severity === 'BREAKING';
84
+ return { ...change, breaking };
85
+ }
86
+ function schemaObject(schema) {
87
+ if (!schema || typeof schema !== 'object')
88
+ return undefined;
89
+ return schema;
90
+ }
91
+ function getSchemaType(schema) {
92
+ if (!schema)
93
+ return undefined;
94
+ if (Array.isArray(schema.type))
95
+ return schema.type.join('|');
96
+ return typeof schema.type === 'string' ? schema.type : undefined;
97
+ }
98
+ function getRequiredFields(schema) {
99
+ return new Set(Array.isArray(schema === null || schema === void 0 ? void 0 : schema.required) ? schema.required : []);
100
+ }
101
+ function describeFieldRemoval(context, fieldPath) {
102
+ if (context.kind === 'response') {
103
+ return `Response field removed from ${context.method.toUpperCase()} ${context.path}: ${fieldPath}`;
104
+ }
105
+ return `Schema field removed from ${context.method.toUpperCase()} ${context.path}: ${fieldPath}`;
106
+ }
107
+ function getOperationSecurity(api, operation) {
108
+ var _a;
109
+ if (operation.security)
110
+ return operation.security;
111
+ return ((_a = api.security) !== null && _a !== void 0 ? _a : []);
112
+ }
113
+ function isPublicEndpoint(security) {
114
+ return security.length === 0;
115
+ }
116
+ function compareEnumValues(oldSchema, newSchema, context, fieldPath) {
117
+ const oldEnum = Array.isArray(oldSchema.enum) ? oldSchema.enum.map(String) : [];
118
+ const newEnum = Array.isArray(newSchema.enum) ? newSchema.enum.map(String) : [];
119
+ if (oldEnum.length === 0 && newEnum.length === 0)
120
+ return [];
121
+ const changes = [];
122
+ const removedValues = oldEnum.filter((value) => !newEnum.includes(value));
123
+ for (const value of removedValues) {
124
+ changes.push(createEndpointChange({
125
+ method: context.method,
126
+ path: context.path,
127
+ changeType: 'removed-enum-value',
128
+ severity: 'BREAKING',
129
+ description: `Enum value removed from ${context.method.toUpperCase()} ${context.path}: ${fieldPath} no longer allows '${value}'`,
130
+ affectedField: fieldPath,
131
+ oldValue: value,
132
+ }));
133
+ }
134
+ const addedValues = newEnum.filter((value) => !oldEnum.includes(value));
135
+ for (const value of addedValues) {
136
+ changes.push(createEndpointChange({
137
+ method: context.method,
138
+ path: context.path,
139
+ changeType: 'added-enum-value',
140
+ severity: 'SAFE',
141
+ description: `Enum value added for ${context.method.toUpperCase()} ${context.path}: ${fieldPath} now allows '${value}'`,
142
+ affectedField: fieldPath,
143
+ newValue: value,
144
+ }));
145
+ }
146
+ return changes;
147
+ }
148
+ function compareSchemasRecursive(oldSchema, newSchema, context, fieldPath) {
149
+ var _a, _b, _c;
150
+ if (fieldPath === void 0) { fieldPath = (_a = context.location) !== null && _a !== void 0 ? _a : 'body'; }
151
+ if (!oldSchema || !newSchema)
152
+ return [];
153
+ const changes = [];
154
+ const oldType = getSchemaType(oldSchema);
155
+ const newType = getSchemaType(newSchema);
156
+ if (oldType && newType && oldType !== newType) {
157
+ changes.push(createEndpointChange({
158
+ method: context.method,
159
+ path: context.path,
160
+ changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
161
+ severity: 'BREAKING',
162
+ description: `Schema type changed for ${context.method.toUpperCase()} ${context.path}: ${fieldPath} (${oldType} → ${newType})`,
163
+ affectedField: fieldPath,
164
+ oldValue: oldType,
165
+ newValue: newType,
166
+ }));
167
+ return changes;
168
+ }
169
+ changes.push(...compareEnumValues(oldSchema, newSchema, context, fieldPath));
170
+ if (oldSchema.nullable !== true && newSchema.nullable === true) {
171
+ changes.push(createEndpointChange({
172
+ method: context.method,
173
+ path: context.path,
174
+ changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
175
+ severity: 'DANGEROUS',
176
+ description: `Field became nullable in ${context.method.toUpperCase()} ${context.path}: ${fieldPath}`,
177
+ affectedField: fieldPath,
178
+ oldValue: 'non-nullable',
179
+ newValue: 'nullable',
180
+ }));
181
+ }
182
+ if (oldSchema.default !== undefined && newSchema.default !== undefined && oldSchema.default !== newSchema.default) {
183
+ changes.push(createEndpointChange({
184
+ method: context.method,
185
+ path: context.path,
186
+ changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
187
+ severity: 'DANGEROUS',
188
+ description: `Default value changed in ${context.method.toUpperCase()} ${context.path}: ${fieldPath}`,
189
+ affectedField: fieldPath,
190
+ oldValue: JSON.stringify(oldSchema.default),
191
+ newValue: JSON.stringify(newSchema.default),
192
+ }));
193
+ }
194
+ const oldProperties = ((_b = oldSchema.properties) !== null && _b !== void 0 ? _b : {});
195
+ const newProperties = ((_c = newSchema.properties) !== null && _c !== void 0 ? _c : {});
196
+ const oldRequired = getRequiredFields(oldSchema);
197
+ const newRequired = getRequiredFields(newSchema);
198
+ for (const [propName, propSchema] of Object.entries(oldProperties)) {
199
+ const nestedPath = fieldPath === 'body' ? propName : `${fieldPath}.${propName}`;
200
+ const newPropSchema = newProperties[propName];
201
+ if (!newPropSchema) {
202
+ changes.push(createEndpointChange({
203
+ method: context.method,
204
+ path: context.path,
205
+ changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
206
+ severity: 'BREAKING',
207
+ description: describeFieldRemoval(context, nestedPath),
208
+ affectedField: nestedPath,
209
+ }));
210
+ continue;
211
+ }
212
+ changes.push(...compareSchemasRecursive(propSchema, newPropSchema, context, nestedPath));
213
+ }
214
+ for (const [propName] of Object.entries(newProperties)) {
215
+ if (oldProperties[propName])
216
+ continue;
217
+ const nestedPath = fieldPath === 'body' ? propName : `${fieldPath}.${propName}`;
218
+ const requiredAdded = newRequired.has(propName) && !oldRequired.has(propName);
219
+ const severity = context.kind === 'response' ? 'SAFE' : requiredAdded ? 'BREAKING' : 'SAFE';
220
+ changes.push(createEndpointChange({
221
+ method: context.method,
222
+ path: context.path,
223
+ changeType: context.kind === 'response' ? 'changed-response-schema' : 'changed-schema',
224
+ severity,
225
+ description: `${requiredAdded ? 'Required' : 'Optional'} field added to ${context.kind} schema for ${context.method.toUpperCase()} ${context.path}: ${nestedPath}`,
226
+ affectedField: nestedPath,
227
+ }));
228
+ }
229
+ const oldItems = oldSchema.type === 'array' ? schemaObject(oldSchema.items) : undefined;
230
+ const newItems = newSchema.type === 'array' ? schemaObject(newSchema.items) : undefined;
231
+ if (oldItems && newItems) {
232
+ changes.push(...compareSchemasRecursive(oldItems, newItems, context, `${fieldPath}[]`));
233
+ }
234
+ return changes;
235
+ }
236
+ function getRequestBodySchema(op) {
237
+ var _a;
238
+ const requestBody = op.requestBody;
239
+ const content = (_a = requestBody === null || requestBody === void 0 ? void 0 : requestBody.content) !== null && _a !== void 0 ? _a : {};
240
+ for (const media of Object.values(content)) {
241
+ const schema = schemaObject(media.schema);
242
+ if (schema)
243
+ return schema;
244
+ }
245
+ return undefined;
246
+ }
247
+ function getResponseSchema(op, code) {
248
+ var _a, _b;
249
+ const response = (_a = op.responses) === null || _a === void 0 ? void 0 : _a[code];
250
+ const content = (_b = response === null || response === void 0 ? void 0 : response.content) !== null && _b !== void 0 ? _b : {};
251
+ for (const media of Object.values(content)) {
252
+ const schema = schemaObject(media.schema);
253
+ if (schema)
254
+ return schema;
255
+ }
256
+ return undefined;
257
+ }
80
258
  // ─── Spec comparison ──────────────────────────────────────────────────────────
81
259
  /**
82
260
  * Load and dereference an OpenAPI/Swagger spec from a file path.
@@ -89,7 +267,7 @@ async function loadSpec(specPath) {
89
267
  * Compare two API specs and return a list of detected changes.
90
268
  */
91
269
  function compareSpecs(oldApi, newApi) {
92
- var _a, _b;
270
+ var _a, _b, _c, _d;
93
271
  const changes = [];
94
272
  const oldEndpoints = extractEndpoints(oldApi);
95
273
  const newEndpoints = extractEndpoints(newApi);
@@ -101,99 +279,134 @@ function compareSpecs(oldApi, newApi) {
101
279
  for (const ep of newEndpoints) {
102
280
  newMap.set(`${ep.method}:${ep.path}`, ep);
103
281
  }
104
- // Removed endpoints (breaking)
105
282
  for (const [key, oldEp] of oldMap) {
106
283
  if (!newMap.has(key)) {
107
- changes.push({
284
+ const removedDeprecated = oldEp.operation.deprecated === true;
285
+ changes.push(createEndpointChange({
108
286
  method: oldEp.method,
109
287
  path: oldEp.path,
110
- changeType: 'removed',
111
- breaking: true,
288
+ changeType: removedDeprecated ? 'removed-deprecated' : 'removed',
289
+ severity: 'BREAKING',
112
290
  description: `Endpoint removed: ${oldEp.method.toUpperCase()} ${oldEp.path}`,
113
- });
291
+ }));
114
292
  }
115
293
  }
116
- // Added endpoints (non-breaking)
117
294
  for (const [key, newEp] of newMap) {
118
295
  if (!oldMap.has(key)) {
119
- changes.push({
296
+ changes.push(createEndpointChange({
120
297
  method: newEp.method,
121
298
  path: newEp.path,
122
299
  changeType: 'added',
123
- breaking: false,
300
+ severity: 'SAFE',
124
301
  description: `Endpoint added: ${newEp.method.toUpperCase()} ${newEp.path}`,
125
- });
302
+ }));
126
303
  }
127
304
  }
128
- // Changed endpoints
129
305
  for (const [key, oldEp] of oldMap) {
130
306
  const newEp = newMap.get(key);
131
307
  if (!newEp)
132
308
  continue;
133
- // Check response codes
309
+ if (!oldEp.operation.deprecated && newEp.operation.deprecated) {
310
+ changes.push(createEndpointChange({
311
+ method: oldEp.method,
312
+ path: oldEp.path,
313
+ changeType: 'deprecated',
314
+ severity: 'SAFE',
315
+ description: `Endpoint deprecated: ${oldEp.method.toUpperCase()} ${oldEp.path}`,
316
+ }));
317
+ }
318
+ const oldSecurity = getOperationSecurity(oldApi, oldEp.operation);
319
+ const newSecurity = getOperationSecurity(newApi, newEp.operation);
320
+ if (JSON.stringify(oldSecurity) !== JSON.stringify(newSecurity)) {
321
+ const oldPublic = isPublicEndpoint(oldSecurity);
322
+ const newPublic = isPublicEndpoint(newSecurity);
323
+ changes.push(createEndpointChange({
324
+ method: oldEp.method,
325
+ path: oldEp.path,
326
+ changeType: 'changed-auth-requirement',
327
+ severity: oldPublic && !newPublic ? 'BREAKING' : 'SAFE',
328
+ description: `Authentication requirement changed for ${oldEp.method.toUpperCase()} ${oldEp.path}`,
329
+ oldValue: oldPublic ? 'public' : 'authenticated',
330
+ newValue: newPublic ? 'public' : 'authenticated',
331
+ }));
332
+ }
134
333
  const oldCodes = new Set(getResponseCodes(oldEp.operation));
135
334
  const newCodes = new Set(getResponseCodes(newEp.operation));
136
335
  const removedCodes = [...oldCodes].filter((c) => !newCodes.has(c));
137
336
  if (removedCodes.length > 0) {
138
- changes.push({
337
+ changes.push(createEndpointChange({
139
338
  method: oldEp.method,
140
339
  path: oldEp.path,
141
340
  changeType: 'changed-response-codes',
142
- breaking: true,
341
+ severity: 'BREAKING',
143
342
  description: `Response codes removed from ${oldEp.method.toUpperCase()} ${oldEp.path}: ${removedCodes.join(', ')}`,
144
- });
343
+ oldValue: removedCodes.join(', '),
344
+ }));
345
+ }
346
+ for (const responseCode of [...oldCodes].filter((code) => newCodes.has(code))) {
347
+ changes.push(...compareSchemasRecursive(getResponseSchema(oldEp.operation, responseCode), getResponseSchema(newEp.operation, responseCode), { kind: 'response', method: oldEp.method, path: oldEp.path, location: `responses.${responseCode}` }));
145
348
  }
146
- // Check parameters
349
+ changes.push(...compareSchemasRecursive(getRequestBodySchema(oldEp.operation), getRequestBodySchema(newEp.operation), { kind: 'request', method: oldEp.method, path: oldEp.path }));
147
350
  const oldParams = getParameters(oldEp.operation);
148
351
  const newParams = getParameters(newEp.operation);
149
352
  const newParamMap = new Map(newParams.map((p) => [paramKey(p), p]));
150
353
  for (const oldParam of oldParams) {
151
354
  const newParam = newParamMap.get(paramKey(oldParam));
152
355
  if (!newParam) {
153
- // Parameter removed — breaking if it was required
154
- changes.push({
356
+ changes.push(createEndpointChange({
155
357
  method: oldEp.method,
156
358
  path: oldEp.path,
157
359
  changeType: 'changed-parameter',
158
- breaking: oldParam.required === true,
360
+ severity: oldParam.required === true ? 'BREAKING' : 'DANGEROUS',
159
361
  description: `Parameter removed from ${oldEp.method.toUpperCase()} ${oldEp.path}: ${oldParam.in} '${oldParam.name}'${oldParam.required ? ' (required)' : ''}`,
160
- });
362
+ affectedField: `${oldParam.in}.${oldParam.name}`,
363
+ }));
161
364
  continue;
162
365
  }
163
- // Parameter type/format changed — breaking
164
- const oldSchema = ((_a = oldParam.schema) !== null && _a !== void 0 ? _a : {});
165
- const newSchema = ((_b = newParam.schema) !== null && _b !== void 0 ? _b : {});
166
- if (oldSchema['type'] !== newSchema['type'] || oldSchema['format'] !== newSchema['format']) {
167
- changes.push({
366
+ const oldSchema = schemaObject(oldParam.schema);
367
+ const newSchema = schemaObject(newParam.schema);
368
+ if (getSchemaType(oldSchema) !== getSchemaType(newSchema) || (oldSchema === null || oldSchema === void 0 ? void 0 : oldSchema.format) !== (newSchema === null || newSchema === void 0 ? void 0 : newSchema.format)) {
369
+ changes.push(createEndpointChange({
168
370
  method: oldEp.method,
169
371
  path: oldEp.path,
170
- changeType: 'changed-parameter',
171
- breaking: true,
172
- description: `Parameter type/format changed for ${oldEp.method.toUpperCase()} ${oldEp.path}: '${oldParam.name}' (${oldSchema['type']}/${oldSchema['format']} → ${newSchema['type']}/${newSchema['format']})`,
173
- });
372
+ changeType: 'changed-parameter-type',
373
+ severity: 'BREAKING',
374
+ description: `Parameter type/format changed for ${oldEp.method.toUpperCase()} ${oldEp.path}: '${oldParam.name}' (${getSchemaType(oldSchema)}/${oldSchema === null || oldSchema === void 0 ? void 0 : oldSchema.format} → ${getSchemaType(newSchema)}/${newSchema === null || newSchema === void 0 ? void 0 : newSchema.format})`,
375
+ affectedField: `${oldParam.in}.${oldParam.name}`,
376
+ oldValue: `${(_a = getSchemaType(oldSchema)) !== null && _a !== void 0 ? _a : 'unknown'}/${(_b = oldSchema === null || oldSchema === void 0 ? void 0 : oldSchema.format) !== null && _b !== void 0 ? _b : 'none'}`,
377
+ newValue: `${(_c = getSchemaType(newSchema)) !== null && _c !== void 0 ? _c : 'unknown'}/${(_d = newSchema === null || newSchema === void 0 ? void 0 : newSchema.format) !== null && _d !== void 0 ? _d : 'none'}`,
378
+ }));
174
379
  }
175
- // Required flag changed from optional to required — breaking
176
380
  if (!oldParam.required && newParam.required) {
177
- changes.push({
381
+ changes.push(createEndpointChange({
178
382
  method: oldEp.method,
179
383
  path: oldEp.path,
180
- changeType: 'changed-parameter',
181
- breaking: true,
384
+ changeType: 'changed-parameter-required',
385
+ severity: 'BREAKING',
182
386
  description: `Parameter '${oldParam.name}' became required in ${oldEp.method.toUpperCase()} ${oldEp.path}`,
183
- });
387
+ affectedField: `${oldParam.in}.${oldParam.name}`,
388
+ }));
389
+ }
390
+ if (oldSchema && newSchema) {
391
+ changes.push(...compareSchemasRecursive(oldSchema, newSchema, {
392
+ kind: 'parameter',
393
+ method: oldEp.method,
394
+ path: oldEp.path,
395
+ location: `${oldParam.in}.${oldParam.name}`,
396
+ }));
184
397
  }
185
398
  }
186
- // New required parameters added — breaking
187
399
  const oldParamMap = new Map(oldParams.map((p) => [paramKey(p), p]));
188
400
  for (const newParam of newParams) {
189
- if (!oldParamMap.has(paramKey(newParam)) && newParam.required) {
190
- changes.push({
401
+ if (!oldParamMap.has(paramKey(newParam))) {
402
+ changes.push(createEndpointChange({
191
403
  method: newEp.method,
192
404
  path: newEp.path,
193
- changeType: 'changed-parameter',
194
- breaking: true,
195
- description: `Required parameter added to ${newEp.method.toUpperCase()} ${newEp.path}: ${newParam.in} '${newParam.name}'`,
196
- });
405
+ changeType: newParam.required ? 'changed-parameter-required' : 'changed-parameter',
406
+ severity: newParam.required ? 'BREAKING' : 'SAFE',
407
+ description: `${newParam.required ? 'Required' : 'Optional'} parameter added to ${newEp.method.toUpperCase()} ${newEp.path}: ${newParam.in} '${newParam.name}'`,
408
+ affectedField: `${newParam.in}.${newParam.name}`,
409
+ }));
197
410
  }
198
411
  }
199
412
  }
@@ -204,14 +417,16 @@ function compareSpecs(oldApi, newApi) {
204
417
  * Parse Pact-format JSON contract files matching the given glob pattern or directory.
205
418
  */
206
419
  async function parseContractFiles(contractsGlob) {
207
- let pattern = contractsGlob;
208
- // If the provided path is a directory, search recursively for JSON files inside it
209
- if (fs.existsSync(contractsGlob) && fs.statSync(contractsGlob).isDirectory()) {
210
- pattern = path.join(contractsGlob, '**/*.json').replace(/\\/g, '/');
420
+ let patterns = contractsGlob
421
+ .split(',')
422
+ .map((entry) => entry.trim())
423
+ .filter(Boolean);
424
+ if (patterns.length === 1 && fs.existsSync(patterns[0]) && fs.statSync(patterns[0]).isDirectory()) {
425
+ patterns = [path.join(patterns[0], '**/*.json').replace(/\\/g, '/')];
211
426
  }
212
- const files = await (0, fast_glob_1.default)(pattern, { absolute: true });
427
+ const files = await (0, fast_glob_1.default)(patterns, { absolute: true, onlyFiles: true, unique: true });
213
428
  const contracts = [];
214
- for (const filePath of files) {
429
+ for (const filePath of files.filter((filePath) => filePath.endsWith('.json'))) {
215
430
  try {
216
431
  const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
217
432
  const contract = parsePactContract(raw, filePath);
@@ -263,33 +478,20 @@ function parsePactContract(raw, filePath) {
263
478
  return { consumer, provider, interactions, filePath };
264
479
  }
265
480
  // ─── Contract verification ────────────────────────────────────────────────────
266
- /**
267
- * Normalise a path template by replacing Pact-style `:param` segments with
268
- * OpenAPI-style `{param}` segments so the two formats can be compared.
269
- */
270
481
  function normalisePath(p) {
271
482
  return p.replace(/:([^/]+)/g, '{$1}');
272
483
  }
273
- /**
274
- * Check whether a path from a contract matches an OpenAPI spec path,
275
- * accounting for path parameter placeholders in different styles.
276
- */
277
484
  function pathMatchesSpec(contractPath, specPath) {
278
485
  const normalised = normalisePath(contractPath);
279
486
  if (normalised === specPath)
280
487
  return true;
281
- // Build a regex from the spec path template and test the contract path
282
488
  const regexStr = '^' + specPath.replace(/\{[^}]+\}/g, '[^/]+') + '$';
283
489
  return new RegExp(regexStr).test(contractPath);
284
490
  }
285
- /**
286
- * Verify consumer contracts against a new API spec.
287
- */
288
491
  function verifyContracts(contracts, newApi) {
289
492
  const endpoints = extractEndpoints(newApi);
290
493
  return contracts.map((contract) => {
291
494
  const interactionResults = contract.interactions.map((interaction) => {
292
- // Find matching endpoint in spec
293
495
  const match = endpoints.find((ep) => ep.method === interaction.method && pathMatchesSpec(interaction.path, ep.path));
294
496
  if (!match) {
295
497
  return {
@@ -298,11 +500,9 @@ function verifyContracts(contracts, newApi) {
298
500
  reason: `Endpoint not found in new spec: ${interaction.method.toUpperCase()} ${interaction.path}`,
299
501
  };
300
502
  }
301
- // Check expected response status code is present in spec
302
503
  const responseCodes = getResponseCodes(match.operation);
303
504
  const statusStr = String(interaction.expectedStatus);
304
- if (!responseCodes.includes(statusStr) &&
305
- !responseCodes.includes('default')) {
505
+ if (!responseCodes.includes(statusStr) && !responseCodes.includes('default')) {
306
506
  return {
307
507
  interaction,
308
508
  passed: false,
@@ -316,23 +516,18 @@ function verifyContracts(contracts, newApi) {
316
516
  });
317
517
  }
318
518
  // ─── Metrics ──────────────────────────────────────────────────────────────────
319
- /**
320
- * Compute compatibility percentage based on detected breaking changes.
321
- */
322
519
  function computeCompatibilityPercent(totalOldEndpoints, breakingChanges) {
323
520
  if (totalOldEndpoints === 0)
324
521
  return 100;
325
- // Count uniquely affected old endpoints (by method+path)
522
+ const unaffected = countCompatibleEndpoints(totalOldEndpoints, breakingChanges);
523
+ return parseFloat(((unaffected / totalOldEndpoints) * 100).toFixed(2));
524
+ }
525
+ function countCompatibleEndpoints(totalOldEndpoints, breakingChanges) {
326
526
  const affectedKeys = new Set(breakingChanges
327
527
  .filter((c) => c.changeType !== 'added')
328
528
  .map((c) => `${c.method}:${c.path}`));
329
- const unaffected = Math.max(0, totalOldEndpoints - affectedKeys.size);
330
- return parseFloat(((unaffected / totalOldEndpoints) * 100).toFixed(2));
529
+ return Math.max(0, totalOldEndpoints - affectedKeys.size);
331
530
  }
332
- /**
333
- * Compute what fraction of new spec endpoints are covered by at least one
334
- * contract interaction.
335
- */
336
531
  function computeContractCoveragePercent(contracts, newApi) {
337
532
  const endpoints = extractEndpoints(newApi);
338
533
  const coveredKeys = new Set();
@@ -348,14 +543,69 @@ function computeContractCoveragePercent(contracts, newApi) {
348
543
  const coveragePercent = total === 0 ? 100 : parseFloat(((covered / total) * 100).toFixed(2));
349
544
  return { coveredEndpoints: covered, totalEndpoints: total, coveragePercent };
350
545
  }
546
+ function buildSeveritySummary(changes) {
547
+ return changes.reduce((summary, change) => {
548
+ switch (change.severity) {
549
+ case 'BREAKING':
550
+ summary.breaking += 1;
551
+ break;
552
+ case 'DANGEROUS':
553
+ summary.dangerous += 1;
554
+ break;
555
+ case 'SAFE':
556
+ summary.safe += 1;
557
+ break;
558
+ case 'INFO':
559
+ summary.info += 1;
560
+ break;
561
+ }
562
+ return summary;
563
+ }, { breaking: 0, dangerous: 0, safe: 0, info: 0 });
564
+ }
565
+ function buildPerConsumerCoverage(verificationResults) {
566
+ return verificationResults.map((result) => {
567
+ const interactionCount = result.interactionResults.length;
568
+ const passingInteractions = result.interactionResults.filter((entry) => entry.passed).length;
569
+ const coveragePercent = interactionCount === 0 ? 100 : parseFloat(((passingInteractions / interactionCount) * 100).toFixed(2));
570
+ return {
571
+ consumerName: result.contract.consumer,
572
+ interactionCount,
573
+ passingInteractions,
574
+ coveragePercent,
575
+ };
576
+ });
577
+ }
578
+ function buildDeployBlockers(report) {
579
+ const blockers = [];
580
+ if (report.breakingChanges.some((c) => c.severity === 'BREAKING')) {
581
+ blockers.push('BREAKING API changes detected');
582
+ }
583
+ if (report.contractVerificationResults.some((r) => !r.passed)) {
584
+ blockers.push('One or more consumer contract verifications failed');
585
+ }
586
+ if (report.schemaEvolutionResults.some((r) => r.overallRisk === 'BREAKING')) {
587
+ blockers.push('Breaking schema evolution detected');
588
+ }
589
+ return blockers;
590
+ }
591
+ function computeCanDeploy(report) {
592
+ if (report.breakingChanges.some((c) => c.severity === 'BREAKING'))
593
+ return false;
594
+ if (report.contractVerificationResults.some((r) => !r.passed))
595
+ return false;
596
+ if (report.schemaEvolutionResults.some((r) => r.overallRisk === 'BREAKING'))
597
+ return false;
598
+ return true;
599
+ }
351
600
  // ─── Report builder ───────────────────────────────────────────────────────────
352
- function buildCompatibilityReport(oldApi, newApi, changes, verificationResults, oldSpecPath, newSpecPath) {
601
+ function buildCompatibilityReport(oldApi, newApi, changes, verificationResults, oldSpecPath, newSpecPath, options = {}) {
602
+ var _a, _b, _c;
353
603
  const oldEndpoints = extractEndpoints(oldApi);
354
604
  const breakingChanges = changes.filter((c) => c.breaking);
355
605
  const nonBreakingChanges = changes.filter((c) => !c.breaking);
356
606
  const compatibilityPercent = computeCompatibilityPercent(oldEndpoints.length, breakingChanges);
357
607
  const coverageMetrics = computeContractCoveragePercent(verificationResults.map((r) => r.contract), newApi);
358
- return {
608
+ const report = {
359
609
  generatedAt: new Date().toISOString(),
360
610
  oldSpecPath,
361
611
  newSpecPath,
@@ -367,12 +617,35 @@ function buildCompatibilityReport(oldApi, newApi, changes, verificationResults,
367
617
  totalNewEndpoints: coverageMetrics.totalEndpoints,
368
618
  contractCoveredEndpoints: coverageMetrics.coveredEndpoints,
369
619
  contractCoveragePercent: coverageMetrics.coveragePercent,
620
+ severitySummary: buildSeveritySummary(changes),
621
+ springCloudContracts: (_a = options.springCloudContracts) !== null && _a !== void 0 ? _a : [],
622
+ schemaEvolutionResults: (_b = options.schemaEvolutionResults) !== null && _b !== void 0 ? _b : [],
623
+ matrix: (_c = options.matrix) !== null && _c !== void 0 ? _c : null,
624
+ canDeploy: true,
625
+ deployBlockers: [],
626
+ perConsumerCoverage: buildPerConsumerCoverage(verificationResults),
370
627
  };
628
+ report.canDeploy = computeCanDeploy(report);
629
+ report.deployBlockers = buildDeployBlockers(report);
630
+ return report;
371
631
  }
372
632
  // ─── Report writers ───────────────────────────────────────────────────────────
373
633
  function writeCompatibilityJson(report, reportsDir) {
374
- const outPath = path.join(reportsDir, 'compatibility-contracts.json');
375
- fs.writeFileSync(outPath, JSON.stringify(report, null, 2), 'utf-8');
634
+ const payload = {
635
+ ...report,
636
+ items: [...report.breakingChanges, ...report.nonBreakingChanges].map((change) => ({
637
+ id: `${change.method.toUpperCase()} ${change.path} (${change.changeType})`,
638
+ covered: !change.breaking,
639
+ description: change.description,
640
+ tests: change.affectedConsumers,
641
+ })),
642
+ coveragePercent: report.compatibilityPercent,
643
+ coveredItems: countCompatibleEndpoints(report.totalOldEndpoints, report.breakingChanges),
644
+ totalItems: report.totalOldEndpoints,
645
+ };
646
+ for (const fileName of ['compatibility-contracts.json', 'compatibility-coverage.json']) {
647
+ fs.writeFileSync(path.join(reportsDir, fileName), JSON.stringify(payload, null, 2), 'utf-8');
648
+ }
376
649
  }
377
650
  function writeCompatibilityHtml(report, reportsDir) {
378
651
  const breakingRows = report.breakingChanges
@@ -380,6 +653,7 @@ function writeCompatibilityHtml(report, reportsDir) {
380
653
  <td>${c.method.toUpperCase()}</td>
381
654
  <td>${c.path}</td>
382
655
  <td>${c.changeType}</td>
656
+ <td>${c.severity}</td>
383
657
  <td>${c.description}</td>
384
658
  </tr>`)
385
659
  .join('\n');
@@ -388,6 +662,7 @@ function writeCompatibilityHtml(report, reportsDir) {
388
662
  <td>${c.method.toUpperCase()}</td>
389
663
  <td>${c.path}</td>
390
664
  <td>${c.changeType}</td>
665
+ <td>${c.severity}</td>
391
666
  <td>${c.description}</td>
392
667
  </tr>`)
393
668
  .join('\n');
@@ -404,6 +679,16 @@ function writeCompatibilityHtml(report, reportsDir) {
404
679
  </tr>`;
405
680
  }))
406
681
  .join('\n');
682
+ const schemaRows = report.schemaEvolutionResults
683
+ .flatMap((result) => result.changes.map((change) => ` <tr class="${change.breaking ? 'fail' : 'pass'}">
684
+ <td>${result.topicOrQueue}</td>
685
+ <td>${result.format}</td>
686
+ <td>${change.field}</td>
687
+ <td>${change.changeType}</td>
688
+ <td>${change.breaking ? 'BREAKING' : 'SAFE'}</td>
689
+ <td>${change.fixHint}</td>
690
+ </tr>`))
691
+ .join('\n');
407
692
  const compatColor = report.compatibilityPercent >= 80
408
693
  ? '#e6ffe6'
409
694
  : report.compatibilityPercent >= 50
@@ -424,7 +709,7 @@ function writeCompatibilityHtml(report, reportsDir) {
424
709
  h1 { margin-bottom: 0.25rem; }
425
710
  h2 { margin-top: 2rem; }
426
711
  .meta { color: #666; font-size: 0.9rem; margin-bottom: 1.5rem; }
427
- table { border-collapse: collapse; width: 100%; max-width: 1000px; margin-bottom: 1.5rem; }
712
+ table { border-collapse: collapse; width: 100%; max-width: 1100px; margin-bottom: 1.5rem; }
428
713
  th, td { border: 1px solid #ccc; padding: 0.5rem 1rem; text-align: left; }
429
714
  th { background: #f0f0f0; }
430
715
  tr.breaking { background: #ffe6e6; }
@@ -447,6 +732,9 @@ function writeCompatibilityHtml(report, reportsDir) {
447
732
  Contract Coverage: ${report.contractCoveragePercent}%
448
733
  (${report.contractCoveredEndpoints}/${report.totalNewEndpoints} endpoints)
449
734
  </div>
735
+ <div class="summary-box" style="background:${report.canDeploy ? '#e6ffe6' : '#ffe6e6'}">
736
+ Can Deploy: ${report.canDeploy ? 'YES' : 'NO'}
737
+ </div>
450
738
  </div>
451
739
 
452
740
  <h2>Breaking Changes</h2>
@@ -454,7 +742,7 @@ function writeCompatibilityHtml(report, reportsDir) {
454
742
  ? '<p>No breaking changes detected. ✅</p>'
455
743
  : `<table>
456
744
  <thead>
457
- <tr><th>Method</th><th>Path</th><th>Change Type</th><th>Description</th></tr>
745
+ <tr><th>Method</th><th>Path</th><th>Change Type</th><th>Severity</th><th>Description</th></tr>
458
746
  </thead>
459
747
  <tbody>
460
748
  ${breakingRows}
@@ -466,7 +754,7 @@ ${breakingRows}
466
754
  ? '<p>No non-breaking changes detected.</p>'
467
755
  : `<table>
468
756
  <thead>
469
- <tr><th>Method</th><th>Path</th><th>Change Type</th><th>Description</th></tr>
757
+ <tr><th>Method</th><th>Path</th><th>Change Type</th><th>Severity</th><th>Description</th></tr>
470
758
  </thead>
471
759
  <tbody>
472
760
  ${nonBreakingRows}
@@ -484,14 +772,28 @@ ${nonBreakingRows}
484
772
  ${contractRows}
485
773
  </tbody>
486
774
  </table>`}
775
+
776
+ <h2>Schema Evolution</h2>
777
+ ${schemaRows.length === 0
778
+ ? '<p>No schema evolution changes detected.</p>'
779
+ : `<table>
780
+ <thead>
781
+ <tr><th>Topic / Queue</th><th>Format</th><th>Field</th><th>Change Type</th><th>Risk</th><th>Fix Hint</th></tr>
782
+ </thead>
783
+ <tbody>
784
+ ${schemaRows}
785
+ </tbody>
786
+ </table>`}
787
+
788
+ ${report.deployBlockers.length === 0
789
+ ? '<p>No deployment blockers.</p>'
790
+ : `<h2>Deploy Blockers</h2><ul>${report.deployBlockers.map((blocker) => `<li>${blocker}</li>`).join('')}</ul>`}
487
791
  </body>
488
792
  </html>`;
489
- const outPath = path.join(reportsDir, 'compatibility-contracts.html');
490
- fs.writeFileSync(outPath, html, 'utf-8');
793
+ for (const fileName of ['compatibility-contracts.html', 'compatibility-coverage.html']) {
794
+ fs.writeFileSync(path.join(reportsDir, fileName), html, 'utf-8');
795
+ }
491
796
  }
492
- /**
493
- * Generate JSON and HTML compatibility reports.
494
- */
495
797
  function generateCompatibilityReports(report, reportsDir) {
496
798
  if (!fs.existsSync(reportsDir)) {
497
799
  fs.mkdirSync(reportsDir, { recursive: true });