@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.
Files changed (53) hide show
  1. package/dist/cli/index.mjs +1339 -451
  2. package/dist/cli/index.mjs.map +1 -1
  3. package/dist/compat/index.d.mts +144 -57
  4. package/dist/compat/index.d.mts.map +1 -1
  5. package/dist/compat/index.mjs +365 -35
  6. package/dist/compat/index.mjs.map +1 -1
  7. package/dist/{diff-CltJMcwG.mjs → diff-BaH9qa8k.mjs} +100 -23
  8. package/dist/diff-BaH9qa8k.mjs.map +1 -0
  9. package/dist/differ-CTl98yoS.d.mts +116 -0
  10. package/dist/differ-CTl98yoS.d.mts.map +1 -0
  11. package/dist/index.d.mts +4 -2
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +177 -4
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/{differ-CG5ZErKl.mjs → overlay-TYxHOCow.mjs} +1112 -553
  16. package/dist/overlay-TYxHOCow.mjs.map +1 -0
  17. package/dist/scripts/smoke/baseline.mjs +176 -3
  18. package/dist/scripts/smoke/baseline.mjs.map +1 -1
  19. package/dist/smoke.d.mts +43 -0
  20. package/dist/smoke.d.mts.map +1 -1
  21. package/dist/smoke.mjs +176 -3
  22. package/dist/smoke.mjs.map +1 -1
  23. package/dist/tree-sitter-C53w_0WE.mjs.map +1 -1
  24. package/dist/{types-C7g5sLuX.d.mts → types-mTtkgy_h.d.mts} +213 -2
  25. package/dist/types-mTtkgy_h.d.mts.map +1 -0
  26. package/dist/verify/index.d.mts +4 -3
  27. package/dist/verify/index.d.mts.map +1 -1
  28. package/dist/verify/index.mjs +12 -10
  29. package/dist/verify/index.mjs.map +1 -1
  30. package/docs/architecture/emitter-contract.md +2 -2
  31. package/docs/architecture/extractor-contract.md +6 -5
  32. package/docs/architecture/pipeline.md +4 -3
  33. package/docs/architecture/workflows.md +5 -3
  34. package/docs/cli.md +148 -6
  35. package/docs/contributor/public-api.md +1 -1
  36. package/docs/core/compatibility-ir.md +200 -0
  37. package/docs/core/compatibility-policy.md +170 -0
  38. package/docs/core/compatibility.md +83 -0
  39. package/docs/core/index.md +5 -0
  40. package/docs/core/manifest-schema.md +76 -0
  41. package/docs/core/quickstart.md +9 -2
  42. package/docs/core/verify-compat.md +235 -0
  43. package/package.json +1 -1
  44. package/skills/generate-emitter/SKILL.md +12 -7
  45. package/skills/generate-emitter/references/project-scaffold.md +27 -6
  46. package/skills/generate-extractor/SKILL.md +14 -8
  47. package/skills/generate-sdk/SKILL.md +2 -2
  48. package/skills/generate-smoke-test/SKILL.md +9 -5
  49. package/skills/review-operations/SKILL.md +2 -2
  50. package/skills/verify-compat/SKILL.md +1 -1
  51. package/dist/diff-CltJMcwG.mjs.map +0 -1
  52. package/dist/differ-CG5ZErKl.mjs.map +0 -1
  53. 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/extractor-registry.ts
3
- const extractors = /* @__PURE__ */ new Map();
4
- function registerExtractor(extractor) {
5
- extractors.set(extractor.language, extractor);
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
- 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;
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/overlay.ts
684
+ //#region src/compat/spec-filter.ts
161
685
  /**
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".
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 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;
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 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
- }
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
- 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
- }
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
- 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;
734
+ return paths;
374
735
  }
375
736
  /**
376
- * Normalize a field name to a canonical form for comparison.
377
- * Handles camelCase → snake_case so "createdAt" and "created_at" match.
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 normalizeFieldName(name) {
380
- return toSnakeCase(name);
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 Jaccard similarity between two sets of normalized field names.
384
- * Returns score (0–1) and raw intersection count.
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 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
- };
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
- * 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)));
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
- * 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));
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
- * Populate the modelNameByIR map using two strategies:
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
- * 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.
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
- * 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.
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 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
- }
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
- 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;
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
- 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
- });
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 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
- }
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
- * 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)
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
- * 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.
884
+ * Compare two ApiSurface objects and return a DiffResult.
649
885
  *
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.
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
- 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 };
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=differ-CG5ZErKl.mjs.map
1659
+ //# sourceMappingURL=overlay-TYxHOCow.mjs.map