@workos/oagen 0.1.1 → 0.3.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.
- package/dist/cli/index.d.mts +2 -0
- package/dist/cli/index.mjs +6233 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/compat/{index.d.ts → index.d.mts} +43 -114
- package/dist/compat/index.d.mts.map +1 -0
- package/dist/compat/index.mjs +4562 -0
- package/dist/compat/index.mjs.map +1 -0
- package/dist/diff-Cn6S74vV.mjs +2432 -0
- package/dist/diff-Cn6S74vV.mjs.map +1 -0
- package/dist/differ-B6472Yef.mjs +1100 -0
- package/dist/differ-B6472Yef.mjs.map +1 -0
- package/dist/index.d.mts +324 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1553 -0
- package/dist/index.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.d.mts +1 -0
- package/dist/scripts/smoke/baseline.mjs +1951 -0
- package/dist/scripts/smoke/baseline.mjs.map +1 -0
- package/dist/scripts/smoke/sdk-test.d.mts +1 -0
- package/dist/scripts/smoke/sdk-test.mjs +111 -0
- package/dist/scripts/smoke/sdk-test.mjs.map +1 -0
- package/dist/smoke.d.mts +331 -0
- package/dist/smoke.d.mts.map +1 -0
- package/dist/smoke.mjs +2119 -0
- package/dist/smoke.mjs.map +1 -0
- package/dist/tree-sitter-DFRXfWg9.mjs +394 -0
- package/dist/tree-sitter-DFRXfWg9.mjs.map +1 -0
- package/dist/types-O25qZxMl.d.mts +397 -0
- package/dist/types-O25qZxMl.d.mts.map +1 -0
- package/dist/verify/index.d.mts +82 -0
- package/dist/verify/index.d.mts.map +1 -0
- package/dist/verify/index.mjs +327 -0
- package/dist/verify/index.mjs.map +1 -0
- package/package.json +19 -16
- package/dist/chunk-2JXFTDTO.js +0 -1669
- package/dist/chunk-CIX5NOBF.js +0 -263
- package/dist/chunk-IJ5E5AE7.js +0 -1040
- package/dist/cli/index.js +0 -5322
- package/dist/compat/index.js +0 -4491
- package/dist/index.d.ts +0 -277
- package/dist/index.js +0 -1565
- package/dist/scripts/smoke/baseline.js +0 -1813
- package/dist/scripts/smoke/sdk-test.js +0 -93
- package/dist/smoke.js +0 -2025
- package/dist/types-B5t9O9Wm.d.ts +0 -367
- package/dist/verify/index.d.ts +0 -76
- package/dist/verify/index.js +0 -336
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
import { A as walkTypeRef, f as toSnakeCase, o as splitWords, x as RegistryError } from "./tree-sitter-DFRXfWg9.mjs";
|
|
2
|
+
//#region src/compat/extractor-registry.ts
|
|
3
|
+
const extractors = /* @__PURE__ */ new Map();
|
|
4
|
+
function registerExtractor(extractor) {
|
|
5
|
+
extractors.set(extractor.language, extractor);
|
|
6
|
+
}
|
|
7
|
+
function getExtractor(language) {
|
|
8
|
+
const extractor = extractors.get(language);
|
|
9
|
+
if (!extractor) throw new RegistryError(`No extractor registered for language: ${language}. Available: ${[...extractors.keys()].join(", ") || "(none)"}`, `Register an extractor for "${language}" via registerExtractor() or add one in your oagen.config.ts plugin.`);
|
|
10
|
+
return extractor;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/compat/language-hints.ts
|
|
14
|
+
/** Untyped map patterns — hoisted to avoid allocation on every isTypeEquivalent call. */
|
|
15
|
+
const UNTYPED_MAP_PATTERNS = new Set([
|
|
16
|
+
"Record<string, unknown>",
|
|
17
|
+
"Record<string, any>",
|
|
18
|
+
"{ [key: string]: any; }",
|
|
19
|
+
"{ [key: string]: unknown; }",
|
|
20
|
+
"any"
|
|
21
|
+
]);
|
|
22
|
+
const NAMED_TYPE_RE = /^[A-Z][a-zA-Z0-9]*$/;
|
|
23
|
+
/** Check whether a type name exists as an interface, class, or enum in a surface. */
|
|
24
|
+
function typeExistsInSurface(name, surface) {
|
|
25
|
+
return !!(surface.interfaces[name] || surface.classes[name] || surface.enums[name]);
|
|
26
|
+
}
|
|
27
|
+
/** Split a PascalCase string into words, keeping only words > 2 chars. */
|
|
28
|
+
function splitPascalWords(s) {
|
|
29
|
+
return splitWords(s).filter((w) => w.length > 2);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check whether two named types share enough PascalCase word components to
|
|
33
|
+
* be considered equivalent (handles name reordering from Json merges, etc.).
|
|
34
|
+
* Both names have `Response$` stripped before comparison.
|
|
35
|
+
*/
|
|
36
|
+
function namedTypeWordsOverlap(a, b, minOverlap = 1) {
|
|
37
|
+
const aNoResp = a.replace(/Response$/, "");
|
|
38
|
+
const bNoResp = b.replace(/Response$/, "");
|
|
39
|
+
const aWords = new Set(splitPascalWords(aNoResp).map((w) => w.toLowerCase()));
|
|
40
|
+
const bWords = new Set(splitPascalWords(bNoResp).map((w) => w.toLowerCase()));
|
|
41
|
+
const overlap = [...aWords].filter((w) => bWords.has(w));
|
|
42
|
+
return overlap.length >= minOverlap && overlap.length >= Math.min(aWords.size, bWords.size) - 1;
|
|
43
|
+
}
|
|
44
|
+
/** Split a pipe-delimited union string into trimmed, non-empty members. */
|
|
45
|
+
function parseUnionMembers(s) {
|
|
46
|
+
return s.split("|").map((p) => p.trim()).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Default implementation of isNullableOnlyDifference — shared across extractors.
|
|
50
|
+
* Returns true when stripping nullable wrappers from both sides yields the same
|
|
51
|
+
* type, but the original types differ (i.e., the only difference is nullability).
|
|
52
|
+
*/
|
|
53
|
+
function defaultIsNullableOnlyDifference(hints, a, b) {
|
|
54
|
+
return (hints.stripNullable(a) ?? a) === (hints.stripNullable(b) ?? b) && a !== b;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Node/TypeScript language hints — reference implementation.
|
|
58
|
+
* Extracted from the hardcoded logic previously in differ.ts and overlay.ts.
|
|
59
|
+
*/
|
|
60
|
+
const nodeHints = {
|
|
61
|
+
stripNullable(type) {
|
|
62
|
+
const members = parseUnionMembers(type);
|
|
63
|
+
const stripped = members.filter((p) => p !== "null");
|
|
64
|
+
if (stripped.length === members.length) return null;
|
|
65
|
+
return stripped.join(" | ");
|
|
66
|
+
},
|
|
67
|
+
isNullableOnlyDifference(a, b) {
|
|
68
|
+
return (nodeHints.stripNullable(a) ?? a) === (nodeHints.stripNullable(b) ?? b) && a !== b;
|
|
69
|
+
},
|
|
70
|
+
isUnionReorder(a, b) {
|
|
71
|
+
const membersA = parseUnionMembers(a).sort();
|
|
72
|
+
const membersB = parseUnionMembers(b).sort();
|
|
73
|
+
if (membersA.length !== membersB.length || membersA.length < 2) return false;
|
|
74
|
+
return membersA.every((m, i) => m === membersB[i]);
|
|
75
|
+
},
|
|
76
|
+
isGenericTypeParam(type) {
|
|
77
|
+
if (/^[A-Z]$/.test(type)) return true;
|
|
78
|
+
if (/^T[A-Z][a-zA-Z]*$/.test(type)) return true;
|
|
79
|
+
if (/^[A-Z]\[\]$/.test(type) || /^T[A-Z][a-zA-Z]*\[\]$/.test(type)) return true;
|
|
80
|
+
return false;
|
|
81
|
+
},
|
|
82
|
+
isExtractionArtifact(type) {
|
|
83
|
+
return type === "any" || type === "Record<string, unknown>";
|
|
84
|
+
},
|
|
85
|
+
tolerateCategoryMismatch: true,
|
|
86
|
+
extractReturnTypeName(returnType) {
|
|
87
|
+
let inner = returnType;
|
|
88
|
+
while (inner.startsWith("Promise<") && inner.endsWith(">")) inner = inner.slice(8, -1);
|
|
89
|
+
const genericMatch = inner.match(/^[A-Za-z]+<(.+)>$/);
|
|
90
|
+
if (genericMatch) inner = genericMatch[1];
|
|
91
|
+
inner = inner.replace(/\[\]$/, "");
|
|
92
|
+
if ([
|
|
93
|
+
"void",
|
|
94
|
+
"string",
|
|
95
|
+
"number",
|
|
96
|
+
"boolean",
|
|
97
|
+
"any",
|
|
98
|
+
"unknown",
|
|
99
|
+
"null",
|
|
100
|
+
"undefined"
|
|
101
|
+
].includes(inner)) return null;
|
|
102
|
+
return inner;
|
|
103
|
+
},
|
|
104
|
+
extractParamTypeName(paramType) {
|
|
105
|
+
if ([
|
|
106
|
+
"string",
|
|
107
|
+
"number",
|
|
108
|
+
"boolean",
|
|
109
|
+
"any",
|
|
110
|
+
"unknown"
|
|
111
|
+
].includes(paramType)) return null;
|
|
112
|
+
return paramType;
|
|
113
|
+
},
|
|
114
|
+
propertyMatchesClass(propertyName, className) {
|
|
115
|
+
return propertyName.replace(/[-_]/g, "").toLowerCase() === className.toLowerCase();
|
|
116
|
+
},
|
|
117
|
+
derivedModelNames(modelName) {
|
|
118
|
+
return [`${modelName}Response`, `Serialized${modelName}`];
|
|
119
|
+
},
|
|
120
|
+
isTypeEquivalent(baselineType, candidateType, candidateSurface) {
|
|
121
|
+
const candEnum = candidateSurface.enums[candidateType];
|
|
122
|
+
if (candEnum) {
|
|
123
|
+
const enumValuesSet = new Set(Object.values(candEnum.members).flatMap((v) => [`"${v}"`, `'${v}'`]));
|
|
124
|
+
const baseMembers = parseUnionMembers(baselineType);
|
|
125
|
+
if (baseMembers.length > 0 && baseMembers.every((m) => enumValuesSet.has(m))) return true;
|
|
126
|
+
}
|
|
127
|
+
if (UNTYPED_MAP_PATTERNS.has(baselineType) && UNTYPED_MAP_PATTERNS.has(candidateType)) return true;
|
|
128
|
+
if (baselineType.startsWith("{") && baselineType.endsWith("}")) {
|
|
129
|
+
if (typeExistsInSurface(candidateType, candidateSurface)) return true;
|
|
130
|
+
}
|
|
131
|
+
const baseClean = baselineType.replace(/\[\]$/, "");
|
|
132
|
+
const candClean = candidateType.replace(/\[\]$/, "");
|
|
133
|
+
const sameArrayness = baselineType.endsWith("[]") === candidateType.endsWith("[]");
|
|
134
|
+
if (sameArrayness) {
|
|
135
|
+
if (candClean === baseClean + "Response" || baseClean === candClean + "Response") return true;
|
|
136
|
+
}
|
|
137
|
+
if (sameArrayness && NAMED_TYPE_RE.test(baseClean) && NAMED_TYPE_RE.test(candClean)) {
|
|
138
|
+
if (typeExistsInSurface(candClean, candidateSurface)) {
|
|
139
|
+
if (candClean.includes(baseClean) || baseClean.includes(candClean)) return true;
|
|
140
|
+
const baseNoResp = baseClean.replace(/Response$/, "");
|
|
141
|
+
const candNoResp = candClean.replace(/Response$/, "");
|
|
142
|
+
if (candNoResp.includes(baseNoResp) || baseNoResp.includes(candNoResp)) return true;
|
|
143
|
+
if (namedTypeWordsOverlap(baseClean, candClean, 2)) return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Merge partial language hint overrides over nodeHints defaults.
|
|
151
|
+
* Any hint not provided falls back to the Node/TypeScript implementation.
|
|
152
|
+
*/
|
|
153
|
+
function resolveHints(overrides) {
|
|
154
|
+
return {
|
|
155
|
+
...nodeHints,
|
|
156
|
+
...overrides
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/compat/overlay.ts
|
|
161
|
+
/**
|
|
162
|
+
* Find the class name that owns a given SDK resource property.
|
|
163
|
+
* E.g. if the surface has a class "Organizations" with property "organizations",
|
|
164
|
+
* and sdkResourceProperty is "organizations", return "Organizations".
|
|
165
|
+
*/
|
|
166
|
+
function findClassForProperty(surface, sdkResourceProperty, hints) {
|
|
167
|
+
for (const [className, cls] of Object.entries(surface.classes)) {
|
|
168
|
+
if (hints.propertyMatchesClass(sdkResourceProperty, className)) return className;
|
|
169
|
+
for (const [propName, prop] of Object.entries(cls.properties)) if (propName === sdkResourceProperty) {
|
|
170
|
+
const propType = prop.type;
|
|
171
|
+
if (propType && surface.classes[propType]) return propType;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const propWords = splitWords(sdkResourceProperty).map((w) => w.toLowerCase());
|
|
176
|
+
if (propWords.length > 1) for (const [className] of Object.entries(surface.classes)) {
|
|
177
|
+
const classWords = splitWords(className).map((w) => w.toLowerCase());
|
|
178
|
+
if (classWords.length > 0 && classWords.length < propWords.length) {
|
|
179
|
+
const suffix = propWords.slice(propWords.length - classWords.length);
|
|
180
|
+
if (classWords.every((w, i) => w === suffix[i])) return className;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function buildOverlayLookup(surface, manifest, spec, hints, options) {
|
|
185
|
+
const resolvedHints = hints ?? nodeHints;
|
|
186
|
+
const lookup = {
|
|
187
|
+
methodByOperation: /* @__PURE__ */ new Map(),
|
|
188
|
+
httpKeyByMethod: /* @__PURE__ */ new Map(),
|
|
189
|
+
interfaceByName: /* @__PURE__ */ new Map(),
|
|
190
|
+
typeAliasByName: /* @__PURE__ */ new Map(),
|
|
191
|
+
requiredExports: /* @__PURE__ */ new Map(),
|
|
192
|
+
modelNameByIR: /* @__PURE__ */ new Map(),
|
|
193
|
+
fileBySymbol: /* @__PURE__ */ new Map()
|
|
194
|
+
};
|
|
195
|
+
if (manifest) for (const entry of manifest) {
|
|
196
|
+
const key = `${entry.httpMethod.toUpperCase()} ${entry.path}`;
|
|
197
|
+
const className = findClassForProperty(surface, entry.sdkResourceProperty, resolvedHints);
|
|
198
|
+
if (className) {
|
|
199
|
+
let resolvedMethodName = entry.sdkMethodName;
|
|
200
|
+
let methods = surface.classes[className]?.methods[resolvedMethodName];
|
|
201
|
+
if (!methods) {
|
|
202
|
+
const classMethods = surface.classes[className]?.methods ?? {};
|
|
203
|
+
const prefix = resolvedMethodName.toLowerCase();
|
|
204
|
+
const candidates = [];
|
|
205
|
+
for (const [name, overloads] of Object.entries(classMethods)) if (name.toLowerCase().startsWith(prefix) && name !== resolvedMethodName) candidates.push([name, overloads]);
|
|
206
|
+
if (candidates.length === 1) {
|
|
207
|
+
methods = candidates[0][1];
|
|
208
|
+
resolvedMethodName = candidates[0][0];
|
|
209
|
+
} else if (candidates.length > 1) {
|
|
210
|
+
const firstSegment = entry.path.split("/").filter(Boolean)[0]?.replace(/[{}_-]/g, "") ?? "";
|
|
211
|
+
if (firstSegment) {
|
|
212
|
+
const segLower = firstSegment.toLowerCase();
|
|
213
|
+
const segSingular = segLower.endsWith("s") ? segLower.slice(0, -1) : segLower;
|
|
214
|
+
const best = candidates.find(([name]) => {
|
|
215
|
+
const lower = name.toLowerCase();
|
|
216
|
+
return lower === prefix + segLower || lower === prefix + segSingular;
|
|
217
|
+
});
|
|
218
|
+
if (best) {
|
|
219
|
+
methods = best[1];
|
|
220
|
+
resolvedMethodName = best[0];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!methods) {
|
|
224
|
+
const verbMatch = disambiguateByHttpVerb(candidates, entry.httpMethod);
|
|
225
|
+
if (verbMatch) {
|
|
226
|
+
methods = verbMatch[1];
|
|
227
|
+
resolvedMethodName = verbMatch[0];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!methods) {
|
|
233
|
+
const classMethods = surface.classes[className]?.methods ?? {};
|
|
234
|
+
const suffix = resolvedMethodName.toLowerCase();
|
|
235
|
+
const candidates = [];
|
|
236
|
+
for (const [name, overloads] of Object.entries(classMethods)) if (name.toLowerCase().endsWith(suffix) && name !== resolvedMethodName) candidates.push([name, overloads]);
|
|
237
|
+
if (candidates.length === 1) {
|
|
238
|
+
methods = candidates[0][1];
|
|
239
|
+
resolvedMethodName = candidates[0][0];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!methods) {
|
|
243
|
+
const manifestWords = splitCamelCase(resolvedMethodName);
|
|
244
|
+
if (manifestWords.length >= 2) {
|
|
245
|
+
const classMethods = surface.classes[className]?.methods ?? {};
|
|
246
|
+
const candidates = [];
|
|
247
|
+
for (const [name, overloads] of Object.entries(classMethods)) {
|
|
248
|
+
if (name === resolvedMethodName) continue;
|
|
249
|
+
const surfaceWords = splitCamelCase(name);
|
|
250
|
+
if (surfaceWords.length > manifestWords.length && isSubsequence(manifestWords, surfaceWords)) candidates.push([name, overloads]);
|
|
251
|
+
}
|
|
252
|
+
if (candidates.length === 1) {
|
|
253
|
+
methods = candidates[0][1];
|
|
254
|
+
resolvedMethodName = candidates[0][0];
|
|
255
|
+
} else if (candidates.length > 1) {
|
|
256
|
+
const verbMatch = disambiguateByHttpVerb(candidates, entry.httpMethod);
|
|
257
|
+
if (verbMatch) {
|
|
258
|
+
methods = verbMatch[1];
|
|
259
|
+
resolvedMethodName = verbMatch[0];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!methods) {
|
|
265
|
+
const classMethods = surface.classes[className]?.methods ?? {};
|
|
266
|
+
const manifestLower = resolvedMethodName.toLowerCase();
|
|
267
|
+
const candidates = [];
|
|
268
|
+
for (const [name, overloads] of Object.entries(classMethods)) {
|
|
269
|
+
if (name === resolvedMethodName) continue;
|
|
270
|
+
if (manifestLower.startsWith(name.toLowerCase()) && name.length >= 3) candidates.push([name, overloads]);
|
|
271
|
+
}
|
|
272
|
+
if (candidates.length === 1) {
|
|
273
|
+
methods = candidates[0][1];
|
|
274
|
+
resolvedMethodName = candidates[0][0];
|
|
275
|
+
} else if (candidates.length > 1) {
|
|
276
|
+
const verbMatch = disambiguateByHttpVerb(candidates, entry.httpMethod);
|
|
277
|
+
if (verbMatch) {
|
|
278
|
+
methods = verbMatch[1];
|
|
279
|
+
resolvedMethodName = verbMatch[0];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const method = methods?.[0];
|
|
284
|
+
if (method) {
|
|
285
|
+
lookup.methodByOperation.set(key, {
|
|
286
|
+
className,
|
|
287
|
+
methodName: resolvedMethodName,
|
|
288
|
+
params: method.params,
|
|
289
|
+
returnType: method.returnType
|
|
290
|
+
});
|
|
291
|
+
lookup.httpKeyByMethod.set(`${className}.${resolvedMethodName}`, key);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const name of Object.keys(surface.interfaces)) lookup.interfaceByName.set(name, name);
|
|
296
|
+
for (const name of Object.keys(surface.typeAliases)) lookup.typeAliasByName.set(name, name);
|
|
297
|
+
for (const [path, symbols] of Object.entries(surface.exports)) lookup.requiredExports.set(path, new Set(symbols));
|
|
298
|
+
for (const record of [
|
|
299
|
+
surface.classes,
|
|
300
|
+
surface.interfaces,
|
|
301
|
+
surface.typeAliases,
|
|
302
|
+
surface.enums
|
|
303
|
+
]) for (const [name, item] of Object.entries(record)) if (item.sourceFile) lookup.fileBySymbol.set(name, item.sourceFile);
|
|
304
|
+
if (spec) {
|
|
305
|
+
buildModelNameMap(surface, spec, lookup, resolvedHints, options);
|
|
306
|
+
for (const [irName, sdkName] of lookup.modelNameByIR) {
|
|
307
|
+
const filePath = lookup.fileBySymbol.get(sdkName);
|
|
308
|
+
if (filePath) lookup.fileBySymbol.set(irName, filePath);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return lookup;
|
|
312
|
+
}
|
|
313
|
+
/** Map HTTP methods to preferred method name prefixes for disambiguation. */
|
|
314
|
+
const HTTP_VERB_PREFIXES = {
|
|
315
|
+
GET: [
|
|
316
|
+
"list",
|
|
317
|
+
"get",
|
|
318
|
+
"fetch",
|
|
319
|
+
"retrieve",
|
|
320
|
+
"find"
|
|
321
|
+
],
|
|
322
|
+
POST: [
|
|
323
|
+
"create",
|
|
324
|
+
"add",
|
|
325
|
+
"insert",
|
|
326
|
+
"send"
|
|
327
|
+
],
|
|
328
|
+
PUT: [
|
|
329
|
+
"set",
|
|
330
|
+
"update",
|
|
331
|
+
"replace",
|
|
332
|
+
"put"
|
|
333
|
+
],
|
|
334
|
+
PATCH: [
|
|
335
|
+
"update",
|
|
336
|
+
"patch",
|
|
337
|
+
"modify"
|
|
338
|
+
],
|
|
339
|
+
DELETE: [
|
|
340
|
+
"delete",
|
|
341
|
+
"remove",
|
|
342
|
+
"revoke"
|
|
343
|
+
]
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Disambiguate multiple method candidates using HTTP verb semantics.
|
|
347
|
+
* Returns the best match or undefined if no unique match is found.
|
|
348
|
+
*/
|
|
349
|
+
function disambiguateByHttpVerb(candidates, httpMethod) {
|
|
350
|
+
const prefixes = HTTP_VERB_PREFIXES[httpMethod.toUpperCase()];
|
|
351
|
+
if (!prefixes) return void 0;
|
|
352
|
+
const verbCandidates = candidates.filter(([name]) => {
|
|
353
|
+
const lower = name.toLowerCase();
|
|
354
|
+
return prefixes.some((p) => lower.startsWith(p));
|
|
355
|
+
});
|
|
356
|
+
if (verbCandidates.length === 1) return verbCandidates[0];
|
|
357
|
+
}
|
|
358
|
+
/** Split a camelCase identifier into its word parts (e.g., "getByExternalId" → ["get","By","External","Id"]). */
|
|
359
|
+
function splitCamelCase(name) {
|
|
360
|
+
const parts = [];
|
|
361
|
+
let start = 0;
|
|
362
|
+
for (let i = 1; i < name.length; i++) if (name[i] >= "A" && name[i] <= "Z") {
|
|
363
|
+
parts.push(name.slice(start, i));
|
|
364
|
+
start = i;
|
|
365
|
+
}
|
|
366
|
+
parts.push(name.slice(start));
|
|
367
|
+
return parts;
|
|
368
|
+
}
|
|
369
|
+
/** Check if `needle` words form a subsequence of `haystack` words (case-insensitive). */
|
|
370
|
+
function isSubsequence(needle, haystack) {
|
|
371
|
+
let ni = 0;
|
|
372
|
+
for (let hi = 0; hi < haystack.length && ni < needle.length; hi++) if (needle[ni].toLowerCase() === haystack[hi].toLowerCase()) ni++;
|
|
373
|
+
return ni === needle.length;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Normalize a field name to a canonical form for comparison.
|
|
377
|
+
* Handles camelCase → snake_case so "createdAt" and "created_at" match.
|
|
378
|
+
*/
|
|
379
|
+
function normalizeFieldName(name) {
|
|
380
|
+
return toSnakeCase(name);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Compute the Jaccard similarity between two sets of normalized field names.
|
|
384
|
+
* Returns score (0–1) and raw intersection count.
|
|
385
|
+
*/
|
|
386
|
+
function jaccardSimilarity(a, b) {
|
|
387
|
+
if (a.size === 0 && b.size === 0) return {
|
|
388
|
+
score: 0,
|
|
389
|
+
intersection: 0
|
|
390
|
+
};
|
|
391
|
+
let intersection = 0;
|
|
392
|
+
for (const item of a) if (b.has(item)) intersection++;
|
|
393
|
+
const union = a.size + b.size - intersection;
|
|
394
|
+
return {
|
|
395
|
+
score: union === 0 ? 0 : intersection / union,
|
|
396
|
+
intersection
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Build a set of normalized field names from an IR model.
|
|
401
|
+
*/
|
|
402
|
+
function irModelFieldSignature(model) {
|
|
403
|
+
return new Set(model.fields.map((f) => normalizeFieldName(f.name)));
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Build a set of normalized field names from an SDK interface.
|
|
407
|
+
*/
|
|
408
|
+
function sdkInterfaceFieldSignature(iface) {
|
|
409
|
+
return new Set(Object.keys(iface.fields).map(normalizeFieldName));
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Populate the modelNameByIR map using two strategies:
|
|
413
|
+
*
|
|
414
|
+
* 1. Operation-based matching (high confidence):
|
|
415
|
+
* When a manifest maps an HTTP operation to an SDK method, extract the
|
|
416
|
+
* method's return type and parameter types. Match them against the IR
|
|
417
|
+
* operation's response model and request body model.
|
|
418
|
+
*
|
|
419
|
+
* 2. Field-structure matching (fallback):
|
|
420
|
+
* For IR models not matched by operations, compare their field names
|
|
421
|
+
* against all SDK interfaces using Jaccard similarity. A match requires
|
|
422
|
+
* ≥60% field overlap with ≥3 fields in common.
|
|
423
|
+
*/
|
|
424
|
+
function buildModelNameMap(surface, spec, lookup, hints, options) {
|
|
425
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
426
|
+
const usedSdkNames = /* @__PURE__ */ new Set();
|
|
427
|
+
if (lookup.methodByOperation.size > 0) for (const service of spec.services) for (const op of service.operations) {
|
|
428
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
429
|
+
const methodOverlay = lookup.methodByOperation.get(httpKey);
|
|
430
|
+
if (!methodOverlay) continue;
|
|
431
|
+
if (op.response.kind === "model" && !mapped.has(op.response.name)) {
|
|
432
|
+
const sdkTypeName = hints.extractReturnTypeName(methodOverlay.returnType);
|
|
433
|
+
if (sdkTypeName && surface.interfaces[sdkTypeName]) {
|
|
434
|
+
lookup.modelNameByIR.set(op.response.name, sdkTypeName);
|
|
435
|
+
mapped.add(op.response.name);
|
|
436
|
+
usedSdkNames.add(sdkTypeName);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (op.requestBody?.kind === "model" && !mapped.has(op.requestBody.name)) for (const param of methodOverlay.params) {
|
|
440
|
+
const sdkTypeName = hints.extractParamTypeName(param.type);
|
|
441
|
+
if (sdkTypeName && surface.interfaces[sdkTypeName]) {
|
|
442
|
+
lookup.modelNameByIR.set(op.requestBody.name, sdkTypeName);
|
|
443
|
+
mapped.add(op.requestBody.name);
|
|
444
|
+
usedSdkNames.add(sdkTypeName);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const sdkSignatures = [];
|
|
450
|
+
for (const [name, iface] of Object.entries(surface.interfaces)) {
|
|
451
|
+
if (usedSdkNames.has(name)) continue;
|
|
452
|
+
const fields = sdkInterfaceFieldSignature(iface);
|
|
453
|
+
if (fields.size >= 2) sdkSignatures.push({
|
|
454
|
+
name,
|
|
455
|
+
fields,
|
|
456
|
+
fieldCount: fields.size
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const sdkSignatureNames = new Set(sdkSignatures.map((s) => s.name));
|
|
460
|
+
for (const model of spec.models) {
|
|
461
|
+
if (mapped.has(model.name)) continue;
|
|
462
|
+
if (model.fields.length < 2) continue;
|
|
463
|
+
if (sdkSignatureNames.has(model.name) && !usedSdkNames.has(model.name)) {
|
|
464
|
+
lookup.modelNameByIR.set(model.name, model.name);
|
|
465
|
+
mapped.add(model.name);
|
|
466
|
+
usedSdkNames.add(model.name);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const derivedNames = hints.derivedModelNames(model.name);
|
|
470
|
+
for (const derived of derivedNames) if (sdkSignatureNames.has(derived) && !usedSdkNames.has(derived)) {
|
|
471
|
+
lookup.modelNameByIR.set(model.name, derived);
|
|
472
|
+
mapped.add(model.name);
|
|
473
|
+
usedSdkNames.add(derived);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
if (mapped.has(model.name)) continue;
|
|
477
|
+
if (options?.strictModelMatch) continue;
|
|
478
|
+
const irFields = irModelFieldSignature(model);
|
|
479
|
+
let bestMatch = null;
|
|
480
|
+
let bestScore = 0;
|
|
481
|
+
for (const sdk of sdkSignatures) {
|
|
482
|
+
if (usedSdkNames.has(sdk.name)) continue;
|
|
483
|
+
const { score, intersection } = jaccardSimilarity(irFields, sdk.fields);
|
|
484
|
+
const minIntersection = Math.max(3, Math.ceil(irFields.size * .5));
|
|
485
|
+
if (score > bestScore && score >= .6 && intersection >= minIntersection) {
|
|
486
|
+
bestScore = score;
|
|
487
|
+
bestMatch = sdk.name;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (bestMatch) {
|
|
491
|
+
lookup.modelNameByIR.set(model.name, bestMatch);
|
|
492
|
+
mapped.add(model.name);
|
|
493
|
+
usedSdkNames.add(bestMatch);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Patch overlay with violations from a failed verification.
|
|
499
|
+
* Adds explicit name mappings for symbols that were generated with wrong names.
|
|
500
|
+
* Returns a new OverlayLookup (immutable).
|
|
501
|
+
*/
|
|
502
|
+
function patchOverlay(overlay, violations, baseline) {
|
|
503
|
+
const patched = {
|
|
504
|
+
methodByOperation: new Map(overlay.methodByOperation),
|
|
505
|
+
httpKeyByMethod: new Map(overlay.httpKeyByMethod),
|
|
506
|
+
interfaceByName: new Map(overlay.interfaceByName),
|
|
507
|
+
typeAliasByName: new Map(overlay.typeAliasByName),
|
|
508
|
+
requiredExports: new Map(Array.from(overlay.requiredExports.entries()).map(([k, v]) => [k, new Set(v)])),
|
|
509
|
+
modelNameByIR: new Map(overlay.modelNameByIR),
|
|
510
|
+
fileBySymbol: new Map(overlay.fileBySymbol)
|
|
511
|
+
};
|
|
512
|
+
let warnedManifest = false;
|
|
513
|
+
for (const v of violations) {
|
|
514
|
+
if (v.category === "public-api") {
|
|
515
|
+
const parts = v.symbolPath.split(".");
|
|
516
|
+
if (parts.length === 2) {
|
|
517
|
+
const [className, methodName] = parts;
|
|
518
|
+
const baseClass = baseline.classes[className];
|
|
519
|
+
if (baseClass && baseClass.methods[methodName]?.length > 0) {
|
|
520
|
+
const httpKey = overlay.httpKeyByMethod.get(`${className}.${methodName}`);
|
|
521
|
+
if (!httpKey && overlay.httpKeyByMethod.size === 0 && !warnedManifest) {
|
|
522
|
+
console.warn("Warning: No smoke-manifest.json available. Method-level violations cannot be auto-patched. Implement generateManifest in your emitter for the self-correcting loop to resolve these.");
|
|
523
|
+
warnedManifest = true;
|
|
524
|
+
}
|
|
525
|
+
if (httpKey) {
|
|
526
|
+
const method = baseClass.methods[methodName][0];
|
|
527
|
+
patched.methodByOperation.set(httpKey, {
|
|
528
|
+
className,
|
|
529
|
+
methodName,
|
|
530
|
+
params: method.params,
|
|
531
|
+
returnType: method.returnType
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (baseline.interfaces[className]) patched.interfaceByName.set(className, className);
|
|
536
|
+
} else if (parts.length === 1) {
|
|
537
|
+
const name = parts[0];
|
|
538
|
+
if (baseline.interfaces[name]) patched.interfaceByName.set(name, name);
|
|
539
|
+
if (baseline.typeAliases[name]) patched.typeAliasByName.set(name, name);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (v.category === "export-structure") {
|
|
543
|
+
const match = v.symbolPath.match(/^exports\[(.+?)\]\.(.+)$/);
|
|
544
|
+
if (match) {
|
|
545
|
+
const [, path, symbol] = match;
|
|
546
|
+
if (!patched.requiredExports.has(path)) patched.requiredExports.set(path, /* @__PURE__ */ new Set());
|
|
547
|
+
patched.requiredExports.get(path).add(symbol);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return patched;
|
|
552
|
+
}
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/compat/spec-filter.ts
|
|
555
|
+
/**
|
|
556
|
+
* Compute the set of symbol names that are derivable from the OpenAPI spec.
|
|
557
|
+
* Only these names should be compared during compat verification — everything
|
|
558
|
+
* else in the baseline is hand-written and out of scope for generation.
|
|
559
|
+
*/
|
|
560
|
+
function specDerivedNames(spec, hints) {
|
|
561
|
+
const names = /* @__PURE__ */ new Set();
|
|
562
|
+
const topLevelEnumNames = new Set(spec.enums.map((e) => e.name));
|
|
563
|
+
for (const service of spec.services) {
|
|
564
|
+
names.add(service.name);
|
|
565
|
+
for (const op of service.operations) {
|
|
566
|
+
collectTypeRefNames(op.response, names, hints, topLevelEnumNames);
|
|
567
|
+
if (op.requestBody) collectTypeRefNames(op.requestBody, names, hints, topLevelEnumNames);
|
|
568
|
+
for (const p of [
|
|
569
|
+
...op.pathParams,
|
|
570
|
+
...op.queryParams,
|
|
571
|
+
...op.headerParams,
|
|
572
|
+
...op.cookieParams ?? []
|
|
573
|
+
]) collectTypeRefNames(p.type, names, hints, topLevelEnumNames);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
for (const model of spec.models) {
|
|
577
|
+
names.add(model.name);
|
|
578
|
+
for (const derived of hints.derivedModelNames(model.name)) names.add(derived);
|
|
579
|
+
for (const field of model.fields) collectTypeRefNames(field.type, names, hints, topLevelEnumNames);
|
|
580
|
+
}
|
|
581
|
+
for (const e of spec.enums) names.add(e.name);
|
|
582
|
+
return names;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Compute a map of enum name → set of wire values that appear in the spec.
|
|
586
|
+
* Used by filterSurface to exclude hand-added enum members that aren't in the spec.
|
|
587
|
+
*/
|
|
588
|
+
function specDerivedEnumValues(spec) {
|
|
589
|
+
const result = /* @__PURE__ */ new Map();
|
|
590
|
+
for (const e of spec.enums) result.set(e.name, new Set(e.values.map((v) => v.value)));
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Compute the set of field paths (e.g., "Organization.name") that are defined
|
|
595
|
+
* in the OpenAPI spec's model schemas. Used by filterSurface to exclude
|
|
596
|
+
* hand-added SDK fields that reference spec-derived types.
|
|
597
|
+
*/
|
|
598
|
+
function specDerivedFieldPaths(spec, hints) {
|
|
599
|
+
const paths = /* @__PURE__ */ new Set();
|
|
600
|
+
for (const model of spec.models) {
|
|
601
|
+
for (const field of model.fields) paths.add(`${model.name}.${field.name}`);
|
|
602
|
+
for (const derived of hints.derivedModelNames(model.name)) for (const field of model.fields) paths.add(`${derived}.${field.name}`);
|
|
603
|
+
}
|
|
604
|
+
return paths;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Compute the set of method paths (e.g., "SSO.authorize") that are defined
|
|
608
|
+
* as operations in the OpenAPI spec's services. Used by filterSurface to
|
|
609
|
+
* exclude hand-written SDK methods that don't correspond to spec operations.
|
|
610
|
+
*/
|
|
611
|
+
function specDerivedMethodPaths(spec) {
|
|
612
|
+
const paths = /* @__PURE__ */ new Set();
|
|
613
|
+
for (const service of spec.services) for (const op of service.operations) paths.add(`${service.name}.${op.name}`);
|
|
614
|
+
return paths;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Compute the set of HTTP operation keys (e.g., "GET /sso/authorize") that
|
|
618
|
+
* exist in the spec. Used for overlay-based method matching.
|
|
619
|
+
*/
|
|
620
|
+
function specDerivedHttpKeys(spec) {
|
|
621
|
+
const keys = /* @__PURE__ */ new Set();
|
|
622
|
+
for (const service of spec.services) for (const op of service.operations) keys.add(`${op.httpMethod.toUpperCase()} ${op.path}`);
|
|
623
|
+
return keys;
|
|
624
|
+
}
|
|
625
|
+
function collectTypeRefNames(ref, out, hints, topLevelEnums) {
|
|
626
|
+
walkTypeRef(ref, {
|
|
627
|
+
model: (r) => {
|
|
628
|
+
out.add(r.name);
|
|
629
|
+
for (const derived of hints.derivedModelNames(r.name)) out.add(derived);
|
|
630
|
+
},
|
|
631
|
+
enum: (r) => {
|
|
632
|
+
if (!topLevelEnums || topLevelEnums.has(r.name)) out.add(r.name);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function filterRecord(record, allowed) {
|
|
637
|
+
const result = {};
|
|
638
|
+
for (const [name, value] of Object.entries(record)) if (allowed.has(name)) result[name] = value;
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Filter an ApiSurface to only include symbols whose names appear in the
|
|
643
|
+
* allowed set. Symbols not in the set are dropped entirely — they won't
|
|
644
|
+
* count toward the total or produce violations.
|
|
645
|
+
*
|
|
646
|
+
* For interfaces: when fieldPaths is provided, only keeps fields that appear
|
|
647
|
+
* in the spec-derived field paths. This prevents false positives from hand-added
|
|
648
|
+
* SDK fields that reference spec-derived types but aren't defined in the spec.
|
|
649
|
+
*
|
|
650
|
+
* For classes: when methodPaths is provided, only keeps methods whose names
|
|
651
|
+
* appear in the spec-derived method paths. This prevents false positives from
|
|
652
|
+
* hand-written SDK methods (e.g., SSO.authorization_url, Webhooks.construct_event)
|
|
653
|
+
* that don't correspond to spec operations. Properties on service classes are
|
|
654
|
+
* also filtered: only UPPER_CASE constants that are spec-derivable are kept.
|
|
655
|
+
*/
|
|
656
|
+
function filterSurface(surface, allowedNames, opts) {
|
|
657
|
+
const { fieldPaths, methodPaths, enumValues } = opts ?? {};
|
|
658
|
+
const filteredInterfaces = filterRecord(surface.interfaces, allowedNames);
|
|
659
|
+
if (fieldPaths) for (const [name, iface] of Object.entries(filteredInterfaces)) {
|
|
660
|
+
const filteredFields = {};
|
|
661
|
+
for (const [fieldName, field] of Object.entries(iface.fields)) if (fieldPaths.has(`${name}.${fieldName}`)) filteredFields[fieldName] = field;
|
|
662
|
+
filteredInterfaces[name] = {
|
|
663
|
+
...iface,
|
|
664
|
+
fields: filteredFields
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
let filteredClasses = filterRecord(surface.classes, allowedNames);
|
|
668
|
+
if (methodPaths) {
|
|
669
|
+
const serviceClassNames = /* @__PURE__ */ new Set();
|
|
670
|
+
for (const path of methodPaths) {
|
|
671
|
+
const dot = path.indexOf(".");
|
|
672
|
+
if (dot !== -1) serviceClassNames.add(path.slice(0, dot));
|
|
673
|
+
}
|
|
674
|
+
const newClasses = {};
|
|
675
|
+
for (const [name, cls] of Object.entries(filteredClasses)) if (serviceClassNames.has(name)) {
|
|
676
|
+
const filteredMethods = {};
|
|
677
|
+
for (const [methodName, overloads] of Object.entries(cls.methods)) if (methodPaths.has(`${name}.${methodName}`)) filteredMethods[methodName] = overloads;
|
|
678
|
+
const filteredProperties = {};
|
|
679
|
+
for (const [propName, prop] of Object.entries(cls.properties)) if (methodPaths.has(`${name}.${propName}`)) filteredProperties[propName] = prop;
|
|
680
|
+
newClasses[name] = {
|
|
681
|
+
...cls,
|
|
682
|
+
methods: filteredMethods,
|
|
683
|
+
properties: filteredProperties
|
|
684
|
+
};
|
|
685
|
+
} else if (fieldPaths) {
|
|
686
|
+
const filteredProperties = {};
|
|
687
|
+
for (const [propName, prop] of Object.entries(cls.properties)) if (fieldPaths.has(`${name}.${propName}`)) filteredProperties[propName] = prop;
|
|
688
|
+
const filteredMethods = {};
|
|
689
|
+
for (const [methodName, overloads] of Object.entries(cls.methods)) if (fieldPaths.has(`${name}.${methodName}`) || methodPaths.has(`${name}.${methodName}`)) filteredMethods[methodName] = overloads;
|
|
690
|
+
newClasses[name] = {
|
|
691
|
+
...cls,
|
|
692
|
+
methods: filteredMethods,
|
|
693
|
+
properties: filteredProperties
|
|
694
|
+
};
|
|
695
|
+
} else newClasses[name] = cls;
|
|
696
|
+
filteredClasses = newClasses;
|
|
697
|
+
}
|
|
698
|
+
let filteredEnums = filterRecord(surface.enums, allowedNames);
|
|
699
|
+
if (enumValues) {
|
|
700
|
+
const newEnums = {};
|
|
701
|
+
for (const [name, enumDef] of Object.entries(filteredEnums)) {
|
|
702
|
+
const specValues = enumValues.get(name);
|
|
703
|
+
if (!specValues) {
|
|
704
|
+
newEnums[name] = enumDef;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const specValuesLower = new Set([...specValues].map((v) => String(v).toLowerCase()));
|
|
708
|
+
const filteredMembers = {};
|
|
709
|
+
for (const [member, value] of Object.entries(enumDef.members)) if (specValues.has(value) || specValuesLower.has(String(value).toLowerCase())) filteredMembers[member] = value;
|
|
710
|
+
newEnums[name] = {
|
|
711
|
+
...enumDef,
|
|
712
|
+
members: filteredMembers
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
filteredEnums = newEnums;
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
...surface,
|
|
719
|
+
classes: filteredClasses,
|
|
720
|
+
interfaces: filteredInterfaces,
|
|
721
|
+
typeAliases: filterRecord(surface.typeAliases, allowedNames),
|
|
722
|
+
enums: filteredEnums,
|
|
723
|
+
exports: {}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
//#endregion
|
|
727
|
+
//#region src/compat/differ.ts
|
|
728
|
+
function diffSurfaces(baseline, candidate, hints) {
|
|
729
|
+
const violations = [];
|
|
730
|
+
const additions = [];
|
|
731
|
+
let totalBaseline = 0;
|
|
732
|
+
let preserved = 0;
|
|
733
|
+
for (const [name, baseClass] of Object.entries(baseline.classes)) {
|
|
734
|
+
totalBaseline++;
|
|
735
|
+
const candClass = candidate.classes[name];
|
|
736
|
+
if (!candClass) {
|
|
737
|
+
violations.push({
|
|
738
|
+
category: "public-api",
|
|
739
|
+
severity: "breaking",
|
|
740
|
+
symbolPath: name,
|
|
741
|
+
baseline: name,
|
|
742
|
+
candidate: "(missing)",
|
|
743
|
+
message: `Class "${name}" exists in baseline but not in generated output`
|
|
744
|
+
});
|
|
745
|
+
totalBaseline += Object.values(baseClass.methods).reduce((sum, overloads) => sum + overloads.length, 0);
|
|
746
|
+
totalBaseline += Object.keys(baseClass.properties).length;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
preserved++;
|
|
750
|
+
for (const [methodName, baseOverloads] of Object.entries(baseClass.methods)) {
|
|
751
|
+
const candOverloads = candClass.methods[methodName];
|
|
752
|
+
for (const baseMethod of baseOverloads) {
|
|
753
|
+
totalBaseline++;
|
|
754
|
+
if (!candOverloads || candOverloads.length === 0) {
|
|
755
|
+
violations.push({
|
|
756
|
+
category: "public-api",
|
|
757
|
+
severity: "breaking",
|
|
758
|
+
symbolPath: `${name}.${methodName}`,
|
|
759
|
+
baseline: methodName,
|
|
760
|
+
candidate: "(missing)",
|
|
761
|
+
message: `Method "${name}.${methodName}" exists in baseline but not in generated output`
|
|
762
|
+
});
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (!candOverloads.find((c) => signaturesMatch(baseMethod, c))) {
|
|
766
|
+
if (hints.isSignatureEquivalent ? candOverloads.find((c) => hints.isSignatureEquivalent(baseMethod, c, candidate)) : void 0) {
|
|
767
|
+
preserved++;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
violations.push({
|
|
771
|
+
category: "signature",
|
|
772
|
+
severity: "breaking",
|
|
773
|
+
symbolPath: `${name}.${methodName}`,
|
|
774
|
+
baseline: formatSignature(baseMethod),
|
|
775
|
+
candidate: formatSignature(candOverloads[0]),
|
|
776
|
+
message: `Signature mismatch for "${name}.${methodName}"`
|
|
777
|
+
});
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
preserved++;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
for (const methodName of Object.keys(candClass.methods)) if (!baseClass.methods[methodName]) additions.push({
|
|
784
|
+
symbolPath: `${name}.${methodName}`,
|
|
785
|
+
symbolType: "method"
|
|
786
|
+
});
|
|
787
|
+
for (const [propName, baseProp] of Object.entries(baseClass.properties)) {
|
|
788
|
+
totalBaseline++;
|
|
789
|
+
const candProp = candClass.properties[propName];
|
|
790
|
+
if (!candProp) {
|
|
791
|
+
violations.push({
|
|
792
|
+
category: "public-api",
|
|
793
|
+
severity: "breaking",
|
|
794
|
+
symbolPath: `${name}.${propName}`,
|
|
795
|
+
baseline: baseProp.type,
|
|
796
|
+
candidate: "(missing)",
|
|
797
|
+
message: `Property "${name}.${propName}" exists in baseline but not in generated output`
|
|
798
|
+
});
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (baseProp.type !== candProp.type) {
|
|
802
|
+
const nullableOnly = hints.isNullableOnlyDifference(baseProp.type, candProp.type);
|
|
803
|
+
violations.push({
|
|
804
|
+
category: "signature",
|
|
805
|
+
severity: nullableOnly ? "warning" : "breaking",
|
|
806
|
+
symbolPath: `${name}.${propName}`,
|
|
807
|
+
baseline: baseProp.type,
|
|
808
|
+
candidate: candProp.type,
|
|
809
|
+
message: `Property type mismatch for "${name}.${propName}"`
|
|
810
|
+
});
|
|
811
|
+
if (nullableOnly) preserved++;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
preserved++;
|
|
815
|
+
}
|
|
816
|
+
for (const propName of Object.keys(candClass.properties)) if (!baseClass.properties[propName]) additions.push({
|
|
817
|
+
symbolPath: `${name}.${propName}`,
|
|
818
|
+
symbolType: "property"
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
for (const name of Object.keys(candidate.classes)) if (!baseline.classes[name]) additions.push({
|
|
822
|
+
symbolPath: name,
|
|
823
|
+
symbolType: "class"
|
|
824
|
+
});
|
|
825
|
+
const candIfaceFieldSets = /* @__PURE__ */ new Map();
|
|
826
|
+
for (const [n, iface] of Object.entries(candidate.interfaces)) candIfaceFieldSets.set(n, new Set(Object.keys(iface.fields).map((f) => f.toLowerCase())));
|
|
827
|
+
const candClassPropSets = /* @__PURE__ */ new Map();
|
|
828
|
+
for (const [n, cls] of Object.entries(candidate.classes)) candClassPropSets.set(n, new Set(Object.keys(cls.properties).map((f) => f.toLowerCase())));
|
|
829
|
+
for (const [name, baseIface] of Object.entries(baseline.interfaces)) {
|
|
830
|
+
totalBaseline++;
|
|
831
|
+
const candIface = candidate.interfaces[name];
|
|
832
|
+
if (!candIface) {
|
|
833
|
+
let tolerated = false;
|
|
834
|
+
if (hints.tolerateCategoryMismatch && name.startsWith("Serialized")) {
|
|
835
|
+
const baseName = name.slice(10);
|
|
836
|
+
if (candidate.interfaces[baseName] || candidate.classes[baseName]) tolerated = true;
|
|
837
|
+
}
|
|
838
|
+
if (tolerated) {
|
|
839
|
+
preserved++;
|
|
840
|
+
totalBaseline += Object.keys(baseIface.fields).length;
|
|
841
|
+
preserved += Object.keys(baseIface.fields).length;
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (!tolerated) {
|
|
845
|
+
const baseFieldNamesLower = new Set(Object.keys(baseIface.fields).map((f) => f.toLowerCase()));
|
|
846
|
+
if (baseFieldNamesLower.size > 0) {
|
|
847
|
+
for (const [, candFieldNamesLower] of candIfaceFieldSets) if (candFieldNamesLower.size === baseFieldNamesLower.size && [...baseFieldNamesLower].every((f) => candFieldNamesLower.has(f))) {
|
|
848
|
+
tolerated = true;
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
if (!tolerated) {
|
|
852
|
+
for (const [, candPropNamesLower] of candClassPropSets) if (candPropNamesLower.size === baseFieldNamesLower.size && [...baseFieldNamesLower].every((f) => candPropNamesLower.has(f))) {
|
|
853
|
+
tolerated = true;
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
} else tolerated = true;
|
|
858
|
+
}
|
|
859
|
+
if (tolerated) {
|
|
860
|
+
preserved++;
|
|
861
|
+
totalBaseline += Object.keys(baseIface.fields).length;
|
|
862
|
+
preserved += Object.keys(baseIface.fields).length;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
violations.push({
|
|
866
|
+
category: "public-api",
|
|
867
|
+
severity: "breaking",
|
|
868
|
+
symbolPath: name,
|
|
869
|
+
baseline: name,
|
|
870
|
+
candidate: "(missing)",
|
|
871
|
+
message: `Interface "${name}" exists in baseline but not in generated output`
|
|
872
|
+
});
|
|
873
|
+
totalBaseline += Object.keys(baseIface.fields).length;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
preserved++;
|
|
877
|
+
for (const [fieldName, baseField] of Object.entries(baseIface.fields)) {
|
|
878
|
+
totalBaseline++;
|
|
879
|
+
const candField = candIface.fields[fieldName];
|
|
880
|
+
if (!candField) {
|
|
881
|
+
const baseTypeClean = baseField.type.replace(/\[\]$/, "").replace(/ \| null$/, "");
|
|
882
|
+
const typeIsUnresolvable = NAMED_TYPE_RE.test(baseTypeClean) && !typeExistsInSurface(baseTypeClean, candidate);
|
|
883
|
+
violations.push({
|
|
884
|
+
category: "public-api",
|
|
885
|
+
severity: typeIsUnresolvable ? "warning" : "breaking",
|
|
886
|
+
symbolPath: `${name}.${fieldName}`,
|
|
887
|
+
baseline: baseField.type,
|
|
888
|
+
candidate: "(missing)",
|
|
889
|
+
message: `Field "${name}.${fieldName}" exists in baseline but not in generated output`
|
|
890
|
+
});
|
|
891
|
+
if (typeIsUnresolvable) preserved++;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (baseField.type !== candField.type) {
|
|
895
|
+
if (hints.isUnionReorder(baseField.type, candField.type)) {
|
|
896
|
+
preserved++;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (hints.isTypeEquivalent?.(baseField.type, candField.type, candidate)) {
|
|
900
|
+
preserved++;
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const nullableOnly = hints.isNullableOnlyDifference(baseField.type, candField.type);
|
|
904
|
+
const genericParam = hints.isGenericTypeParam(baseField.type);
|
|
905
|
+
const extractionArtifact = hints.isExtractionArtifact(candField.type);
|
|
906
|
+
const isWarning = nullableOnly || genericParam || extractionArtifact;
|
|
907
|
+
violations.push({
|
|
908
|
+
category: "signature",
|
|
909
|
+
severity: isWarning ? "warning" : "breaking",
|
|
910
|
+
symbolPath: `${name}.${fieldName}`,
|
|
911
|
+
baseline: baseField.type,
|
|
912
|
+
candidate: candField.type,
|
|
913
|
+
message: `Field type mismatch for "${name}.${fieldName}"`
|
|
914
|
+
});
|
|
915
|
+
if (isWarning) preserved++;
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
preserved++;
|
|
919
|
+
}
|
|
920
|
+
for (const fieldName of Object.keys(candIface.fields)) if (!baseIface.fields[fieldName]) additions.push({
|
|
921
|
+
symbolPath: `${name}.${fieldName}`,
|
|
922
|
+
symbolType: "property"
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
for (const name of Object.keys(candidate.interfaces)) if (!baseline.interfaces[name]) additions.push({
|
|
926
|
+
symbolPath: name,
|
|
927
|
+
symbolType: "interface"
|
|
928
|
+
});
|
|
929
|
+
for (const [name, baseAlias] of Object.entries(baseline.typeAliases)) {
|
|
930
|
+
totalBaseline++;
|
|
931
|
+
const candAlias = candidate.typeAliases[name];
|
|
932
|
+
if (!candAlias) {
|
|
933
|
+
if (hints.tolerateCategoryMismatch && typeExistsInSurface(name, candidate)) {
|
|
934
|
+
preserved++;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
violations.push({
|
|
938
|
+
category: "public-api",
|
|
939
|
+
severity: "breaking",
|
|
940
|
+
symbolPath: name,
|
|
941
|
+
baseline: baseAlias.value,
|
|
942
|
+
candidate: "(missing)",
|
|
943
|
+
message: `Type alias "${name}" exists in baseline but not in generated output`
|
|
944
|
+
});
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (baseAlias.value !== candAlias.value) {
|
|
948
|
+
if (hints.isUnionReorder(baseAlias.value, candAlias.value)) {
|
|
949
|
+
preserved++;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const nullableOnly = hints.isNullableOnlyDifference(baseAlias.value, candAlias.value);
|
|
953
|
+
violations.push({
|
|
954
|
+
category: "signature",
|
|
955
|
+
severity: nullableOnly ? "warning" : "breaking",
|
|
956
|
+
symbolPath: name,
|
|
957
|
+
baseline: baseAlias.value,
|
|
958
|
+
candidate: candAlias.value,
|
|
959
|
+
message: `Type alias value mismatch for "${name}"`
|
|
960
|
+
});
|
|
961
|
+
if (nullableOnly) preserved++;
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
preserved++;
|
|
965
|
+
}
|
|
966
|
+
for (const name of Object.keys(candidate.typeAliases)) if (!baseline.typeAliases[name]) additions.push({
|
|
967
|
+
symbolPath: name,
|
|
968
|
+
symbolType: "type-alias"
|
|
969
|
+
});
|
|
970
|
+
for (const [name, baseEnum] of Object.entries(baseline.enums)) {
|
|
971
|
+
totalBaseline++;
|
|
972
|
+
const candEnum = candidate.enums[name];
|
|
973
|
+
if (!candEnum) {
|
|
974
|
+
violations.push({
|
|
975
|
+
category: "public-api",
|
|
976
|
+
severity: "breaking",
|
|
977
|
+
symbolPath: name,
|
|
978
|
+
baseline: name,
|
|
979
|
+
candidate: "(missing)",
|
|
980
|
+
message: `Enum "${name}" exists in baseline but not in generated output`
|
|
981
|
+
});
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
const candValueToMembers = /* @__PURE__ */ new Map();
|
|
985
|
+
for (const [candMember, candValue] of Object.entries(candEnum.members)) {
|
|
986
|
+
const existing = candValueToMembers.get(candValue);
|
|
987
|
+
if (existing) existing.push(candMember);
|
|
988
|
+
else candValueToMembers.set(candValue, [candMember]);
|
|
989
|
+
}
|
|
990
|
+
let enumMatch = true;
|
|
991
|
+
for (const [member, value] of Object.entries(baseEnum.members)) {
|
|
992
|
+
if (candEnum.members[member] === value) continue;
|
|
993
|
+
const valueMatches = candValueToMembers.get(value);
|
|
994
|
+
if (valueMatches && valueMatches.length > 0) {
|
|
995
|
+
violations.push({
|
|
996
|
+
category: "signature",
|
|
997
|
+
severity: "warning",
|
|
998
|
+
symbolPath: `${name}.${member}`,
|
|
999
|
+
baseline: `${member}=${String(value)}`,
|
|
1000
|
+
candidate: `${valueMatches[0]}=${String(value)}`,
|
|
1001
|
+
message: `Enum member name differs for "${name}.${member}" (value "${value}" preserved as "${valueMatches[0]}")`
|
|
1002
|
+
});
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
const lowerValue = String(value).toLowerCase();
|
|
1006
|
+
const caseInsensitiveMatch = [...candValueToMembers.entries()].find(([candVal]) => String(candVal).toLowerCase() === lowerValue);
|
|
1007
|
+
if (caseInsensitiveMatch) {
|
|
1008
|
+
violations.push({
|
|
1009
|
+
category: "signature",
|
|
1010
|
+
severity: "warning",
|
|
1011
|
+
symbolPath: `${name}.${member}`,
|
|
1012
|
+
baseline: `${member}=${String(value)}`,
|
|
1013
|
+
candidate: `${caseInsensitiveMatch[1][0]}=${String(caseInsensitiveMatch[0])}`,
|
|
1014
|
+
message: `Enum member value case differs for "${name}.${member}" (baseline "${value}" vs candidate "${caseInsensitiveMatch[0]}")`
|
|
1015
|
+
});
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (String(value) === member || member === "JsonEnumDefaultValue" || member === "JsonProperty") {
|
|
1019
|
+
violations.push({
|
|
1020
|
+
category: "signature",
|
|
1021
|
+
severity: "warning",
|
|
1022
|
+
symbolPath: `${name}.${member}`,
|
|
1023
|
+
baseline: String(value),
|
|
1024
|
+
candidate: "(extraction artifact)",
|
|
1025
|
+
message: `Enum member "${name}.${member}" appears to be an extraction artifact`
|
|
1026
|
+
});
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
violations.push({
|
|
1030
|
+
category: "signature",
|
|
1031
|
+
severity: "breaking",
|
|
1032
|
+
symbolPath: `${name}.${member}`,
|
|
1033
|
+
baseline: String(value),
|
|
1034
|
+
candidate: member in candEnum.members ? String(candEnum.members[member]) : "(missing)",
|
|
1035
|
+
message: `Enum member mismatch for "${name}.${member}"`
|
|
1036
|
+
});
|
|
1037
|
+
enumMatch = false;
|
|
1038
|
+
}
|
|
1039
|
+
if (enumMatch) preserved++;
|
|
1040
|
+
}
|
|
1041
|
+
for (const name of Object.keys(candidate.enums)) if (!baseline.enums[name]) additions.push({
|
|
1042
|
+
symbolPath: name,
|
|
1043
|
+
symbolType: "enum"
|
|
1044
|
+
});
|
|
1045
|
+
for (const [path, baseExports] of Object.entries(baseline.exports)) {
|
|
1046
|
+
const candExports = candidate.exports[path];
|
|
1047
|
+
if (!candExports) {
|
|
1048
|
+
for (const exp of baseExports) violations.push({
|
|
1049
|
+
category: "export-structure",
|
|
1050
|
+
severity: "warning",
|
|
1051
|
+
symbolPath: `exports[${path}].${exp}`,
|
|
1052
|
+
baseline: exp,
|
|
1053
|
+
candidate: "(missing)",
|
|
1054
|
+
message: `Export "${exp}" from "${path}" not found in generated output`
|
|
1055
|
+
});
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const candSet = new Set(candExports);
|
|
1059
|
+
for (const exp of baseExports) if (!candSet.has(exp)) violations.push({
|
|
1060
|
+
category: "export-structure",
|
|
1061
|
+
severity: "warning",
|
|
1062
|
+
symbolPath: `exports[${path}].${exp}`,
|
|
1063
|
+
baseline: exp,
|
|
1064
|
+
candidate: "(missing)",
|
|
1065
|
+
message: `Export "${exp}" from "${path}" not found in generated output`
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
return {
|
|
1069
|
+
preservationScore: totalBaseline > 0 ? Math.round(preserved / totalBaseline * 100) : 100,
|
|
1070
|
+
totalBaselineSymbols: totalBaseline,
|
|
1071
|
+
preservedSymbols: preserved,
|
|
1072
|
+
violations,
|
|
1073
|
+
additions
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Compare method signatures using raw type strings.
|
|
1078
|
+
* NOTE: This only produces meaningful results when both surfaces were extracted
|
|
1079
|
+
* by the same extractor for the same language. Cross-language or cross-extractor
|
|
1080
|
+
* comparisons will always fail because type representations differ.
|
|
1081
|
+
*/
|
|
1082
|
+
function signaturesMatch(baseline, candidate) {
|
|
1083
|
+
if (baseline.returnType !== candidate.returnType) return false;
|
|
1084
|
+
for (let i = 0; i < baseline.params.length; i++) {
|
|
1085
|
+
const baseParam = baseline.params[i];
|
|
1086
|
+
const candParam = candidate.params[i];
|
|
1087
|
+
if (!candParam) return false;
|
|
1088
|
+
if (baseParam.type !== candParam.type) return false;
|
|
1089
|
+
if (baseParam.name !== candParam.name) return false;
|
|
1090
|
+
}
|
|
1091
|
+
for (let i = baseline.params.length; i < candidate.params.length; i++) if (!candidate.params[i].optional) return false;
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
function formatSignature(method) {
|
|
1095
|
+
return `(${method.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ")}) => ${method.returnType}`;
|
|
1096
|
+
}
|
|
1097
|
+
//#endregion
|
|
1098
|
+
export { registerExtractor as _, specDerivedHttpKeys as a, buildOverlayLookup as c, defaultIsNullableOnlyDifference as d, namedTypeWordsOverlap as f, getExtractor as g, typeExistsInSurface as h, specDerivedFieldPaths as i, patchOverlay as l, resolveHints as m, filterSurface as n, specDerivedMethodPaths as o, nodeHints as p, specDerivedEnumValues as r, specDerivedNames as s, diffSurfaces as t, NAMED_TYPE_RE as u };
|
|
1099
|
+
|
|
1100
|
+
//# sourceMappingURL=differ-B6472Yef.mjs.map
|