@workos/oagen 0.6.0 → 0.8.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.mjs +1339 -451
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +144 -57
- package/dist/compat/index.d.mts.map +1 -1
- package/dist/compat/index.mjs +365 -35
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-CltJMcwG.mjs → diff-BaH9qa8k.mjs} +100 -23
- package/dist/diff-BaH9qa8k.mjs.map +1 -0
- package/dist/differ-CTl98yoS.d.mts +116 -0
- package/dist/differ-CTl98yoS.d.mts.map +1 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +177 -4
- package/dist/index.mjs.map +1 -1
- package/dist/{differ-CG5ZErKl.mjs → overlay-TYxHOCow.mjs} +1112 -553
- package/dist/overlay-TYxHOCow.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.mjs +176 -3
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +43 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +176 -3
- package/dist/smoke.mjs.map +1 -1
- package/dist/tree-sitter-C53w_0WE.mjs.map +1 -1
- package/dist/{types-C7g5sLuX.d.mts → types-mTtkgy_h.d.mts} +213 -2
- package/dist/types-mTtkgy_h.d.mts.map +1 -0
- package/dist/verify/index.d.mts +4 -3
- package/dist/verify/index.d.mts.map +1 -1
- package/dist/verify/index.mjs +12 -10
- package/dist/verify/index.mjs.map +1 -1
- package/docs/architecture/emitter-contract.md +2 -2
- package/docs/architecture/extractor-contract.md +6 -5
- package/docs/architecture/pipeline.md +4 -3
- package/docs/architecture/workflows.md +5 -3
- package/docs/cli.md +148 -6
- package/docs/contributor/public-api.md +1 -1
- package/docs/core/compatibility-ir.md +200 -0
- package/docs/core/compatibility-policy.md +170 -0
- package/docs/core/compatibility.md +83 -0
- package/docs/core/index.md +5 -0
- package/docs/core/manifest-schema.md +76 -0
- package/docs/core/quickstart.md +9 -2
- package/docs/core/verify-compat.md +235 -0
- package/package.json +1 -1
- package/skills/generate-emitter/SKILL.md +12 -7
- package/skills/generate-emitter/references/project-scaffold.md +27 -6
- package/skills/generate-extractor/SKILL.md +14 -8
- package/skills/generate-sdk/SKILL.md +2 -2
- package/skills/generate-smoke-test/SKILL.md +9 -5
- package/skills/review-operations/SKILL.md +2 -2
- package/skills/verify-compat/SKILL.md +1 -1
- package/dist/diff-CltJMcwG.mjs.map +0 -1
- package/dist/differ-CG5ZErKl.mjs.map +0 -1
- package/dist/types-C7g5sLuX.d.mts.map +0 -1
|
@@ -1,13 +1,537 @@
|
|
|
1
1
|
import { A as walkTypeRef, f as toSnakeCase, o as splitWords, x as RegistryError } from "./tree-sitter-C53w_0WE.mjs";
|
|
2
|
-
//#region src/compat/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
//#region src/compat/policy.ts
|
|
3
|
+
/** Built-in language defaults. */
|
|
4
|
+
const LANGUAGE_DEFAULTS = {
|
|
5
|
+
php: {
|
|
6
|
+
callerUsesParamNames: true,
|
|
7
|
+
constructorOrderMatters: true,
|
|
8
|
+
methodParameterNamesArePublicApi: true,
|
|
9
|
+
constructorParameterNamesArePublicApi: true,
|
|
10
|
+
overloadsArePublicApi: false,
|
|
11
|
+
arityIsPublicApi: false
|
|
12
|
+
},
|
|
13
|
+
python: {
|
|
14
|
+
callerUsesParamNames: true,
|
|
15
|
+
constructorOrderMatters: true,
|
|
16
|
+
methodParameterNamesArePublicApi: true,
|
|
17
|
+
constructorParameterNamesArePublicApi: true,
|
|
18
|
+
overloadsArePublicApi: false,
|
|
19
|
+
arityIsPublicApi: false
|
|
20
|
+
},
|
|
21
|
+
ruby: {
|
|
22
|
+
callerUsesParamNames: true,
|
|
23
|
+
constructorOrderMatters: true,
|
|
24
|
+
methodParameterNamesArePublicApi: true,
|
|
25
|
+
constructorParameterNamesArePublicApi: true,
|
|
26
|
+
overloadsArePublicApi: false,
|
|
27
|
+
arityIsPublicApi: false
|
|
28
|
+
},
|
|
29
|
+
go: {
|
|
30
|
+
callerUsesParamNames: false,
|
|
31
|
+
constructorOrderMatters: true,
|
|
32
|
+
methodParameterNamesArePublicApi: false,
|
|
33
|
+
constructorParameterNamesArePublicApi: false,
|
|
34
|
+
overloadsArePublicApi: false,
|
|
35
|
+
arityIsPublicApi: true
|
|
36
|
+
},
|
|
37
|
+
kotlin: {
|
|
38
|
+
callerUsesParamNames: true,
|
|
39
|
+
constructorOrderMatters: false,
|
|
40
|
+
methodParameterNamesArePublicApi: true,
|
|
41
|
+
constructorParameterNamesArePublicApi: true,
|
|
42
|
+
overloadsArePublicApi: true,
|
|
43
|
+
arityIsPublicApi: false
|
|
44
|
+
},
|
|
45
|
+
dotnet: {
|
|
46
|
+
callerUsesParamNames: true,
|
|
47
|
+
constructorOrderMatters: false,
|
|
48
|
+
methodParameterNamesArePublicApi: true,
|
|
49
|
+
constructorParameterNamesArePublicApi: true,
|
|
50
|
+
overloadsArePublicApi: true,
|
|
51
|
+
arityIsPublicApi: false
|
|
52
|
+
},
|
|
53
|
+
elixir: {
|
|
54
|
+
callerUsesParamNames: true,
|
|
55
|
+
constructorOrderMatters: false,
|
|
56
|
+
methodParameterNamesArePublicApi: true,
|
|
57
|
+
constructorParameterNamesArePublicApi: false,
|
|
58
|
+
overloadsArePublicApi: false,
|
|
59
|
+
arityIsPublicApi: true
|
|
60
|
+
},
|
|
61
|
+
rust: {
|
|
62
|
+
callerUsesParamNames: false,
|
|
63
|
+
constructorOrderMatters: true,
|
|
64
|
+
methodParameterNamesArePublicApi: false,
|
|
65
|
+
constructorParameterNamesArePublicApi: false,
|
|
66
|
+
overloadsArePublicApi: false,
|
|
67
|
+
arityIsPublicApi: true
|
|
68
|
+
},
|
|
69
|
+
node: {
|
|
70
|
+
callerUsesParamNames: false,
|
|
71
|
+
constructorOrderMatters: false,
|
|
72
|
+
methodParameterNamesArePublicApi: false,
|
|
73
|
+
constructorParameterNamesArePublicApi: false,
|
|
74
|
+
overloadsArePublicApi: false,
|
|
75
|
+
arityIsPublicApi: false
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/** Get the built-in default policy for a language. */
|
|
79
|
+
function getDefaultPolicy(language) {
|
|
80
|
+
return { ...LANGUAGE_DEFAULTS[language] };
|
|
6
81
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
82
|
+
/** Merge user overrides onto language defaults. Only provided keys are overridden. */
|
|
83
|
+
function mergePolicy(defaults, overrides) {
|
|
84
|
+
return {
|
|
85
|
+
...defaults,
|
|
86
|
+
...overrides
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** All supported language IDs. */
|
|
90
|
+
const ALL_LANGUAGE_IDS = Object.keys(LANGUAGE_DEFAULTS);
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/compat/schema.ts
|
|
93
|
+
/** Current schema version for compatibility snapshots. */
|
|
94
|
+
const COMPAT_SCHEMA_VERSION = "1";
|
|
95
|
+
/** Validate that a parsed snapshot has a compatible schema version. */
|
|
96
|
+
function isCompatibleSchemaVersion(snapshot) {
|
|
97
|
+
return snapshot.schemaVersion === "1";
|
|
98
|
+
}
|
|
99
|
+
/** Validate the basic structure of a parsed compat snapshot. */
|
|
100
|
+
function validateSnapshot(data) {
|
|
101
|
+
if (typeof data !== "object" || data === null) return false;
|
|
102
|
+
const obj = data;
|
|
103
|
+
return typeof obj.schemaVersion === "string" && typeof obj.source === "object" && obj.source !== null && typeof obj.policies === "object" && obj.policies !== null && Array.isArray(obj.symbols);
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/compat/ir.ts
|
|
107
|
+
/**
|
|
108
|
+
* Convert a legacy ApiSurface to a CompatSnapshot.
|
|
109
|
+
*
|
|
110
|
+
* This bridge allows existing extractors (which produce ApiSurface) to feed
|
|
111
|
+
* into the new classified diff engine without requiring immediate migration.
|
|
112
|
+
*/
|
|
113
|
+
function apiSurfaceToSnapshot(surface) {
|
|
114
|
+
const language = surface.language || "node";
|
|
115
|
+
const symbols = [];
|
|
116
|
+
for (const [className, cls] of Object.entries(surface.classes)) {
|
|
117
|
+
symbols.push({
|
|
118
|
+
id: `class:${className}`,
|
|
119
|
+
kind: "service_accessor",
|
|
120
|
+
fqName: className,
|
|
121
|
+
displayName: className,
|
|
122
|
+
visibility: "public",
|
|
123
|
+
stability: cls.deprecationMessage ? "deprecated" : "stable",
|
|
124
|
+
sourceKind: "generated_service_wrapper"
|
|
125
|
+
});
|
|
126
|
+
if (cls.constructorParams.length > 0) symbols.push({
|
|
127
|
+
id: `ctor:${className}`,
|
|
128
|
+
kind: "constructor",
|
|
129
|
+
fqName: `${className}.constructor`,
|
|
130
|
+
ownerFqName: className,
|
|
131
|
+
displayName: `new ${className}`,
|
|
132
|
+
visibility: "public",
|
|
133
|
+
stability: "stable",
|
|
134
|
+
sourceKind: "generated_resource_constructor",
|
|
135
|
+
parameters: cls.constructorParams.map((p, i) => apiParamToCompatParam(p, i, language))
|
|
136
|
+
});
|
|
137
|
+
for (const [methodName, overloads] of Object.entries(cls.methods)) for (let oi = 0; oi < overloads.length; oi++) {
|
|
138
|
+
const method = overloads[oi];
|
|
139
|
+
const suffix = overloads.length > 1 ? `#${oi}` : "";
|
|
140
|
+
symbols.push({
|
|
141
|
+
id: `method:${className}.${methodName}${suffix}`,
|
|
142
|
+
kind: "callable",
|
|
143
|
+
fqName: `${className}.${methodName}`,
|
|
144
|
+
ownerFqName: className,
|
|
145
|
+
displayName: `${className}.${methodName}`,
|
|
146
|
+
visibility: "public",
|
|
147
|
+
stability: "stable",
|
|
148
|
+
sourceKind: "generated_service_wrapper",
|
|
149
|
+
parameters: method.params.map((p, i) => apiParamToCompatParam(p, i, language)),
|
|
150
|
+
returns: { name: method.returnType }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
for (const [propName, prop] of Object.entries(cls.properties)) symbols.push({
|
|
154
|
+
id: `prop:${className}.${propName}`,
|
|
155
|
+
kind: "property",
|
|
156
|
+
fqName: `${className}.${propName}`,
|
|
157
|
+
ownerFqName: className,
|
|
158
|
+
displayName: `${className}.${propName}`,
|
|
159
|
+
visibility: "public",
|
|
160
|
+
stability: "stable",
|
|
161
|
+
sourceKind: "generated_service_wrapper",
|
|
162
|
+
typeRef: { name: prop.type }
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const [ifaceName, iface] of Object.entries(surface.interfaces)) {
|
|
166
|
+
symbols.push({
|
|
167
|
+
id: `iface:${ifaceName}`,
|
|
168
|
+
kind: "alias",
|
|
169
|
+
fqName: ifaceName,
|
|
170
|
+
displayName: ifaceName,
|
|
171
|
+
visibility: "public",
|
|
172
|
+
stability: "stable",
|
|
173
|
+
sourceKind: "generated_resource_constructor"
|
|
174
|
+
});
|
|
175
|
+
for (const [fieldName, field] of Object.entries(iface.fields)) symbols.push({
|
|
176
|
+
id: `field:${ifaceName}.${fieldName}`,
|
|
177
|
+
kind: "field",
|
|
178
|
+
fqName: `${ifaceName}.${fieldName}`,
|
|
179
|
+
ownerFqName: ifaceName,
|
|
180
|
+
displayName: `${ifaceName}.${fieldName}`,
|
|
181
|
+
visibility: "public",
|
|
182
|
+
stability: "stable",
|
|
183
|
+
sourceKind: "generated_resource_constructor",
|
|
184
|
+
typeRef: { name: field.type }
|
|
185
|
+
});
|
|
186
|
+
if (iface.hasCustomConstructor) symbols.push({
|
|
187
|
+
id: `ctor:${ifaceName}`,
|
|
188
|
+
kind: "constructor",
|
|
189
|
+
fqName: `${ifaceName}.constructor`,
|
|
190
|
+
ownerFqName: ifaceName,
|
|
191
|
+
displayName: `new ${ifaceName}`,
|
|
192
|
+
visibility: "public",
|
|
193
|
+
stability: "stable",
|
|
194
|
+
sourceKind: "generated_resource_constructor"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
for (const [aliasName] of Object.entries(surface.typeAliases)) symbols.push({
|
|
198
|
+
id: `alias:${aliasName}`,
|
|
199
|
+
kind: "alias",
|
|
200
|
+
fqName: aliasName,
|
|
201
|
+
displayName: aliasName,
|
|
202
|
+
visibility: "public",
|
|
203
|
+
stability: "stable",
|
|
204
|
+
sourceKind: "generated_resource_constructor"
|
|
205
|
+
});
|
|
206
|
+
for (const [enumName, enumDef] of Object.entries(surface.enums)) {
|
|
207
|
+
symbols.push({
|
|
208
|
+
id: `enum:${enumName}`,
|
|
209
|
+
kind: "enum",
|
|
210
|
+
fqName: enumName,
|
|
211
|
+
displayName: enumName,
|
|
212
|
+
visibility: "public",
|
|
213
|
+
stability: "stable",
|
|
214
|
+
sourceKind: "generated_enum"
|
|
215
|
+
});
|
|
216
|
+
for (const [memberName, memberValue] of Object.entries(enumDef.members)) symbols.push({
|
|
217
|
+
id: `enum_member:${enumName}.${memberName}`,
|
|
218
|
+
kind: "enum_member",
|
|
219
|
+
fqName: `${enumName}.${memberName}`,
|
|
220
|
+
ownerFqName: enumName,
|
|
221
|
+
displayName: `${enumName}.${memberName}`,
|
|
222
|
+
visibility: "public",
|
|
223
|
+
stability: "stable",
|
|
224
|
+
sourceKind: "generated_enum",
|
|
225
|
+
value: memberValue
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
schemaVersion: "1",
|
|
230
|
+
source: { extractedAt: surface.extractedAt },
|
|
231
|
+
policies: getDefaultPolicy(language),
|
|
232
|
+
symbols
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** Convert a legacy ApiParam to a CompatParameter. */
|
|
236
|
+
function apiParamToCompatParam(param, position, language) {
|
|
237
|
+
const policy = getDefaultPolicy(language);
|
|
238
|
+
return {
|
|
239
|
+
publicName: param.name,
|
|
240
|
+
position,
|
|
241
|
+
required: !param.optional,
|
|
242
|
+
nullable: false,
|
|
243
|
+
hasDefault: param.optional,
|
|
244
|
+
passing: param.passingStyle ?? inferPassingStyle(language),
|
|
245
|
+
type: { name: param.type },
|
|
246
|
+
sensitivity: {
|
|
247
|
+
order: policy.constructorOrderMatters,
|
|
248
|
+
publicName: policy.methodParameterNamesArePublicApi,
|
|
249
|
+
requiredness: true,
|
|
250
|
+
type: true
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** Infer the default parameter passing style for a language. */
|
|
255
|
+
function inferPassingStyle(language) {
|
|
256
|
+
switch (language) {
|
|
257
|
+
case "python":
|
|
258
|
+
case "ruby":
|
|
259
|
+
case "elixir": return "keyword";
|
|
260
|
+
case "php":
|
|
261
|
+
case "kotlin":
|
|
262
|
+
case "dotnet": return "named";
|
|
263
|
+
case "node": return "options_object";
|
|
264
|
+
default: return "positional";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/compat/config.ts
|
|
269
|
+
const BREAKING_CATEGORIES = new Set([
|
|
270
|
+
"symbol_removed",
|
|
271
|
+
"symbol_renamed",
|
|
272
|
+
"parameter_removed",
|
|
273
|
+
"parameter_renamed",
|
|
274
|
+
"parameter_requiredness_increased",
|
|
275
|
+
"parameter_type_narrowed",
|
|
276
|
+
"parameter_position_changed_order_sensitive",
|
|
277
|
+
"constructor_position_changed_order_sensitive",
|
|
278
|
+
"named_arg_name_removed",
|
|
279
|
+
"keyword_name_removed",
|
|
280
|
+
"overload_removed",
|
|
281
|
+
"union_wrapper_migration_without_compat_alias",
|
|
282
|
+
"field_type_changed",
|
|
283
|
+
"return_type_changed",
|
|
284
|
+
"enum_member_value_changed"
|
|
285
|
+
]);
|
|
286
|
+
const SOFT_RISK_CATEGORIES = new Set([
|
|
287
|
+
"parameter_added_non_terminal_optional",
|
|
288
|
+
"constructor_reordered_named_friendly",
|
|
289
|
+
"default_value_changed",
|
|
290
|
+
"wrapper_stricter_than_previous_sdk_but_matches_spec",
|
|
291
|
+
"doc_surface_drift"
|
|
292
|
+
]);
|
|
293
|
+
/** Get the default severity for a change category. */
|
|
294
|
+
function defaultSeverityForCategory(category) {
|
|
295
|
+
if (BREAKING_CATEGORIES.has(category)) return "breaking";
|
|
296
|
+
if (SOFT_RISK_CATEGORIES.has(category)) return "soft-risk";
|
|
297
|
+
return "additive";
|
|
298
|
+
}
|
|
299
|
+
/** Check whether a severity meets or exceeds a fail threshold. */
|
|
300
|
+
function severityMeetsThreshold(severity, threshold) {
|
|
301
|
+
if (threshold === "none") return false;
|
|
302
|
+
if (threshold === "soft-risk") return severity === "breaking" || severity === "soft-risk";
|
|
303
|
+
return severity === "breaking";
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/compat/classify.ts
|
|
307
|
+
/**
|
|
308
|
+
* Classify changes between a baseline and candidate symbol.
|
|
309
|
+
* Returns one or more classified changes for the diff.
|
|
310
|
+
*/
|
|
311
|
+
function classifySymbolChanges(baseline, candidate, policy) {
|
|
312
|
+
const changes = [];
|
|
313
|
+
if (!candidate) {
|
|
314
|
+
changes.push(makeChange({
|
|
315
|
+
category: "symbol_removed",
|
|
316
|
+
symbol: baseline.fqName,
|
|
317
|
+
old: { symbol: baseline.fqName },
|
|
318
|
+
new: { symbol: "(removed)" },
|
|
319
|
+
message: `Symbol "${baseline.displayName}" was removed`,
|
|
320
|
+
policy
|
|
321
|
+
}));
|
|
322
|
+
return changes;
|
|
323
|
+
}
|
|
324
|
+
if (baseline.fqName !== candidate.fqName && baseline.id === candidate.id) changes.push(makeChange({
|
|
325
|
+
category: "symbol_renamed",
|
|
326
|
+
symbol: baseline.fqName,
|
|
327
|
+
old: { name: baseline.fqName },
|
|
328
|
+
new: { name: candidate.fqName },
|
|
329
|
+
message: `Symbol renamed from "${baseline.displayName}" to "${candidate.displayName}"`,
|
|
330
|
+
policy
|
|
331
|
+
}));
|
|
332
|
+
if (baseline.parameters && candidate.parameters) changes.push(...classifyParameterChanges(baseline, candidate, policy));
|
|
333
|
+
if (baseline.returns && candidate.returns && baseline.returns.name !== candidate.returns.name) changes.push(makeChange({
|
|
334
|
+
category: "return_type_changed",
|
|
335
|
+
symbol: baseline.fqName,
|
|
336
|
+
old: { returnType: baseline.returns.name },
|
|
337
|
+
new: { returnType: candidate.returns.name },
|
|
338
|
+
message: `Return type changed for "${baseline.displayName}" from "${baseline.returns.name}" to "${candidate.returns.name}"`,
|
|
339
|
+
policy
|
|
340
|
+
}));
|
|
341
|
+
if (baseline.typeRef && candidate.typeRef && baseline.typeRef.name !== candidate.typeRef.name) changes.push(makeChange({
|
|
342
|
+
category: "field_type_changed",
|
|
343
|
+
symbol: baseline.fqName,
|
|
344
|
+
old: { type: baseline.typeRef.name },
|
|
345
|
+
new: { type: candidate.typeRef.name },
|
|
346
|
+
message: `Type changed for "${baseline.displayName}" from "${baseline.typeRef.name}" to "${candidate.typeRef.name}"`,
|
|
347
|
+
policy
|
|
348
|
+
}));
|
|
349
|
+
if (baseline.kind === "enum_member" && candidate.kind === "enum_member" && baseline.value !== void 0 && candidate.value !== void 0 && baseline.value !== candidate.value) changes.push(makeChange({
|
|
350
|
+
category: "enum_member_value_changed",
|
|
351
|
+
symbol: baseline.fqName,
|
|
352
|
+
old: { value: String(baseline.value) },
|
|
353
|
+
new: { value: String(candidate.value) },
|
|
354
|
+
message: `Enum value changed for "${baseline.displayName}" from "${baseline.value}" to "${candidate.value}"`,
|
|
355
|
+
policy
|
|
356
|
+
}));
|
|
357
|
+
return changes;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Classify parameter-level changes between two symbol versions.
|
|
361
|
+
*/
|
|
362
|
+
function classifyParameterChanges(baseline, candidate, policy) {
|
|
363
|
+
const changes = [];
|
|
364
|
+
const baseParams = baseline.parameters ?? [];
|
|
365
|
+
const candParams = candidate.parameters ?? [];
|
|
366
|
+
const baseByName = new Map(baseParams.map((p) => [p.publicName, p]));
|
|
367
|
+
const candByName = new Map(candParams.map((p) => [p.publicName, p]));
|
|
368
|
+
const isConstructor = baseline.kind === "constructor";
|
|
369
|
+
for (const baseParam of baseParams) {
|
|
370
|
+
const candParam = candByName.get(baseParam.publicName);
|
|
371
|
+
if (!candParam) {
|
|
372
|
+
const positionalMatch = candParams[baseParam.position];
|
|
373
|
+
if (positionalMatch && !baseByName.has(positionalMatch.publicName)) {
|
|
374
|
+
const isBreakingRename = parameterNameIsPublicApi(baseParam, policy, isConstructor);
|
|
375
|
+
changes.push(makeChange({
|
|
376
|
+
category: "parameter_renamed",
|
|
377
|
+
symbol: baseline.fqName,
|
|
378
|
+
old: { parameter: baseParam.publicName },
|
|
379
|
+
new: { parameter: positionalMatch.publicName },
|
|
380
|
+
message: `Parameter "${baseParam.publicName}" renamed to "${positionalMatch.publicName}" on "${baseline.displayName}"`,
|
|
381
|
+
policy,
|
|
382
|
+
severityOverride: isBreakingRename ? void 0 : "soft-risk"
|
|
383
|
+
}));
|
|
384
|
+
} else changes.push(makeChange({
|
|
385
|
+
category: "parameter_removed",
|
|
386
|
+
symbol: baseline.fqName,
|
|
387
|
+
old: { parameter: baseParam.publicName },
|
|
388
|
+
new: { parameter: "(removed)" },
|
|
389
|
+
message: `Parameter "${baseParam.publicName}" removed from "${baseline.displayName}"`,
|
|
390
|
+
policy
|
|
391
|
+
}));
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (!baseParam.required && candParam.required) changes.push(makeChange({
|
|
395
|
+
category: "parameter_requiredness_increased",
|
|
396
|
+
symbol: baseline.fqName,
|
|
397
|
+
old: {
|
|
398
|
+
parameter: baseParam.publicName,
|
|
399
|
+
required: "false"
|
|
400
|
+
},
|
|
401
|
+
new: {
|
|
402
|
+
parameter: candParam.publicName,
|
|
403
|
+
required: "true"
|
|
404
|
+
},
|
|
405
|
+
message: `Parameter "${baseParam.publicName}" became required on "${baseline.displayName}"`,
|
|
406
|
+
policy
|
|
407
|
+
}));
|
|
408
|
+
if (baseParam.type.name !== candParam.type.name) changes.push(makeChange({
|
|
409
|
+
category: "parameter_type_narrowed",
|
|
410
|
+
symbol: baseline.fqName,
|
|
411
|
+
old: {
|
|
412
|
+
parameter: baseParam.publicName,
|
|
413
|
+
type: baseParam.type.name
|
|
414
|
+
},
|
|
415
|
+
new: {
|
|
416
|
+
parameter: candParam.publicName,
|
|
417
|
+
type: candParam.type.name
|
|
418
|
+
},
|
|
419
|
+
message: `Parameter type changed for "${baseParam.publicName}" on "${baseline.displayName}"`,
|
|
420
|
+
policy
|
|
421
|
+
}));
|
|
422
|
+
if (baseParam.position !== candParam.position) if (isConstructor ? policy.constructorOrderMatters : baseParam.sensitivity.order) {
|
|
423
|
+
const category = isConstructor ? "constructor_position_changed_order_sensitive" : "parameter_position_changed_order_sensitive";
|
|
424
|
+
changes.push(makeChange({
|
|
425
|
+
category,
|
|
426
|
+
symbol: baseline.fqName,
|
|
427
|
+
old: {
|
|
428
|
+
parameter: baseParam.publicName,
|
|
429
|
+
position: String(baseParam.position)
|
|
430
|
+
},
|
|
431
|
+
new: {
|
|
432
|
+
parameter: candParam.publicName,
|
|
433
|
+
position: String(candParam.position)
|
|
434
|
+
},
|
|
435
|
+
message: `Parameter "${baseParam.publicName}" moved from position ${baseParam.position} to ${candParam.position} on "${baseline.displayName}"`,
|
|
436
|
+
policy
|
|
437
|
+
}));
|
|
438
|
+
} else changes.push(makeChange({
|
|
439
|
+
category: "constructor_reordered_named_friendly",
|
|
440
|
+
symbol: baseline.fqName,
|
|
441
|
+
old: {
|
|
442
|
+
parameter: baseParam.publicName,
|
|
443
|
+
position: String(baseParam.position)
|
|
444
|
+
},
|
|
445
|
+
new: {
|
|
446
|
+
parameter: candParam.publicName,
|
|
447
|
+
position: String(candParam.position)
|
|
448
|
+
},
|
|
449
|
+
message: `Parameter "${baseParam.publicName}" reordered on "${baseline.displayName}" (named-friendly language)`,
|
|
450
|
+
policy
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
for (const candParam of candParams) if (!baseByName.has(candParam.publicName)) {
|
|
454
|
+
if (changes.some((c) => c.category === "parameter_renamed" && c.new.parameter === candParam.publicName)) continue;
|
|
455
|
+
const isTerminal = candParam.position === candParams.length - 1;
|
|
456
|
+
const category = candParam.required ? "parameter_requiredness_increased" : isTerminal ? "parameter_added_optional_terminal" : "parameter_added_non_terminal_optional";
|
|
457
|
+
if (candParam.required) changes.push(makeChange({
|
|
458
|
+
category,
|
|
459
|
+
symbol: baseline.fqName,
|
|
460
|
+
old: { parameter: "(absent)" },
|
|
461
|
+
new: {
|
|
462
|
+
parameter: candParam.publicName,
|
|
463
|
+
required: "true"
|
|
464
|
+
},
|
|
465
|
+
message: `Required parameter "${candParam.publicName}" added to "${baseline.displayName}"`,
|
|
466
|
+
policy
|
|
467
|
+
}));
|
|
468
|
+
else changes.push(makeChange({
|
|
469
|
+
category,
|
|
470
|
+
symbol: baseline.fqName,
|
|
471
|
+
old: { parameter: "(absent)" },
|
|
472
|
+
new: { parameter: candParam.publicName },
|
|
473
|
+
message: `Optional parameter "${candParam.publicName}" added to "${baseline.displayName}"`,
|
|
474
|
+
policy
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
return changes;
|
|
478
|
+
}
|
|
479
|
+
function parameterNameIsPublicApi(param, policy, isConstructor) {
|
|
480
|
+
if (param.sensitivity.publicName) return true;
|
|
481
|
+
if (isConstructor) return policy.constructorParameterNamesArePublicApi;
|
|
482
|
+
return policy.methodParameterNamesArePublicApi;
|
|
483
|
+
}
|
|
484
|
+
/** Build a deterministic conceptual change ID. */
|
|
485
|
+
function buildConceptualChangeId(category, symbol, match) {
|
|
486
|
+
const parts = [
|
|
487
|
+
"chg",
|
|
488
|
+
category,
|
|
489
|
+
symbol.replace(/[^a-zA-Z0-9_.]/g, "_")
|
|
490
|
+
];
|
|
491
|
+
if (match.parameter) parts.push(match.parameter);
|
|
492
|
+
if (match.member) parts.push(match.member);
|
|
493
|
+
return parts.join("_").toLowerCase();
|
|
494
|
+
}
|
|
495
|
+
function makeChange(opts) {
|
|
496
|
+
return {
|
|
497
|
+
category: opts.category,
|
|
498
|
+
severity: opts.severityOverride ?? defaultSeverityForCategory(opts.category),
|
|
499
|
+
symbol: opts.symbol,
|
|
500
|
+
conceptualChangeId: buildConceptualChangeId(opts.category, opts.symbol, opts.old),
|
|
501
|
+
provenance: opts.provenance ?? "unknown",
|
|
502
|
+
old: opts.old,
|
|
503
|
+
new: opts.new,
|
|
504
|
+
message: opts.message
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Classify a new symbol as additive.
|
|
509
|
+
*/
|
|
510
|
+
function classifyAddedSymbol(symbol) {
|
|
511
|
+
return {
|
|
512
|
+
category: "symbol_added",
|
|
513
|
+
severity: "additive",
|
|
514
|
+
symbol: symbol.fqName,
|
|
515
|
+
conceptualChangeId: buildConceptualChangeId("symbol_added", symbol.fqName, {}),
|
|
516
|
+
provenance: "unknown",
|
|
517
|
+
old: { symbol: "(absent)" },
|
|
518
|
+
new: { symbol: symbol.fqName },
|
|
519
|
+
message: `Symbol "${symbol.displayName}" was added`
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/** Summarize a list of classified changes by severity. */
|
|
523
|
+
function summarizeChanges(changes) {
|
|
524
|
+
let breaking = 0;
|
|
525
|
+
let softRisk = 0;
|
|
526
|
+
let additive = 0;
|
|
527
|
+
for (const c of changes) if (c.severity === "breaking") breaking++;
|
|
528
|
+
else if (c.severity === "soft-risk") softRisk++;
|
|
529
|
+
else additive++;
|
|
530
|
+
return {
|
|
531
|
+
breaking,
|
|
532
|
+
softRisk,
|
|
533
|
+
additive
|
|
534
|
+
};
|
|
11
535
|
}
|
|
12
536
|
//#endregion
|
|
13
537
|
//#region src/compat/language-hints.ts
|
|
@@ -157,574 +681,213 @@ function resolveHints(overrides) {
|
|
|
157
681
|
};
|
|
158
682
|
}
|
|
159
683
|
//#endregion
|
|
160
|
-
//#region src/compat/
|
|
684
|
+
//#region src/compat/spec-filter.ts
|
|
161
685
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
686
|
+
* Compute the set of symbol names that are derivable from the OpenAPI spec.
|
|
687
|
+
* Only these names should be compared during compat verification — everything
|
|
688
|
+
* else in the baseline is hand-written and out of scope for generation.
|
|
165
689
|
*/
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
690
|
+
function specDerivedNames(spec, hints) {
|
|
691
|
+
const names = /* @__PURE__ */ new Set();
|
|
692
|
+
const topLevelEnumNames = new Set(spec.enums.map((e) => e.name));
|
|
693
|
+
for (const service of spec.services) {
|
|
694
|
+
names.add(service.name);
|
|
695
|
+
for (const op of service.operations) {
|
|
696
|
+
collectTypeRefNames(op.response, names, hints, topLevelEnumNames);
|
|
697
|
+
if (op.requestBody) collectTypeRefNames(op.requestBody, names, hints, topLevelEnumNames);
|
|
698
|
+
for (const p of [
|
|
699
|
+
...op.pathParams,
|
|
700
|
+
...op.queryParams,
|
|
701
|
+
...op.headerParams,
|
|
702
|
+
...op.cookieParams ?? []
|
|
703
|
+
]) collectTypeRefNames(p.type, names, hints, topLevelEnumNames);
|
|
173
704
|
}
|
|
174
705
|
}
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const suffix = propWords.slice(propWords.length - classWords.length);
|
|
180
|
-
if (classWords.every((w, i) => w === suffix[i])) return className;
|
|
181
|
-
}
|
|
706
|
+
for (const model of spec.models) {
|
|
707
|
+
names.add(model.name);
|
|
708
|
+
for (const derived of hints.derivedModelNames(model.name)) names.add(derived);
|
|
709
|
+
for (const field of model.fields) collectTypeRefNames(field.type, names, hints, topLevelEnumNames);
|
|
182
710
|
}
|
|
711
|
+
for (const e of spec.enums) names.add(e.name);
|
|
712
|
+
return names;
|
|
183
713
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
}
|
|
714
|
+
/**
|
|
715
|
+
* Compute a map of enum name → set of wire values that appear in the spec.
|
|
716
|
+
* Used by filterSurface to exclude hand-added enum members that aren't in the spec.
|
|
717
|
+
*/
|
|
718
|
+
function specDerivedEnumValues(spec) {
|
|
719
|
+
const result = /* @__PURE__ */ new Map();
|
|
720
|
+
for (const e of spec.enums) result.set(e.name, new Set(e.values.map((v) => v.value)));
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Compute the set of field paths (e.g., "Organization.name") that are defined
|
|
725
|
+
* in the OpenAPI spec's model schemas. Used by filterSurface to exclude
|
|
726
|
+
* hand-added SDK fields that reference spec-derived types.
|
|
727
|
+
*/
|
|
728
|
+
function specDerivedFieldPaths(spec, hints) {
|
|
729
|
+
const paths = /* @__PURE__ */ new Set();
|
|
730
|
+
for (const model of spec.models) {
|
|
731
|
+
for (const field of model.fields) paths.add(`${model.name}.${field.name}`);
|
|
732
|
+
for (const derived of hints.derivedModelNames(model.name)) for (const field of model.fields) paths.add(`${derived}.${field.name}`);
|
|
294
733
|
}
|
|
295
|
-
|
|
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;
|
|
734
|
+
return paths;
|
|
374
735
|
}
|
|
375
736
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
737
|
+
* Compute the set of method paths (e.g., "SSO.authorize") that are defined
|
|
738
|
+
* as operations in the OpenAPI spec's services. Used by filterSurface to
|
|
739
|
+
* exclude hand-written SDK methods that don't correspond to spec operations.
|
|
378
740
|
*/
|
|
379
|
-
function
|
|
380
|
-
|
|
741
|
+
function specDerivedMethodPaths(spec) {
|
|
742
|
+
const paths = /* @__PURE__ */ new Set();
|
|
743
|
+
for (const service of spec.services) for (const op of service.operations) paths.add(`${service.name}.${op.name}`);
|
|
744
|
+
return paths;
|
|
381
745
|
}
|
|
382
746
|
/**
|
|
383
|
-
* Compute the
|
|
384
|
-
*
|
|
747
|
+
* Compute the set of HTTP operation keys (e.g., "GET /sso/authorize") that
|
|
748
|
+
* exist in the spec. Used for overlay-based method matching.
|
|
385
749
|
*/
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
};
|
|
750
|
+
function specDerivedHttpKeys(spec) {
|
|
751
|
+
const keys = /* @__PURE__ */ new Set();
|
|
752
|
+
for (const service of spec.services) for (const op of service.operations) keys.add(`${op.httpMethod.toUpperCase()} ${op.path}`);
|
|
753
|
+
return keys;
|
|
398
754
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
755
|
+
function collectTypeRefNames(ref, out, hints, topLevelEnums) {
|
|
756
|
+
walkTypeRef(ref, {
|
|
757
|
+
model: (r) => {
|
|
758
|
+
out.add(r.name);
|
|
759
|
+
for (const derived of hints.derivedModelNames(r.name)) out.add(derived);
|
|
760
|
+
},
|
|
761
|
+
enum: (r) => {
|
|
762
|
+
if (!topLevelEnums || topLevelEnums.has(r.name)) out.add(r.name);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
404
765
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return new Set(Object.keys(iface.fields).map(normalizeFieldName));
|
|
766
|
+
function filterRecord(record, allowed) {
|
|
767
|
+
const result = {};
|
|
768
|
+
for (const [name, value] of Object.entries(record)) if (allowed.has(name)) result[name] = value;
|
|
769
|
+
return result;
|
|
410
770
|
}
|
|
411
771
|
/**
|
|
412
|
-
*
|
|
772
|
+
* Filter an ApiSurface to only include symbols whose names appear in the
|
|
773
|
+
* allowed set. Symbols not in the set are dropped entirely — they won't
|
|
774
|
+
* count toward the total or produce violations.
|
|
413
775
|
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* operation's response model and request body model.
|
|
776
|
+
* For interfaces: when fieldPaths is provided, only keeps fields that appear
|
|
777
|
+
* in the spec-derived field paths. This prevents false positives from hand-added
|
|
778
|
+
* SDK fields that reference spec-derived types but aren't defined in the spec.
|
|
418
779
|
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
780
|
+
* For classes: when methodPaths is provided, only keeps methods whose names
|
|
781
|
+
* appear in the spec-derived method paths. This prevents false positives from
|
|
782
|
+
* hand-written SDK methods (e.g., SSO.authorization_url, Webhooks.construct_event)
|
|
783
|
+
* that don't correspond to spec operations. Properties on service classes are
|
|
784
|
+
* also filtered: only UPPER_CASE constants that are spec-derivable are kept.
|
|
423
785
|
*/
|
|
424
|
-
function
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
786
|
+
function filterSurface(surface, allowedNames, opts) {
|
|
787
|
+
const { fieldPaths, methodPaths, enumValues } = opts ?? {};
|
|
788
|
+
const filteredInterfaces = filterRecord(surface.interfaces, allowedNames);
|
|
789
|
+
if (fieldPaths) for (const [name, iface] of Object.entries(filteredInterfaces)) {
|
|
790
|
+
const filteredFields = {};
|
|
791
|
+
for (const [fieldName, field] of Object.entries(iface.fields)) if (fieldPaths.has(`${name}.${fieldName}`)) filteredFields[fieldName] = field;
|
|
792
|
+
filteredInterfaces[name] = {
|
|
793
|
+
...iface,
|
|
794
|
+
fields: filteredFields
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
let filteredClasses = filterRecord(surface.classes, allowedNames);
|
|
798
|
+
if (methodPaths) {
|
|
799
|
+
const serviceClassNames = /* @__PURE__ */ new Set();
|
|
800
|
+
for (const path of methodPaths) {
|
|
801
|
+
const dot = path.indexOf(".");
|
|
802
|
+
if (dot !== -1) serviceClassNames.add(path.slice(0, dot));
|
|
438
803
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
804
|
+
const newClasses = {};
|
|
805
|
+
for (const [name, cls] of Object.entries(filteredClasses)) if (serviceClassNames.has(name)) {
|
|
806
|
+
const filteredMethods = {};
|
|
807
|
+
for (const [methodName, overloads] of Object.entries(cls.methods)) if (methodPaths.has(`${name}.${methodName}`)) filteredMethods[methodName] = overloads;
|
|
808
|
+
const filteredProperties = {};
|
|
809
|
+
for (const [propName, prop] of Object.entries(cls.properties)) if (methodPaths.has(`${name}.${propName}`)) filteredProperties[propName] = prop;
|
|
810
|
+
newClasses[name] = {
|
|
811
|
+
...cls,
|
|
812
|
+
methods: filteredMethods,
|
|
813
|
+
properties: filteredProperties
|
|
814
|
+
};
|
|
815
|
+
} else if (fieldPaths) {
|
|
816
|
+
const filteredProperties = {};
|
|
817
|
+
for (const [propName, prop] of Object.entries(cls.properties)) if (fieldPaths.has(`${name}.${propName}`)) filteredProperties[propName] = prop;
|
|
818
|
+
const filteredMethods = {};
|
|
819
|
+
for (const [methodName, overloads] of Object.entries(cls.methods)) if (fieldPaths.has(`${name}.${methodName}`) || methodPaths.has(`${name}.${methodName}`)) filteredMethods[methodName] = overloads;
|
|
820
|
+
newClasses[name] = {
|
|
821
|
+
...cls,
|
|
822
|
+
methods: filteredMethods,
|
|
823
|
+
properties: filteredProperties
|
|
824
|
+
};
|
|
825
|
+
} else newClasses[name] = cls;
|
|
826
|
+
filteredClasses = newClasses;
|
|
827
|
+
}
|
|
828
|
+
let filteredEnums = filterRecord(surface.enums, allowedNames);
|
|
829
|
+
if (enumValues) {
|
|
830
|
+
const newEnums = {};
|
|
831
|
+
for (const [name, enumDef] of Object.entries(filteredEnums)) {
|
|
832
|
+
const specValues = enumValues.get(name);
|
|
833
|
+
if (!specValues) {
|
|
834
|
+
newEnums[name] = enumDef;
|
|
835
|
+
continue;
|
|
446
836
|
}
|
|
837
|
+
const specValuesLower = new Set([...specValues].map((v) => String(v).toLowerCase()));
|
|
838
|
+
const filteredMembers = {};
|
|
839
|
+
for (const [member, value] of Object.entries(enumDef.members)) if (specValues.has(value) || specValuesLower.has(String(value).toLowerCase())) filteredMembers[member] = value;
|
|
840
|
+
newEnums[name] = {
|
|
841
|
+
...enumDef,
|
|
842
|
+
members: filteredMembers
|
|
843
|
+
};
|
|
447
844
|
}
|
|
845
|
+
filteredEnums = newEnums;
|
|
448
846
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
847
|
+
return {
|
|
848
|
+
...surface,
|
|
849
|
+
classes: filteredClasses,
|
|
850
|
+
interfaces: filteredInterfaces,
|
|
851
|
+
typeAliases: filterRecord(surface.typeAliases, allowedNames),
|
|
852
|
+
enums: filteredEnums,
|
|
853
|
+
exports: {}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/compat/differ.ts
|
|
858
|
+
/**
|
|
859
|
+
* Diff two compat snapshots, producing classified changes with
|
|
860
|
+
* policy-aware severity.
|
|
861
|
+
*/
|
|
862
|
+
function diffSnapshots(baseline, candidate, policy) {
|
|
863
|
+
const effectivePolicy = policy ?? baseline.policies;
|
|
864
|
+
const changes = [];
|
|
865
|
+
const candById = /* @__PURE__ */ new Map();
|
|
866
|
+
const candByFqName = /* @__PURE__ */ new Map();
|
|
867
|
+
for (const sym of candidate.symbols) {
|
|
868
|
+
candById.set(sym.id, sym);
|
|
869
|
+
candByFqName.set(sym.fqName, sym);
|
|
458
870
|
}
|
|
459
|
-
const
|
|
460
|
-
for (const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
}
|
|
871
|
+
const baseByFqName = /* @__PURE__ */ new Set();
|
|
872
|
+
for (const sym of baseline.symbols) baseByFqName.add(sym.fqName);
|
|
873
|
+
for (const baseSym of baseline.symbols) {
|
|
874
|
+
const candSym = candById.get(baseSym.id) ?? candByFqName.get(baseSym.fqName);
|
|
875
|
+
changes.push(...classifySymbolChanges(baseSym, candSym, effectivePolicy));
|
|
495
876
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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)
|
|
877
|
+
for (const candSym of candidate.symbols) if (!baseByFqName.has(candSym.fqName)) changes.push(classifyAddedSymbol(candSym));
|
|
878
|
+
return {
|
|
879
|
+
changes,
|
|
880
|
+
summary: summarizeChanges(changes)
|
|
511
881
|
};
|
|
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
882
|
}
|
|
641
883
|
/**
|
|
642
|
-
*
|
|
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.
|
|
884
|
+
* Compare two ApiSurface objects and return a DiffResult.
|
|
649
885
|
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
* also filtered: only UPPER_CASE constants that are spec-derivable are kept.
|
|
886
|
+
* This preserves the existing diffing behavior used by the overlay retry loop,
|
|
887
|
+
* compat check, and all existing tests. The internal implementation uses the
|
|
888
|
+
* same algorithms as before to maintain full backward compatibility with
|
|
889
|
+
* overlay patching and verification workflows.
|
|
655
890
|
*/
|
|
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
891
|
function diffSurfaces(baseline, candidate, hints) {
|
|
729
892
|
const violations = [];
|
|
730
893
|
const additions = [];
|
|
@@ -1073,12 +1236,6 @@ function diffSurfaces(baseline, candidate, hints) {
|
|
|
1073
1236
|
additions
|
|
1074
1237
|
};
|
|
1075
1238
|
}
|
|
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
1239
|
function signaturesMatch(baseline, candidate) {
|
|
1083
1240
|
if (baseline.returnType !== candidate.returnType) return false;
|
|
1084
1241
|
for (let i = 0; i < baseline.params.length; i++) {
|
|
@@ -1095,6 +1252,408 @@ function formatSignature(method) {
|
|
|
1095
1252
|
return `(${method.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ")}) => ${method.returnType}`;
|
|
1096
1253
|
}
|
|
1097
1254
|
//#endregion
|
|
1098
|
-
|
|
1255
|
+
//#region src/compat/extractor-registry.ts
|
|
1256
|
+
const extractors = /* @__PURE__ */ new Map();
|
|
1257
|
+
function registerExtractor(extractor) {
|
|
1258
|
+
extractors.set(extractor.language, extractor);
|
|
1259
|
+
}
|
|
1260
|
+
function getExtractor(language) {
|
|
1261
|
+
const extractor = extractors.get(language);
|
|
1262
|
+
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.`);
|
|
1263
|
+
return extractor;
|
|
1264
|
+
}
|
|
1265
|
+
//#endregion
|
|
1266
|
+
//#region src/compat/overlay.ts
|
|
1267
|
+
/**
|
|
1268
|
+
* Find the class name that owns a given SDK resource property.
|
|
1269
|
+
* E.g. if the surface has a class "Organizations" with property "organizations",
|
|
1270
|
+
* and sdkResourceProperty is "organizations", return "Organizations".
|
|
1271
|
+
*/
|
|
1272
|
+
function findClassForProperty(surface, sdkResourceProperty, hints) {
|
|
1273
|
+
for (const [className, cls] of Object.entries(surface.classes)) {
|
|
1274
|
+
if (hints.propertyMatchesClass(sdkResourceProperty, className)) return className;
|
|
1275
|
+
for (const [propName, prop] of Object.entries(cls.properties)) if (propName === sdkResourceProperty) {
|
|
1276
|
+
const propType = prop.type;
|
|
1277
|
+
if (propType && surface.classes[propType]) return propType;
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const propWords = splitWords(sdkResourceProperty).map((w) => w.toLowerCase());
|
|
1282
|
+
if (propWords.length > 1) for (const [className] of Object.entries(surface.classes)) {
|
|
1283
|
+
const classWords = splitWords(className).map((w) => w.toLowerCase());
|
|
1284
|
+
if (classWords.length > 0 && classWords.length < propWords.length) {
|
|
1285
|
+
const suffix = propWords.slice(propWords.length - classWords.length);
|
|
1286
|
+
if (classWords.every((w, i) => w === suffix[i])) return className;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function buildOverlayLookup(surface, manifest, spec, hints, options) {
|
|
1291
|
+
const resolvedHints = hints ?? nodeHints;
|
|
1292
|
+
const lookup = {
|
|
1293
|
+
methodByOperation: /* @__PURE__ */ new Map(),
|
|
1294
|
+
httpKeyByMethod: /* @__PURE__ */ new Map(),
|
|
1295
|
+
interfaceByName: /* @__PURE__ */ new Map(),
|
|
1296
|
+
typeAliasByName: /* @__PURE__ */ new Map(),
|
|
1297
|
+
requiredExports: /* @__PURE__ */ new Map(),
|
|
1298
|
+
modelNameByIR: /* @__PURE__ */ new Map(),
|
|
1299
|
+
fileBySymbol: /* @__PURE__ */ new Map()
|
|
1300
|
+
};
|
|
1301
|
+
if (manifest) for (const entry of manifest) {
|
|
1302
|
+
const key = `${entry.httpMethod.toUpperCase()} ${entry.path}`;
|
|
1303
|
+
const className = findClassForProperty(surface, entry.sdkResourceProperty, resolvedHints);
|
|
1304
|
+
if (className) {
|
|
1305
|
+
let resolvedMethodName = entry.sdkMethodName;
|
|
1306
|
+
let methods = surface.classes[className]?.methods[resolvedMethodName];
|
|
1307
|
+
if (!methods) {
|
|
1308
|
+
const classMethods = surface.classes[className]?.methods ?? {};
|
|
1309
|
+
const prefix = resolvedMethodName.toLowerCase();
|
|
1310
|
+
const candidates = [];
|
|
1311
|
+
for (const [name, overloads] of Object.entries(classMethods)) if (name.toLowerCase().startsWith(prefix) && name !== resolvedMethodName) candidates.push([name, overloads]);
|
|
1312
|
+
if (candidates.length === 1) {
|
|
1313
|
+
methods = candidates[0][1];
|
|
1314
|
+
resolvedMethodName = candidates[0][0];
|
|
1315
|
+
} else if (candidates.length > 1) {
|
|
1316
|
+
const firstSegment = entry.path.split("/").filter(Boolean)[0]?.replace(/[{}_-]/g, "") ?? "";
|
|
1317
|
+
if (firstSegment) {
|
|
1318
|
+
const segLower = firstSegment.toLowerCase();
|
|
1319
|
+
const segSingular = segLower.endsWith("s") ? segLower.slice(0, -1) : segLower;
|
|
1320
|
+
const best = candidates.find(([name]) => {
|
|
1321
|
+
const lower = name.toLowerCase();
|
|
1322
|
+
return lower === prefix + segLower || lower === prefix + segSingular;
|
|
1323
|
+
});
|
|
1324
|
+
if (best) {
|
|
1325
|
+
methods = best[1];
|
|
1326
|
+
resolvedMethodName = best[0];
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
if (!methods) {
|
|
1330
|
+
const verbMatch = disambiguateByHttpVerb(candidates, entry.httpMethod);
|
|
1331
|
+
if (verbMatch) {
|
|
1332
|
+
methods = verbMatch[1];
|
|
1333
|
+
resolvedMethodName = verbMatch[0];
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (!methods) {
|
|
1339
|
+
const classMethods = surface.classes[className]?.methods ?? {};
|
|
1340
|
+
const suffix = resolvedMethodName.toLowerCase();
|
|
1341
|
+
const candidates = [];
|
|
1342
|
+
for (const [name, overloads] of Object.entries(classMethods)) if (name.toLowerCase().endsWith(suffix) && name !== resolvedMethodName) candidates.push([name, overloads]);
|
|
1343
|
+
if (candidates.length === 1) {
|
|
1344
|
+
methods = candidates[0][1];
|
|
1345
|
+
resolvedMethodName = candidates[0][0];
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (!methods) {
|
|
1349
|
+
const manifestWords = splitCamelCase(resolvedMethodName);
|
|
1350
|
+
if (manifestWords.length >= 2) {
|
|
1351
|
+
const classMethods = surface.classes[className]?.methods ?? {};
|
|
1352
|
+
const candidates = [];
|
|
1353
|
+
for (const [name, overloads] of Object.entries(classMethods)) {
|
|
1354
|
+
if (name === resolvedMethodName) continue;
|
|
1355
|
+
const surfaceWords = splitCamelCase(name);
|
|
1356
|
+
if (surfaceWords.length > manifestWords.length && isSubsequence(manifestWords, surfaceWords)) candidates.push([name, overloads]);
|
|
1357
|
+
}
|
|
1358
|
+
if (candidates.length === 1) {
|
|
1359
|
+
methods = candidates[0][1];
|
|
1360
|
+
resolvedMethodName = candidates[0][0];
|
|
1361
|
+
} else if (candidates.length > 1) {
|
|
1362
|
+
const verbMatch = disambiguateByHttpVerb(candidates, entry.httpMethod);
|
|
1363
|
+
if (verbMatch) {
|
|
1364
|
+
methods = verbMatch[1];
|
|
1365
|
+
resolvedMethodName = verbMatch[0];
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
if (!methods) {
|
|
1371
|
+
const classMethods = surface.classes[className]?.methods ?? {};
|
|
1372
|
+
const manifestLower = resolvedMethodName.toLowerCase();
|
|
1373
|
+
const candidates = [];
|
|
1374
|
+
for (const [name, overloads] of Object.entries(classMethods)) {
|
|
1375
|
+
if (name === resolvedMethodName) continue;
|
|
1376
|
+
if (manifestLower.startsWith(name.toLowerCase()) && name.length >= 3) candidates.push([name, overloads]);
|
|
1377
|
+
}
|
|
1378
|
+
if (candidates.length === 1) {
|
|
1379
|
+
methods = candidates[0][1];
|
|
1380
|
+
resolvedMethodName = candidates[0][0];
|
|
1381
|
+
} else if (candidates.length > 1) {
|
|
1382
|
+
const verbMatch = disambiguateByHttpVerb(candidates, entry.httpMethod);
|
|
1383
|
+
if (verbMatch) {
|
|
1384
|
+
methods = verbMatch[1];
|
|
1385
|
+
resolvedMethodName = verbMatch[0];
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
const method = methods?.[0];
|
|
1390
|
+
if (method) {
|
|
1391
|
+
lookup.methodByOperation.set(key, {
|
|
1392
|
+
className,
|
|
1393
|
+
methodName: resolvedMethodName,
|
|
1394
|
+
params: method.params,
|
|
1395
|
+
returnType: method.returnType
|
|
1396
|
+
});
|
|
1397
|
+
lookup.httpKeyByMethod.set(`${className}.${resolvedMethodName}`, key);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
for (const name of Object.keys(surface.interfaces)) lookup.interfaceByName.set(name, name);
|
|
1402
|
+
for (const name of Object.keys(surface.typeAliases)) lookup.typeAliasByName.set(name, name);
|
|
1403
|
+
for (const [path, symbols] of Object.entries(surface.exports)) lookup.requiredExports.set(path, new Set(symbols));
|
|
1404
|
+
for (const record of [
|
|
1405
|
+
surface.classes,
|
|
1406
|
+
surface.interfaces,
|
|
1407
|
+
surface.typeAliases,
|
|
1408
|
+
surface.enums
|
|
1409
|
+
]) for (const [name, item] of Object.entries(record)) if (item.sourceFile) lookup.fileBySymbol.set(name, item.sourceFile);
|
|
1410
|
+
if (spec) {
|
|
1411
|
+
buildModelNameMap(surface, spec, lookup, resolvedHints, options);
|
|
1412
|
+
for (const [irName, sdkName] of lookup.modelNameByIR) {
|
|
1413
|
+
const filePath = lookup.fileBySymbol.get(sdkName);
|
|
1414
|
+
if (filePath) lookup.fileBySymbol.set(irName, filePath);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return lookup;
|
|
1418
|
+
}
|
|
1419
|
+
/** Map HTTP methods to preferred method name prefixes for disambiguation. */
|
|
1420
|
+
const HTTP_VERB_PREFIXES = {
|
|
1421
|
+
GET: [
|
|
1422
|
+
"list",
|
|
1423
|
+
"get",
|
|
1424
|
+
"fetch",
|
|
1425
|
+
"retrieve",
|
|
1426
|
+
"find"
|
|
1427
|
+
],
|
|
1428
|
+
POST: [
|
|
1429
|
+
"create",
|
|
1430
|
+
"add",
|
|
1431
|
+
"insert",
|
|
1432
|
+
"send"
|
|
1433
|
+
],
|
|
1434
|
+
PUT: [
|
|
1435
|
+
"set",
|
|
1436
|
+
"update",
|
|
1437
|
+
"replace",
|
|
1438
|
+
"put"
|
|
1439
|
+
],
|
|
1440
|
+
PATCH: [
|
|
1441
|
+
"update",
|
|
1442
|
+
"patch",
|
|
1443
|
+
"modify"
|
|
1444
|
+
],
|
|
1445
|
+
DELETE: [
|
|
1446
|
+
"delete",
|
|
1447
|
+
"remove",
|
|
1448
|
+
"revoke"
|
|
1449
|
+
]
|
|
1450
|
+
};
|
|
1451
|
+
/**
|
|
1452
|
+
* Disambiguate multiple method candidates using HTTP verb semantics.
|
|
1453
|
+
* Returns the best match or undefined if no unique match is found.
|
|
1454
|
+
*/
|
|
1455
|
+
function disambiguateByHttpVerb(candidates, httpMethod) {
|
|
1456
|
+
const prefixes = HTTP_VERB_PREFIXES[httpMethod.toUpperCase()];
|
|
1457
|
+
if (!prefixes) return void 0;
|
|
1458
|
+
const verbCandidates = candidates.filter(([name]) => {
|
|
1459
|
+
const lower = name.toLowerCase();
|
|
1460
|
+
return prefixes.some((p) => lower.startsWith(p));
|
|
1461
|
+
});
|
|
1462
|
+
if (verbCandidates.length === 1) return verbCandidates[0];
|
|
1463
|
+
}
|
|
1464
|
+
/** Split a camelCase identifier into its word parts (e.g., "getByExternalId" → ["get","By","External","Id"]). */
|
|
1465
|
+
function splitCamelCase(name) {
|
|
1466
|
+
const parts = [];
|
|
1467
|
+
let start = 0;
|
|
1468
|
+
for (let i = 1; i < name.length; i++) if (name[i] >= "A" && name[i] <= "Z") {
|
|
1469
|
+
parts.push(name.slice(start, i));
|
|
1470
|
+
start = i;
|
|
1471
|
+
}
|
|
1472
|
+
parts.push(name.slice(start));
|
|
1473
|
+
return parts;
|
|
1474
|
+
}
|
|
1475
|
+
/** Check if `needle` words form a subsequence of `haystack` words (case-insensitive). */
|
|
1476
|
+
function isSubsequence(needle, haystack) {
|
|
1477
|
+
let ni = 0;
|
|
1478
|
+
for (let hi = 0; hi < haystack.length && ni < needle.length; hi++) if (needle[ni].toLowerCase() === haystack[hi].toLowerCase()) ni++;
|
|
1479
|
+
return ni === needle.length;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Normalize a field name to a canonical form for comparison.
|
|
1483
|
+
* Handles camelCase → snake_case so "createdAt" and "created_at" match.
|
|
1484
|
+
*/
|
|
1485
|
+
function normalizeFieldName(name) {
|
|
1486
|
+
return toSnakeCase(name);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Compute the Jaccard similarity between two sets of normalized field names.
|
|
1490
|
+
* Returns score (0–1) and raw intersection count.
|
|
1491
|
+
*/
|
|
1492
|
+
function jaccardSimilarity(a, b) {
|
|
1493
|
+
if (a.size === 0 && b.size === 0) return {
|
|
1494
|
+
score: 0,
|
|
1495
|
+
intersection: 0
|
|
1496
|
+
};
|
|
1497
|
+
let intersection = 0;
|
|
1498
|
+
for (const item of a) if (b.has(item)) intersection++;
|
|
1499
|
+
const union = a.size + b.size - intersection;
|
|
1500
|
+
return {
|
|
1501
|
+
score: union === 0 ? 0 : intersection / union,
|
|
1502
|
+
intersection
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Build a set of normalized field names from an IR model.
|
|
1507
|
+
*/
|
|
1508
|
+
function irModelFieldSignature(model) {
|
|
1509
|
+
return new Set(model.fields.map((f) => normalizeFieldName(f.name)));
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Build a set of normalized field names from an SDK interface.
|
|
1513
|
+
*/
|
|
1514
|
+
function sdkInterfaceFieldSignature(iface) {
|
|
1515
|
+
return new Set(Object.keys(iface.fields).map(normalizeFieldName));
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Populate the modelNameByIR map using two strategies:
|
|
1519
|
+
*
|
|
1520
|
+
* 1. Operation-based matching (high confidence):
|
|
1521
|
+
* When a manifest maps an HTTP operation to an SDK method, extract the
|
|
1522
|
+
* method's return type and parameter types. Match them against the IR
|
|
1523
|
+
* operation's response model and request body model.
|
|
1524
|
+
*
|
|
1525
|
+
* 2. Field-structure matching (fallback):
|
|
1526
|
+
* For IR models not matched by operations, compare their field names
|
|
1527
|
+
* against all SDK interfaces using Jaccard similarity. A match requires
|
|
1528
|
+
* ≥60% field overlap with ≥3 fields in common.
|
|
1529
|
+
*/
|
|
1530
|
+
function buildModelNameMap(surface, spec, lookup, hints, options) {
|
|
1531
|
+
const mapped = /* @__PURE__ */ new Set();
|
|
1532
|
+
const usedSdkNames = /* @__PURE__ */ new Set();
|
|
1533
|
+
if (lookup.methodByOperation.size > 0) for (const service of spec.services) for (const op of service.operations) {
|
|
1534
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
1535
|
+
const methodOverlay = lookup.methodByOperation.get(httpKey);
|
|
1536
|
+
if (!methodOverlay) continue;
|
|
1537
|
+
if (op.response.kind === "model" && !mapped.has(op.response.name)) {
|
|
1538
|
+
const sdkTypeName = hints.extractReturnTypeName(methodOverlay.returnType);
|
|
1539
|
+
if (sdkTypeName && surface.interfaces[sdkTypeName]) {
|
|
1540
|
+
lookup.modelNameByIR.set(op.response.name, sdkTypeName);
|
|
1541
|
+
mapped.add(op.response.name);
|
|
1542
|
+
usedSdkNames.add(sdkTypeName);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (op.requestBody?.kind === "model" && !mapped.has(op.requestBody.name)) for (const param of methodOverlay.params) {
|
|
1546
|
+
const sdkTypeName = hints.extractParamTypeName(param.type);
|
|
1547
|
+
if (sdkTypeName && surface.interfaces[sdkTypeName]) {
|
|
1548
|
+
lookup.modelNameByIR.set(op.requestBody.name, sdkTypeName);
|
|
1549
|
+
mapped.add(op.requestBody.name);
|
|
1550
|
+
usedSdkNames.add(sdkTypeName);
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const sdkSignatures = [];
|
|
1556
|
+
for (const [name, iface] of Object.entries(surface.interfaces)) {
|
|
1557
|
+
if (usedSdkNames.has(name)) continue;
|
|
1558
|
+
const fields = sdkInterfaceFieldSignature(iface);
|
|
1559
|
+
if (fields.size >= 2) sdkSignatures.push({
|
|
1560
|
+
name,
|
|
1561
|
+
fields,
|
|
1562
|
+
fieldCount: fields.size
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
const sdkSignatureNames = new Set(sdkSignatures.map((s) => s.name));
|
|
1566
|
+
for (const model of spec.models) {
|
|
1567
|
+
if (mapped.has(model.name)) continue;
|
|
1568
|
+
if (model.fields.length < 2) continue;
|
|
1569
|
+
if (sdkSignatureNames.has(model.name) && !usedSdkNames.has(model.name)) {
|
|
1570
|
+
lookup.modelNameByIR.set(model.name, model.name);
|
|
1571
|
+
mapped.add(model.name);
|
|
1572
|
+
usedSdkNames.add(model.name);
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const derivedNames = hints.derivedModelNames(model.name);
|
|
1576
|
+
for (const derived of derivedNames) if (sdkSignatureNames.has(derived) && !usedSdkNames.has(derived)) {
|
|
1577
|
+
lookup.modelNameByIR.set(model.name, derived);
|
|
1578
|
+
mapped.add(model.name);
|
|
1579
|
+
usedSdkNames.add(derived);
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
if (mapped.has(model.name)) continue;
|
|
1583
|
+
if (options?.strictModelMatch) continue;
|
|
1584
|
+
const irFields = irModelFieldSignature(model);
|
|
1585
|
+
let bestMatch = null;
|
|
1586
|
+
let bestScore = 0;
|
|
1587
|
+
for (const sdk of sdkSignatures) {
|
|
1588
|
+
if (usedSdkNames.has(sdk.name)) continue;
|
|
1589
|
+
const { score, intersection } = jaccardSimilarity(irFields, sdk.fields);
|
|
1590
|
+
const minIntersection = Math.max(3, Math.ceil(irFields.size * .5));
|
|
1591
|
+
if (score > bestScore && score >= .6 && intersection >= minIntersection) {
|
|
1592
|
+
bestScore = score;
|
|
1593
|
+
bestMatch = sdk.name;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (bestMatch) {
|
|
1597
|
+
lookup.modelNameByIR.set(model.name, bestMatch);
|
|
1598
|
+
mapped.add(model.name);
|
|
1599
|
+
usedSdkNames.add(bestMatch);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/** Categories that can be patched by the overlay retry loop. */
|
|
1604
|
+
const PATCHABLE_CATEGORIES = new Set(["symbol_removed", "symbol_renamed"]);
|
|
1605
|
+
/** Check if a classified change is patchable by the overlay loop. */
|
|
1606
|
+
function isPatchableChange(change) {
|
|
1607
|
+
return PATCHABLE_CATEGORIES.has(change.category);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Patch overlay with classified changes from a failed verification.
|
|
1611
|
+
* Adds explicit name mappings for symbols that were generated with wrong names.
|
|
1612
|
+
* Returns a new OverlayLookup (immutable).
|
|
1613
|
+
*/
|
|
1614
|
+
function patchOverlay(overlay, changes, baseline) {
|
|
1615
|
+
const patched = {
|
|
1616
|
+
methodByOperation: new Map(overlay.methodByOperation),
|
|
1617
|
+
httpKeyByMethod: new Map(overlay.httpKeyByMethod),
|
|
1618
|
+
interfaceByName: new Map(overlay.interfaceByName),
|
|
1619
|
+
typeAliasByName: new Map(overlay.typeAliasByName),
|
|
1620
|
+
requiredExports: new Map(Array.from(overlay.requiredExports.entries()).map(([k, v]) => [k, new Set(v)])),
|
|
1621
|
+
modelNameByIR: new Map(overlay.modelNameByIR),
|
|
1622
|
+
fileBySymbol: new Map(overlay.fileBySymbol)
|
|
1623
|
+
};
|
|
1624
|
+
let warnedManifest = false;
|
|
1625
|
+
for (const c of changes) {
|
|
1626
|
+
if (!isPatchableChange(c)) continue;
|
|
1627
|
+
const parts = c.symbol.split(".");
|
|
1628
|
+
if (parts.length === 2) {
|
|
1629
|
+
const [className, methodName] = parts;
|
|
1630
|
+
const baseClass = baseline.classes[className];
|
|
1631
|
+
if (baseClass && baseClass.methods[methodName]?.length > 0) {
|
|
1632
|
+
const httpKey = overlay.httpKeyByMethod.get(`${className}.${methodName}`);
|
|
1633
|
+
if (!httpKey && overlay.httpKeyByMethod.size === 0 && !warnedManifest) {
|
|
1634
|
+
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.");
|
|
1635
|
+
warnedManifest = true;
|
|
1636
|
+
}
|
|
1637
|
+
if (httpKey) {
|
|
1638
|
+
const method = baseClass.methods[methodName][0];
|
|
1639
|
+
patched.methodByOperation.set(httpKey, {
|
|
1640
|
+
className,
|
|
1641
|
+
methodName,
|
|
1642
|
+
params: method.params,
|
|
1643
|
+
returnType: method.returnType
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (baseline.interfaces[className]) patched.interfaceByName.set(className, className);
|
|
1648
|
+
} else if (parts.length === 1) {
|
|
1649
|
+
const name = parts[0];
|
|
1650
|
+
if (baseline.interfaces[name]) patched.interfaceByName.set(name, name);
|
|
1651
|
+
if (baseline.typeAliases[name]) patched.typeAliasByName.set(name, name);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return patched;
|
|
1655
|
+
}
|
|
1656
|
+
//#endregion
|
|
1657
|
+
export { getDefaultPolicy as A, defaultSeverityForCategory as C, isCompatibleSchemaVersion as D, COMPAT_SCHEMA_VERSION as E, validateSnapshot as O, summarizeChanges as S, apiSurfaceToSnapshot as T, nodeHints as _, registerExtractor as a, classifyAddedSymbol as b, filterSurface as c, specDerivedHttpKeys as d, specDerivedMethodPaths as f, namedTypeWordsOverlap as g, defaultIsNullableOnlyDifference as h, getExtractor as i, mergePolicy as j, ALL_LANGUAGE_IDS as k, specDerivedEnumValues as l, NAMED_TYPE_RE as m, isPatchableChange as n, diffSnapshots as o, specDerivedNames as p, patchOverlay as r, diffSurfaces as s, buildOverlayLookup as t, specDerivedFieldPaths as u, resolveHints as v, severityMeetsThreshold as w, classifySymbolChanges as x, typeExistsInSurface as y };
|
|
1099
1658
|
|
|
1100
|
-
//# sourceMappingURL=
|
|
1659
|
+
//# sourceMappingURL=overlay-TYxHOCow.mjs.map
|